using RobotNet10.RobotApp.Client.Shared.Devices; using RobotNet10.RobotApp.Devices; using RobotNet10.ScriptEngine.IO; using System.Net.Sockets; namespace RobotNet10.RobotApp.Drivers.PhenikaaX; /// /// Cấu hình một vùng đọc Modbus từ configuration /// public class ModbusReadRangeConfig { /// /// Địa chỉ bắt đầu của vùng đọc /// public ushort StartAddress { get; set; } /// /// Số lượng register/coil cần đọc /// public ushort Quantity { get; set; } /// /// Tên của group range này (để mô tả và hiển thị UI) /// public string Name { get; set; } = string.Empty; /// /// Danh sách tên các register/coil con (không bắt buộc phải khai báo đủ, dùng để hiển thị UI) /// public List? ChildrenNames { get; set; } } /// /// Driver cho thiết bị ModbusTCP /// Kế thừa DeviceBase và implement IModbusTcpDevice /// [Device(DeviceType.ModbusTcp, "Phenikaa X", "ModbusTCP", "1.0.0", Description = "ModbusTCP Device Driver")] public class ModbusTcpDevice : DeviceBase, IModbusTcpDevice { private readonly string _ipAddress; private readonly int _port; private readonly byte _slaveId; private readonly int _syncIntervalMs; private readonly int _connectTimeoutMs; private readonly List _holdingRegisterRanges; private readonly List _inputRegisterRanges; private readonly List _coilRanges; private readonly List _discreteInputRanges; private ModbusTcpConnection? _connection; private CancellationTokenSource? _syncLoopCts; private Thread? _syncLoopThread; private readonly Lock _cacheLock = new(); // Cache cho các giá trị đã đọc private readonly Dictionary _holdingRegisterCache = []; private readonly Dictionary _inputRegisterCache = []; private readonly Dictionary _coilCache = []; private readonly Dictionary _discreteInputCache = []; // Queue cho các giá trị cần ghi (pending writes) private readonly Dictionary _pendingHoldingRegisterWrites = []; private readonly Dictionary _pendingCoilWrites = []; private readonly List<(ushort StartAddress, ushort[] Values)> _pendingHoldingRegisterMultiWrites = []; private readonly List<(ushort StartAddress, bool[] Values)> _pendingCoilMultiWrites = []; public event Action? DataRegisterChanged; /// /// Constructor /// public ModbusTcpDevice(string deviceId, string deviceName, IConfigurationSection connection) : base(deviceId, deviceName, DeviceType.ModbusTcp) { // Đọc cấu hình kết nối _ipAddress = connection["IpAddress"] ?? throw new InvalidOperationException("IpAddress is required in connection configuration"); var portStr = connection["Port"] ?? "502"; if (!int.TryParse(portStr, out _port) || _port < 1 || _port > 65535) { throw new InvalidOperationException($"Invalid Port: {portStr}. Must be between 1 and 65535"); } var slaveIdStr = connection["SlaveId"] ?? "1"; if (!byte.TryParse(slaveIdStr, out _slaveId) || _slaveId == 0) { throw new InvalidOperationException($"Invalid SlaveId: {slaveIdStr}. Must be between 1 and 255"); } // Đọc tần số đồng bộ (mặc định: 100ms = 10Hz) var syncIntervalStr = connection["SyncIntervalMs"] ?? "100"; if (!int.TryParse(syncIntervalStr, out _syncIntervalMs) || _syncIntervalMs < 10) { _syncIntervalMs = 100; // Default 100ms } // Đọc connect timeout (mặc định: 5000ms = 5 giây) var connectTimeoutStr = connection["ConnectTimeoutMs"] ?? "5000"; if (!int.TryParse(connectTimeoutStr, out _connectTimeoutMs) || _connectTimeoutMs < 1000) { _connectTimeoutMs = 5000; // Default 5 seconds } // Đọc các vùng đọc cho từng nhóm _holdingRegisterRanges = ParseReadRanges(connection, "HoldingRegisters"); _inputRegisterRanges = ParseReadRanges(connection, "InputRegisters"); _coilRanges = ParseReadRanges(connection, "Coils"); _discreteInputRanges = ParseReadRanges(connection, "DiscreteInputs"); // Đọc các cấu hình khác var autoReconnectEnabled = connection.GetValue("AutoReconnectEnabled"); var reconnectDelayMs = connection.GetValue("ReconnectDelayMs"); var maxReconnectAttempts = connection.GetValue("MaxReconnectAttempts"); if (autoReconnectEnabled.HasValue) AutoReconnectEnabled = autoReconnectEnabled.Value; else AutoReconnectEnabled = true; // Default if (reconnectDelayMs.HasValue) ReconnectDelayMs = reconnectDelayMs.Value; else ReconnectDelayMs = 3000; // Default 3 seconds if (maxReconnectAttempts.HasValue) MaxReconnectAttempts = maxReconnectAttempts.Value; else MaxReconnectAttempts = 0; // Unlimited by default // Khởi tạo giá trị properties SetProperty("IpAddress", _ipAddress); SetProperty("Port", _port.ToString()); SetProperty("SlaveId", _slaveId.ToString()); SetProperty("SyncIntervalMs", _syncIntervalMs.ToString()); SetProperty("ConnectTimeoutMs", _connectTimeoutMs.ToString()); } private static List ParseReadRanges(IConfigurationSection connection, string key) { var ranges = new List(); var section = connection.GetSection(key); if (!section.Exists()) return ranges; // Chỉ hỗ trợ object format - parse từ config section với nhiều entries foreach (var child in section.GetChildren()) { var startAddressStr = child["StartAddress"]; var quantityStr = child["Quantity"]; var name = child["Name"] ?? string.Empty; var childrenNamesSection = child.GetSection("ChildrenNames"); if (string.IsNullOrWhiteSpace(startAddressStr) || string.IsNullOrWhiteSpace(quantityStr)) { throw new InvalidOperationException( $"Invalid configuration for {key}: StartAddress and Quantity are required. " + $"Section: {child.Key}"); } if (!ushort.TryParse(startAddressStr, out var startAddress)) { throw new InvalidOperationException( $"Invalid StartAddress '{startAddressStr}' in {key} section '{child.Key}'. " + "Must be a valid unsigned 16-bit integer (0-65535)."); } if (!ushort.TryParse(quantityStr, out var quantity) || quantity == 0) { throw new InvalidOperationException( $"Invalid Quantity '{quantityStr}' in {key} section '{child.Key}'. " + "Must be a valid unsigned 16-bit integer greater than 0."); } // Parse ChildrenNames nếu có List childrenNames = []; if (childrenNamesSection.Exists()) { childrenNames = []; foreach (var childNameValue in childrenNamesSection.GetChildren()) { var childName = childNameValue.Value; if (!string.IsNullOrWhiteSpace(childName)) { childrenNames.Add(childName); } } } ranges.Add(new ModbusRangeData() { StartAddress = startAddress, Quantity = quantity, Name = name, ChildrenNames = [.. childrenNames], }); } return ranges; } protected override IEnumerable CreatePropertyDescriptions() { yield return new PropertyDescription("IpAddress", "IP Address", "Địa chỉ IP của ModbusTCP server") { DataType = "string", IsReadOnly = true, DisplayOrder = 1, Category = "Kết nối", DefaultValue = "" }; yield return new PropertyDescription("Port", "Port", "Port của ModbusTCP server") { DataType = "number", IsReadOnly = true, DisplayOrder = 2, Category = "Kết nối", DefaultValue = "502" }; yield return new PropertyDescription("SlaveId", "Slave ID", "Modbus Slave ID (Unit Identifier)") { DataType = "number", IsReadOnly = true, DisplayOrder = 3, Category = "Kết nối", DefaultValue = "1" }; yield return new PropertyDescription("SyncIntervalMs", "Sync Interval (ms)", "Tần số đồng bộ dữ liệu (milliseconds)") { DataType = "number", IsReadOnly = true, DisplayOrder = 4, Category = "Cấu hình", DefaultValue = "100" }; yield return new PropertyDescription("ConnectTimeoutMs", "Connect Timeout (ms)", "Thời gian timeout khi kết nối (milliseconds)") { DataType = "number", IsReadOnly = true, DisplayOrder = 5, Category = "Cấu hình", DefaultValue = "5000" }; yield return new PropertyDescription("ConnectionStatus", "Connection Status", "Trạng thái kết nối") { DataType = "string", IsReadOnly = true, DisplayOrder = 6, Category = "Trạng thái", DefaultValue = "Disconnected" }; yield return new PropertyDescription("LastSyncTime", "Last Sync Time", "Thời gian đồng bộ cuối cùng") { DataType = "string", IsReadOnly = true, DisplayOrder = 7, Category = "Trạng thái", DefaultValue = "" }; } protected override async Task OnInitializeAsync(CancellationToken cancellationToken) { // Tạo ModbusTCP connection với connect timeout _connection = new ModbusTcpConnection(_ipAddress, _port, _slaveId, _connectTimeoutMs); } protected override async Task OnConnectAsync(CancellationToken cancellationToken) { if (_connection == null) throw new InvalidOperationException("Connection not initialized. Call InitializeAsync first."); // Kết nối await _connection.ConnectAsync(); // Khởi động vòng lặp đồng bộ StartSyncLoop(); // Cập nhật properties SetProperty("ConnectionStatus", "Connected"); SetProperty("LastSyncTime", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); } protected override async Task OnDisconnectAsync(CancellationToken cancellationToken) { // Dừng vòng lặp đồng bộ StopSyncLoop(); // Ngắt kết nối if (_connection != null) { await _connection.DisconnectAsync(); } // Cập nhật properties SetProperty("ConnectionStatus", "Disconnected"); } protected override async Task OnResetAsync(CancellationToken cancellationToken) { // Xóa cache và pending writes lock (_cacheLock) { _holdingRegisterCache.Clear(); _inputRegisterCache.Clear(); _coilCache.Clear(); _discreteInputCache.Clear(); _pendingHoldingRegisterWrites.Clear(); _pendingCoilWrites.Clear(); _pendingHoldingRegisterMultiWrites.Clear(); _pendingCoilMultiWrites.Clear(); } // Cập nhật properties SetProperty("LastSyncTime", ""); } protected override async Task OnCheckConnectionAsync(CancellationToken cancellationToken) { if (_connection == null) return false; try { // Kiểm tra IsConnected property trước if (!_connection.IsConnected) return false; // Thử đọc một register nhỏ để verify connection thực sự hoạt động // Nếu có ít nhất một range được khai báo, thử đọc 1 register từ range đầu tiên if (_holdingRegisterRanges.Count > 0) { var firstRange = _holdingRegisterRanges[0]; var testAddress = firstRange.StartAddress; var testQuantity = firstRange.Quantity > 0 ? (ushort)1 : (ushort)0; await _connection.ReadHoldingRegistersAsync(testAddress, testQuantity); return true; } else if (_inputRegisterRanges.Count > 0) { var firstRange = _inputRegisterRanges[0]; var testAddress = firstRange.StartAddress; var testQuantity = firstRange.Quantity > 0 ? (ushort)1 : (ushort)0; await _connection.ReadInputRegistersAsync(testAddress, testQuantity); return true; } else if (_coilRanges.Count > 0) { var firstRange = _coilRanges[0]; var testAddress = firstRange.StartAddress; var testQuantity = firstRange.Quantity > 0 ? (ushort)1 : (ushort)0; await _connection.ReadCoilsAsync(testAddress, testQuantity); return true; } else if (_discreteInputRanges.Count > 0) { var firstRange = _discreteInputRanges[0]; var testAddress = firstRange.StartAddress; var testQuantity = firstRange.Quantity > 0 ? (ushort)1 : (ushort)0; await _connection.ReadDiscreteInputsAsync(testAddress, testQuantity); return true; } // Nếu không có range nào được khai báo, chỉ check IsConnected return _connection.IsConnected; } catch { return false; } } private void StartSyncLoop() { lock (_cacheLock) { if (_syncLoopThread != null && _syncLoopThread.IsAlive) return; // Đã chạy rồi _syncLoopCts = new CancellationTokenSource(); _syncLoopThread = new Thread(() => SyncLoop(_syncLoopCts.Token)) { Priority = ThreadPriority.Highest, IsBackground = false, Name = $"ModbusTcpSync-{DeviceId}" }; _syncLoopThread.Start(); } } private void StopSyncLoop() { Thread? syncThread; lock (_cacheLock) { _syncLoopCts?.Cancel(); syncThread = _syncLoopThread; } // Wait for thread to finish (with timeout) if (syncThread != null) { if (!syncThread.Join(TimeSpan.FromSeconds(2))) { // Thread didn't finish in time, but continue cleanup } } lock (_cacheLock) { _syncLoopCts?.Dispose(); _syncLoopCts = null; _syncLoopThread = null; } } private void SyncLoop(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { if (_connection == null || !_connection.IsConnected) { Thread.Sleep(_syncIntervalMs); continue; } // Xử lý pending writes trước ProcessPendingWritesSync(); // Đọc tất cả các vùng đã khai báo ReadAllRangesSync(); // Cập nhật LastSyncTime SetProperty("LastSyncTime", DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); // Đợi đến lần đồng bộ tiếp theo Thread.Sleep(_syncIntervalMs); } catch (Exception ex) { LastError = ex; OnErrorOccurred(ex, "Sync loop error"); // Đợi một chút trước khi retry Thread.Sleep(_syncIntervalMs); } } } private void ProcessPendingWritesSync() { if (_connection == null || !_connection.IsConnected) return; // Lấy danh sách pending writes ra ngoài lock List<(ushort Address, ushort Value)> singleHoldingWrites; List<(ushort StartAddress, ushort[] Values)> multiHoldingWrites; List<(ushort Address, bool Value)> singleCoilWrites; List<(ushort StartAddress, bool[] Values)> multiCoilWrites; lock (_cacheLock) { singleHoldingWrites = [.. _pendingHoldingRegisterWrites.Select(kvp => (kvp.Key, kvp.Value))]; _pendingHoldingRegisterWrites.Clear(); multiHoldingWrites = [.. _pendingHoldingRegisterMultiWrites]; _pendingHoldingRegisterMultiWrites.Clear(); singleCoilWrites = [.. _pendingCoilWrites.Select(kvp => (kvp.Key, kvp.Value))]; _pendingCoilWrites.Clear(); multiCoilWrites = [.. _pendingCoilMultiWrites]; _pendingCoilMultiWrites.Clear(); } bool disconnectDetected = false; // Xử lý single holding register writes foreach (var (address, value) in singleHoldingWrites) { if (disconnectDetected || _connection == null || !_connection.IsConnected) { lock (_cacheLock) { _pendingHoldingRegisterWrites[address] = value; } continue; } try { _connection.WriteSingleRegisterAsync(address, value).Wait(); lock (_cacheLock) { _holdingRegisterCache[address] = value; // Cập nhật cache ngay } } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"WriteSingleRegisterAsync({address})"); } // Giữ lại trong pending để retry lock (_cacheLock) { _pendingHoldingRegisterWrites[address] = value; } } } // Xử lý multiple holding register writes foreach (var (startAddress, values) in multiHoldingWrites) { if (disconnectDetected || _connection == null || !_connection.IsConnected) { lock (_cacheLock) { _pendingHoldingRegisterMultiWrites.Add((startAddress, values)); } continue; } try { _connection.WriteMultipleRegistersAsync(startAddress, values).Wait(); lock (_cacheLock) { // Cập nhật cache for (ushort i = 0; i < values.Length; i++) { _holdingRegisterCache[(ushort)(startAddress + i)] = values[i]; } } } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"WriteMultipleRegistersAsync({startAddress}, {values.Length})"); } // Giữ lại trong pending để retry lock (_cacheLock) { _pendingHoldingRegisterMultiWrites.Add((startAddress, values)); } } } // Xử lý single coil writes foreach (var (address, value) in singleCoilWrites) { if (disconnectDetected || _connection == null || !_connection.IsConnected) { lock (_cacheLock) { _pendingCoilWrites[address] = value; } continue; } try { _connection.WriteSingleCoilAsync(address, value).Wait(); lock (_cacheLock) { _coilCache[address] = value; // Cập nhật cache ngay } } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"WriteSingleCoilAsync({address})"); } // Giữ lại trong pending để retry lock (_cacheLock) { _pendingCoilWrites[address] = value; } } } // Xử lý multiple coil writes foreach (var (startAddress, values) in multiCoilWrites) { if (disconnectDetected || _connection == null || !_connection.IsConnected) { lock (_cacheLock) { _pendingCoilMultiWrites.Add((startAddress, values)); } continue; } try { _connection.WriteMultipleCoilsAsync(startAddress, values).Wait(); lock (_cacheLock) { // Cập nhật cache for (ushort i = 0; i < values.Length; i++) { _coilCache[(ushort)(startAddress + i)] = values[i]; } } } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"WriteMultipleCoilsAsync({startAddress}, {values.Length})"); } // Giữ lại trong pending để retry lock (_cacheLock) { _pendingCoilMultiWrites.Add((startAddress, values)); } } } } private void ReadAllRangesSync() { if (_connection == null || !_connection.IsConnected) return; bool disconnectDetected = false; // Đọc Holding Registers foreach (var range in _holdingRegisterRanges) { if (disconnectDetected || _connection == null || !_connection.IsConnected) break; try { bool isChanged = false; var values = _connection.ReadHoldingRegistersAsync(range.StartAddress, range.Quantity).Result; lock (_cacheLock) { for (ushort i = 0; i < values.Length; i++) { var isCached = _holdingRegisterCache.TryGetValue((ushort)(range.StartAddress + i), out ushort value); if (!isCached || (!isChanged && value != values[i])) isChanged = true; _holdingRegisterCache[(ushort)(range.StartAddress + i)] = values[i]; } } if (isChanged) DataRegisterChanged?.Invoke(ModbusRegisterType.Holding); } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"ReadHoldingRegistersAsync({range.StartAddress}, {range.Quantity})"); break; // Dừng đọc các range khác khi disconnect } // Ignore other errors for individual ranges, continue with others } } // Đọc Input Registers foreach (var range in _inputRegisterRanges) { if (disconnectDetected || _connection == null || !_connection.IsConnected) break; try { bool isChanged = false; var values = _connection.ReadInputRegistersAsync(range.StartAddress, range.Quantity).Result; lock (_cacheLock) { for (ushort i = 0; i < values.Length; i++) { var isCached = _inputRegisterCache.TryGetValue((ushort)(range.StartAddress + i), out ushort value); if (!isCached || (!isChanged && value != values[i])) isChanged = true; _inputRegisterCache[(ushort)(range.StartAddress + i)] = values[i]; } } if (isChanged) DataRegisterChanged?.Invoke(ModbusRegisterType.Input); } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"ReadInputRegistersAsync({range.StartAddress}, {range.Quantity})"); break; // Dừng đọc các range khác khi disconnect } // Ignore other errors for individual ranges, continue with others } } // Đọc Coils foreach (var range in _coilRanges) { if (disconnectDetected || _connection == null || !_connection.IsConnected) break; try { bool isChanged = false; var values = _connection.ReadCoilsAsync(range.StartAddress, range.Quantity).Result; lock (_cacheLock) { for (ushort i = 0; i < values.Length; i++) { var isCached = _coilCache.TryGetValue((ushort)(range.StartAddress + i), out bool value); if (!isCached || (!isChanged && value != values[i])) isChanged = true; _coilCache[(ushort)(range.StartAddress + i)] = values[i]; } } if (isChanged) DataRegisterChanged?.Invoke(ModbusRegisterType.Coil); } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"ReadCoilsAsync({range.StartAddress}, {range.Quantity})"); break; // Dừng đọc các range khác khi disconnect } // Ignore other errors for individual ranges, continue with others } } // Đọc Discrete Inputs foreach (var range in _discreteInputRanges) { if (disconnectDetected || _connection == null || !_connection.IsConnected) break; try { bool isChanged = false; var values = _connection.ReadDiscreteInputsAsync(range.StartAddress, range.Quantity).Result; lock (_cacheLock) { for (ushort i = 0; i < values.Length; i++) { var isCached = _discreteInputCache.TryGetValue((ushort)(range.StartAddress + i), out bool value); if (!isCached || (!isChanged && value != values[i])) isChanged = true; _discreteInputCache[(ushort)(range.StartAddress + i)] = values[i]; } } if (isChanged) DataRegisterChanged?.Invoke(ModbusRegisterType.DiscreteInput); } catch (Exception ex) { if (IsDisconnectException(ex)) { disconnectDetected = true; HandleDisconnect($"ReadDiscreteInputsAsync({range.StartAddress}, {range.Quantity})"); break; // Dừng đọc các range khác khi disconnect } // Ignore other errors for individual ranges, continue with others } } } private static bool IsAddressInRange(ushort address, List ranges) { return ranges.Any(range => address >= range.StartAddress && address < range.StartAddress + range.Quantity); } private static bool IsAddressRangeInRanges(ushort startAddress, ushort quantity, List ranges) { var endAddress = (ushort)(startAddress + quantity - 1); return ranges.Any(range => startAddress >= range.StartAddress && endAddress < range.StartAddress + range.Quantity); } /// /// Kiểm tra xem exception có phải là lỗi disconnect không /// private static bool IsDisconnectException(Exception ex) { return ex is SocketException || ex is IOException || ex is InvalidOperationException || ex is ObjectDisposedException || (ex.Message.Contains("not connected", StringComparison.OrdinalIgnoreCase)) || (ex.Message.Contains("connection", StringComparison.OrdinalIgnoreCase) && ex.Message.Contains("closed", StringComparison.OrdinalIgnoreCase)); } /// /// Xử lý khi phát hiện disconnect /// private void HandleDisconnect(string context) { if (Status == DeviceStatus.Connected) { var error = new InvalidOperationException($"Connection lost during {context}"); LastError = error; OnErrorOccurred(error, $"Connection lost: {context}"); SetProperty("ConnectionStatus", "Disconnected"); // Disconnect connection ngay lập tức để đảm bảo IsConnected = false // Sử dụng Thread với priority cao để tránh deadlock var disconnectThread = new Thread(() => { try { if (_connection != null) { _connection.DisconnectAsync().Wait(); } } catch { // Ignore errors khi disconnect } }) { Priority = ThreadPriority.Highest, IsBackground = false, Name = $"ModbusTcpDisconnect-{DeviceId}" }; disconnectThread.Start(); // Trigger CheckConnectionAsync để DeviceBase tự động xử lý state machine // Sử dụng Thread với priority cao để không block var checkThread = new Thread(() => { try { // Đợi một chút để đảm bảo disconnect đã hoàn tất Thread.Sleep(200); CheckConnectionAsync().Wait(); } catch { // Ignore errors from CheckConnectionAsync } }) { Priority = ThreadPriority.Highest, IsBackground = false, Name = $"ModbusTcpCheck-{DeviceId}" }; checkThread.Start(); } } #region IModbusTcpDevice Implementation public string IpAddress => _ipAddress; public int Port => _port; public byte SlaveId => _slaveId; bool IModbusTcpDevice.IsConnected => IsConnected; // Holding Registers public ushort ReadHoldingRegister(ushort address) { if (!IsAddressInRange(address, _holdingRegisterRanges)) { throw new ArgumentException($"Holding register address {address} is not in any declared range"); } lock (_cacheLock) { if (_holdingRegisterCache.TryGetValue(address, out var value)) { return value; } } throw new InvalidOperationException($"Holding register address {address} has not been read yet"); } public ushort[] ReadHoldingRegisters(ushort startAddress, ushort quantity) { if (!IsAddressRangeInRanges(startAddress, quantity, _holdingRegisterRanges)) { throw new ArgumentException($"Holding register range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { var result = new ushort[quantity]; for (ushort i = 0; i < quantity; i++) { var address = (ushort)(startAddress + i); if (_holdingRegisterCache.TryGetValue(address, out var value)) { result[i] = value; } else { throw new InvalidOperationException($"Holding register address {address} has not been read yet"); } } return result; } } public async Task WriteHoldingRegisterAsync(ushort address, ushort value, CancellationToken cancellationToken = default) { if (!IsAddressInRange(address, _holdingRegisterRanges)) { throw new ArgumentException($"Holding register address {address} is not in any declared range"); } lock (_cacheLock) { _pendingHoldingRegisterWrites[address] = value; } // Không cần await, vòng lặp sẽ xử lý await Task.CompletedTask; } public async Task WriteHoldingRegistersAsync(ushort startAddress, ushort[] values, CancellationToken cancellationToken = default) { if (values == null || values.Length == 0) throw new ArgumentException("Values array cannot be null or empty", nameof(values)); var quantity = (ushort)values.Length; if (!IsAddressRangeInRanges(startAddress, quantity, _holdingRegisterRanges)) { throw new ArgumentException($"Holding register range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { _pendingHoldingRegisterMultiWrites.Add((startAddress, values)); } // Không cần await, vòng lặp sẽ xử lý await Task.CompletedTask; } // Input Registers public ushort ReadInputRegister(ushort address) { if (!IsAddressInRange(address, _inputRegisterRanges)) { throw new ArgumentException($"Input register address {address} is not in any declared range"); } lock (_cacheLock) { if (_inputRegisterCache.TryGetValue(address, out var value)) { return value; } } throw new InvalidOperationException($"Input register address {address} has not been read yet"); } public ushort[] ReadInputRegisters(ushort startAddress, ushort quantity) { if (!IsAddressRangeInRanges(startAddress, quantity, _inputRegisterRanges)) { throw new ArgumentException($"Input register range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { var result = new ushort[quantity]; for (ushort i = 0; i < quantity; i++) { var address = (ushort)(startAddress + i); if (_inputRegisterCache.TryGetValue(address, out var value)) { result[i] = value; } else { throw new InvalidOperationException($"Input register address {address} has not been read yet"); } } return result; } } // Coils public bool ReadCoil(ushort address) { if (!IsAddressInRange(address, _coilRanges)) { throw new ArgumentException($"Coil address {address} is not in any declared range"); } lock (_cacheLock) { if (_coilCache.TryGetValue(address, out var value)) { return value; } } throw new InvalidOperationException($"Coil address {address} has not been read yet"); } public bool[] ReadCoils(ushort startAddress, ushort quantity) { if (!IsAddressRangeInRanges(startAddress, quantity, _coilRanges)) { throw new ArgumentException($"Coil range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { var result = new bool[quantity]; for (ushort i = 0; i < quantity; i++) { var address = (ushort)(startAddress + i); if (_coilCache.TryGetValue(address, out var value)) { result[i] = value; } else { throw new InvalidOperationException($"Coil address {address} has not been read yet"); } } return result; } } public async Task WriteCoilAsync(ushort address, bool value, CancellationToken cancellationToken = default) { if (!IsAddressInRange(address, _coilRanges)) { throw new ArgumentException($"Coil address {address} is not in any declared range"); } lock (_cacheLock) { _pendingCoilWrites[address] = value; } // Không cần await, vòng lặp sẽ xử lý await Task.CompletedTask; } public async Task WriteCoilsAsync(ushort startAddress, bool[] values, CancellationToken cancellationToken = default) { if (values == null || values.Length == 0) throw new ArgumentException("Values array cannot be null or empty", nameof(values)); var quantity = (ushort)values.Length; if (!IsAddressRangeInRanges(startAddress, quantity, _coilRanges)) { throw new ArgumentException($"Coil range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { _pendingCoilMultiWrites.Add((startAddress, values)); } // Không cần await, vòng lặp sẽ xử lý await Task.CompletedTask; } /// Ghi một coil xuống PLC ngay (không qua queue) — dùng cho pulse M815. public async Task WriteCoilImmediateAsync(ushort address, bool value, CancellationToken cancellationToken = default) { if (!IsAddressInRange(address, _coilRanges)) throw new ArgumentException($"Coil address {address} is not in any declared range"); if (_connection == null || !_connection.IsConnected) throw new InvalidOperationException("Modbus connection is not available"); await _connection.WriteSingleCoilAsync(address, value); lock (_cacheLock) { _coilCache[address] = value; } } // Discrete Inputs public bool ReadDiscreteInput(ushort address) { if (!IsAddressInRange(address, _discreteInputRanges)) { throw new ArgumentException($"Discrete input address {address} is not in any declared range"); } lock (_cacheLock) { if (_discreteInputCache.TryGetValue(address, out var value)) { return value; } } throw new InvalidOperationException($"Discrete input address {address} has not been read yet"); } public bool[] ReadDiscreteInputs(ushort startAddress, ushort quantity) { if (!IsAddressRangeInRanges(startAddress, quantity, _discreteInputRanges)) { throw new ArgumentException($"Discrete input range [{startAddress}, {startAddress + quantity - 1}] is not in any declared range"); } lock (_cacheLock) { var result = new bool[quantity]; for (ushort i = 0; i < quantity; i++) { var address = (ushort)(startAddress + i); if (_discreteInputCache.TryGetValue(address, out var value)) { result[i] = value; } else { throw new InvalidOperationException($"Discrete input address {address} has not been read yet"); } } return result; } } #endregion // IModbusTcpDevice Configuration Properties public IReadOnlyList HoldingRegisterRanges => _holdingRegisterRanges; public IReadOnlyList InputRegisterRanges => _inputRegisterRanges; public IReadOnlyList CoilRanges => _coilRanges; public IReadOnlyList DiscreteInputRanges => _discreteInputRanges; protected override void Dispose(bool disposing) { if (disposing) { // Dừng vòng lặp đồng bộ StopSyncLoop(); // Dispose connection _connection?.Dispose(); _connection = null; } base.Dispose(disposing); } }