diff --git a/PetWashControl/PetWashControl.csproj b/PetWashControl/PetWashControl.csproj index 5d79734..fa4cbde 100644 --- a/PetWashControl/PetWashControl.csproj +++ b/PetWashControl/PetWashControl.csproj @@ -1,4 +1,4 @@ - + WinExe @@ -12,6 +12,7 @@ + diff --git a/PetWashControl/Services/ConfigurationService.cs b/PetWashControl/Services/ConfigurationService.cs index da4b5af..2a50773 100644 --- a/PetWashControl/Services/ConfigurationService.cs +++ b/PetWashControl/Services/ConfigurationService.cs @@ -7,6 +7,13 @@ public class ConfigurationService public int MqttBrokerPort { get; set; } = 1883; public string MqttClientId { get; set; } = "PetWashControl"; + // Modbus TCP 配置 + public string ModbusIpAddress { get; set; } = "192.168.1.10"; + public int ModbusPort { get; set; } = 502; + public byte ModbusSlaveId { get; set; } = 1; + public int ModbusConnectTimeoutMs { get; set; } = 5000; + public int ModbusReadTimeoutMs { get; set; } = 3000; + public int PaymentCheckIntervalSeconds { get; set; } = 2; public int WashSimulationSeconds { get; set; } = 10; diff --git a/PetWashControl/Services/ModbusService.cs b/PetWashControl/Services/ModbusService.cs new file mode 100644 index 0000000..85a4a78 --- /dev/null +++ b/PetWashControl/Services/ModbusService.cs @@ -0,0 +1,396 @@ +using NModbus; +using System.Net.Sockets; + +namespace PetWashControl.Services; + +/// +/// Modbus TCP 服务,用于与设备进行通信 +/// +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? ConnectionStatusChanged; + + public ModbusService(ConfigurationService config, LogService logger) + { + _config = config; + _logger = logger; + } + + /// + /// 连接到 Modbus TCP 设备 + /// + public async Task 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; + } + } + + /// + /// 断开连接 + /// + 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); + } + } + + /// + /// 读取保持寄存器 + /// + public async Task 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; + } + } + + /// + /// 读取输入寄存器 + /// + public async Task 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; + } + } + + /// + /// 写入单个保持寄存器 + /// + 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; + } + } + + /// + /// 写入多个保持寄存器 + /// + 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; + } + } + + /// + /// 读取线圈状态 + /// + public async Task 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; + } + } + + /// + /// 写入单个线圈 + /// + 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; + } + } + + /// + /// 写入多个线圈 + /// + 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; + } + } + + /// + /// 启动心跳检测 + /// + 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 心跳检测已启动"); + } + + /// + /// 停止心跳检测 + /// + private void StopHeartbeat() + { + if (_heartbeatTimer != null) + { + _heartbeatTimer.Stop(); + _heartbeatTimer.Dispose(); + _heartbeatTimer = null; + _logger.LogInfo("Modbus 心跳检测已停止"); + } + } + + /// + /// 处理连接错误 + /// + private async Task HandleConnectionError() + { + lock (_lockObject) + { + if (_isConnected) + { + _isConnected = false; + ConnectionStatusChanged?.Invoke(false); + } + } + + _logger.LogWarning("检测到连接错误,尝试重新连接..."); + + // 尝试重新连接 + await Task.Delay(2000); + await ConnectAsync(); + } + + /// + /// 确保已连接 + /// + private void EnsureConnected() + { + if (!IsConnected) + { + throw new InvalidOperationException("Modbus TCP 未连接,请先调用 ConnectAsync()"); + } + } + + public void Dispose() + { + Disconnect(); + } +} diff --git a/PetWashControl/ViewModels/MainViewModel.cs b/PetWashControl/ViewModels/MainViewModel.cs index 0f299f8..7460070 100644 --- a/PetWashControl/ViewModels/MainViewModel.cs +++ b/PetWashControl/ViewModels/MainViewModel.cs @@ -24,6 +24,7 @@ public partial class MainViewModel : ObservableObject { private readonly ApiService _apiService; private readonly MqttClientService _mqttService; + private readonly ModbusService _modbusService; private readonly ConfigurationService _config; private readonly LogService _logger; @@ -50,6 +51,9 @@ public partial class MainViewModel : ObservableObject [ObservableProperty] private bool _isConnected; + [ObservableProperty] + private bool _isModbusConnected; + [ObservableProperty] private string _currentView = "Idle"; @@ -141,7 +145,10 @@ public partial class MainViewModel : ObservableObject _logger = new LogService(); _apiService = new ApiService(_config); _mqttService = new MqttClientService(_config); + _modbusService = new ModbusService(_config, _logger); + _mqttService.MessageReceived += OnMqttMessageReceived; + _modbusService.ConnectionStatusChanged += OnModbusConnectionStatusChanged; // 从配置加载时间参数 FirstSprayWaterTime = _config.FirstSprayWaterTime; @@ -204,19 +211,43 @@ public partial class MainViewModel : ObservableObject { _logger.LogInfo("开始初始化系统..."); + // 连接 Modbus TCP 设备 + _logger.LogInfo($"正在连接 Modbus TCP 设备: {_config.ModbusIpAddress}:{_config.ModbusPort}"); + var modbusConnected = await _modbusService.ConnectAsync(); + IsModbusConnected = modbusConnected; + + if (modbusConnected) + { + _logger.LogInfo("Modbus TCP 设备连接成功"); + } + else + { + _logger.LogWarning("Modbus TCP 设备连接失败,系统将继续运行但设备控制功能可能不可用"); + } + + // 连接 MQTT await _mqttService.ConnectAsync(); IsConnected = _mqttService.IsConnected; _logger.LogInfo("MQTT连接成功"); await LoadPackagesAsync(); - StatusMessage = "系统就绪,请点击开始"; + + if (IsModbusConnected) + { + StatusMessage = "系统就绪,设备已连接"; + } + else + { + StatusMessage = "系统就绪,设备未连接"; + } + _logger.LogInfo("系统初始化完成"); } catch (Exception ex) { _logger.LogError("初始化失败", ex); StatusMessage = $"初始化失败: {ex.Message}"; - MessageBox.Show($"系统初始化失败,请检查后端服务是否启动。\n\n错误: {ex.Message}", + MessageBox.Show($"系统初始化失败,请检查后端服务和设备连接。\n\n错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); } } @@ -755,6 +786,31 @@ public partial class MainViewModel : ObservableObject }); } + private void OnModbusConnectionStatusChanged(bool isConnected) + { + Application.Current.Dispatcher.Invoke(() => + { + IsModbusConnected = isConnected; + + if (isConnected) + { + _logger.LogInfo("Modbus TCP 设备已连接"); + if (StatusMessage.Contains("设备未连接")) + { + StatusMessage = "系统就绪,设备已连接"; + } + } + else + { + _logger.LogWarning("Modbus TCP 设备连接断开"); + if (!StatusMessage.Contains("失败") && !StatusMessage.Contains("错误")) + { + StatusMessage = "警告:设备连接断开"; + } + } + }); + } + private void UpdateTemperatures(string stepName, int currentTime, int totalTime) { // 根据不同步骤模拟温度变化