Files
petwash/PetWashControl/Services/ModbusService.cs
2026-02-28 10:24:18 +08:00

641 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}