1165 lines
42 KiB
C#
1165 lines
42 KiB
C#
using RobotNet10.RobotApp.Client.Shared.Devices;
|
|
using RobotNet10.RobotApp.Devices;
|
|
using RobotNet10.ScriptEngine.IO;
|
|
using System.Net.Sockets;
|
|
|
|
namespace RobotNet10.RobotApp.Drivers.PhenikaaX;
|
|
|
|
/// <summary>
|
|
/// Cấu hình một vùng đọc Modbus từ configuration
|
|
/// </summary>
|
|
public class ModbusReadRangeConfig
|
|
{
|
|
/// <summary>
|
|
/// Địa chỉ bắt đầu của vùng đọc
|
|
/// </summary>
|
|
public ushort StartAddress { get; set; }
|
|
|
|
/// <summary>
|
|
/// Số lượng register/coil cần đọc
|
|
/// </summary>
|
|
public ushort Quantity { get; set; }
|
|
|
|
/// <summary>
|
|
/// Tên của group range này (để mô tả và hiển thị UI)
|
|
/// </summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
public List<string>? ChildrenNames { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Driver cho thiết bị ModbusTCP
|
|
/// Kế thừa DeviceBase và implement IModbusTcpDevice
|
|
/// </summary>
|
|
[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<ModbusRangeData> _holdingRegisterRanges;
|
|
private readonly List<ModbusRangeData> _inputRegisterRanges;
|
|
private readonly List<ModbusRangeData> _coilRanges;
|
|
private readonly List<ModbusRangeData> _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<ushort, ushort> _holdingRegisterCache = [];
|
|
private readonly Dictionary<ushort, ushort> _inputRegisterCache = [];
|
|
private readonly Dictionary<ushort, bool> _coilCache = [];
|
|
private readonly Dictionary<ushort, bool> _discreteInputCache = [];
|
|
|
|
// Queue cho các giá trị cần ghi (pending writes)
|
|
private readonly Dictionary<ushort, ushort> _pendingHoldingRegisterWrites = [];
|
|
private readonly Dictionary<ushort, bool> _pendingCoilWrites = [];
|
|
private readonly List<(ushort StartAddress, ushort[] Values)> _pendingHoldingRegisterMultiWrites = [];
|
|
private readonly List<(ushort StartAddress, bool[] Values)> _pendingCoilMultiWrites = [];
|
|
|
|
public event Action<ModbusRegisterType>? DataRegisterChanged;
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
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<bool?>("AutoReconnectEnabled");
|
|
var reconnectDelayMs = connection.GetValue<int?>("ReconnectDelayMs");
|
|
var maxReconnectAttempts = connection.GetValue<int?>("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<ModbusRangeData> ParseReadRanges(IConfigurationSection connection, string key)
|
|
{
|
|
var ranges = new List<ModbusRangeData>();
|
|
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<string> 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<PropertyDescription> 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<bool> 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<ModbusRangeData> ranges)
|
|
{
|
|
return ranges.Any(range => address >= range.StartAddress && address < range.StartAddress + range.Quantity);
|
|
}
|
|
|
|
private static bool IsAddressRangeInRanges(ushort startAddress, ushort quantity, List<ModbusRangeData> ranges)
|
|
{
|
|
var endAddress = (ushort)(startAddress + quantity - 1);
|
|
return ranges.Any(range =>
|
|
startAddress >= range.StartAddress &&
|
|
endAddress < range.StartAddress + range.Quantity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Kiểm tra xem exception có phải là lỗi disconnect không
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Xử lý khi phát hiện disconnect
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Ghi một coil xuống PLC ngay (không qua queue) — dùng cho pulse M815.</summary>
|
|
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<ModbusRangeData> HoldingRegisterRanges => _holdingRegisterRanges;
|
|
public IReadOnlyList<ModbusRangeData> InputRegisterRanges => _inputRegisterRanges;
|
|
public IReadOnlyList<ModbusRangeData> CoilRanges => _coilRanges;
|
|
public IReadOnlyList<ModbusRangeData> 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);
|
|
}
|
|
}
|
|
|