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);
}
}