421 lines
11 KiB
Markdown
421 lines
11 KiB
Markdown
|
|
# Modbus TCP 快速入门指南
|
|||
|
|
|
|||
|
|
## 1. 配置设备连接
|
|||
|
|
|
|||
|
|
在 `PetWashControl/Services/ConfigurationService.cs` 中修改设备连接参数:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// Modbus TCP 配置
|
|||
|
|
public string ModbusIpAddress { get; set; } = "192.168.1.10"; // 修改为实际设备IP
|
|||
|
|
public int ModbusPort { get; set; } = 502; // 默认502端口
|
|||
|
|
public byte ModbusSlaveId { get; set; } = 1; // 从站ID,根据设备配置
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 2. 系统自动连接
|
|||
|
|
|
|||
|
|
系统启动时会自动连接 Modbus TCP 设备:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// MainViewModel.InitializeAsync() 中自动执行
|
|||
|
|
var modbusConnected = await _modbusService.ConnectAsync();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
连接状态会显示在界面上:
|
|||
|
|
- ✅ "系统就绪,设备已连接" - 连接成功
|
|||
|
|
- ⚠️ "系统就绪,设备未连接" - 连接失败
|
|||
|
|
|
|||
|
|
## 3. 使用 Modbus 服务
|
|||
|
|
|
|||
|
|
### 在 MainViewModel 中访问服务
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// _modbusService 已在构造函数中初始化
|
|||
|
|
private readonly ModbusService _modbusService;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 常用操作示例
|
|||
|
|
|
|||
|
|
#### 读取设备状态
|
|||
|
|
```csharp
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 读取设备状态寄存器(地址0)
|
|||
|
|
var status = await _modbusService.ReadHoldingRegistersAsync(0, 1);
|
|||
|
|
_logger.LogInfo($"设备状态: {status[0]}");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("读取设备状态失败", ex);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 控制设备门
|
|||
|
|
```csharp
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 打开门(线圈地址0)
|
|||
|
|
await _modbusService.WriteSingleCoilAsync(0, true);
|
|||
|
|
IsDoorOpen = true;
|
|||
|
|
_logger.LogInfo("设备门已打开");
|
|||
|
|
|
|||
|
|
// 关闭门
|
|||
|
|
await _modbusService.WriteSingleCoilAsync(0, false);
|
|||
|
|
IsDoorOpen = false;
|
|||
|
|
_logger.LogInfo("设备门已关闭");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("控制门失败", ex);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 读取温度传感器
|
|||
|
|
```csharp
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 读取温度传感器(输入寄存器100-101)
|
|||
|
|
var temps = await _modbusService.ReadInputRegistersAsync(100, 2);
|
|||
|
|
WaterTemperature = temps[0] / 10.0; // 假设数据需要除以10
|
|||
|
|
RoomTemperature = temps[1] / 10.0;
|
|||
|
|
_logger.LogInfo($"水温: {WaterTemperature}°C, 室温: {RoomTemperature}°C");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("读取温度失败", ex);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 启动洗护流程
|
|||
|
|
```csharp
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 写入控制寄存器(地址200-201)
|
|||
|
|
ushort[] controlData = new ushort[]
|
|||
|
|
{
|
|||
|
|
1, // 启动命令
|
|||
|
|
30 // 运行时长(分钟)
|
|||
|
|
};
|
|||
|
|
await _modbusService.WriteMultipleRegistersAsync(200, controlData);
|
|||
|
|
_logger.LogInfo("洗护流程已启动");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("启动洗护失败", ex);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 读取液位传感器
|
|||
|
|
```csharp
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 读取三个液位传感器(输入寄存器300-302)
|
|||
|
|
var levels = await _modbusService.ReadInputRegistersAsync(300, 3);
|
|||
|
|
Shampoo1Level = levels[0];
|
|||
|
|
Shampoo2Level = levels[1];
|
|||
|
|
Shampoo3Level = levels[2];
|
|||
|
|
_logger.LogInfo($"液位: {Shampoo1Level}%, {Shampoo2Level}%, {Shampoo3Level}%");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("读取液位失败", ex);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 4. 集成到现有流程
|
|||
|
|
|
|||
|
|
### 在支付成功后打开门
|
|||
|
|
|
|||
|
|
修改 `SimulatePaymentAsync` 方法:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
[RelayCommand]
|
|||
|
|
private async Task SimulatePaymentAsync()
|
|||
|
|
{
|
|||
|
|
if (CurrentOrder == null) return;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
StatusMessage = "正在处理支付...";
|
|||
|
|
await Task.Delay(1500);
|
|||
|
|
|
|||
|
|
// 确认支付
|
|||
|
|
CurrentOrder = await _apiService.ConfirmPaymentAsync(CurrentOrder.Id);
|
|||
|
|
|
|||
|
|
// 通过 Modbus 打开设备门
|
|||
|
|
if (IsModbusConnected)
|
|||
|
|
{
|
|||
|
|
await _modbusService.WriteSingleCoilAsync(0, true);
|
|||
|
|
_logger.LogInfo("通过 Modbus 打开设备门");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
IsDoorOpen = true;
|
|||
|
|
StatusMessage = "支付成功!设备门已打开,请将宠物放入";
|
|||
|
|
|
|||
|
|
MessageBox.Show("支付成功!\n\n设备门已自动打开\n请将宠物放入设备后关闭门开始洗护",
|
|||
|
|
"支付成功", MessageBoxButton.OK, MessageBoxImage.Information);
|
|||
|
|
|
|||
|
|
CurrentView = "Idle";
|
|||
|
|
ViewChanged?.Invoke("Idle");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("支付失败", ex);
|
|||
|
|
StatusMessage = $"支付失败: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 在关门时启动洗护
|
|||
|
|
|
|||
|
|
修改 `CloseDoorAsync` 方法:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
[RelayCommand]
|
|||
|
|
private async Task CloseDoorAsync()
|
|||
|
|
{
|
|||
|
|
if (CurrentOrder == null || !IsDoorOpen) return;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
_logger.LogInfo($"关门,订单ID: {CurrentOrder.Id}");
|
|||
|
|
|
|||
|
|
// 通过 Modbus 关闭设备门
|
|||
|
|
if (IsModbusConnected)
|
|||
|
|
{
|
|||
|
|
await _modbusService.WriteSingleCoilAsync(0, false);
|
|||
|
|
_logger.LogInfo("通过 Modbus 关闭设备门");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通过MQTT发送关门状态
|
|||
|
|
await _mqttService.PublishAsync("device/status", new
|
|||
|
|
{
|
|||
|
|
status = "door_closed",
|
|||
|
|
orderId = CurrentOrder.Id,
|
|||
|
|
timestamp = DateTime.Now
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
CurrentOrder = await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.DoorClosed);
|
|||
|
|
|
|||
|
|
IsDoorOpen = false;
|
|||
|
|
StatusMessage = "门已关闭,清洗即将开始...";
|
|||
|
|
|
|||
|
|
// 通过 Modbus 启动洗护流程
|
|||
|
|
if (IsModbusConnected)
|
|||
|
|
{
|
|||
|
|
int totalMinutes = CalculateTotalWashTime();
|
|||
|
|
ushort[] controlData = new ushort[] { 1, (ushort)totalMinutes };
|
|||
|
|
await _modbusService.WriteMultipleRegistersAsync(200, controlData);
|
|||
|
|
_logger.LogInfo($"通过 Modbus 启动洗护流程,时长: {totalMinutes}分钟");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CurrentView = "Washing";
|
|||
|
|
ViewChanged?.Invoke("Washing");
|
|||
|
|
IsWashing = true;
|
|||
|
|
|
|||
|
|
_ = SimulateWashingProcessAsync();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("关门失败", ex);
|
|||
|
|
StatusMessage = $"关门失败: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private int CalculateTotalWashTime()
|
|||
|
|
{
|
|||
|
|
return FirstSprayWaterTime + SprayShampoo1Time + AfterShampoo1SprayTime +
|
|||
|
|
SprayShampoo2Time + AfterShampoo2SprayTime + SprayShampoo3Time +
|
|||
|
|
AfterShampoo3SprayTime + HotAirTime + ColdAirTime + UvSterilizationTime;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 在洗护过程中读取实时数据
|
|||
|
|
|
|||
|
|
修改 `SimulateWashingProcessAsync` 方法,添加实时数据读取:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
private async Task SimulateWashingProcessAsync()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var steps = new[]
|
|||
|
|
{
|
|||
|
|
("第一次冲水", FirstSprayWaterTime * 60),
|
|||
|
|
// ... 其他步骤
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
for (int stepIndex = 0; stepIndex < steps.Length; stepIndex++)
|
|||
|
|
{
|
|||
|
|
var (stepName, duration) = steps[stepIndex];
|
|||
|
|
CurrentStep = stepName;
|
|||
|
|
|
|||
|
|
for (int i = 0; i <= duration; i++)
|
|||
|
|
{
|
|||
|
|
if (!IsWashing) return;
|
|||
|
|
|
|||
|
|
// 从 Modbus 读取实时温度
|
|||
|
|
if (IsModbusConnected && i % 10 == 0) // 每10秒读取一次
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var temps = await _modbusService.ReadInputRegistersAsync(100, 2);
|
|||
|
|
WaterTemperature = temps[0] / 10.0;
|
|||
|
|
RoomTemperature = temps[1] / 10.0;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("读取实时温度失败", ex);
|
|||
|
|
// 失败时使用模拟数据
|
|||
|
|
UpdateTemperatures(stepName, i, duration);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
UpdateTemperatures(stepName, i, duration);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await Task.Delay(100);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await CompleteWashingAsync();
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("洗护流程失败", ex);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 5. 监控连接状态
|
|||
|
|
|
|||
|
|
系统会自动监控 Modbus 连接状态:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 连接状态变化时自动触发
|
|||
|
|
private void OnModbusConnectionStatusChanged(bool isConnected)
|
|||
|
|
{
|
|||
|
|
Application.Current.Dispatcher.Invoke(() =>
|
|||
|
|
{
|
|||
|
|
IsModbusConnected = isConnected;
|
|||
|
|
|
|||
|
|
if (isConnected)
|
|||
|
|
{
|
|||
|
|
_logger.LogInfo("Modbus TCP 设备已连接");
|
|||
|
|
StatusMessage = "系统就绪,设备已连接";
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning("Modbus TCP 设备连接断开");
|
|||
|
|
StatusMessage = "警告:设备连接断开";
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
在界面中绑定连接状态:
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<!-- 在 MainWindow.xaml 中添加连接状态指示器 -->
|
|||
|
|
<StackPanel Orientation="Horizontal">
|
|||
|
|
<Ellipse Width="10" Height="10"
|
|||
|
|
Fill="{Binding IsModbusConnected, Converter={StaticResource BoolToColorConverter}}" />
|
|||
|
|
<TextBlock Text="{Binding IsModbusConnected, Converter={StaticResource BoolToStatusConverter}}"
|
|||
|
|
Margin="5,0,0,0" />
|
|||
|
|
</StackPanel>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 6. 错误处理最佳实践
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
// 始终使用 try-catch 包装 Modbus 操作
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 检查连接状态
|
|||
|
|
if (!IsModbusConnected)
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning("Modbus 未连接,跳过设备控制");
|
|||
|
|
// 使用备用方案或提示用户
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行 Modbus 操作
|
|||
|
|
await _modbusService.WriteSingleCoilAsync(0, true);
|
|||
|
|
|
|||
|
|
// 记录成功日志
|
|||
|
|
_logger.LogInfo("Modbus 操作成功");
|
|||
|
|
}
|
|||
|
|
catch (TimeoutException ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("Modbus 操作超时", ex);
|
|||
|
|
StatusMessage = "设备响应超时,请检查连接";
|
|||
|
|
}
|
|||
|
|
catch (InvalidOperationException ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("Modbus 未连接", ex);
|
|||
|
|
StatusMessage = "设备未连接";
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError("Modbus 操作失败", ex);
|
|||
|
|
StatusMessage = $"设备控制失败: {ex.Message}";
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 7. 测试步骤
|
|||
|
|
|
|||
|
|
### 测试连接
|
|||
|
|
1. 启动应用程序
|
|||
|
|
2. 查看日志输出:
|
|||
|
|
```
|
|||
|
|
[INFO] 正在连接 Modbus TCP 设备: 192.168.1.10:502
|
|||
|
|
[INFO] Modbus TCP 连接成功: 192.168.1.10:502
|
|||
|
|
```
|
|||
|
|
3. 界面显示 "系统就绪,设备已连接"
|
|||
|
|
|
|||
|
|
### 测试读取
|
|||
|
|
```csharp
|
|||
|
|
// 在 InitializeAsync 后添加测试代码
|
|||
|
|
var testData = await _modbusService.ReadHoldingRegistersAsync(0, 1);
|
|||
|
|
_logger.LogInfo($"测试读取成功,值: {testData[0]}");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 测试写入
|
|||
|
|
```csharp
|
|||
|
|
// 测试写入寄存器
|
|||
|
|
await _modbusService.WriteSingleRegisterAsync(0, 100);
|
|||
|
|
var verify = await _modbusService.ReadHoldingRegistersAsync(0, 1);
|
|||
|
|
_logger.LogInfo($"测试写入成功,验证值: {verify[0]}");
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 8. 常见问题
|
|||
|
|
|
|||
|
|
### Q: 连接失败怎么办?
|
|||
|
|
A: 检查以下几点:
|
|||
|
|
1. 设备 IP 地址是否正确
|
|||
|
|
2. 网络是否连通(ping 测试)
|
|||
|
|
3. 设备是否启用 Modbus TCP 服务
|
|||
|
|
4. 防火墙是否阻止 502 端口
|
|||
|
|
|
|||
|
|
### Q: 读写操作失败?
|
|||
|
|
A: 确认:
|
|||
|
|
1. 寄存器地址是否正确
|
|||
|
|
2. 从站 ID 是否匹配
|
|||
|
|
3. 数据格式是否符合设备要求
|
|||
|
|
4. 查看详细错误日志
|
|||
|
|
|
|||
|
|
### Q: 如何调试 Modbus 通信?
|
|||
|
|
A:
|
|||
|
|
1. 查看日志文件中的详细信息
|
|||
|
|
2. 使用 Modbus 测试工具(如 Modbus Poll)验证设备
|
|||
|
|
3. 使用网络抓包工具(如 Wireshark)分析通信数据
|
|||
|
|
|
|||
|
|
## 9. 下一步
|
|||
|
|
|
|||
|
|
- 根据实际设备的 Modbus 地址映射表修改代码
|
|||
|
|
- 实现完整的设备控制逻辑
|
|||
|
|
- 添加更多的错误处理和用户提示
|
|||
|
|
- 进行完整的集成测试
|
|||
|
|
|
|||
|
|
详细文档请参考:`MODBUS_INTEGRATION.md`
|