641 lines
20 KiB
C#
641 lines
20 KiB
C#
using NModbus;
|
||
using System.Net.Sockets;
|
||
|
||
namespace PetWashControl.Services;
|
||
|
||
/// <summary>
|
||
/// 浮点数字节序枚举
|
||
/// </summary>
|
||
public enum FloatByteOrder
|
||
{
|
||
/// <summary>
|
||
/// 标准Modbus大端序: 寄存器0=高16位(AB),寄存器1=低16位(CD)
|
||
/// </summary>
|
||
ABCD,
|
||
|
||
/// <summary>
|
||
/// 字交换: 寄存器0=低16位(CD),寄存器1=高16位(AB)
|
||
/// </summary>
|
||
CDAB,
|
||
|
||
/// <summary>
|
||
/// 字节交换: 寄存器0=BA,寄存器1=DC
|
||
/// </summary>
|
||
BADC,
|
||
|
||
/// <summary>
|
||
/// 完全反转: 寄存器0=DC,寄存器1=BA
|
||
/// </summary>
|
||
DCBA
|
||
}
|
||
|
||
/// <summary>
|
||
/// Modbus TCP 服务,用于与设备进行通信
|
||
/// </summary>
|
||
public class ModbusService : IDisposable
|
||
{
|
||
private readonly ConfigurationService _config;
|
||
private readonly LogService _logger;
|
||
private TcpClient? _tcpClient;
|
||
private IModbusMaster? _modbusMaster;
|
||
private bool _isConnected;
|
||
private readonly object _lockObject = new();
|
||
private System.Timers.Timer? _heartbeatTimer;
|
||
|
||
public bool IsConnected
|
||
{
|
||
get
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
return _isConnected && _tcpClient?.Connected == true;
|
||
}
|
||
}
|
||
}
|
||
|
||
public event Action<bool>? ConnectionStatusChanged;
|
||
|
||
public ModbusService(ConfigurationService config, LogService logger)
|
||
{
|
||
_config = config;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 连接到 Modbus TCP 设备
|
||
/// </summary>
|
||
public async Task<bool> ConnectAsync()
|
||
{
|
||
try
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
if (_isConnected && _tcpClient?.Connected == true)
|
||
{
|
||
_logger.LogInfo("Modbus TCP 已经连接");
|
||
return true;
|
||
}
|
||
}
|
||
|
||
_logger.LogInfo($"正在连接 Modbus TCP 设备: {_config.ModbusIpAddress}:{_config.ModbusPort}");
|
||
|
||
// 创建 TCP 客户端
|
||
var tcpClient = new TcpClient
|
||
{
|
||
SendTimeout = _config.ModbusReadTimeoutMs,
|
||
ReceiveTimeout = _config.ModbusReadTimeoutMs
|
||
};
|
||
|
||
// 异步连接,带超时控制
|
||
var connectTask = tcpClient.ConnectAsync(_config.ModbusIpAddress, _config.ModbusPort);
|
||
var timeoutTask = Task.Delay(_config.ModbusConnectTimeoutMs);
|
||
|
||
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
|
||
|
||
if (completedTask == timeoutTask)
|
||
{
|
||
tcpClient.Close();
|
||
throw new TimeoutException($"连接超时 ({_config.ModbusConnectTimeoutMs}ms)");
|
||
}
|
||
|
||
if (!tcpClient.Connected)
|
||
{
|
||
throw new Exception("TCP 连接失败");
|
||
}
|
||
|
||
// 创建 Modbus Master
|
||
var factory = new ModbusFactory();
|
||
var modbusMaster = factory.CreateMaster(tcpClient);
|
||
|
||
// 测试连接 - 读取保持寄存器地址 0
|
||
try
|
||
{
|
||
await Task.Run(() => modbusMaster.ReadHoldingRegisters(_config.ModbusSlaveId, 0, 1));
|
||
_logger.LogInfo("Modbus 设备响应正常");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogWarning($"Modbus 设备测试读取失败: {ex.Message},但连接已建立");
|
||
}
|
||
|
||
lock (_lockObject)
|
||
{
|
||
_tcpClient = tcpClient;
|
||
_modbusMaster = modbusMaster;
|
||
_isConnected = true;
|
||
}
|
||
|
||
_logger.LogInfo($"Modbus TCP 连接成功: {_config.ModbusIpAddress}:{_config.ModbusPort}");
|
||
|
||
// 启动心跳检测
|
||
StartHeartbeat();
|
||
|
||
ConnectionStatusChanged?.Invoke(true);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"Modbus TCP 连接失败", ex);
|
||
|
||
lock (_lockObject)
|
||
{
|
||
_isConnected = false;
|
||
}
|
||
|
||
ConnectionStatusChanged?.Invoke(false);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开连接
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
try
|
||
{
|
||
StopHeartbeat();
|
||
|
||
lock (_lockObject)
|
||
{
|
||
if (_tcpClient != null)
|
||
{
|
||
_tcpClient.Close();
|
||
_tcpClient.Dispose();
|
||
_tcpClient = null;
|
||
}
|
||
|
||
_modbusMaster?.Dispose();
|
||
_modbusMaster = null;
|
||
_isConnected = false;
|
||
}
|
||
|
||
_logger.LogInfo("Modbus TCP 已断开连接");
|
||
ConnectionStatusChanged?.Invoke(false);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError("断开 Modbus TCP 连接时出错", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取保持寄存器
|
||
/// </summary>
|
||
public async Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort numberOfPoints)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
var result = await Task.Run(() =>
|
||
_modbusMaster!.ReadHoldingRegisters(_config.ModbusSlaveId, startAddress, numberOfPoints));
|
||
|
||
_logger.LogInfo($"读取保持寄存器成功: 地址={startAddress}, 数量={numberOfPoints}");
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"读取保持寄存器失败: 地址={startAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取输入寄存器
|
||
/// </summary>
|
||
public async Task<ushort[]> ReadInputRegistersAsync(ushort startAddress, ushort numberOfPoints)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
var result = await Task.Run(() =>
|
||
_modbusMaster!.ReadInputRegisters(_config.ModbusSlaveId, startAddress, numberOfPoints));
|
||
|
||
_logger.LogInfo($"读取输入寄存器成功: 地址={startAddress}, 数量={numberOfPoints}");
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"读取输入寄存器失败: 地址={startAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入单个保持寄存器
|
||
/// </summary>
|
||
public async Task WriteSingleRegisterAsync(ushort registerAddress, ushort value)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
await Task.Run(() =>
|
||
_modbusMaster!.WriteSingleRegister(_config.ModbusSlaveId, registerAddress, value));
|
||
|
||
_logger.LogInfo($"写入单个寄存器成功: 地址={registerAddress}, 值={value}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"写入单个寄存器失败: 地址={registerAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入多个保持寄存器
|
||
/// </summary>
|
||
public async Task WriteMultipleRegistersAsync(ushort startAddress, ushort[] data)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
await Task.Run(() =>
|
||
_modbusMaster!.WriteMultipleRegisters(_config.ModbusSlaveId, startAddress, data));
|
||
|
||
_logger.LogInfo($"写入多个寄存器成功: 地址={startAddress}, 数量={data.Length}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"写入多个寄存器失败: 地址={startAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取线圈状态
|
||
/// </summary>
|
||
public async Task<bool[]> ReadCoilsAsync(ushort startAddress, ushort numberOfPoints)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
var result = await Task.Run(() =>
|
||
_modbusMaster!.ReadCoils(_config.ModbusSlaveId, startAddress, numberOfPoints));
|
||
|
||
_logger.LogInfo($"读取线圈成功: 地址={startAddress}, 数量={numberOfPoints}");
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"读取线圈失败: 地址={startAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入单个线圈
|
||
/// </summary>
|
||
public async Task WriteSingleCoilAsync(ushort coilAddress, bool value)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
await Task.Run(() =>
|
||
_modbusMaster!.WriteSingleCoil(_config.ModbusSlaveId, coilAddress, value));
|
||
|
||
_logger.LogInfo($"写入单个线圈成功: 地址={coilAddress}, 值={value}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"写入单个线圈失败: 地址={coilAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入多个线圈
|
||
/// </summary>
|
||
public async Task WriteMultipleCoilsAsync(ushort startAddress, bool[] data)
|
||
{
|
||
EnsureConnected();
|
||
|
||
try
|
||
{
|
||
await Task.Run(() =>
|
||
_modbusMaster!.WriteMultipleCoils(_config.ModbusSlaveId, startAddress, data));
|
||
|
||
_logger.LogInfo($"写入多个线圈成功: 地址={startAddress}, 数量={data.Length}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"写入多个线圈失败: 地址={startAddress}", ex);
|
||
await HandleConnectionError();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动心跳检测
|
||
/// </summary>
|
||
private void StartHeartbeat()
|
||
{
|
||
StopHeartbeat();
|
||
|
||
_heartbeatTimer = new System.Timers.Timer(10000); // 每10秒检测一次
|
||
_heartbeatTimer.Elapsed += async (s, e) =>
|
||
{
|
||
try
|
||
{
|
||
if (!IsConnected)
|
||
{
|
||
_logger.LogWarning("心跳检测: 连接已断开,尝试重连...");
|
||
await ConnectAsync();
|
||
}
|
||
else
|
||
{
|
||
// 尝试读取一个寄存器来验证连接
|
||
await ReadHoldingRegistersAsync(0, 1);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError("心跳检测失败", ex);
|
||
await HandleConnectionError();
|
||
}
|
||
};
|
||
_heartbeatTimer.Start();
|
||
_logger.LogInfo("Modbus 心跳检测已启动");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止心跳检测
|
||
/// </summary>
|
||
private void StopHeartbeat()
|
||
{
|
||
if (_heartbeatTimer != null)
|
||
{
|
||
_heartbeatTimer.Stop();
|
||
_heartbeatTimer.Dispose();
|
||
_heartbeatTimer = null;
|
||
_logger.LogInfo("Modbus 心跳检测已停止");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理连接错误
|
||
/// </summary>
|
||
private async Task HandleConnectionError()
|
||
{
|
||
lock (_lockObject)
|
||
{
|
||
if (_isConnected)
|
||
{
|
||
_isConnected = false;
|
||
ConnectionStatusChanged?.Invoke(false);
|
||
}
|
||
}
|
||
|
||
_logger.LogWarning("检测到连接错误,尝试重新连接...");
|
||
|
||
// 尝试重新连接
|
||
await Task.Delay(2000);
|
||
await ConnectAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保已连接
|
||
/// </summary>
|
||
private void EnsureConnected()
|
||
{
|
||
if (!IsConnected)
|
||
{
|
||
throw new InvalidOperationException("Modbus TCP 未连接,请先调用 ConnectAsync()");
|
||
}
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
Disconnect();
|
||
}
|
||
|
||
#region 浮点数转换方法
|
||
|
||
/// <summary>
|
||
/// 将 Modbus 寄存器转换为浮点数
|
||
/// </summary>
|
||
/// <param name="registers">寄存器数组</param>
|
||
/// <param name="startIndex">起始索引</param>
|
||
/// <param name="byteOrder">字节序</param>
|
||
/// <returns>浮点数值</returns>
|
||
public float ConvertRegistersToFloat(ushort[] registers, int startIndex = 0,
|
||
FloatByteOrder byteOrder = FloatByteOrder.CDAB)
|
||
{
|
||
// 参数验证
|
||
if (registers == null || registers.Length < startIndex + 2)
|
||
{
|
||
throw new ArgumentException($"寄存器数组长度不足,需要至少 {startIndex + 2} 个元素");
|
||
}
|
||
|
||
// 读取两个寄存器
|
||
ushort reg0 = registers[startIndex];
|
||
ushort reg1 = registers[startIndex + 1];
|
||
|
||
// 创建字节数组(4字节 = 32位浮点数)
|
||
byte[] floatBytes = new byte[4];
|
||
|
||
// 根据字节序填充字节数组
|
||
switch (byteOrder)
|
||
{
|
||
case FloatByteOrder.ABCD:
|
||
// 标准Modbus大端序: Reg0=AB, Reg1=CD
|
||
floatBytes[0] = (byte)(reg0 >> 8); // A (高字节)
|
||
floatBytes[1] = (byte)(reg0 & 0xFF); // B (低字节)
|
||
floatBytes[2] = (byte)(reg1 >> 8); // C (高字节)
|
||
floatBytes[3] = (byte)(reg1 & 0xFF); // D (低字节)
|
||
break;
|
||
|
||
case FloatByteOrder.CDAB:
|
||
// 字交换: Reg0=CD, Reg1=AB
|
||
floatBytes[0] = (byte)(reg1 >> 8); // A
|
||
floatBytes[1] = (byte)(reg1 & 0xFF); // B
|
||
floatBytes[2] = (byte)(reg0 >> 8); // C
|
||
floatBytes[3] = (byte)(reg0 & 0xFF); // D
|
||
break;
|
||
|
||
case FloatByteOrder.BADC:
|
||
// 字节交换: Reg0=BA, Reg1=DC
|
||
floatBytes[0] = (byte)(reg0 & 0xFF); // B
|
||
floatBytes[1] = (byte)(reg0 >> 8); // A
|
||
floatBytes[2] = (byte)(reg1 & 0xFF); // D
|
||
floatBytes[3] = (byte)(reg1 >> 8); // C
|
||
break;
|
||
|
||
case FloatByteOrder.DCBA:
|
||
// 完全反转: Reg0=DC, Reg1=BA
|
||
floatBytes[0] = (byte)(reg1 & 0xFF); // D
|
||
floatBytes[1] = (byte)(reg1 >> 8); // C
|
||
floatBytes[2] = (byte)(reg0 & 0xFF); // B
|
||
floatBytes[3] = (byte)(reg0 >> 8); // A
|
||
break;
|
||
|
||
default:
|
||
throw new InvalidOperationException($"未知的字节顺序: {byteOrder}");
|
||
}
|
||
|
||
// 转换为浮点数
|
||
float result = BitConverter.ToSingle(floatBytes, 0);
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将浮点数转换为 Modbus 寄存器
|
||
/// </summary>
|
||
/// <param name="value">浮点数值</param>
|
||
/// <param name="byteOrder">字节序</param>
|
||
/// <returns>寄存器数组(2个元素)</returns>
|
||
public ushort[] ConvertFloatToRegisters(float value, FloatByteOrder byteOrder = FloatByteOrder.CDAB)
|
||
{
|
||
// 将浮点数转换为字节数组(小端序)
|
||
byte[] bytes = BitConverter.GetBytes(value);
|
||
|
||
// 参数验证
|
||
if (bytes == null || bytes.Length != 4)
|
||
{
|
||
throw new InvalidOperationException($"浮点数字节长度异常: 期望 4,实际 {bytes?.Length ?? 0}");
|
||
}
|
||
|
||
// 分配寄存器数组
|
||
ushort[] registers = new ushort[2];
|
||
|
||
// BitConverter.GetBytes 在小端系统上产生: [Byte0(LSB), Byte1, Byte2, Byte3(MSB)]
|
||
// 即: [D, C, B, A],其中 A 是最高有效字节
|
||
// 需要根据目标字节序重新组装
|
||
|
||
switch (byteOrder)
|
||
{
|
||
case FloatByteOrder.ABCD:
|
||
// 标准Modbus大端序: Reg0=AB(高16位), Reg1=CD(低16位)
|
||
// 小端字节 [D,C,B,A] → Reg0=AB, Reg1=CD
|
||
registers[0] = (ushort)((bytes[3] << 8) | bytes[2]); // A,B
|
||
registers[1] = (ushort)((bytes[1] << 8) | bytes[0]); // C,D
|
||
break;
|
||
|
||
case FloatByteOrder.CDAB:
|
||
// 字交换: Reg0=CD(低16位), Reg1=AB(高16位)
|
||
// 小端字节 [D,C,B,A] → Reg0=CD, Reg1=AB
|
||
registers[0] = (ushort)((bytes[1] << 8) | bytes[0]); // C,D
|
||
registers[1] = (ushort)((bytes[3] << 8) | bytes[2]); // A,B
|
||
break;
|
||
|
||
case FloatByteOrder.BADC:
|
||
// 字节交换: Reg0=BA, Reg1=DC
|
||
// 小端字节 [D,C,B,A] → Reg0=BA, Reg1=DC
|
||
registers[0] = (ushort)((bytes[2] << 8) | bytes[3]); // B,A
|
||
registers[1] = (ushort)((bytes[0] << 8) | bytes[1]); // D,C
|
||
break;
|
||
|
||
case FloatByteOrder.DCBA:
|
||
// 完全反转: Reg0=DC, Reg1=BA
|
||
// 小端字节 [D,C,B,A] → Reg0=DC, Reg1=BA
|
||
registers[0] = (ushort)((bytes[0] << 8) | bytes[1]); // D,C
|
||
registers[1] = (ushort)((bytes[2] << 8) | bytes[3]); // B,A
|
||
break;
|
||
|
||
default:
|
||
throw new InvalidOperationException($"未知的字节顺序: {byteOrder}");
|
||
}
|
||
|
||
return registers;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 复归型按钮控制方法
|
||
|
||
/// <summary>
|
||
/// 触发关闭门并开始洗护按钮 M80(复归型)
|
||
/// M80 是复归型按钮,需要写入脉冲信号:true → 延迟 → false
|
||
/// </summary>
|
||
public async Task TriggerStartWashAsync()
|
||
{
|
||
try
|
||
{
|
||
if (_modbusMaster != null && _isConnected)
|
||
{
|
||
const ushort START_WASH_BUTTON_ADDRESS = 80; // M80 复归型按钮
|
||
_logger.LogInfo($"[Modbus] 触发关闭门并开始洗护按钮 M{START_WASH_BUTTON_ADDRESS}");
|
||
|
||
// 写入 true(触发)
|
||
await WriteSingleCoilAsync(START_WASH_BUTTON_ADDRESS, true);
|
||
_logger.LogInfo($"[Modbus] M{START_WASH_BUTTON_ADDRESS} = true");
|
||
|
||
// 延迟 100ms
|
||
await Task.Delay(100);
|
||
|
||
// 写入 false(复位)
|
||
await WriteSingleCoilAsync(START_WASH_BUTTON_ADDRESS, false);
|
||
_logger.LogInfo($"[Modbus] M{START_WASH_BUTTON_ADDRESS} = false");
|
||
|
||
// 延迟 100ms 确保 PLC 处理完成
|
||
await Task.Delay(100);
|
||
|
||
_logger.LogInfo($"[Modbus] M{START_WASH_BUTTON_ADDRESS} 脉冲信号发送完成");
|
||
}
|
||
else
|
||
{
|
||
throw new InvalidOperationException("Modbus 未连接");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"触发关闭门并开始洗护失败: {ex.Message}", ex);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发紧急停止按钮 M83(复归型)
|
||
/// M83 是复归型按钮,需要写入脉冲信号:true → 延迟 → false
|
||
/// </summary>
|
||
public async Task TriggerEmergencyStopAsync()
|
||
{
|
||
try
|
||
{
|
||
if (_modbusMaster != null && _isConnected)
|
||
{
|
||
const ushort EMERGENCY_STOP_BUTTON_ADDRESS = 83; // M83 复归型按钮
|
||
_logger.LogInfo($"[Modbus] 触发紧急停止按钮 M{EMERGENCY_STOP_BUTTON_ADDRESS}");
|
||
|
||
// 写入 true(触发)
|
||
await WriteSingleCoilAsync(EMERGENCY_STOP_BUTTON_ADDRESS, true);
|
||
_logger.LogInfo($"[Modbus] M{EMERGENCY_STOP_BUTTON_ADDRESS} = true");
|
||
|
||
// 延迟 100ms
|
||
await Task.Delay(100);
|
||
|
||
// 写入 false(复位)
|
||
await WriteSingleCoilAsync(EMERGENCY_STOP_BUTTON_ADDRESS, false);
|
||
_logger.LogInfo($"[Modbus] M{EMERGENCY_STOP_BUTTON_ADDRESS} = false");
|
||
|
||
// 延迟 100ms 确保 PLC 处理完成
|
||
await Task.Delay(100);
|
||
|
||
_logger.LogInfo($"[Modbus] M{EMERGENCY_STOP_BUTTON_ADDRESS} 脉冲信号发送完成");
|
||
}
|
||
else
|
||
{
|
||
throw new InvalidOperationException("Modbus 未连接");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"触发紧急停止失败: {ex.Message}", ex);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|