From e3ad85e1f5323f3baf46c6c983883883019e59e7 Mon Sep 17 00:00:00 2001
From: cli <377476583@qq.com>
Date: Thu, 8 May 2025 17:26:10 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=A1=B9=E7=9B=AE=E6=96=87?=
=?UTF-8?q?=E4=BB=B6=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CommandProcessor/CommandHandler.cs | 739 ++++++++++++++++++++++++++++
Factory/SimulatorFactory.cs | 269 ++++++++++
Interfaces/IProtocolMessage.cs | 41 ++
Interfaces/ISimulator.cs | 69 +++
Models/MessageAnalyzer.cs | 374 ++++++++++++++
Models/Parameter.cs | 18 +
Models/Protocol376Message.cs | 443 +++++++++++++++++
Models/RandomDataGenerator.cs | 249 ++++++++++
Models/TestScenario.cs | 191 +++++++
NuGet.Config | 6 +
Program.cs | 152 ++++++
Protocol376Simulator.csproj | 16 +
Protocol376Simulator.sln | 25 +
README.md | 295 +++++++++++
Services/DeviceDataService.cs | 191 +++++++
Services/HeartbeatService.cs | 216 ++++++++
Services/NetworkService.cs | 269 ++++++++++
Services/ReconnectionService.cs | 263 ++++++++++
Services/StatisticsService.cs | 214 ++++++++
Simulators/ConcentratorSimulator.cs | 738 +++++++++++++++++++++++++++
20 files changed, 4778 insertions(+)
create mode 100644 CommandProcessor/CommandHandler.cs
create mode 100644 Factory/SimulatorFactory.cs
create mode 100644 Interfaces/IProtocolMessage.cs
create mode 100644 Interfaces/ISimulator.cs
create mode 100644 Models/MessageAnalyzer.cs
create mode 100644 Models/Parameter.cs
create mode 100644 Models/Protocol376Message.cs
create mode 100644 Models/RandomDataGenerator.cs
create mode 100644 Models/TestScenario.cs
create mode 100644 NuGet.Config
create mode 100644 Program.cs
create mode 100644 Protocol376Simulator.csproj
create mode 100644 Protocol376Simulator.sln
create mode 100644 README.md
create mode 100644 Services/DeviceDataService.cs
create mode 100644 Services/HeartbeatService.cs
create mode 100644 Services/NetworkService.cs
create mode 100644 Services/ReconnectionService.cs
create mode 100644 Services/StatisticsService.cs
create mode 100644 Simulators/ConcentratorSimulator.cs
diff --git a/CommandProcessor/CommandHandler.cs b/CommandProcessor/CommandHandler.cs
new file mode 100644
index 0000000..784fcfb
--- /dev/null
+++ b/CommandProcessor/CommandHandler.cs
@@ -0,0 +1,739 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Protocol376Simulator.Factory;
+using Protocol376Simulator.Interfaces;
+using Protocol376Simulator.Models;
+using Protocol376Simulator.Simulators;
+using Serilog;
+
+namespace Protocol376Simulator.CommandProcessor
+{
+ ///
+ /// 命令处理器类,负责处理控制台命令
+ ///
+ public class CommandHandler
+ {
+ private readonly RandomDataGenerator _dataGenerator = new RandomDataGenerator();
+ private bool _useRandomData = false;
+ private bool _displayHexLog = false;
+
+ ///
+ /// 处理命令
+ ///
+ /// 命令及参数
+ /// 处理任务
+ public async Task ProcessCommand(string[] commandParts)
+ {
+ if (commandParts == null || commandParts.Length == 0)
+ {
+ return;
+ }
+
+ string command = commandParts[0].ToLower();
+
+ try
+ {
+ switch (command)
+ {
+ case "help":
+ DisplayHelp();
+ break;
+
+ case "server":
+ // 设置服务器地址和端口
+ if (commandParts.Length >= 3)
+ {
+ string serverAddress = commandParts[1];
+ if (int.TryParse(commandParts[2], out int serverPort))
+ {
+ SimulatorFactory.SetServerConfig(serverAddress, serverPort);
+ }
+ else
+ {
+ Log.Warning("无效的端口号");
+ }
+ }
+ else
+ {
+ Log.Warning("语法: server
");
+ }
+ break;
+
+ case "create":
+ // 创建集中器
+ if (commandParts.Length >= 2)
+ {
+ string address = commandParts[1];
+ var simulator = SimulatorFactory.CreateConcentrator(address);
+
+ // 启用十六进制日志
+ if (_displayHexLog)
+ {
+ simulator.MessageReceived += (sender, message) => {
+ string hexString = BitConverter.ToString(message).Replace("-", " ");
+ Log.Debug("集中器 (地址: {Address}) 收到原始报文: {HexMessage}", address, hexString);
+ };
+ }
+ }
+ else
+ {
+ Log.Warning("语法: create ");
+ }
+ break;
+
+ case "auto":
+ // 自动创建集中器
+ var newSimulator = SimulatorFactory.CreateConcentratorWithAutoAddress();
+ Log.Information("已自动创建集中器: {Address}",
+ ((ConcentratorSimulator)newSimulator)._concentratorAddress);
+ break;
+
+ case "batch":
+ // 批量创建集中器
+ if (commandParts.Length >= 2 && int.TryParse(commandParts[1], out int count))
+ {
+ SimulatorFactory.BatchCreateConcentrators(count);
+ }
+ else
+ {
+ Log.Warning("语法: batch ");
+ }
+ break;
+
+ case "connect":
+ // 连接集中器
+ if (commandParts.Length >= 2)
+ {
+ string address = commandParts[1];
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ await simulator.StartAsync();
+ }
+ }
+ else
+ {
+ Log.Warning("语法: connect ");
+ }
+ break;
+
+ case "login":
+ // 发送登录消息
+ if (commandParts.Length >= 2)
+ {
+ await SendLoginCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: login ");
+ }
+ break;
+
+ case "heartbeat":
+ // 发送心跳消息
+ if (commandParts.Length >= 2)
+ {
+ await SendHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: heartbeat ");
+ }
+ break;
+
+ case "valve":
+ // 发送阀控消息
+ if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte operation))
+ {
+ await SendValveControlCommand(commandParts[1], operation);
+ }
+ else
+ {
+ Log.Warning("语法: valve (1=开阀, 2=关阀, 3=查询状态)");
+ }
+ break;
+
+ case "upload":
+ // 发送数据上传消息
+ if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte dataType))
+ {
+ await SendDataUploadCommand(commandParts[1], dataType);
+ }
+ else
+ {
+ Log.Warning("语法: upload (1=水表, 2=电表, 3=气表, 4=状态)");
+ }
+ break;
+
+ case "read":
+ // 发送读数据消息
+ if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte readType))
+ {
+ await SendReadDataCommand(commandParts[1], readType);
+ }
+ else
+ {
+ Log.Warning("语法: read (1=水表, 2=电表, 3=气表, 4=状态)");
+ }
+ break;
+
+ case "setparam":
+ // 发送设置参数消息
+ if (commandParts.Length >= 4 && byte.TryParse(commandParts[2], out byte paramType))
+ {
+ string paramValue = commandParts[3];
+ await SendSetParameterCommand(commandParts[1], paramType, paramValue);
+ }
+ else
+ {
+ Log.Warning("语法: setparam ");
+ }
+ break;
+
+ case "startbeat":
+ // 启动自动心跳
+ if (commandParts.Length >= 2)
+ {
+ await StartAutoHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: startbeat ");
+ }
+ break;
+
+ case "stopbeat":
+ // 停止自动心跳
+ if (commandParts.Length >= 2)
+ {
+ await StopAutoHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: stopbeat ");
+ }
+ break;
+
+ case "status":
+ // 显示集中器状态
+ if (commandParts.Length >= 2)
+ {
+ ShowConcentratorStatusCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: status ");
+ }
+ break;
+
+ case "setdata":
+ // 设置表计数据
+ if (commandParts.Length >= 4 && byte.TryParse(commandParts[2], out byte setDataType))
+ {
+ string dataValue = commandParts[3];
+ SetMeterDataCommand(commandParts[1], setDataType, dataValue);
+ }
+ else
+ {
+ Log.Warning("语法: setdata ");
+ }
+ break;
+
+ case "autoresponse":
+ // 设置自动响应
+ if (commandParts.Length >= 3 && bool.TryParse(commandParts[2], out bool enabled))
+ {
+ SetAutoResponseCommand(commandParts[1], enabled);
+ }
+ else
+ {
+ Log.Warning("语法: autoresponse ");
+ }
+ break;
+
+ case "disconnect":
+ // 断开集中器
+ if (commandParts.Length >= 2)
+ {
+ await DisconnectConcentratorCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: disconnect ");
+ }
+ break;
+
+ case "list":
+ // 列出所有集中器
+ ListAllConcentratorsCommand();
+ break;
+
+ case "batchlogin":
+ // 批量登录
+ if (commandParts.Length >= 2)
+ {
+ await BatchLoginCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: batchlogin ");
+ }
+ break;
+
+ case "batchbeat":
+ // 批量发送心跳
+ if (commandParts.Length >= 2)
+ {
+ await BatchSendHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: batchbeat ");
+ }
+ break;
+
+ case "batchvalve":
+ // 批量阀控
+ if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte batchOperation))
+ {
+ await BatchValveControlCommand(commandParts[1], batchOperation);
+ }
+ else
+ {
+ Log.Warning("语法: batchvalve ");
+ }
+ break;
+
+ case "batchstartbeat":
+ // 批量启动自动心跳
+ if (commandParts.Length >= 2)
+ {
+ await BatchStartAutoHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: batchstartbeat ");
+ }
+ break;
+
+ case "batchstopbeat":
+ // 批量停止自动心跳
+ if (commandParts.Length >= 2)
+ {
+ await BatchStopAutoHeartbeatCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: batchstopbeat ");
+ }
+ break;
+
+ case "stats":
+ // 显示通信统计
+ if (commandParts.Length >= 2)
+ {
+ ShowCommunicationStatisticsCommand(commandParts[1]);
+ }
+ else
+ {
+ Log.Warning("语法: stats ");
+ }
+ break;
+
+ case "reconnect":
+ // 设置重连参数
+ if (commandParts.Length >= 5 &&
+ bool.TryParse(commandParts[2], out bool autoReconnect) &&
+ int.TryParse(commandParts[3], out int maxAttempts) &&
+ int.TryParse(commandParts[4], out int delaySeconds))
+ {
+ SetReconnectParametersCommand(commandParts[1], autoReconnect, maxAttempts, delaySeconds);
+ }
+ else
+ {
+ Log.Warning("语法: reconnect ");
+ }
+ break;
+
+ case "userand":
+ // 设置是否使用随机数据
+ if (commandParts.Length >= 2 && bool.TryParse(commandParts[1], out bool useRandom))
+ {
+ _useRandomData = useRandom;
+ Log.Information("随机数据生成已{Status}", useRandom ? "启用" : "禁用");
+ }
+ else
+ {
+ Log.Warning("语法: userand ");
+ }
+ break;
+
+ case "hexlog":
+ // 设置是否显示十六进制日志
+ if (commandParts.Length >= 2 && bool.TryParse(commandParts[1], out bool displayHex))
+ {
+ _displayHexLog = displayHex;
+ Log.Information("十六进制日志已{Status}", displayHex ? "启用" : "禁用");
+ }
+ else
+ {
+ Log.Warning("语法: hexlog ");
+ }
+ break;
+
+ case "clear":
+ // 清空所有模拟器
+ await SimulatorFactory.ClearAllSimulatorsAsync();
+ break;
+
+ default:
+ Log.Warning("未知命令: {Command}", command);
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "执行命令 {Command} 时发生错误: {ErrorMessage}", command, ex.Message);
+ }
+ }
+
+ private async Task SendLoginCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ await simulator.SendLoginMessageAsync();
+ Log.Information("集中器 (地址: {Address}) 已发送登录消息", address);
+ }
+ }
+
+ private async Task SendHeartbeatCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ await simulator.SendHeartbeatMessageAsync();
+ Log.Information("集中器 (地址: {Address}) 已发送心跳消息", address);
+ }
+ }
+
+ private async Task SendValveControlCommand(string address, byte operation)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ await concentrator.SendValveControlMessageAsync(operation);
+ Log.Information("集中器 (地址: {Address}) 已发送阀控消息, 操作码: {Operation}",
+ address, operation);
+ }
+ }
+
+ private async Task SendDataUploadCommand(string address, byte dataType)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ await concentrator.SendDataUploadMessageAsync(dataType);
+ Log.Information("集中器 (地址: {Address}) 已发送数据上传消息, 数据类型: {DataType}",
+ address, dataType);
+ }
+ }
+
+ private async Task SendReadDataCommand(string address, byte dataType)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ await concentrator.SendReadDataMessageAsync(dataType);
+ Log.Information("集中器 (地址: {Address}) 已发送读数据消息, 数据类型: {DataType}",
+ address, dataType);
+ }
+ }
+
+ private async Task SendSetParameterCommand(string address, byte paramType, string paramValue)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ // 将参数值转换为字节数组
+ byte[] paramData;
+
+ // 使用随机数据
+ if (paramValue.ToLower() == "random")
+ {
+ paramData = _dataGenerator.GenerateParameter(paramType);
+ Log.Information("已生成随机参数数据: 类型={Type}", paramType);
+ }
+ else
+ {
+ // 使用指定的数据值
+ if (paramValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ // 十六进制数据
+ string hexString = paramValue.Substring(2);
+ paramData = new byte[hexString.Length / 2];
+
+ for (int i = 0; i < paramData.Length; i++)
+ {
+ paramData[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
+ }
+ }
+ else
+ {
+ // 默认作为整数处理
+ if (int.TryParse(paramValue, out int value))
+ {
+ paramData = BitConverter.GetBytes(value);
+ }
+ else
+ {
+ // 字符串处理
+ paramData = System.Text.Encoding.ASCII.GetBytes(paramValue);
+ }
+ }
+ }
+
+ await concentrator.SendSetParameterMessageAsync(paramType, paramData);
+ Log.Information("集中器 (地址: {Address}) 已发送设置参数消息, 参数类型: {ParamType}",
+ address, paramType);
+ }
+ }
+
+ private async Task StartAutoHeartbeatCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ simulator.StartHeartbeat();
+ Log.Information("集中器 (地址: {Address}) 已启动自动心跳", address);
+ }
+ }
+
+ private async Task StopAutoHeartbeatCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ simulator.StopHeartbeat();
+ Log.Information("集中器 (地址: {Address}) 已停止自动心跳", address);
+ }
+ }
+
+ private void ShowConcentratorStatusCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ string status = simulator.GetStatus();
+ Console.WriteLine(status);
+ }
+ }
+
+ private void SetMeterDataCommand(string address, byte dataType, string dataValue)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ // 根据dataValue设置数据
+ byte[] meterData;
+
+ // 使用随机数据
+ if (dataValue.ToLower() == "random")
+ {
+ if (dataType <= 3) // 水表、电表、气表
+ {
+ meterData = _dataGenerator.GenerateMeterData(dataType);
+ }
+ else // 状态数据
+ {
+ meterData = _dataGenerator.GenerateStatusData();
+ }
+ Log.Information("已生成随机表计数据: 类型={Type}", dataType);
+ }
+ else
+ {
+ // 使用指定的数据值
+ if (dataValue.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
+ {
+ // 十六进制数据
+ string hexString = dataValue.Substring(2);
+ meterData = new byte[hexString.Length / 2];
+
+ for (int i = 0; i < meterData.Length; i++)
+ {
+ meterData[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
+ }
+ }
+ else
+ {
+ // 默认作为整数处理
+ if (int.TryParse(dataValue, out int value))
+ {
+ meterData = BitConverter.GetBytes(value);
+ }
+ else
+ {
+ // 字符串处理
+ meterData = System.Text.Encoding.ASCII.GetBytes(dataValue);
+ }
+ }
+ }
+
+ concentrator.UpdateMeterData(dataType, meterData);
+ Log.Information("集中器 (地址: {Address}) 表计数据已设置, 类型: {DataType}",
+ address, dataType);
+ }
+ }
+
+ private void SetAutoResponseCommand(string address, bool enabled)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ concentrator.SetAutoResponse(enabled);
+ Log.Information("集中器 (地址: {Address}) 自动响应已设置为: {Enabled}",
+ address, enabled);
+ }
+ }
+
+ private async Task DisconnectConcentratorCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ await simulator.StopAsync();
+ Log.Information("集中器 (地址: {Address}) 已断开连接", address);
+ }
+ }
+
+ private void ListAllConcentratorsCommand()
+ {
+ var simulators = SimulatorFactory.GetAllSimulators();
+
+ if (simulators.Count == 0)
+ {
+ Console.WriteLine("没有集中器实例");
+ return;
+ }
+
+ Console.WriteLine($"共有 {simulators.Count} 个集中器实例:");
+ foreach (var entry in simulators)
+ {
+ string connectionStatus = entry.Value.IsConnected ? "已连接" : "未连接";
+ Console.WriteLine($" {entry.Key}: {connectionStatus}");
+ }
+ }
+
+ private async Task BatchLoginCommand(string range)
+ {
+ await SimulatorFactory.BatchOperationAsync(range, async (simulator) => {
+ await simulator.SendLoginMessageAsync();
+ });
+
+ Log.Information("批量登录操作已完成");
+ }
+
+ private async Task BatchSendHeartbeatCommand(string range)
+ {
+ await SimulatorFactory.BatchOperationAsync(range, async (simulator) => {
+ await simulator.SendHeartbeatMessageAsync();
+ });
+
+ Log.Information("批量心跳操作已完成");
+ }
+
+ private async Task BatchValveControlCommand(string range, byte operation)
+ {
+ await SimulatorFactory.BatchOperationAsync(range, async (simulator) => {
+ if (simulator is ConcentratorSimulator concentrator)
+ {
+ await concentrator.SendValveControlMessageAsync(operation);
+ }
+ });
+
+ Log.Information("批量阀控操作已完成, 操作码: {Operation}", operation);
+ }
+
+ private async Task BatchStartAutoHeartbeatCommand(string range)
+ {
+ await SimulatorFactory.BatchOperationAsync(range, (simulator) => {
+ simulator.StartHeartbeat();
+ return Task.CompletedTask;
+ });
+
+ Log.Information("批量启动自动心跳操作已完成");
+ }
+
+ private async Task BatchStopAutoHeartbeatCommand(string range)
+ {
+ await SimulatorFactory.BatchOperationAsync(range, (simulator) => {
+ simulator.StopHeartbeat();
+ return Task.CompletedTask;
+ });
+
+ Log.Information("批量停止自动心跳操作已完成");
+ }
+
+ private void ShowCommunicationStatisticsCommand(string address)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ string stats = concentrator.GetCommunicationStatistics();
+ Console.WriteLine(stats);
+ }
+ }
+
+ private void SetReconnectParametersCommand(string address, bool autoReconnect, int maxAttempts, int delaySeconds)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null && simulator is ConcentratorSimulator concentrator)
+ {
+ concentrator.SetReconnectParameters(autoReconnect, maxAttempts, delaySeconds);
+ Log.Information("集中器 (地址: {Address}) 重连参数已设置: 自动重连={AutoReconnect}, 最大尝试次数={MaxAttempts}, 延迟={Delay}秒",
+ address, autoReconnect, maxAttempts, delaySeconds);
+ }
+ }
+
+ private void DisplayHelp()
+ {
+ Console.WriteLine("可用命令:");
+ Console.WriteLine(" help - 显示帮助信息");
+ Console.WriteLine(" server - 设置服务器地址和端口");
+ Console.WriteLine(" create - 创建集中器");
+ Console.WriteLine(" auto - 自动创建集中器(自动生成地址)");
+ Console.WriteLine(" batch - 批量创建集中器");
+ Console.WriteLine(" connect - 连接集中器到服务器");
+ Console.WriteLine(" login - 发送登录消息");
+ Console.WriteLine(" heartbeat - 发送心跳消息");
+ Console.WriteLine(" valve - 发送阀控消息 (1=开阀, 2=关阀, 3=查询)");
+ Console.WriteLine(" upload - 发送数据上传消息 (1=水表, 2=电表, 3=气表, 4=状态)");
+ Console.WriteLine(" read - 发送数据读取消息");
+ Console.WriteLine(" setparam - 发送设置参数消息");
+ Console.WriteLine(" startbeat - 启动自动心跳");
+ Console.WriteLine(" stopbeat - 停止自动心跳");
+ Console.WriteLine(" status - 显示集中器状态");
+ Console.WriteLine(" setdata - 设置表计数据");
+ Console.WriteLine(" autoresponse - 设置自动响应");
+ Console.WriteLine(" disconnect - 断开集中器连接");
+ Console.WriteLine(" list - 显示所有集中器");
+ Console.WriteLine(" batchlogin - 批量登录");
+ Console.WriteLine(" batchbeat - 批量发送心跳");
+ Console.WriteLine(" batchvalve - 批量阀控");
+ Console.WriteLine(" batchstartbeat - 批量启动自动心跳");
+ Console.WriteLine(" batchstopbeat - 批量停止自动心跳");
+ Console.WriteLine(" stats - 显示通信统计");
+ Console.WriteLine(" reconnect - 设置重连参数");
+ Console.WriteLine(" userand - 设置是否使用随机数据");
+ Console.WriteLine(" hexlog - 设置是否显示十六进制日志");
+ Console.WriteLine(" clear - 清空所有模拟器");
+ Console.WriteLine(" exit/quit/0 - 退出程序");
+ Console.WriteLine("");
+ Console.WriteLine("范围表达式说明:");
+ Console.WriteLine(" all - 所有集中器");
+ Console.WriteLine(" 1-5 - 地址后缀为1到5的集中器");
+ Console.WriteLine(" 312003001,312003002 - 指定的集中器列表");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Factory/SimulatorFactory.cs b/Factory/SimulatorFactory.cs
new file mode 100644
index 0000000..d30427a
--- /dev/null
+++ b/Factory/SimulatorFactory.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Protocol376Simulator.Interfaces;
+using Protocol376Simulator.Simulators;
+using Serilog;
+
+namespace Protocol376Simulator.Factory
+{
+ ///
+ /// 模拟器工厂类,负责创建和管理模拟器实例
+ ///
+ public class SimulatorFactory
+ {
+ private static readonly Dictionary _simulators = new Dictionary();
+ private static string _serverAddress = "127.0.0.1";
+ private static int _serverPort = 10502;
+ private static int _addressCounter = 1;
+
+ ///
+ /// 设置服务器地址和端口
+ ///
+ /// 服务器地址
+ /// 服务器端口
+ public static void SetServerConfig(string serverAddress, int serverPort)
+ {
+ _serverAddress = serverAddress;
+ _serverPort = serverPort;
+
+ Log.Information("服务器配置已更新: {ServerAddress}:{ServerPort}", _serverAddress, _serverPort);
+ }
+
+ ///
+ /// 创建集中器模拟器
+ ///
+ /// 集中器地址
+ /// 创建的模拟器实例
+ public static ISimulator CreateConcentrator(string address)
+ {
+ // 检查地址是否已存在
+ if (_simulators.ContainsKey(address))
+ {
+ Log.Warning("集中器地址 {Address} 已存在", address);
+ return _simulators[address];
+ }
+
+ // 创建新的模拟器实例
+ var simulator = new ConcentratorSimulator(address, _serverAddress, _serverPort);
+
+ // 订阅事件
+ simulator.StatusChanged += (sender, status) => {
+ Log.Information("集中器 (地址: {Address}) 状态变更: {Status}", address, status);
+ };
+
+ // 添加到集合
+ _simulators[address] = simulator;
+
+ Log.Information("已创建集中器 (地址: {Address})", address);
+
+ return simulator;
+ }
+
+ ///
+ /// 创建带自动生成地址的集中器模拟器
+ ///
+ /// 创建的模拟器实例
+ public static ISimulator CreateConcentratorWithAutoAddress()
+ {
+ string address = GenerateNextAddress();
+ return CreateConcentrator(address);
+ }
+
+ ///
+ /// 批量创建集中器模拟器
+ ///
+ /// 创建数量
+ /// 创建的模拟器地址列表
+ public static List BatchCreateConcentrators(int count)
+ {
+ var addresses = new List();
+
+ for (int i = 0; i < count; i++)
+ {
+ string address = GenerateNextAddress();
+ CreateConcentrator(address);
+ addresses.Add(address);
+ }
+
+ Log.Information("已批量创建 {Count} 个集中器", count);
+
+ return addresses;
+ }
+
+ ///
+ /// 生成下一个集中器地址
+ ///
+ /// 生成的地址
+ private static string GenerateNextAddress()
+ {
+ // 生成9位十六进制地址,格式如:312001001
+ string address = $"31{_addressCounter:D7}";
+ _addressCounter++;
+ return address;
+ }
+
+ ///
+ /// 获取模拟器实例
+ ///
+ /// 集中器地址
+ /// 模拟器实例,如果不存在则返回null
+ public static ISimulator GetSimulator(string address)
+ {
+ if (_simulators.TryGetValue(address, out var simulator))
+ {
+ return simulator;
+ }
+
+ Log.Warning("集中器地址 {Address} 不存在", address);
+ return null;
+ }
+
+ ///
+ /// 获取所有模拟器实例
+ ///
+ /// 模拟器实例列表
+ public static Dictionary GetAllSimulators()
+ {
+ return _simulators;
+ }
+
+ ///
+ /// 按范围获取模拟器地址
+ ///
+ /// 范围表达式
+ /// 模拟器地址列表
+ public static List GetAddressesInRange(string range)
+ {
+ var addresses = new List();
+
+ // 如果是"all",返回所有地址
+ if (range.ToLower() == "all")
+ {
+ addresses.AddRange(_simulators.Keys);
+ return addresses;
+ }
+
+ // 解析范围表达式
+ string[] parts = range.Split(',');
+ foreach (var part in parts)
+ {
+ if (part.Contains("-"))
+ {
+ // 处理范围形式:"1-5"
+ string[] rangeParts = part.Trim().Split('-');
+ if (rangeParts.Length == 2 && int.TryParse(rangeParts[0], out int start) && int.TryParse(rangeParts[1], out int end))
+ {
+ // 将索引转换为地址
+ for (int i = start; i <= end; i++)
+ {
+ string address = $"31{i:D7}";
+ if (_simulators.ContainsKey(address))
+ {
+ addresses.Add(address);
+ }
+ }
+ }
+ }
+ else
+ {
+ // 处理单个地址形式
+ string address = part.Trim();
+ if (_simulators.ContainsKey(address))
+ {
+ addresses.Add(address);
+ }
+ }
+ }
+
+ return addresses;
+ }
+
+ ///
+ /// 按范围批量操作模拟器
+ ///
+ /// 范围表达式
+ /// 操作委托
+ /// 操作任务
+ public static async Task BatchOperationAsync(string range, Func action)
+ {
+ var addresses = GetAddressesInRange(range);
+
+ Log.Information("批量操作 {Count} 个集中器", addresses.Count);
+
+ foreach (var address in addresses)
+ {
+ if (_simulators.TryGetValue(address, out var simulator))
+ {
+ try
+ {
+ await action(simulator);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "批量操作集中器 {Address} 时发生错误: {ErrorMessage}",
+ address, ex.Message);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 删除模拟器
+ ///
+ /// 集中器地址
+ /// 是否成功删除
+ public static async Task RemoveSimulatorAsync(string address)
+ {
+ if (_simulators.TryGetValue(address, out var simulator))
+ {
+ try
+ {
+ // 停止模拟器
+ await simulator.StopAsync();
+
+ // 从集合中移除
+ _simulators.Remove(address);
+
+ Log.Information("已删除集中器 (地址: {Address})", address);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "删除集中器 {Address} 时发生错误: {ErrorMessage}",
+ address, ex.Message);
+ return false;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// 清空所有模拟器
+ ///
+ public static async Task ClearAllSimulatorsAsync()
+ {
+ // 停止所有模拟器
+ foreach (var simulator in _simulators.Values)
+ {
+ try
+ {
+ await simulator.StopAsync();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "停止模拟器时发生错误: {ErrorMessage}", ex.Message);
+ }
+ }
+
+ // 清空集合
+ _simulators.Clear();
+
+ // 重置地址计数器
+ _addressCounter = 1;
+
+ Log.Information("已清空所有模拟器");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Interfaces/IProtocolMessage.cs b/Interfaces/IProtocolMessage.cs
new file mode 100644
index 0000000..20ddc89
--- /dev/null
+++ b/Interfaces/IProtocolMessage.cs
@@ -0,0 +1,41 @@
+using System;
+
+namespace Protocol376Simulator.Interfaces
+{
+ ///
+ /// 定义协议消息的基本接口
+ ///
+ public interface IProtocolMessage
+ {
+ ///
+ /// 将消息转换为字节数组
+ ///
+ /// 表示消息的字节数组
+ byte[] ToBytes();
+
+ ///
+ /// 获取消息的详细信息
+ ///
+ /// 格式化的消息信息字符串
+ string GetMessageInfo();
+
+ ///
+ /// 消息类型
+ ///
+ MessageType Type { get; }
+ }
+
+ ///
+ /// 定义消息类型枚举
+ ///
+ public enum MessageType
+ {
+ Login = 1,
+ Heartbeat = 2,
+ ValveControl = 3,
+ DataUpload = 4,
+ ReadData = 5,
+ SetParameter = 6,
+ Response = 99
+ }
+}
\ No newline at end of file
diff --git a/Interfaces/ISimulator.cs b/Interfaces/ISimulator.cs
new file mode 100644
index 0000000..65e6cfa
--- /dev/null
+++ b/Interfaces/ISimulator.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Protocol376Simulator.Interfaces
+{
+ ///
+ /// 定义模拟器的基本接口
+ ///
+ public interface ISimulator
+ {
+ ///
+ /// 启动模拟器
+ ///
+ /// 是否自动登录
+ /// 是否自动发送心跳
+ Task StartAsync(bool autoLogin = false, bool autoHeartbeat = false);
+
+ ///
+ /// 停止模拟器
+ ///
+ Task StopAsync();
+
+ ///
+ /// 发送登录消息
+ ///
+ Task SendLoginMessageAsync();
+
+ ///
+ /// 发送心跳消息
+ ///
+ Task SendHeartbeatMessageAsync();
+
+ ///
+ /// 启动心跳发送
+ ///
+ void StartHeartbeat();
+
+ ///
+ /// 停止心跳发送
+ ///
+ void StopHeartbeat();
+
+ ///
+ /// 获取模拟器状态
+ ///
+ /// 格式化的状态信息字符串
+ string GetStatus();
+
+ ///
+ /// 当状态变更时触发的事件
+ ///
+ event EventHandler StatusChanged;
+
+ ///
+ /// 当接收到消息时触发的事件
+ ///
+ event EventHandler MessageReceived;
+
+ ///
+ /// 模拟器是否已连接
+ ///
+ bool IsConnected { get; }
+
+ ///
+ /// 模拟器是否已登录
+ ///
+ bool IsLoggedIn { get; }
+ }
+}
\ No newline at end of file
diff --git a/Models/MessageAnalyzer.cs b/Models/MessageAnalyzer.cs
new file mode 100644
index 0000000..4a501c6
--- /dev/null
+++ b/Models/MessageAnalyzer.cs
@@ -0,0 +1,374 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Protocol376Simulator.Models
+{
+ public class MessageAnalyzer
+ {
+ // AFN功能码描述
+ private static readonly Dictionary AfnDescriptions = new Dictionary
+ {
+ { 0x00, "确认/否认" },
+ { 0x01, "复位" },
+ { 0x02, "链路接口检测" },
+ { 0x04, "设置参数" },
+ { 0x05, "控制命令" },
+ { 0x06, "身份认证及密钥协商" },
+ { 0x07, "自定义数据请求" },
+ { 0x08, "主动上报" },
+ { 0x09, "读取数据" },
+ { 0x0A, "查询参数" },
+ { 0x0B, "软件升级" },
+ { 0x0F, "文件传输" }
+ };
+
+ ///
+ /// 分析报文内容并生成详细解释
+ ///
+ /// 原始报文字节数组
+ /// 报文分析结果
+ public static string AnalyzeMessage(byte[] message)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("====== 报文详细分析 ======");
+
+ // 检查报文合法性
+ if (message.Length < 13 || message[0] != 0x68 || message[5] != 0x68 || message[message.Length - 1] != 0x16)
+ {
+ sb.AppendLine("❌ 报文格式不合法!");
+ sb.AppendLine($"原始报文: {BitConverter.ToString(message).Replace("-", " ")}");
+ return sb.ToString();
+ }
+
+ // 报文基本信息
+ sb.AppendLine($"原始报文: {BitConverter.ToString(message).Replace("-", " ")}");
+ sb.AppendLine($"报文长度: {message.Length} 字节");
+
+ // 头部分析
+ sb.AppendLine("\n【帧头结构】");
+ sb.AppendLine($"起始字节1: 0x{message[0]:X2}");
+ sb.AppendLine($"长度域1: 0x{message[1]:X2} 0x{message[2]:X2}");
+ sb.AppendLine($"长度域2: 0x{message[3]:X2} 0x{message[4]:X2}");
+ sb.AppendLine($"起始字节2: 0x{message[5]:X2}");
+
+ // 控制域分析
+ byte controlCode = message[6];
+ sb.AppendLine("\n【控制域】");
+ sb.AppendLine($"控制码: 0x{controlCode:X2}");
+
+ // 解析控制码
+ sb.AppendLine(AnalyzeControlCode(controlCode));
+
+ // 地址域分析
+ byte[] address = message.Skip(7).Take(4).ToArray();
+ sb.AppendLine("\n【地址域】");
+ sb.AppendLine($"地址域: {BitConverter.ToString(address).Replace("-", " ")}");
+ sb.AppendLine($"BCD编码地址: {BcdToString(address)}");
+
+ // 数据域分析
+ int dataLength = message.Length - 13; // 减去帧头、控制域、地址域、校验和、结束符
+ if (dataLength > 0)
+ {
+ byte[] data = message.Skip(11).Take(dataLength).ToArray();
+ sb.AppendLine("\n【数据域】");
+ sb.AppendLine($"数据域: {BitConverter.ToString(data).Replace("-", " ")}");
+ sb.AppendLine($"数据长度: {dataLength} 字节");
+
+ // AFN分析
+ if (data.Length > 0)
+ {
+ byte afn = data[0];
+ string afnDesc = AfnDescriptions.ContainsKey(afn) ? AfnDescriptions[afn] : "未知功能";
+ sb.AppendLine($"应用功能码(AFN): 0x{afn:X2} - {afnDesc}");
+ }
+
+ // 数据单元标识分析
+ if (data.Length > 2)
+ {
+ sb.AppendLine($"数据单元标识: 0x{data[1]:X2} 0x{data[2]:X2}");
+ }
+
+ // 根据AFN进一步分析数据内容
+ if (data.Length > 0)
+ {
+ sb.AppendLine(AnalyzeDataByAfn(data));
+ }
+ }
+ else
+ {
+ sb.AppendLine("\n【数据域】");
+ sb.AppendLine("无数据域内容");
+ }
+
+ // 校验和分析
+ byte calculatedCs = CalculateChecksum(message);
+ byte actualCs = message[message.Length - 2];
+ sb.AppendLine("\n【校验和】");
+ sb.AppendLine($"校验和: 0x{actualCs:X2} ({(calculatedCs == actualCs ? "✓ 正确" : "❌ 错误,应为 0x" + calculatedCs.ToString("X2"))})");
+
+ // 结束符
+ sb.AppendLine("\n【结束符】");
+ sb.AppendLine($"结束字节: 0x{message[message.Length - 1]:X2}");
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 解析控制码
+ ///
+ private static string AnalyzeControlCode(byte controlCode)
+ {
+ var sb = new StringBuilder();
+
+ // 方向位D7 (DIR)
+ bool isDownlink = (controlCode & 0x80) != 0;
+ sb.AppendLine($"方向位(D7): {(isDownlink ? "下行 (主站→集中器)" : "上行 (集中器→主站)")}");
+
+ // 启动标志位D6 (PRM)
+ bool isPrimaryStation = (controlCode & 0x40) != 0;
+ sb.AppendLine($"启动标志位(D6): {(isPrimaryStation ? "启动站" : "从动站")}");
+
+ // 帧计数位D5 (FCB)
+ bool fcb = (controlCode & 0x20) != 0;
+
+ // 功能码D0-D3
+ byte functionCode = (byte)(controlCode & 0x0F);
+
+ // 根据PRM和功能码解析不同的含义
+ if (isPrimaryStation)
+ {
+ sb.AppendLine($"帧计数位(D5): {fcb}");
+ sb.AppendLine($"功能码(D0-D3): 0x{functionCode:X1} - {GetPrimaryFunctionCodeDesc(functionCode)}");
+ }
+ else
+ {
+ bool dfc = (controlCode & 0x10) != 0;
+ sb.AppendLine($"数据流控制位(D4): {dfc} - {(dfc ? "从站接收缓冲区已满" : "从站可以接收数据")}");
+ sb.AppendLine($"功能码(D0-D3): 0x{functionCode:X1} - {GetSecondaryFunctionCodeDesc(functionCode)}");
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 获取启动站功能码描述
+ ///
+ private static string GetPrimaryFunctionCodeDesc(byte code)
+ {
+ return code switch
+ {
+ 0 => "复位远方链路",
+ 1 => "读取状态",
+ 3 => "发送/确认用户数据",
+ 4 => "发送/无需确认用户数据",
+ 9 => "请求/响应链路状态",
+ 10 => "请求/响应用户数据1",
+ 11 => "请求/响应用户数据2",
+ _ => "未知功能"
+ };
+ }
+
+ ///
+ /// 获取从动站功能码描述
+ ///
+ private static string GetSecondaryFunctionCodeDesc(byte code)
+ {
+ return code switch
+ {
+ 0 => "确认",
+ 1 => "链路忙",
+ 9 => "从站状态",
+ 11 => "响应用户数据",
+ _ => "未知功能"
+ };
+ }
+
+ ///
+ /// 根据不同的AFN值分析数据域
+ ///
+ private static string AnalyzeDataByAfn(byte[] data)
+ {
+ if (data.Length == 0)
+ return "数据为空";
+
+ var sb = new StringBuilder();
+ byte afn = data[0];
+
+ sb.AppendLine("\n【数据域解析】");
+
+ switch (afn)
+ {
+ case 0x00: // 确认/否认
+ sb.AppendLine("确认/否认报文");
+ if (data.Length > 5)
+ {
+ byte resultCode = data[5];
+ sb.AppendLine($"结果码: 0x{resultCode:X2} - {(resultCode == 0 ? "成功" : "失败")}");
+ }
+ break;
+
+ case 0x02: // 链路接口检测
+ sb.AppendLine("链路接口检测报文");
+ break;
+
+ case 0x04: // 设置参数
+ sb.AppendLine("设置参数报文");
+ if (data.Length > 5)
+ {
+ byte paramType = data[5];
+ sb.AppendLine($"参数类型: 0x{paramType:X2}");
+
+ // 解析参数内容
+ if (data.Length > 7)
+ {
+ byte paramLength = data[6];
+ byte[] paramValue = data.Skip(7).Take(paramLength).ToArray();
+ sb.AppendLine($"参数长度: {paramLength} 字节");
+ sb.AppendLine($"参数值: {BitConverter.ToString(paramValue).Replace("-", " ")}");
+
+ // 尝试转换为ASCII显示
+ try
+ {
+ string asciiValue = System.Text.Encoding.ASCII.GetString(paramValue)
+ .Replace('\0', '.')
+ .Replace('\r', '.')
+ .Replace('\n', '.');
+ sb.AppendLine($"参数值(ASCII): \"{asciiValue}\"");
+ }
+ catch
+ {
+ // 忽略无法转换的情况
+ }
+ }
+ }
+ break;
+
+ case 0x07: // 自定义数据
+ sb.AppendLine("自定义数据报文");
+ if (data.Length > 2)
+ {
+ byte dataFlag = data[1];
+
+ if (dataFlag == 0x01) // 登录请求
+ {
+ sb.AppendLine("登录请求");
+ }
+ else if (dataFlag == 0x02) // 心跳
+ {
+ sb.AppendLine("心跳报文");
+ if (data.Length > 8)
+ {
+ byte[] statusInfo = data.Skip(7).Take(2).ToArray();
+ sb.AppendLine($"自检状态信息: 0x{statusInfo[0]:X2} 0x{statusInfo[1]:X2}");
+ }
+ }
+ else if (dataFlag == 0x03) // 阀控
+ {
+ sb.AppendLine("阀控操作报文");
+ if (data.Length > 5)
+ {
+ byte operation = data[5];
+ string operationDesc = operation switch
+ {
+ 0x01 => "开阀",
+ 0x02 => "关阀",
+ 0x03 => "查询状态",
+ _ => $"未知操作(0x{operation:X2})"
+ };
+ sb.AppendLine($"阀控操作: {operationDesc}");
+ }
+ }
+ }
+ break;
+
+ case 0x08: // 主动上报
+ sb.AppendLine("数据上传报文");
+ if (data.Length > 5)
+ {
+ byte dataType = data[5];
+ byte dataLen = data.Length > 6 ? data[6] : (byte)0;
+
+ string dataTypeDesc = dataType switch
+ {
+ 0x01 => "水表数据",
+ 0x02 => "电表数据",
+ 0x03 => "气表数据",
+ 0x04 => "状态数据",
+ _ => $"未知类型(0x{dataType:X2})"
+ };
+
+ sb.AppendLine($"数据类型: {dataTypeDesc}");
+ sb.AppendLine($"数据长度: {dataLen} 字节");
+
+ if (data.Length > 7 && dataLen > 0)
+ {
+ byte[] meterData = data.Skip(7).Take(dataLen).ToArray();
+ sb.AppendLine($"表计数据: {BitConverter.ToString(meterData).Replace("-", " ")}");
+
+ // 转换为数值显示(大端序)
+ if (meterData.Length == 4)
+ {
+ uint value = ((uint)meterData[0] << 24) | ((uint)meterData[1] << 16) |
+ ((uint)meterData[2] << 8) | meterData[3];
+ sb.AppendLine($"数值: {value}");
+ }
+ }
+ }
+ break;
+
+ case 0x09: // 读取数据
+ sb.AppendLine("数据读取报文");
+ if (data.Length > 5)
+ {
+ byte dataType = data[5];
+ string dataTypeDesc = dataType switch
+ {
+ 0x01 => "水表数据",
+ 0x02 => "电表数据",
+ 0x03 => "气表数据",
+ 0x04 => "状态数据",
+ _ => $"未知类型(0x{dataType:X2})"
+ };
+ sb.AppendLine($"请求的数据类型: {dataTypeDesc}");
+ }
+ break;
+
+ default:
+ sb.AppendLine($"未知AFN(0x{afn:X2})报文,无法解析具体内容");
+ break;
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 计算校验和
+ ///
+ private static byte CalculateChecksum(byte[] message)
+ {
+ byte sum = 0;
+ // 从第二个68H开始计算校验和
+ for (int i = 5; i < message.Length - 2; i++)
+ {
+ sum += message[i];
+ }
+ return sum;
+ }
+
+ ///
+ /// 将BCD码转换为字符串
+ ///
+ private static string BcdToString(byte[] bcd)
+ {
+ var sb = new StringBuilder();
+ foreach (byte b in bcd)
+ {
+ sb.Append((b >> 4) & 0x0F);
+ sb.Append(b & 0x0F);
+ }
+ return sb.ToString().TrimStart('0');
+ }
+ }
+}
\ No newline at end of file
diff --git a/Models/Parameter.cs b/Models/Parameter.cs
new file mode 100644
index 0000000..5af8df8
--- /dev/null
+++ b/Models/Parameter.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace Protocol376Simulator.Models
+{
+ ///
+ /// 参数生成辅助类
+ ///
+ public static class Parameter
+ {
+ ///
+ /// 创建参数数据
+ ///
+ public static byte[] Create(byte paramType, string value)
+ {
+ return System.Text.Encoding.UTF8.GetBytes(value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Models/Protocol376Message.cs b/Models/Protocol376Message.cs
new file mode 100644
index 0000000..94dd0b2
--- /dev/null
+++ b/Models/Protocol376Message.cs
@@ -0,0 +1,443 @@
+using System;
+using System.Text;
+using Protocol376Simulator.Interfaces;
+
+namespace Protocol376Simulator.Models
+{
+ ///
+ /// 376.1协议消息类
+ ///
+ public class Protocol376Message : IProtocolMessage
+ {
+ public byte StartByte { get; set; } = 0x68;
+ public byte Length { get; set; }
+ public byte ControlCode { get; set; }
+ public byte[] Address { get; set; } = new byte[4];
+ public byte[] Data { get; set; }
+ public byte CheckSum { get; set; }
+ public byte EndByte { get; set; } = 0x16;
+ public MessageType Type { get; set; }
+
+ ///
+ /// 将消息转换为字节数组
+ ///
+ public byte[] ToBytes()
+ {
+ // 根据模板生成报文:68 36 00 36 00 68 C9 XX XX XX XX 07 02 70 00 00 04 00 29 F4 CS 16
+ var dataLength = Data?.Length ?? 0;
+ var message = new byte[dataLength + 13]; // 计算实际长度
+
+ // 前缀部分
+ message[0] = 0x68; // 起始字节
+ message[1] = 0x36; // 长度域1
+ message[2] = 0x00; // 长度域2
+ message[3] = 0x36; // 长度域3(重复)
+ message[4] = 0x00; // 长度域4(重复)
+ message[5] = 0x68; // 起始字节(重复)
+ message[6] = ControlCode;
+
+ // 地址域
+ Array.Copy(Address, 0, message, 7, 4);
+
+ // 数据域
+ if (Data != null)
+ {
+ Array.Copy(Data, 0, message, 11, dataLength);
+ }
+
+ // 计算校验和
+ byte sum = 0;
+ for (int i = 5; i < message.Length - 2; i++) // 从第二个68H开始计算校验和
+ {
+ sum += message[i];
+ }
+ message[message.Length - 2] = sum;
+
+ // 结束字节
+ message[message.Length - 1] = 0x16;
+
+ return message;
+ }
+
+ ///
+ /// 获取消息详细信息
+ ///
+ public string GetMessageInfo()
+ {
+ var sb = new StringBuilder();
+
+ // 打印完整报文
+ var fullMessage = ToBytes();
+ sb.Append("完整报文: ");
+ foreach (var b in fullMessage)
+ {
+ sb.Append($"{b:X2} ");
+ }
+ sb.AppendLine();
+
+ // 打印报文内容解析
+ sb.AppendLine("报文解析:");
+ sb.AppendLine($"报文类型: {Type}");
+ sb.AppendLine($"控制码: 0x{ControlCode:X2}");
+
+ // 地址域
+ sb.Append("地址域: ");
+ foreach (var b in Address)
+ {
+ sb.Append($"{b:X2} ");
+ }
+ sb.AppendLine();
+
+ // 数据域
+ if (Data != null && Data.Length > 0)
+ {
+ sb.Append("数据域: ");
+ foreach (var b in Data)
+ {
+ sb.Append($"{b:X2} ");
+ }
+ sb.AppendLine();
+
+ // 根据不同报文类型解析数据域
+ sb.AppendLine(ParseDataByType());
+ }
+ else
+ {
+ sb.AppendLine("数据域: 无");
+ }
+
+ return sb.ToString();
+ }
+
+ private string ParseDataByType()
+ {
+ if (Data == null || Data.Length == 0)
+ return "数据域解析: 无数据";
+
+ var sb = new StringBuilder();
+ sb.AppendLine("数据域解析:");
+
+ switch (Type)
+ {
+ case MessageType.Login:
+ sb.AppendLine(" 登录报文");
+ if (Data.Length >= 7)
+ {
+ sb.AppendLine($" AFN: 0x{Data[0]:X2} - 登录请求");
+ sb.AppendLine($" 数据单元标识: {BitConverter.ToString(Data, 1, 2).Replace("-", " ")}");
+ }
+ break;
+
+ case MessageType.Heartbeat:
+ sb.AppendLine(" 心跳报文");
+ if (Data.Length >= 9)
+ {
+ sb.AppendLine($" AFN: 0x{Data[0]:X2} - 心跳");
+ sb.AppendLine($" 数据单元标识: {BitConverter.ToString(Data, 1, 2).Replace("-", " ")}");
+ sb.AppendLine($" 自检状态信息: {BitConverter.ToString(Data, 7, 2).Replace("-", " ")}");
+ }
+ break;
+
+ case MessageType.ValveControl:
+ sb.AppendLine(" 阀控操作报文");
+ if (Data.Length >= 7)
+ {
+ sb.AppendLine($" AFN: 0x{Data[0]:X2}");
+ sb.AppendLine($" 数据单元标识: {BitConverter.ToString(Data, 1, 2).Replace("-", " ")}");
+ sb.AppendLine($" 阀控操作码: 0x{Data[5]:X2}");
+ string operation = Data[5] switch
+ {
+ 0x01 => "开阀",
+ 0x02 => "关阀",
+ 0x03 => "查询状态",
+ _ => "未知操作"
+ };
+ sb.AppendLine($" 操作: {operation}");
+ }
+ break;
+
+ case MessageType.DataUpload:
+ sb.AppendLine(" 数据上传报文");
+ if (Data.Length >= 5)
+ {
+ sb.AppendLine($" AFN: 0x{Data[0]:X2}");
+ sb.AppendLine($" 数据单元标识: {BitConverter.ToString(Data, 1, 2).Replace("-", " ")}");
+ sb.AppendLine($" 数据长度: {Data.Length - 5} 字节");
+ }
+ break;
+
+ case MessageType.Response:
+ sb.AppendLine(" 响应报文");
+ if (Data.Length >= 3)
+ {
+ sb.AppendLine($" AFN: 0x{Data[0]:X2}");
+ sb.AppendLine($" 数据单元标识: {BitConverter.ToString(Data, 1, 2).Replace("-", " ")}");
+ if (Data.Length > 3)
+ {
+ sb.AppendLine($" 结果码: 0x{Data[3]:X2}");
+ string result = Data[3] == 0 ? "成功" : "失败";
+ sb.AppendLine($" 操作结果: {result}");
+ }
+ }
+ break;
+
+ default:
+ sb.AppendLine(" 未知报文类型");
+ break;
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 转换集中器地址为字节数组
+ ///
+ private static byte[] ConvertAddress(string concentratorAddress)
+ {
+ try
+ {
+ // 使用 BCD 编码转换集中器地址
+ var address = new byte[4];
+
+ // 假设集中器地址是9位数字,转换为4字节BCD
+ if (concentratorAddress.Length != 9)
+ {
+ throw new ArgumentException("集中器地址必须是9位数字");
+ }
+
+ // 左边高字节,右边低字节
+ address[0] = byte.Parse(concentratorAddress.Substring(0, 2), System.Globalization.NumberStyles.HexNumber);
+ address[1] = byte.Parse(concentratorAddress.Substring(2, 2), System.Globalization.NumberStyles.HexNumber);
+ address[2] = byte.Parse(concentratorAddress.Substring(4, 2), System.Globalization.NumberStyles.HexNumber);
+ address[3] = byte.Parse(concentratorAddress.Substring(6, 2) + "0", System.Globalization.NumberStyles.HexNumber);
+
+ return address;
+ }
+ catch (Exception ex)
+ {
+ throw new ArgumentException($"集中器地址转换失败: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 创建登录消息
+ ///
+ /// 集中器地址
+ public static Protocol376Message CreateLoginMessage(string concentratorAddress)
+ {
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ // 数据域 - 登录请求
+ Data = new byte[] { 0x07, 0x02, 0x70, 0x00, 0x00, 0x04, 0x00, 0x29, 0xF4 },
+ Type = MessageType.Login
+ };
+
+ return message;
+ }
+
+ ///
+ /// 创建心跳消息
+ ///
+ /// 集中器地址
+ public static Protocol376Message CreateHeartbeatMessage(string concentratorAddress)
+ {
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ // 数据域 - 心跳
+ Data = new byte[] { 0x08, 0x02, 0x70, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00 },
+ Type = MessageType.Heartbeat
+ };
+
+ return message;
+ }
+
+ ///
+ /// 创建阀控消息
+ ///
+ /// 集中器地址
+ /// 阀门操作:1=开阀,2=关阀,3=查询状态
+ public static Protocol376Message CreateValveControlMessage(string concentratorAddress, byte valveOperation)
+ {
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ // 数据域 - 阀控操作
+ Data = new byte[] { 0x09, 0x02, 0x70, 0x00, 0x00, valveOperation },
+ Type = MessageType.ValveControl
+ };
+
+ return message;
+ }
+
+ ///
+ /// 创建数据上传消息
+ ///
+ /// 集中器地址
+ /// 表计数据
+ public static Protocol376Message CreateDataUploadMessage(string concentratorAddress, byte[] data)
+ {
+ if (data == null || data.Length == 0)
+ {
+ throw new ArgumentException("表计数据不能为空");
+ }
+
+ // 构造数据域
+ var dataField = new byte[5 + data.Length];
+ dataField[0] = 0x0A; // AFN
+ dataField[1] = 0x02; // 数据单元标识1
+ dataField[2] = 0x70; // 数据单元标识2
+ dataField[3] = 0x00; // 时间标签1
+ dataField[4] = 0x00; // 时间标签2
+
+ // 拷贝表计数据
+ Array.Copy(data, 0, dataField, 5, data.Length);
+
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ Data = dataField,
+ Type = MessageType.DataUpload
+ };
+
+ return message;
+ }
+
+ ///
+ /// 创建读数据消息
+ ///
+ /// 集中器地址
+ /// 数据类型:1=水表,2=电表,3=气表,4=状态
+ public static Protocol376Message CreateReadDataMessage(string concentratorAddress, byte dataType)
+ {
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ // 数据域 - 读数据
+ Data = new byte[] { 0x0B, 0x02, 0x70, 0x00, 0x00, dataType },
+ Type = MessageType.ReadData
+ };
+
+ return message;
+ }
+
+ ///
+ /// 创建设置参数消息
+ ///
+ /// 集中器地址
+ /// 参数类型
+ /// 参数数据
+ public static Protocol376Message CreateSetParameterMessage(string concentratorAddress, byte paramType, byte[] paramData)
+ {
+ if (paramData == null || paramData.Length == 0)
+ {
+ throw new ArgumentException("参数数据不能为空");
+ }
+
+ // 构造数据域
+ var dataField = new byte[6 + paramData.Length];
+ dataField[0] = 0x0C; // AFN
+ dataField[1] = 0x02; // 数据单元标识1
+ dataField[2] = 0x70; // 数据单元标识2
+ dataField[3] = 0x00; // 时间标签1
+ dataField[4] = 0x00; // 时间标签2
+ dataField[5] = paramType; // 参数类型
+
+ // 拷贝参数数据
+ Array.Copy(paramData, 0, dataField, 6, paramData.Length);
+
+ var message = new Protocol376Message
+ {
+ ControlCode = 0xC9, // 控制码
+ Address = ConvertAddress(concentratorAddress),
+ Data = dataField,
+ Type = MessageType.SetParameter
+ };
+
+ return message;
+ }
+
+ ///
+ /// 从字节数组解析消息
+ ///
+ /// 消息字节数组
+ /// 解析后的消息对象
+ public static Protocol376Message ParseFromBytes(byte[] messageBytes)
+ {
+ if (messageBytes == null || messageBytes.Length < 13)
+ {
+ throw new ArgumentException("消息字节数组格式无效");
+ }
+
+ // 检查起始字节和结束字节
+ if (messageBytes[0] != 0x68 || messageBytes[5] != 0x68 || messageBytes[messageBytes.Length - 1] != 0x16)
+ {
+ throw new ArgumentException("消息格式无效:起始或结束字节错误");
+ }
+
+ // 计算校验和
+ byte calculatedChecksum = 0;
+ for (int i = 5; i < messageBytes.Length - 2; i++)
+ {
+ calculatedChecksum += messageBytes[i];
+ }
+
+ // 验证校验和
+ byte receivedChecksum = messageBytes[messageBytes.Length - 2];
+ if (calculatedChecksum != receivedChecksum)
+ {
+ throw new ArgumentException($"校验和错误: 计算={calculatedChecksum:X2}, 接收={receivedChecksum:X2}");
+ }
+
+ // 提取控制码
+ byte controlCode = messageBytes[6];
+
+ // 提取地址域
+ byte[] address = new byte[4];
+ Array.Copy(messageBytes, 7, address, 0, 4);
+
+ // 提取数据域
+ int dataLength = messageBytes.Length - 13;
+ byte[] data = new byte[dataLength];
+ if (dataLength > 0)
+ {
+ Array.Copy(messageBytes, 11, data, 0, dataLength);
+ }
+
+ // 识别消息类型
+ MessageType messageType = MessageType.Response; // 默认为响应类型
+
+ if (dataLength > 0)
+ {
+ messageType = data[0] switch
+ {
+ 0x07 => MessageType.Login,
+ 0x08 => MessageType.Heartbeat,
+ 0x09 => MessageType.ValveControl,
+ 0x0A => MessageType.DataUpload,
+ 0x0B => MessageType.ReadData,
+ 0x0C => MessageType.SetParameter,
+ _ => MessageType.Response
+ };
+ }
+
+ // 构造消息对象
+ var message = new Protocol376Message
+ {
+ ControlCode = controlCode,
+ Address = address,
+ Data = data,
+ Type = messageType,
+ CheckSum = receivedChecksum
+ };
+
+ return message;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Models/RandomDataGenerator.cs b/Models/RandomDataGenerator.cs
new file mode 100644
index 0000000..a145db8
--- /dev/null
+++ b/Models/RandomDataGenerator.cs
@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+
+namespace Protocol376Simulator.Models
+{
+ ///
+ /// 模拟数据随机生成器
+ ///
+ public class RandomDataGenerator
+ {
+ private readonly Random _random = new Random();
+ private readonly Dictionary _lastValues = new Dictionary();
+
+ // 不同时段用水模式
+ private static readonly Dictionary WaterUsagePatterns = new Dictionary
+ {
+ { 0, (0, 2) }, // 0-1点
+ { 1, (0, 1) }, // 1-2点
+ { 2, (0, 1) }, // 2-3点
+ { 3, (0, 1) }, // 3-4点
+ { 4, (0, 1) }, // 4-5点
+ { 5, (1, 3) }, // 5-6点 早晨起床
+ { 6, (3, 15) }, // 6-7点 早晨高峰
+ { 7, (5, 20) }, // 7-8点 早晨高峰
+ { 8, (3, 10) }, // 8-9点
+ { 9, (1, 5) }, // 9-10点
+ { 10, (1, 4) }, // 10-11点
+ { 11, (2, 8) }, // 11-12点 午饭准备
+ { 12, (3, 10) }, // 12-13点 午饭高峰
+ { 13, (1, 5) }, // 13-14点
+ { 14, (1, 3) }, // 14-15点
+ { 15, (1, 4) }, // 15-16点
+ { 16, (2, 6) }, // 16-17点
+ { 17, (3, 12) }, // 17-18点 晚饭准备
+ { 18, (5, 15) }, // 18-19点 晚饭高峰
+ { 19, (4, 10) }, // 19-20点 晚饭高峰
+ { 20, (3, 8) }, // 20-21点
+ { 21, (2, 5) }, // 21-22点
+ { 22, (1, 3) }, // 22-23点
+ { 23, (0, 2) } // 23-24点
+ };
+
+ // 不同时段用电模式
+ private static readonly Dictionary ElectricityUsagePatterns = new Dictionary
+ {
+ { 0, (5, 20) }, // 0-1点 深夜基础用电
+ { 1, (5, 15) }, // 1-2点
+ { 2, (5, 15) }, // 2-3点
+ { 3, (5, 15) }, // 3-4点
+ { 4, (5, 15) }, // 4-5点
+ { 5, (10, 30) }, // 5-6点 早起用电
+ { 6, (20, 50) }, // 6-7点 早晨高峰
+ { 7, (30, 80) }, // 7-8点 早晨高峰
+ { 8, (20, 60) }, // 8-9点
+ { 9, (15, 40) }, // 9-10点
+ { 10, (15, 40) }, // 10-11点
+ { 11, (20, 50) }, // 11-12点
+ { 12, (25, 70) }, // 12-13点 午饭高峰
+ { 13, (20, 50) }, // 13-14点
+ { 14, (15, 40) }, // 14-15点
+ { 15, (15, 35) }, // 15-16点
+ { 16, (20, 45) }, // 16-17点
+ { 17, (30, 80) }, // 17-18点 晚饭准备
+ { 18, (40, 100) }, // 18-19点 晚高峰
+ { 19, (50, 120) }, // 19-20点 晚高峰
+ { 20, (40, 100) }, // 20-21点 晚高峰
+ { 21, (30, 70) }, // 21-22点
+ { 22, (20, 50) }, // 22-23点
+ { 23, (10, 30) } // 23-24点
+ };
+
+ // 不同时段用气模式
+ private static readonly Dictionary GasUsagePatterns = new Dictionary
+ {
+ { 0, (0, 1) }, // 0-1点
+ { 1, (0, 0) }, // 1-2点
+ { 2, (0, 0) }, // 2-3点
+ { 3, (0, 0) }, // 3-4点
+ { 4, (0, 0) }, // 4-5点
+ { 5, (0, 2) }, // 5-6点
+ { 6, (2, 10) }, // 6-7点 早餐准备
+ { 7, (3, 15) }, // 7-8点 早餐高峰
+ { 8, (1, 5) }, // 8-9点
+ { 9, (0, 2) }, // 9-10点
+ { 10, (0, 2) }, // 10-11点
+ { 11, (2, 10) }, // 11-12点 午饭准备
+ { 12, (3, 12) }, // 12-13点 午饭高峰
+ { 13, (1, 3) }, // 13-14点
+ { 14, (0, 2) }, // 14-15点
+ { 15, (0, 2) }, // 15-16点
+ { 16, (1, 3) }, // 16-17点
+ { 17, (3, 15) }, // 17-18点 晚饭准备
+ { 18, (5, 20) }, // 18-19点 晚饭高峰
+ { 19, (3, 10) }, // 19-20点
+ { 20, (1, 5) }, // 20-21点
+ { 21, (1, 3) }, // 21-22点
+ { 22, (0, 2) }, // 22-23点
+ { 23, (0, 1) } // 23-24点
+ };
+
+ ///
+ /// 生成递增的表计数据
+ ///
+ /// 表计类型: 1=水表, 2=电表, 3=气表
+ /// 上一次的数据,如果没有则为null
+ /// 新的表计数据
+ public byte[] GenerateMeterData(byte dataType, byte[] previousData = null)
+ {
+ // 如果有上一次的数据,从中获取当前值
+ uint currentValue = 0;
+
+ // 先检查缓存中是否有上次的值
+ if (!_lastValues.TryGetValue(dataType, out currentValue))
+ {
+ // 如果缓存中没有,尝试从previousData获取
+ if (previousData != null && previousData.Length == 4)
+ {
+ currentValue = ((uint)previousData[0] << 24) | ((uint)previousData[1] << 16) |
+ ((uint)previousData[2] << 8) | previousData[3];
+ }
+ else
+ {
+ // 如果都没有,根据表计类型设置初始值
+ currentValue = dataType switch
+ {
+ 1 => 1000, // 水表初始值,单位立方米
+ 2 => 10000, // 电表初始值,单位千瓦时
+ 3 => 5000, // 气表初始值,单位立方米
+ _ => 0
+ };
+ }
+ }
+
+ // 生成增量
+ uint increment = CalculateIncrement(dataType, DateTime.Now);
+ uint newValue = currentValue + increment;
+
+ // 更新缓存
+ _lastValues[dataType] = newValue;
+
+ // 转换为4字节数组,大端序
+ byte[] result = new byte[4];
+ result[0] = (byte)((newValue >> 24) & 0xFF);
+ result[1] = (byte)((newValue >> 16) & 0xFF);
+ result[2] = (byte)((newValue >> 8) & 0xFF);
+ result[3] = (byte)(newValue & 0xFF);
+
+ return result;
+ }
+
+ ///
+ /// 生成状态数据
+ ///
+ /// 状态数据
+ public byte[] GenerateStatusData()
+ {
+ // 生成随机的状态数据
+ byte[] statusData = new byte[2];
+ statusData[0] = (byte)_random.Next(0, 3); // 状态码
+ statusData[1] = (byte)_random.Next(0, 256); // 状态标志
+
+ return statusData;
+ }
+
+ ///
+ /// 根据参数类型生成参数数据
+ ///
+ /// 参数类型
+ /// 参数数据
+ public byte[] GenerateParameter(byte paramType)
+ {
+ switch (paramType)
+ {
+ case 0x01: // 心跳间隔
+ byte[] heartbeatInterval = new byte[2];
+ int minutes = _random.Next(1, 10);
+ heartbeatInterval[0] = 0;
+ heartbeatInterval[1] = (byte)minutes;
+ return heartbeatInterval;
+
+ case 0x02: // 设备参数
+ byte[] deviceParams = new byte[4];
+ for (int i = 0; i < deviceParams.Length; i++)
+ {
+ deviceParams[i] = (byte)_random.Next(0, 256);
+ }
+ return deviceParams;
+
+ case 0x03: // 阈值参数
+ byte[] thresholdParams = new byte[2];
+ thresholdParams[0] = (byte)_random.Next(10, 100);
+ thresholdParams[1] = (byte)_random.Next(0, 100);
+ return thresholdParams;
+
+ default:
+ byte[] defaultParams = new byte[2];
+ defaultParams[0] = (byte)_random.Next(0, 256);
+ defaultParams[1] = (byte)_random.Next(0, 256);
+ return defaultParams;
+ }
+ }
+
+ ///
+ /// 根据表计类型和时间计算合理的增量
+ ///
+ private uint CalculateIncrement(byte dataType, DateTime time)
+ {
+ int hour = time.Hour;
+
+ // 根据表计类型选择不同的用量模式
+ (int Min, int Max) pattern = dataType switch
+ {
+ 1 => WaterUsagePatterns.ContainsKey(hour) ? WaterUsagePatterns[hour] : (0, 2), // 水表
+ 2 => ElectricityUsagePatterns.ContainsKey(hour) ? ElectricityUsagePatterns[hour] : (10, 30), // 电表
+ 3 => GasUsagePatterns.ContainsKey(hour) ? GasUsagePatterns[hour] : (0, 2), // 气表
+ _ => (0, 1) // 默认
+ };
+
+ // 计算增量,考虑一定的随机性
+ int baseValue = _random.Next(pattern.Min, pattern.Max + 1);
+
+ // 添加一些季节性变化 - 夏季用水电多,冬季用气多
+ int month = time.Month;
+ double seasonalFactor = 1.0;
+
+ if (dataType == 1) // 水表,夏季用水增加
+ {
+ if (month >= 6 && month <= 8) // 夏季
+ seasonalFactor = 1.3;
+ else if (month >= 12 || month <= 2) // 冬季
+ seasonalFactor = 0.8;
+ }
+ else if (dataType == 2) // 电表,夏冬用电增加(空调因素)
+ {
+ if ((month >= 6 && month <= 8) || (month == 12 || month <= 2)) // 夏冬季
+ seasonalFactor = 1.5;
+ }
+ else if (dataType == 3) // 气表,冬季用气增加
+ {
+ if (month >= 12 || month <= 2) // 冬季
+ seasonalFactor = 2.0;
+ else if (month >= 6 && month <= 8) // 夏季
+ seasonalFactor = 0.5;
+ }
+
+ return (uint)(baseValue * seasonalFactor);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Models/TestScenario.cs b/Models/TestScenario.cs
new file mode 100644
index 0000000..d4ad7c8
--- /dev/null
+++ b/Models/TestScenario.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Protocol376Simulator.Models
+{
+ ///
+ /// 测试场景,用于保存和加载测试配置
+ ///
+ public class TestScenario
+ {
+ ///
+ /// 场景名称
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 场景描述
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// 服务器地址
+ ///
+ public string ServerAddress { get; set; }
+
+ ///
+ /// 服务器端口
+ ///
+ public int ServerPort { get; set; }
+
+ ///
+ /// 集中器配置列表
+ ///
+ public List Concentrators { get; set; } = new List();
+
+ ///
+ /// 测试步骤列表
+ ///
+ public List Steps { get; set; } = new List();
+
+ ///
+ /// 将测试场景保存到文件
+ ///
+ public static void SaveToFile(TestScenario scenario, string filePath)
+ {
+ string directory = Path.GetDirectoryName(filePath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var json = JsonSerializer.Serialize(scenario, new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+
+ File.WriteAllText(filePath, json);
+ }
+
+ ///
+ /// 从文件加载测试场景
+ ///
+ public static TestScenario LoadFromFile(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException($"测试场景文件不存在: {filePath}");
+ }
+
+ string json = File.ReadAllText(filePath);
+ return JsonSerializer.Deserialize(json);
+ }
+
+ ///
+ /// 获取所有保存的测试场景文件
+ ///
+ public static List GetAllScenarioFiles(string directory = "Scenarios")
+ {
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ return new List();
+ }
+
+ var files = Directory.GetFiles(directory, "*.json");
+ return new List(files);
+ }
+ }
+
+ ///
+ /// 集中器配置
+ ///
+ public class ConcentratorConfig
+ {
+ ///
+ /// 集中器地址
+ ///
+ public string Address { get; set; }
+
+ ///
+ /// 是否自动登录
+ ///
+ public bool AutoLogin { get; set; } = true;
+
+ ///
+ /// 是否自动心跳
+ ///
+ public bool AutoHeartbeat { get; set; } = true;
+
+ ///
+ /// 是否自动响应
+ ///
+ public bool AutoResponse { get; set; } = true;
+
+ ///
+ /// 是否启用断线重连
+ ///
+ public bool AutoReconnect { get; set; } = true;
+
+ ///
+ /// 最大重连尝试次数
+ ///
+ public int MaxReconnectAttempts { get; set; } = 5;
+
+ ///
+ /// 重连延迟时间(秒)
+ ///
+ public int ReconnectDelaySeconds { get; set; } = 5;
+
+ ///
+ /// 初始表计数据
+ ///
+ public Dictionary InitialMeterData { get; set; } = new Dictionary();
+ }
+
+ ///
+ /// 测试步骤
+ ///
+ public class TestStep
+ {
+ ///
+ /// 步骤类型枚举
+ ///
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public enum StepType
+ {
+ Login,
+ Heartbeat,
+ ValveControl,
+ DataUpload,
+ ReadData,
+ SetParameter,
+ Wait,
+ SetMeterData
+ }
+
+ ///
+ /// 步骤类型
+ ///
+ public StepType Type { get; set; }
+
+ ///
+ /// 目标集中器地址,"all"表示所有集中器
+ ///
+ public string ConcentratorAddress { get; set; }
+
+ ///
+ /// 步骤描述
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// 步骤参数,根据不同步骤类型使用不同的参数
+ ///
+ public Dictionary Parameters { get; set; } = new Dictionary();
+
+ ///
+ /// 执行本步骤前的延迟时间(毫秒)
+ ///
+ public int DelayBeforeStepMs { get; set; } = 0;
+
+ ///
+ /// 执行下一步骤前的延迟时间(毫秒)
+ ///
+ public int DelayAfterStepMs { get; set; } = 1000;
+ }
+}
\ No newline at end of file
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..42624a3
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..6cbd10e
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,152 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Protocol376Simulator.CommandProcessor;
+using Protocol376Simulator.Factory;
+using Serilog;
+
+namespace Protocol376Simulator
+{
+ ///
+ /// 程序入口类
+ ///
+ class Program
+ {
+ private static string _serverAddress = "127.0.0.1";
+ private static int _serverPort = 10502; // 默认端口
+ private static readonly CommandHandler _commandHandler = new CommandHandler();
+
+ ///
+ /// 程序入口点
+ ///
+ static async Task Main(string[] args)
+ {
+ // 配置Serilog
+ ConfigureLogger();
+
+ Log.Information("376.1协议集中器模拟器启动");
+ Log.Information("--------------------------------------");
+
+ // 设置默认服务器配置
+ SimulatorFactory.SetServerConfig(_serverAddress, _serverPort);
+ Log.Information("服务器地址: {ServerAddress}:{ServerPort}", _serverAddress, _serverPort);
+ Log.Information("--------------------------------------");
+ Log.Information("输入help查看所有可用命令");
+ Log.Information("--------------------------------------");
+
+ // 处理命令行参数,支持自动启动
+ if (args.Length > 0)
+ {
+ await ProcessCommandLineArgs(args);
+ }
+
+ // 主循环
+ bool running = true;
+ while (running)
+ {
+ Console.Write("\n> ");
+ string input = Console.ReadLine();
+
+ if (string.IsNullOrWhiteSpace(input))
+ continue;
+
+ if (input.ToLower() == "exit" || input.ToLower() == "quit" || input == "0")
+ {
+ running = false;
+ continue;
+ }
+
+ // 处理命令
+ await _commandHandler.ProcessCommand(input.Split(' '));
+ }
+
+ // 清空所有模拟器
+ await SimulatorFactory.ClearAllSimulatorsAsync();
+
+ Log.Information("程序已退出");
+ Log.CloseAndFlush();
+ }
+
+ ///
+ /// 配置日志记录器
+ ///
+ private static void ConfigureLogger()
+ {
+ // 确保日志目录存在
+ Directory.CreateDirectory("Logs");
+
+ // 配置Serilog
+ Log.Logger = new LoggerConfiguration()
+ .MinimumLevel.Information()
+ .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .WriteTo.File("Logs/Protocol376-.log",
+ rollingInterval: RollingInterval.Day,
+ outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3}] {Message:lj}{NewLine}{Exception}")
+ .CreateLogger();
+ }
+
+ ///
+ /// 处理命令行参数
+ ///
+ private static async Task ProcessCommandLineArgs(string[] args)
+ {
+ try
+ {
+ // 示例: auto-login 10 表示自动创建10个集中器并登录
+ if (args[0].ToLower() == "auto-login" && args.Length > 1 && int.TryParse(args[1], out int count))
+ {
+ Log.Information("自动模式: 创建并登录 {Count} 个集中器", count);
+
+ // 使用工厂创建集中器
+ var addresses = SimulatorFactory.BatchCreateConcentrators(count);
+
+ // 批量连接并登录
+ foreach (var address in addresses)
+ {
+ var simulator = SimulatorFactory.GetSimulator(address);
+ if (simulator != null)
+ {
+ // 启动并自动登录,启用自动心跳
+ await simulator.StartAsync(true, true);
+ }
+ }
+
+ Log.Information("自动登录完成,集中器将在收到登录确认后每4分钟发送一次心跳");
+ }
+ // 示例: auto-server 127.0.0.1 8888 表示设置服务器地址和端口
+ else if (args[0].ToLower() == "auto-server" && args.Length > 2)
+ {
+ _serverAddress = args[1];
+ if (int.TryParse(args[2], out int port))
+ {
+ _serverPort = port;
+ }
+ SimulatorFactory.SetServerConfig(_serverAddress, _serverPort);
+ Log.Information("服务器设置已更新: {ServerAddress}:{ServerPort}", _serverAddress, _serverPort);
+ }
+ // 示例: auto-create 312003001 1 1 表示创建地址为312003001的集中器,启用自动登录和自动心跳
+ else if (args[0].ToLower() == "auto-create" && args.Length > 1)
+ {
+ string address = args[1];
+ bool autoLogin = args.Length > 2 && args[2] == "1";
+ bool autoHeartbeat = args.Length > 3 && args[3] == "1";
+
+ Log.Information("自动创建集中器: 地址={Address}, 自动登录={AutoLogin}, 自动心跳={AutoHeartbeat}",
+ address, autoLogin, autoHeartbeat);
+
+ var simulator = SimulatorFactory.CreateConcentrator(address);
+ await simulator.StartAsync(autoLogin, autoHeartbeat);
+ }
+ // 将命令行参数作为命令处理
+ else
+ {
+ await _commandHandler.ProcessCommand(args);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "处理命令行参数时发生错误: {ErrorMessage}", ex.Message);
+ }
+ }
+ }
+}
diff --git a/Protocol376Simulator.csproj b/Protocol376Simulator.csproj
new file mode 100644
index 0000000..a771105
--- /dev/null
+++ b/Protocol376Simulator.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net8.0
+ disable
+ disable
+
+
+
+
+
+
+
+
+
diff --git a/Protocol376Simulator.sln b/Protocol376Simulator.sln
new file mode 100644
index 0000000..14c5261
--- /dev/null
+++ b/Protocol376Simulator.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.13.35825.156 d17.13
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Protocol376Simulator", "Protocol376Simulator.csproj", "{1B730078-32DC-0E99-09D9-DA797C41B69B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1B730078-32DC-0E99-09D9-DA797C41B69B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B730078-32DC-0E99-09D9-DA797C41B69B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B730078-32DC-0E99-09D9-DA797C41B69B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B730078-32DC-0E99-09D9-DA797C41B69B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {31DFF75B-E97D-4E40-8D23-97F05FC17D27}
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4fb6b1a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,295 @@
+# 376.1协议集中器测试程序
+
+本程序用于模拟集中器与主站通信,支持376.1协议标准。可以同时模拟多个集中器,并支持各种常见操作。
+
+## 功能特点
+
+1. **完整的376.1协议支持**
+ - 支持登录、心跳、阀控等标准操作
+ - 完整实现了报文结构和编码规则
+ - 支持BCD编码的地址域处理
+
+2. **多种消息类型**
+ - 登录消息
+ - 心跳消息
+ - 阀控操作消息
+ - 数据上传消息
+ - 数据读取消息
+ - 参数设置消息
+
+3. **多集中器管理**
+ - 支持创建多个集中器实例
+ - 批量创建集中器
+ - 自动生成集中器地址
+
+4. **自动化功能**
+ - 创建集中器后自动登录
+ - 登录确认后自动发送心跳(4分钟间隔)
+ - 自动响应接收到的请求
+ - 事件通知机制
+
+5. **数据模拟**
+ - 内置模拟表计数据(水表、电表、气表等)
+ - 支持自定义数据更新
+ - **新增**: 随机数据生成功能,模拟真实用水用电用气规律
+
+6. **日志系统**
+ - 集成Serilog日志框架
+ - 支持控制台和文件日志
+ - 可选的十六进制报文展示
+
+7. **报文分析工具** ✨
+ - 详细解析376.1协议报文
+ - 提供报文结构、控制码和数据域的解释
+ - 校验和验证功能
+
+8. **断线重连机制** ✨
+ - 自动检测连接断开
+ - 可配置的重连参数
+ - 重连成功后自动恢复之前的登录状态
+
+9. **通信统计** ✨
+ - 跟踪消息收发成功率
+ - 响应时间统计
+ - 错误类型分析
+
+10. **测试场景管理** ✨
+ - 保存和加载测试配置
+ - 创建自定义测试场景
+ - 支持批量运行测试步骤
+ - **新增**: 导出测试场景为批处理脚本,支持无人值守自动化测试
+
+## 使用方法
+
+### 启动程序
+
+编译并运行程序后,首先需要配置服务器地址和端口:
+
+```
+376.1协议集中器模拟器启动
+--------------------------------------
+服务器地址: 127.0.0.1:10502
+--------------------------------------
+输入help查看所有可用命令
+--------------------------------------
+```
+
+程序默认使用127.0.0.1:10502作为服务器地址和端口。
+
+### 自动启动模式
+
+程序支持命令行参数自动启动:
+
+```
+# 自动创建并登录10个集中器
+Protocol376Simulator.exe auto-login 10
+
+# 指定服务器地址和端口
+Protocol376Simulator.exe auto-server 192.168.1.100 8888
+```
+
+### 命令行界面
+
+程序使用简洁的命令行界面,所有命令都可通过输入`help`查看。所有命令都在同一层级,无需通过菜单导航。
+
+命令示例:
+```
+> c 312003001 # 创建集中器
+> login 312003001 # 登录集中器
+> valve 312003001 1 # 开阀
+> meter 312003001 1 random # 设置随机水表数据
+> stats all # 查看所有集中器的通信统计
+```
+
+界面设计特点:
+- 命令简洁直观
+- 无需在菜单间切换
+- 支持批量操作命令
+- help命令显示完整命令列表
+
+### 自动工作流程
+
+1. 创建集中器时会自动登录
+2. 登录成功确认后会自动启动心跳功能
+3. 心跳报文每4分钟发送一次
+4. 收到请求后自动生成响应报文
+
+### 数据类型说明
+
+数据类型代码:
+- 1: 水表
+- 2: 电表
+- 3: 气表
+- 4: 状态
+
+阀控操作码:
+- 1: 开阀
+- 2: 关阀
+- 3: 查询状态
+
+### 日志系统
+
+日志文件存储在 `Logs` 目录中,按日期自动分割。可以通过命令开启十六进制报文日志查看详细报文内容。
+
+## 断线重连功能
+
+系统支持自动断线重连机制,你可以配置以下参数:
+- 是否启用自动重连
+- 最大重连尝试次数
+- 重连间隔时间
+
+使用以下命令设置断线重连参数:
+```
+reconnect [集中器地址] [开启/关闭(1/0)] [最大尝试次数] [延迟秒数]
+```
+
+## 报文分析工具
+
+可以使用内置的报文分析工具解析任意376.1协议报文:
+```
+analyze [十六进制报文字符串]
+```
+
+报文分析结果会包含详细的帧结构、控制码解释和数据域分析。
+
+## 随机数据生成
+
+可以生成符合真实使用场景的随机表计数据:
+```
+meter [集中器地址] [数据类型] random
+```
+
+也可以开启随机数据模式,所有数据都会使用随机生成:
+```
+random-on
+```
+
+## 测试场景管理
+
+测试场景功能允许您保存、加载和运行测试配置,大大提高测试效率。
+
+### 创建测试场景
+
+创建一个新的测试场景,可以指定服务器配置和集中器数量:
+```
+create-scenario
+```
+系统会引导您输入场景名称、描述、服务器地址/端口和集中器数量。
+
+### 保存当前配置
+
+将当前运行状态保存为测试场景:
+```
+save-scenario [名称] [描述]
+```
+所有当前运行的集中器配置会被保存到场景中。
+
+### 加载测试场景
+
+加载一个已保存的测试场景:
+```
+load-scenario [文件名或路径]
+```
+系统会读取场景配置,并提示您是否要创建其中包含的集中器。
+
+### 查看所有场景
+
+列出所有保存的测试场景及其详细信息:
+```
+list-scenario
+```
+
+### 运行测试场景
+
+执行一个测试场景中定义的所有测试步骤:
+```
+run-scenario [文件名或路径]
+```
+
+### 导出为脚本
+
+将测试场景导出为可执行的批处理脚本:
+```
+export-scenario [场景文件路径] [脚本文件名]
+```
+导出的脚本将保存在`Scripts`目录中,可以在没有程序界面的情况下自动执行测试步骤。
+
+### 场景文件存储
+
+所有测试场景文件保存在 `Scenarios` 目录中,使用JSON格式存储,可以手动编辑。
+
+## 技术细节
+
+1. **地址域编码**
+ 程序使用BCD编码方式处理集中器地址,自动将集中器地址转换为4字节的地址域格式。
+
+2. **报文格式**
+ 符合376.1协议标准:`68 36 00 36 00 68 C9 XX XX XX XX 07 02 70 00 00 04 00 29 F4 CS 16`
+
+3. **校验和计算**
+ 自动计算并添加校验和字节,确保报文完整性。
+
+4. **自动心跳机制**
+ 在收到登录确认后,自动启动心跳发送,间隔为4分钟。
+
+5. **断线重连机制**
+ 自动检测连接断开,并根据设置的参数进行重连。
+
+## 开发技术
+
+- 编程语言:C#
+- 目标框架:.NET 8.0
+- 日志框架:Serilog 4.2.0
+- 网络通信:TCP/IP
+- 序列化:System.Text.Json
+
+## 注意事项
+
+1. 确保服务器地址和端口配置正确
+2. 正确使用集中器地址,推荐使用自动生成的地址
+3. 在退出程序前,建议手动断开所有集中器连接
+4. 创建集中器时会自动登录,无需手动执行登录命令
+
+## 自动化测试脚本使用
+
+本程序支持将测试场景导出为批处理脚本,便于执行无人值守的自动化测试:
+
+1. **创建测试场景**
+ ```
+ create-scenario
+ ```
+ 按照提示创建包含所需配置和测试步骤的场景。
+
+2. **导出为批处理脚本**
+ ```
+ export-scenario 场景文件名.json 脚本名称
+ ```
+ 将生成的脚本保存在`Scripts`目录下。
+
+3. **执行批处理脚本**
+ 直接双击或在命令行中运行生成的`.bat`文件即可自动执行测试场景中定义的所有步骤。
+
+脚本功能特点:
+- 自动配置服务器连接参数
+- 自动创建场景中定义的所有集中器
+- 按顺序执行所有测试步骤,包括登录、发送心跳、数据上传等
+- 支持等待和延时操作
+- 全自动执行,无需人工干预
+
+该功能特别适合:
+- 长时间稳定性测试
+- 压力测试和负载测试
+- 测试环境的快速搭建
+- 测试过程的标准化和规范化
+
+## 版本历史
+
+- V1.0: 基本的报文结构和集中器模拟器
+- V2.0: 添加心跳和阀控功能
+- V3.0: 改进交互方式和地址处理
+- V4.0: 添加批量创建和日志系统
+- V5.0: 增加数据上传、读取和设置功能
+- V6.0: 优化UI界面,分类显示命令
+- V7.0: 添加自动登录和登录确认后自动心跳功能
+- V8.0: 添加报文分析工具、断线重连机制、随机数据生成和测试场景管理功能
+- V8.1: 增加测试场景导出为脚本功能,支持自动化批量测试
\ No newline at end of file
diff --git a/Services/DeviceDataService.cs b/Services/DeviceDataService.cs
new file mode 100644
index 0000000..df5a1bd
--- /dev/null
+++ b/Services/DeviceDataService.cs
@@ -0,0 +1,191 @@
+using System;
+using System.Collections.Generic;
+using Serilog;
+
+namespace Protocol376Simulator.Services
+{
+ ///
+ /// 设备数据服务类,负责管理设备的业务数据
+ ///
+ public class DeviceDataService
+ {
+ private readonly string _deviceIdentifier;
+ private readonly Dictionary _meterData = new Dictionary();
+ private bool _valveStatus = false; // 阀门状态:false表示关闭,true表示打开
+
+ ///
+ /// 当数据更新时触发
+ ///
+ public event EventHandler DataUpdated;
+
+ ///
+ /// 当阀门状态改变时触发
+ ///
+ public event EventHandler ValveStatusChanged;
+
+ ///
+ /// 阀门状态
+ ///
+ public bool ValveStatus
+ {
+ get => _valveStatus;
+ private set
+ {
+ if (_valveStatus != value)
+ {
+ _valveStatus = value;
+ ValveStatusChanged?.Invoke(this, _valveStatus);
+ }
+ }
+ }
+
+ ///
+ /// 数据更新事件参数
+ ///
+ public class DataUpdateEventArgs : EventArgs
+ {
+ public byte DataType { get; }
+ public byte[] Data { get; }
+
+ public DataUpdateEventArgs(byte dataType, byte[] data)
+ {
+ DataType = dataType;
+ Data = data;
+ }
+ }
+
+ ///
+ /// 构造函数
+ ///
+ /// 设备标识(用于日志)
+ public DeviceDataService(string deviceIdentifier)
+ {
+ _deviceIdentifier = deviceIdentifier;
+
+ // 初始化模拟数据
+ InitializeSimulatedData();
+ }
+
+ ///
+ /// 初始化模拟数据
+ ///
+ private void InitializeSimulatedData()
+ {
+ // 类型1:水表数据(模拟立方米读数)
+ _meterData[0x01] = new byte[] { 0x00, 0x00, 0x12, 0x34 }; // 1234 立方米
+
+ // 类型2:电表数据(模拟千瓦时读数)
+ _meterData[0x02] = new byte[] { 0x00, 0x01, 0x23, 0x45 }; // 12345 千瓦时
+
+ // 类型3:气表数据(模拟立方米读数)
+ _meterData[0x03] = new byte[] { 0x00, 0x00, 0x56, 0x78 }; // 5678 立方米
+
+ // 类型4:状态数据
+ _meterData[0x04] = new byte[] { 0x00, 0x01 }; // 状态码
+ }
+
+ ///
+ /// 设置表计数据
+ ///
+ /// 数据类型
+ /// 数据内容
+ public void SetMeterData(byte dataType, byte[] data)
+ {
+ if (data == null)
+ {
+ throw new ArgumentNullException(nameof(data));
+ }
+
+ _meterData[dataType] = data;
+ Log.Debug("{DeviceId} 已设置类型 {DataType} 的表计数据: {Data}",
+ _deviceIdentifier, dataType, BitConverter.ToString(data));
+
+ // 触发数据更新事件
+ DataUpdated?.Invoke(this, new DataUpdateEventArgs(dataType, data));
+ }
+
+ ///
+ /// 获取表计数据
+ ///
+ /// 数据类型
+ /// 数据内容
+ public byte[] GetMeterData(byte dataType)
+ {
+ if (_meterData.TryGetValue(dataType, out byte[] data))
+ {
+ return data;
+ }
+
+ // 如果没有指定类型的数据,返回空数组
+ Log.Warning("{DeviceId} 请求的数据类型 {DataType} 不存在", _deviceIdentifier, dataType);
+ return new byte[0];
+ }
+
+ ///
+ /// 更新阀门状态
+ ///
+ /// 阀门是否打开
+ public void UpdateValveStatus(bool isOpen)
+ {
+ ValveStatus = isOpen;
+ Log.Information("{DeviceId} 阀门状态已更新为: {Status}",
+ _deviceIdentifier, isOpen ? "打开" : "关闭");
+ }
+
+ ///
+ /// 获取所有表计数据类型
+ ///
+ /// 数据类型列表
+ public IEnumerable GetAllDataTypes()
+ {
+ return _meterData.Keys;
+ }
+
+ ///
+ /// 检查是否存在指定类型的数据
+ ///
+ /// 数据类型
+ /// 是否存在
+ public bool HasData(byte dataType)
+ {
+ return _meterData.ContainsKey(dataType);
+ }
+
+ ///
+ /// 获取设备数据状态的字符串表示
+ ///
+ /// 格式化的数据状态
+ public string GetDataStatusReport()
+ {
+ var report = $"{_deviceIdentifier} 数据状态:\n";
+ report += $"阀门状态: {(ValveStatus ? "打开" : "关闭")}\n";
+
+ report += "表计数据:\n";
+ foreach (var entry in _meterData)
+ {
+ string typeName = GetDataTypeName(entry.Key);
+ string hexData = BitConverter.ToString(entry.Value);
+ report += $" 类型 {entry.Key} ({typeName}): {hexData}\n";
+ }
+
+ return report;
+ }
+
+ ///
+ /// 获取数据类型的名称
+ ///
+ /// 数据类型
+ /// 类型名称
+ private string GetDataTypeName(byte dataType)
+ {
+ return dataType switch
+ {
+ 0x01 => "水表数据",
+ 0x02 => "电表数据",
+ 0x03 => "气表数据",
+ 0x04 => "状态数据",
+ _ => "未知类型"
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/HeartbeatService.cs b/Services/HeartbeatService.cs
new file mode 100644
index 0000000..6c24d0a
--- /dev/null
+++ b/Services/HeartbeatService.cs
@@ -0,0 +1,216 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Serilog;
+
+namespace Protocol376Simulator.Services
+{
+ ///
+ /// 心跳服务类,负责处理定时发送心跳
+ ///
+ public class HeartbeatService
+ {
+ private readonly string _deviceIdentifier;
+ private readonly Func _heartbeatAction;
+ private CancellationTokenSource _heartbeatCancellationTokenSource;
+ private Task _heartbeatTask;
+ private readonly object _heartbeatLock = new object();
+ private bool _isRunning = false;
+ private TimeSpan _heartbeatInterval = TimeSpan.FromMinutes(4); // 默认心跳间隔4分钟
+ private int _successfulHeartbeats = 0;
+ private DateTime _lastHeartbeatTime = DateTime.MinValue;
+
+ ///
+ /// 当心跳发送成功时触发
+ ///
+ public event EventHandler HeartbeatSent;
+
+ ///
+ /// 成功的心跳次数
+ ///
+ public int SuccessfulHeartbeats => _successfulHeartbeats;
+
+ ///
+ /// 最后一次心跳时间
+ ///
+ public DateTime LastHeartbeatTime => _lastHeartbeatTime;
+
+ ///
+ /// 心跳间隔
+ ///
+ public TimeSpan HeartbeatInterval
+ {
+ get => _heartbeatInterval;
+ set => _heartbeatInterval = value;
+ }
+
+ ///
+ /// 心跳是否正在运行
+ ///
+ public bool IsRunning => _isRunning;
+
+ ///
+ /// 构造函数
+ ///
+ /// 设备标识(用于日志)
+ /// 执行心跳的委托函数
+ public HeartbeatService(string deviceIdentifier, Func heartbeatAction)
+ {
+ _deviceIdentifier = deviceIdentifier;
+ _heartbeatAction = heartbeatAction ?? throw new ArgumentNullException(nameof(heartbeatAction));
+ }
+
+ ///
+ /// 启动心跳服务
+ ///
+ public void Start()
+ {
+ lock (_heartbeatLock)
+ {
+ if (_isRunning)
+ {
+ return;
+ }
+
+ _heartbeatCancellationTokenSource?.Cancel();
+ _heartbeatCancellationTokenSource = new CancellationTokenSource();
+
+ _heartbeatTask = RunHeartbeatAsync(_heartbeatCancellationTokenSource.Token);
+ _isRunning = true;
+
+ Log.Information("{DeviceId} 已启动心跳服务,间隔: {Interval}分钟",
+ _deviceIdentifier, _heartbeatInterval.TotalMinutes);
+ }
+ }
+
+ ///
+ /// 停止心跳服务
+ ///
+ public void Stop()
+ {
+ lock (_heartbeatLock)
+ {
+ if (!_isRunning)
+ {
+ return;
+ }
+
+ _heartbeatCancellationTokenSource?.Cancel();
+ _isRunning = false;
+
+ Log.Information("{DeviceId} 已停止心跳服务", _deviceIdentifier);
+ }
+ }
+
+ ///
+ /// 重置心跳计数
+ ///
+ public void ResetHeartbeatCount()
+ {
+ _successfulHeartbeats = 0;
+ }
+
+ ///
+ /// 记录心跳成功
+ ///
+ public void RecordHeartbeatSuccess()
+ {
+ _successfulHeartbeats++;
+ _lastHeartbeatTime = DateTime.Now;
+ HeartbeatSent?.Invoke(this, _lastHeartbeatTime);
+ }
+
+ ///
+ /// 设置心跳间隔
+ ///
+ /// 间隔分钟数
+ public void SetHeartbeatInterval(double minutes)
+ {
+ if (minutes <= 0)
+ {
+ throw new ArgumentException("心跳间隔必须大于0分钟", nameof(minutes));
+ }
+
+ _heartbeatInterval = TimeSpan.FromMinutes(minutes);
+ Log.Information("{DeviceId} 心跳间隔已设置为 {Interval}分钟", _deviceIdentifier, minutes);
+
+ // 如果心跳正在运行,重启心跳任务以应用新的间隔
+ if (_isRunning)
+ {
+ Stop();
+ Start();
+ }
+ }
+
+ ///
+ /// 运行心跳任务
+ ///
+ private async Task RunHeartbeatAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ // 初始等待一段随机时间,避免多个设备同时发送心跳
+ int initialDelayMs = new Random().Next(1000, 5000);
+ await Task.Delay(initialDelayMs, cancellationToken);
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ // 执行心跳操作
+ await _heartbeatAction();
+
+ // 记录心跳成功
+ RecordHeartbeatSuccess();
+
+ Log.Debug("{DeviceId} 心跳已发送,下一次将在 {NextTime} 发送",
+ _deviceIdentifier, DateTime.Now.Add(_heartbeatInterval).ToString("HH:mm:ss"));
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 发送心跳时发生错误: {ErrorMessage}",
+ _deviceIdentifier, ex.Message);
+ }
+
+ // 等待下一次心跳
+ await Task.Delay(_heartbeatInterval, cancellationToken);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 任务被取消,正常退出
+ Log.Debug("{DeviceId} 心跳任务已取消", _deviceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 心跳任务发生异常: {ErrorMessage}", _deviceIdentifier, ex.Message);
+ }
+ finally
+ {
+ lock (_heartbeatLock)
+ {
+ _isRunning = false;
+ }
+ }
+ }
+
+ ///
+ /// 立即发送一次心跳
+ ///
+ public async Task SendHeartbeatImmediatelyAsync()
+ {
+ try
+ {
+ await _heartbeatAction();
+ RecordHeartbeatSuccess();
+ Log.Information("{DeviceId} 已立即发送心跳", _deviceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 立即发送心跳时发生错误: {ErrorMessage}",
+ _deviceIdentifier, ex.Message);
+ throw;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/NetworkService.cs b/Services/NetworkService.cs
new file mode 100644
index 0000000..2269591
--- /dev/null
+++ b/Services/NetworkService.cs
@@ -0,0 +1,269 @@
+using System;
+using System.Net.Sockets;
+using System.Threading;
+using System.Threading.Tasks;
+using Serilog;
+
+namespace Protocol376Simulator.Services
+{
+ ///
+ /// 网络服务类,负责TCP连接和网络通信
+ ///
+ public class NetworkService : IDisposable
+ {
+ private readonly string _serverAddress;
+ private readonly int _serverPort;
+ private TcpClient _client;
+ private NetworkStream _stream;
+ private CancellationTokenSource _cancellationTokenSource;
+ private readonly string _deviceIdentifier;
+
+ ///
+ /// 当接收到消息时触发
+ ///
+ public event EventHandler MessageReceived;
+
+ ///
+ /// 当连接状态改变时触发
+ ///
+ public event EventHandler ConnectionStatusChanged;
+
+ ///
+ /// 当发生错误时触发
+ ///
+ public event EventHandler ErrorOccurred;
+
+ ///
+ /// 连接状态枚举
+ ///
+ public enum ConnectionStatus
+ {
+ Disconnected,
+ Connecting,
+ Connected,
+ Failed,
+ Reconnecting
+ }
+
+ ///
+ /// 是否连接到服务器
+ ///
+ public bool IsConnected => _client != null && _client.Connected;
+
+ ///
+ /// 构造函数
+ ///
+ /// 服务器地址
+ /// 服务器端口
+ /// 设备标识(用于日志)
+ public NetworkService(string serverAddress, int serverPort, string deviceIdentifier)
+ {
+ _serverAddress = serverAddress;
+ _serverPort = serverPort;
+ _deviceIdentifier = deviceIdentifier;
+ _cancellationTokenSource = new CancellationTokenSource();
+ }
+
+ ///
+ /// 连接到服务器
+ ///
+ public async Task ConnectAsync()
+ {
+ try
+ {
+ // 如果已有连接,先断开
+ await DisconnectAsync();
+
+ // 重置取消标记
+ _cancellationTokenSource = new CancellationTokenSource();
+
+ // 触发状态改变事件
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Connecting);
+
+ // 连接服务器
+ _client = new TcpClient();
+ Log.Information("{DeviceId} 正在连接到服务器 {ServerAddress}:{ServerPort}...",
+ _deviceIdentifier, _serverAddress, _serverPort);
+
+ await _client.ConnectAsync(_serverAddress, _serverPort);
+ _stream = _client.GetStream();
+
+ Log.Information("{DeviceId} 已成功连接到服务器", _deviceIdentifier);
+
+ // 触发状态改变事件
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Connected);
+
+ // 启动接收消息任务
+ _ = StartReceiveMessagesAsync(_cancellationTokenSource.Token);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 连接失败: {ErrorMessage}", _deviceIdentifier, ex.Message);
+
+ // 触发错误事件
+ ErrorOccurred?.Invoke(this, ex);
+
+ // 触发状态改变事件
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Failed);
+
+ // 确保清理资源
+ _client?.Dispose();
+ _client = null;
+ _stream = null;
+
+ // 重新抛出异常,让调用者处理
+ throw;
+ }
+ }
+
+ ///
+ /// 断开连接
+ ///
+ public async Task DisconnectAsync()
+ {
+ try
+ {
+ // 取消所有后台任务
+ _cancellationTokenSource?.Cancel();
+
+ // 关闭网络流和客户端
+ _stream?.Close();
+ _client?.Close();
+
+ // 等待一段时间确保资源释放
+ await Task.Delay(100);
+
+ // 触发状态改变事件
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Disconnected);
+
+ Log.Information("{DeviceId} 已断开连接", _deviceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 断开连接时发生错误: {ErrorMessage}", _deviceIdentifier, ex.Message);
+ ErrorOccurred?.Invoke(this, ex);
+ }
+ finally
+ {
+ _stream = null;
+ _client = null;
+ }
+ }
+
+ ///
+ /// 发送消息
+ ///
+ /// 要发送的消息字节数组
+ public async Task SendMessageAsync(byte[] message)
+ {
+ if (!IsConnected)
+ {
+ throw new InvalidOperationException("未连接到服务器,无法发送消息");
+ }
+
+ try
+ {
+ await _stream.WriteAsync(message, 0, message.Length);
+ await _stream.FlushAsync();
+
+ Log.Debug("{DeviceId} 发送消息: {HexMessage}",
+ _deviceIdentifier, BitConverter.ToString(message).Replace("-", " "));
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "{DeviceId} 发送消息失败: {ErrorMessage}", _deviceIdentifier, ex.Message);
+ ErrorOccurred?.Invoke(this, ex);
+ throw;
+ }
+ }
+
+ ///
+ /// 开始接收消息的后台任务
+ ///
+ /// 取消标记
+ private async Task StartReceiveMessagesAsync(CancellationToken cancellationToken)
+ {
+ byte[] buffer = new byte[1024];
+
+ try
+ {
+ while (!cancellationToken.IsCancellationRequested && IsConnected)
+ {
+ // 读取消息长度
+ int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
+
+ if (bytesRead > 0)
+ {
+ // 创建一个新数组,只包含实际读取的字节
+ byte[] receivedMessage = new byte[bytesRead];
+ Array.Copy(buffer, receivedMessage, bytesRead);
+
+ Log.Debug("{DeviceId} 收到消息: {HexMessage}",
+ _deviceIdentifier, BitConverter.ToString(receivedMessage).Replace("-", " "));
+
+ // 触发消息接收事件
+ MessageReceived?.Invoke(this, receivedMessage);
+ }
+ else
+ {
+ // 读取0字节表示连接已关闭
+ Log.Warning("{DeviceId} 连接已关闭(服务器端断开)", _deviceIdentifier);
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // 任务被取消,正常退出
+ Log.Information("{DeviceId} 接收消息任务已取消", _deviceIdentifier);
+ }
+ catch (Exception ex)
+ {
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ Log.Error(ex, "{DeviceId} 接收消息时发生错误: {ErrorMessage}", _deviceIdentifier, ex.Message);
+ ErrorOccurred?.Invoke(this, ex);
+
+ // 触发连接状态变更
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Failed);
+ }
+ }
+
+ // 如果不是因为取消而退出循环,表示连接已断开
+ if (!cancellationToken.IsCancellationRequested && _client != null)
+ {
+ // 额外检查并确认连接状态
+ try
+ {
+ if (_client.Client != null && !_client.Client.Poll(0, SelectMode.SelectRead) || _client.Available != 0)
+ {
+ // 客户端仍然连接
+ return;
+ }
+ }
+ catch
+ {
+ // 忽略额外的检查异常
+ }
+
+ // 连接已断开
+ ConnectionStatusChanged?.Invoke(this, ConnectionStatus.Disconnected);
+ }
+ }
+
+ ///
+ /// 释放资源
+ ///
+ public void Dispose()
+ {
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource?.Dispose();
+ _stream?.Dispose();
+ _client?.Dispose();
+
+ _cancellationTokenSource = null;
+ _stream = null;
+ _client = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/ReconnectionService.cs b/Services/ReconnectionService.cs
new file mode 100644
index 0000000..d53e13a
--- /dev/null
+++ b/Services/ReconnectionService.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Serilog;
+
+namespace Protocol376Simulator.Services
+{
+ ///
+ /// 重连服务类,负责处理自动重连逻辑
+ ///
+ public class ReconnectionService
+ {
+ private readonly NetworkService _networkService;
+ private readonly string _deviceIdentifier;
+ private CancellationTokenSource _reconnectCancellationTokenSource;
+ private Task _reconnectTask;
+ private readonly object _reconnectLock = new object();
+
+ // 重连配置
+ private bool _autoReconnect = true;
+ private int _reconnectAttempts = 0;
+ private int _maxReconnectAttempts = 5;
+ private TimeSpan _reconnectDelay = TimeSpan.FromSeconds(5);
+ private TimeSpan _reconnectIncreaseDelay = TimeSpan.FromSeconds(5);
+ private DateTime _lastReconnectTime = DateTime.MinValue;
+ private TimeSpan _minReconnectInterval = TimeSpan.FromSeconds(30);
+ private bool _isReconnecting = false;
+
+ ///
+ /// 当重连尝试开始时触发
+ ///
+ public event EventHandler ReconnectAttemptStarted;
+
+ ///
+ /// 当重连尝试结束时触发
+ ///
+ public event EventHandler ReconnectAttemptCompleted;
+
+ ///
+ /// 是否启用自动重连
+ ///
+ public bool AutoReconnect
+ {
+ get => _autoReconnect;
+ set => _autoReconnect = value;
+ }
+
+ ///
+ /// 重连尝试次数
+ ///
+ public int ReconnectAttempts => _reconnectAttempts;
+
+ ///
+ /// 是否正在重连
+ ///
+ public bool IsReconnecting => _isReconnecting;
+
+ ///
+ /// 构造函数
+ ///
+ /// 网络服务
+ /// 设备标识(用于日志)
+ public ReconnectionService(NetworkService networkService, string deviceIdentifier)
+ {
+ _networkService = networkService ?? throw new ArgumentNullException(nameof(networkService));
+ _deviceIdentifier = deviceIdentifier;
+
+ // 订阅网络服务的连接状态变更事件
+ _networkService.ConnectionStatusChanged += OnConnectionStatusChanged;
+ }
+
+ ///
+ /// 设置重连参数
+ ///
+ /// 是否启用自动重连
+ /// 最大重连尝试次数
+ /// 重连延迟(秒)
+ public void SetReconnectParameters(bool autoReconnect, int maxAttempts, int delaySeconds)
+ {
+ _autoReconnect = autoReconnect;
+ _maxReconnectAttempts = maxAttempts;
+ _reconnectDelay = TimeSpan.FromSeconds(delaySeconds);
+ _reconnectIncreaseDelay = TimeSpan.FromSeconds(delaySeconds);
+
+ Log.Information("{DeviceId} 已设置重连参数: 自动重连={AutoReconnect}, 最大尝试次数={MaxAttempts}, 延迟={Delay}秒",
+ _deviceIdentifier, _autoReconnect, _maxReconnectAttempts, delaySeconds);
+ }
+
+ ///
+ /// 连接状态变更处理
+ ///
+ private void OnConnectionStatusChanged(object sender, NetworkService.ConnectionStatus status)
+ {
+ if (status == NetworkService.ConnectionStatus.Failed ||
+ status == NetworkService.ConnectionStatus.Disconnected)
+ {
+ // 如果启用了自动重连,开始重连任务
+ if (_autoReconnect)
+ {
+ StartReconnectAsync();
+ }
+ }
+ else if (status == NetworkService.ConnectionStatus.Connected)
+ {
+ // 连接成功,重置重连尝试次数
+ _reconnectAttempts = 0;
+ StopReconnect();
+ }
+ }
+
+ ///
+ /// 开始重连任务
+ ///
+ private void StartReconnectAsync()
+ {
+ lock (_reconnectLock)
+ {
+ // 如果已经在重连中,不要重复启动
+ if (_isReconnecting)
+ {
+ return;
+ }
+
+ // 如果距离上次重连时间太短,不要立即重连
+ TimeSpan timeSinceLastReconnect = DateTime.Now - _lastReconnectTime;
+ if (timeSinceLastReconnect < _minReconnectInterval)
+ {
+ Log.Information("{DeviceId} 距离上次重连尝试时间太短,将等待 {WaitTime} 秒后再尝试",
+ _deviceIdentifier, (_minReconnectInterval - timeSinceLastReconnect).TotalSeconds);
+
+ // 等待一段时间后再次检查是否需要重连
+ Task.Delay(_minReconnectInterval - timeSinceLastReconnect)
+ .ContinueWith(_ => StartReconnectAsync());
+ return;
+ }
+
+ _isReconnecting = true;
+ _reconnectCancellationTokenSource?.Cancel();
+ _reconnectCancellationTokenSource = new CancellationTokenSource();
+
+ _reconnectTask = ReconnectAsync(_reconnectCancellationTokenSource.Token);
+ }
+ }
+
+ ///
+ /// 停止重连任务
+ ///
+ private void StopReconnect()
+ {
+ lock (_reconnectLock)
+ {
+ _reconnectCancellationTokenSource?.Cancel();
+ _isReconnecting = false;
+ }
+ }
+
+ ///
+ /// 重连任务实现
+ ///
+ private async Task ReconnectAsync(CancellationToken cancellationToken)
+ {
+ // 记录开始重连的时间
+ _lastReconnectTime = DateTime.Now;
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ _reconnectAttempts++;
+
+ // 检查是否超过最大尝试次数
+ if (_maxReconnectAttempts > 0 && _reconnectAttempts > _maxReconnectAttempts)
+ {
+ Log.Warning("{DeviceId} 已达到最大重连尝试次数 {MaxAttempts},停止重连",
+ _deviceIdentifier, _maxReconnectAttempts);
+
+ lock (_reconnectLock)
+ {
+ _isReconnecting = false;
+ }
+
+ ReconnectAttemptCompleted?.Invoke(this, false);
+ return;
+ }
+
+ Log.Information("{DeviceId} 尝试重连 (第 {Attempt}/{MaxAttempts} 次)...",
+ _deviceIdentifier, _reconnectAttempts, _maxReconnectAttempts > 0 ? _maxReconnectAttempts.ToString() : "∞");
+
+ // 触发重连开始事件
+ ReconnectAttemptStarted?.Invoke(this, _reconnectAttempts);
+
+ try
+ {
+ // 尝试重新连接
+ await _networkService.ConnectAsync();
+
+ // 如果连接成功,退出重连循环
+ Log.Information("{DeviceId} 重连成功", _deviceIdentifier);
+
+ lock (_reconnectLock)
+ {
+ _isReconnecting = false;
+ }
+
+ // 触发重连完成事件
+ ReconnectAttemptCompleted?.Invoke(this, true);
+ return;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("{DeviceId} 重连失败: {ErrorMessage}", _deviceIdentifier, ex.Message);
+
+ // 触发重连完成事件(失败)
+ ReconnectAttemptCompleted?.Invoke(this, false);
+ }
+
+ // 计算下一次重连的延迟时间(逐渐增加)
+ TimeSpan currentDelay = TimeSpan.FromMilliseconds(
+ _reconnectDelay.TotalMilliseconds +
+ (_reconnectAttempts - 1) * _reconnectIncreaseDelay.TotalMilliseconds);
+
+ Log.Information("{DeviceId} 将在 {Delay} 秒后重试...",
+ _deviceIdentifier, currentDelay.TotalSeconds);
+
+ // 等待一段时间后重试
+ try
+ {
+ await Task.Delay(currentDelay, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ // 任务被取消,退出循环
+ break;
+ }
+ }
+
+ // 如果是因为取消而退出循环
+ if (cancellationToken.IsCancellationRequested)
+ {
+ lock (_reconnectLock)
+ {
+ _isReconnecting = false;
+ }
+
+ Log.Information("{DeviceId} 重连任务已取消", _deviceIdentifier);
+ }
+ }
+
+ ///
+ /// 强制立即尝试重连
+ ///
+ public async Task ForceReconnectAsync()
+ {
+ // 停止当前的重连任务
+ StopReconnect();
+
+ // 等待一段时间确保任务停止
+ await Task.Delay(100);
+
+ // 重置计数并开始新的重连
+ _reconnectAttempts = 0;
+ StartReconnectAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Services/StatisticsService.cs b/Services/StatisticsService.cs
new file mode 100644
index 0000000..2a7c518
--- /dev/null
+++ b/Services/StatisticsService.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Protocol376Simulator.Interfaces;
+
+namespace Protocol376Simulator.Services
+{
+ ///
+ /// 统计服务类,用于收集和报告通信统计信息
+ ///
+ public class StatisticsService
+ {
+ private readonly string _deviceIdentifier;
+ private int _totalMessagesSent = 0;
+ private int _totalMessagesReceived = 0;
+ private int _failedMessages = 0;
+ private readonly Dictionary _messageTypeStats = new Dictionary();
+ private readonly List _responseTimes = new List();
+ private DateTime _lastMessageSentTime = DateTime.MinValue;
+ private readonly Dictionary _errorStats = new Dictionary();
+
+ ///
+ /// 构造函数
+ ///
+ /// 设备标识(用于日志和显示)
+ public StatisticsService(string deviceIdentifier)
+ {
+ _deviceIdentifier = deviceIdentifier;
+
+ // 初始化消息类型统计
+ foreach (MessageType type in Enum.GetValues(typeof(MessageType)))
+ {
+ _messageTypeStats[type] = 0;
+ }
+ }
+
+ ///
+ /// 总发送消息数
+ ///
+ public int TotalMessagesSent => _totalMessagesSent;
+
+ ///
+ /// 总接收消息数
+ ///
+ public int TotalMessagesReceived => _totalMessagesReceived;
+
+ ///
+ /// 失败消息数
+ ///
+ public int FailedMessages => _failedMessages;
+
+ ///
+ /// 消息类型统计
+ ///
+ public Dictionary MessageTypeStats => _messageTypeStats;
+
+ ///
+ /// 平均响应时间
+ ///
+ public TimeSpan AverageResponseTime => _responseTimes.Count > 0 ?
+ TimeSpan.FromMilliseconds(_responseTimes.Average(t => t.TotalMilliseconds)) :
+ TimeSpan.Zero;
+
+ ///
+ /// 最大响应时间
+ ///
+ public TimeSpan MaxResponseTime => _responseTimes.Count > 0 ?
+ _responseTimes.Max() : TimeSpan.Zero;
+
+ ///
+ /// 最小响应时间
+ ///
+ public TimeSpan MinResponseTime => _responseTimes.Count > 0 ?
+ _responseTimes.Min() : TimeSpan.Zero;
+
+ ///
+ /// 记录消息发送
+ ///
+ public void RecordMessageSent()
+ {
+ _totalMessagesSent++;
+ _lastMessageSentTime = DateTime.Now;
+ }
+
+ ///
+ /// 记录消息接收
+ ///
+ public void RecordMessageReceived()
+ {
+ _totalMessagesReceived++;
+ }
+
+ ///
+ /// 记录消息失败
+ ///
+ public void RecordMessageFailed()
+ {
+ _failedMessages++;
+ }
+
+ ///
+ /// 记录消息类型统计
+ ///
+ /// 消息类型
+ public void RecordMessageType(MessageType messageType)
+ {
+ if (_messageTypeStats.ContainsKey(messageType))
+ {
+ _messageTypeStats[messageType]++;
+ }
+ }
+
+ ///
+ /// 记录响应时间
+ ///
+ /// 响应时间
+ public void RecordResponseTime(TimeSpan responseTime)
+ {
+ if (responseTime.TotalMilliseconds > 0)
+ {
+ _responseTimes.Add(responseTime);
+
+ // 保持响应时间列表在合理大小范围内,避免内存泄漏
+ if (_responseTimes.Count > 1000)
+ {
+ _responseTimes.RemoveAt(0);
+ }
+ }
+ }
+
+ ///
+ /// 记录错误统计
+ ///
+ /// 错误类型
+ public void RecordError(string errorType)
+ {
+ if (string.IsNullOrEmpty(errorType))
+ {
+ return;
+ }
+
+ if (_errorStats.ContainsKey(errorType))
+ {
+ _errorStats[errorType]++;
+ }
+ else
+ {
+ _errorStats[errorType] = 1;
+ }
+ }
+
+ ///
+ /// 获取统计信息的字符串表示
+ ///
+ /// 格式化的统计信息
+ public string GetStatisticsReport()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine($"设备 {_deviceIdentifier} 通信统计:");
+ sb.AppendLine($"总发送消息数: {_totalMessagesSent}");
+ sb.AppendLine($"总接收消息数: {_totalMessagesReceived}");
+ sb.AppendLine($"失败消息数: {_failedMessages}");
+
+ if (_responseTimes.Count > 0)
+ {
+ sb.AppendLine($"平均响应时间: {AverageResponseTime.TotalMilliseconds:F2}毫秒");
+ sb.AppendLine($"最大响应时间: {MaxResponseTime.TotalMilliseconds:F2}毫秒");
+ sb.AppendLine($"最小响应时间: {MinResponseTime.TotalMilliseconds:F2}毫秒");
+ }
+ else
+ {
+ sb.AppendLine("尚未记录响应时间");
+ }
+
+ sb.AppendLine("消息类型统计:");
+ foreach (var type in _messageTypeStats.Where(x => x.Value > 0).OrderByDescending(x => x.Value))
+ {
+ sb.AppendLine($" {type.Key}: {type.Value}");
+ }
+
+ if (_errorStats.Count > 0)
+ {
+ sb.AppendLine("错误统计:");
+ foreach (var error in _errorStats.OrderByDescending(x => x.Value))
+ {
+ sb.AppendLine($" {error.Key}: {error.Value}");
+ }
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 重置所有统计数据
+ ///
+ public void ResetStatistics()
+ {
+ _totalMessagesSent = 0;
+ _totalMessagesReceived = 0;
+ _failedMessages = 0;
+ _responseTimes.Clear();
+ _lastMessageSentTime = DateTime.MinValue;
+
+ foreach (MessageType type in Enum.GetValues(typeof(MessageType)))
+ {
+ _messageTypeStats[type] = 0;
+ }
+
+ _errorStats.Clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Simulators/ConcentratorSimulator.cs b/Simulators/ConcentratorSimulator.cs
new file mode 100644
index 0000000..f963df0
--- /dev/null
+++ b/Simulators/ConcentratorSimulator.cs
@@ -0,0 +1,738 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Protocol376Simulator.Interfaces;
+using Protocol376Simulator.Models;
+using Protocol376Simulator.Services;
+using Serilog;
+
+namespace Protocol376Simulator.Simulators
+{
+ ///
+ /// 集中器模拟器类
+ ///
+ public class ConcentratorSimulator : ISimulator
+ {
+ public readonly string _concentratorAddress;
+ private readonly NetworkService _networkService;
+ private readonly HeartbeatService _heartbeatService;
+ private readonly ReconnectionService _reconnectionService;
+ private readonly StatisticsService _statisticsService;
+ private readonly DeviceDataService _deviceDataService;
+ private bool _autoResponse = true;
+ private DateTime _lastLoginTime = DateTime.MinValue;
+ private bool _loginConfirmed = false;
+
+ ///
+ /// 当接收到消息时触发
+ ///
+ public event EventHandler MessageReceived;
+
+ ///
+ /// 当状态变更时触发
+ ///
+ public event EventHandler StatusChanged;
+
+ ///
+ /// 是否已连接
+ ///
+ public bool IsConnected => _networkService.IsConnected;
+
+ ///
+ /// 是否已登录
+ ///
+ public bool IsLoggedIn => _lastLoginTime > DateTime.MinValue && _loginConfirmed;
+
+ ///
+ /// 阀门状态
+ ///
+ public bool ValveStatus => _deviceDataService.ValveStatus;
+
+ ///
+ /// 成功发送的心跳次数
+ ///
+ public int SuccessfulHeartbeats => _heartbeatService.SuccessfulHeartbeats;
+
+ ///
+ /// 最后登录时间
+ ///
+ public DateTime LastLoginTime => _lastLoginTime;
+
+ ///
+ /// 是否启用自动重连
+ ///
+ public bool AutoReconnect
+ {
+ get => _reconnectionService.AutoReconnect;
+ set => _reconnectionService.AutoReconnect = value;
+ }
+
+ ///
+ /// 重连尝试次数
+ ///
+ public int ReconnectAttempts => _reconnectionService.ReconnectAttempts;
+
+ ///
+ /// 是否正在重连
+ ///
+ public bool IsReconnecting => _reconnectionService.IsReconnecting;
+
+ ///
+ /// 构造函数
+ ///
+ /// 集中器地址
+ /// 服务器地址
+ /// 服务器端口
+ public ConcentratorSimulator(string concentratorAddress, string serverAddress, int serverPort)
+ {
+ _concentratorAddress = concentratorAddress;
+
+ // 初始化网络服务
+ _networkService = new NetworkService(serverAddress, serverPort, $"集中器({concentratorAddress})");
+ _networkService.MessageReceived += OnMessageReceived;
+ _networkService.ConnectionStatusChanged += OnConnectionStatusChanged;
+ _networkService.ErrorOccurred += OnNetworkError;
+
+ // 初始化心跳服务
+ _heartbeatService = new HeartbeatService($"集中器({concentratorAddress})", SendHeartbeatMessageAsync);
+
+ // 初始化重连服务
+ _reconnectionService = new ReconnectionService(_networkService, $"集中器({concentratorAddress})");
+ _reconnectionService.ReconnectAttemptCompleted += OnReconnectAttemptCompleted;
+
+ // 初始化统计服务
+ _statisticsService = new StatisticsService($"集中器({concentratorAddress})");
+
+ // 初始化设备数据服务
+ _deviceDataService = new DeviceDataService($"集中器({concentratorAddress})");
+ _deviceDataService.ValveStatusChanged += OnValveStatusChanged;
+ }
+
+ ///
+ /// 启动模拟器
+ ///
+ /// 是否自动登录
+ /// 是否自动发送心跳
+ public async Task StartAsync(bool autoLogin = false, bool autoHeartbeat = false)
+ {
+ try
+ {
+ Log.Information("集中器 (地址: {Address}) 正在启动...", _concentratorAddress);
+
+ // 连接到服务器
+ await _networkService.ConnectAsync();
+
+ // 如果启用自动登录,发送登录消息
+ if (autoLogin)
+ {
+ // 短暂延迟,确保连接稳定
+ await Task.Delay(100);
+ await SendLoginMessageAsync();
+ Log.Information("集中器 (地址: {Address}) 自动登录已发送", _concentratorAddress);
+ }
+
+ // 如果启用自动心跳,启动心跳任务
+ if (autoHeartbeat)
+ {
+ StartHeartbeat();
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 启动失败: {ErrorMessage}", _concentratorAddress, ex.Message);
+ StatusChanged?.Invoke(this, $"启动失败: {ex.Message}");
+
+ // 异常继续抛出,由调用者处理
+ throw;
+ }
+ }
+
+ ///
+ /// 停止模拟器
+ ///
+ public async Task StopAsync()
+ {
+ try
+ {
+ // 停止心跳服务
+ _heartbeatService.Stop();
+
+ // 断开网络连接
+ await _networkService.DisconnectAsync();
+
+ Log.Information("集中器 (地址: {Address}) 已停止", _concentratorAddress);
+ StatusChanged?.Invoke(this, "已停止");
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 停止时发生错误: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ }
+ }
+
+ ///
+ /// 发送登录消息
+ ///
+ public async Task SendLoginMessageAsync()
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送登录消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ var loginMessage = Protocol376Message.CreateLoginMessage(_concentratorAddress);
+ await SendMessageAsync(loginMessage);
+ _lastLoginTime = DateTime.Now;
+ StatusChanged?.Invoke(this, "已发送登录请求");
+
+ Log.Information("集中器 (地址: {Address}) 发送登录消息", _concentratorAddress);
+ Log.Debug("集中器 (地址: {Address}) A&C报文详情: {MessageInfo}",
+ _concentratorAddress, loginMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送登录消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("登录消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送心跳消息
+ ///
+ public async Task SendHeartbeatMessageAsync()
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送心跳消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ var heartbeatMessage = Protocol376Message.CreateHeartbeatMessage(_concentratorAddress);
+ await SendMessageAsync(heartbeatMessage);
+
+ Log.Information("集中器 (地址: {Address}) 发送心跳消息", _concentratorAddress);
+ Log.Debug("集中器 (地址: {Address}) 心跳报文详情: {MessageInfo}",
+ _concentratorAddress, heartbeatMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送心跳消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("心跳消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送阀控操作消息
+ ///
+ /// 阀门操作:1=开阀,2=关阀,3=查询状态
+ public async Task SendValveControlMessageAsync(byte valveOperation)
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送阀控消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ var valveMessage = Protocol376Message.CreateValveControlMessage(_concentratorAddress, valveOperation);
+ await SendMessageAsync(valveMessage);
+
+ Log.Information("集中器 (地址: {Address}) 发送阀控消息, 操作: {Operation}",
+ _concentratorAddress, valveOperation);
+ Log.Debug("集中器 (地址: {Address}) 阀控报文详情: {MessageInfo}",
+ _concentratorAddress, valveMessage.GetMessageInfo());
+
+ // 如果是开阀或关阀操作,更新阀门状态
+ if (valveOperation == 1)
+ {
+ _deviceDataService.UpdateValveStatus(true);
+ }
+ else if (valveOperation == 2)
+ {
+ _deviceDataService.UpdateValveStatus(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送阀控消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("阀控消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送数据上传消息
+ ///
+ /// 数据类型
+ public async Task SendDataUploadMessageAsync(byte dataType)
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送数据上传消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ // 获取表计数据
+ byte[] meterData = _deviceDataService.GetMeterData(dataType);
+ if (meterData.Length == 0)
+ {
+ Log.Warning("集中器 (地址: {Address}) 数据类型 {DataType} 不存在表计数据",
+ _concentratorAddress, dataType);
+ return;
+ }
+
+ var dataMessage = Protocol376Message.CreateDataUploadMessage(_concentratorAddress, meterData);
+ await SendMessageAsync(dataMessage);
+
+ Log.Information("集中器 (地址: {Address}) 发送数据上传消息, 类型: {DataType}",
+ _concentratorAddress, dataType);
+ Log.Debug("集中器 (地址: {Address}) 上传报文详情: {MessageInfo}",
+ _concentratorAddress, dataMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送数据上传消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("数据上传消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送读数据消息
+ ///
+ /// 数据类型
+ public async Task SendReadDataMessageAsync(byte dataType)
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送读数据消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ var readMessage = Protocol376Message.CreateReadDataMessage(_concentratorAddress, dataType);
+ await SendMessageAsync(readMessage);
+
+ Log.Information("集中器 (地址: {Address}) 发送读数据消息, 类型: {DataType}",
+ _concentratorAddress, dataType);
+ Log.Debug("集中器 (地址: {Address}) 读数据报文详情: {MessageInfo}",
+ _concentratorAddress, readMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送读数据消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("读数据消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送设置参数消息
+ ///
+ /// 参数类型
+ /// 参数数据
+ public async Task SendSetParameterMessageAsync(byte paramType, byte[] paramData)
+ {
+ if (!IsConnected)
+ {
+ Log.Warning("集中器 (地址: {Address}) 未连接,无法发送设置参数消息", _concentratorAddress);
+ return;
+ }
+
+ try
+ {
+ var paramMessage = Protocol376Message.CreateSetParameterMessage(_concentratorAddress, paramType, paramData);
+ await SendMessageAsync(paramMessage);
+
+ Log.Information("集中器 (地址: {Address}) 发送设置参数消息, 类型: {ParamType}",
+ _concentratorAddress, paramType);
+ Log.Debug("集中器 (地址: {Address}) 参数设置报文详情: {MessageInfo}",
+ _concentratorAddress, paramMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 发送设置参数消息失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError("设置参数消息发送失败");
+ throw;
+ }
+ }
+
+ ///
+ /// 发送消息的通用方法
+ ///
+ /// 要发送的消息
+ private async Task SendMessageAsync(IProtocolMessage message)
+ {
+ if (!IsConnected)
+ {
+ throw new InvalidOperationException("未连接到服务器,无法发送消息");
+ }
+
+ byte[] messageBytes = message.ToBytes();
+ await _networkService.SendMessageAsync(messageBytes);
+
+ // 记录统计信息
+ _statisticsService.RecordMessageSent();
+ _statisticsService.RecordMessageType(message.Type);
+ }
+
+ ///
+ /// 启动心跳
+ ///
+ public void StartHeartbeat()
+ {
+ _heartbeatService.Start();
+ StatusChanged?.Invoke(this, "心跳已启动");
+ Log.Information("集中器 (地址: {Address}) 自动心跳已启动", _concentratorAddress);
+ }
+
+ ///
+ /// 停止心跳
+ ///
+ public void StopHeartbeat()
+ {
+ _heartbeatService.Stop();
+ StatusChanged?.Invoke(this, "心跳已停止");
+ Log.Information("集中器 (地址: {Address}) 自动心跳已停止", _concentratorAddress);
+ }
+
+ ///
+ /// 设置是否自动响应
+ ///
+ /// 是否启用自动响应
+ public void SetAutoResponse(bool enabled)
+ {
+ _autoResponse = enabled;
+ Log.Information("集中器 (地址: {Address}) 自动响应已{Status}",
+ _concentratorAddress, enabled ? "启用" : "禁用");
+ }
+
+ ///
+ /// 更新表计数据
+ ///
+ public void UpdateMeterData(byte dataType, byte[] data)
+ {
+ _deviceDataService.SetMeterData(dataType, data);
+ }
+
+ ///
+ /// 获取模拟器状态
+ ///
+ public string GetStatus()
+ {
+ var status = new StringBuilder();
+ status.AppendLine($"集中器 (地址: {_concentratorAddress}) 状态:");
+ status.AppendLine($"连接状态: {(IsConnected ? "已连接" : "未连接")}");
+ status.AppendLine($"登录状态: {(IsLoggedIn ? "已登录" : "未登录")}");
+
+ if (IsLoggedIn)
+ {
+ status.AppendLine($"登录时间: {_lastLoginTime}");
+ }
+
+ status.AppendLine($"心跳状态: {(_heartbeatService.IsRunning ? "运行中" : "已停止")}");
+ status.AppendLine($"心跳次数: {_heartbeatService.SuccessfulHeartbeats}");
+ status.AppendLine($"阀门状态: {(_deviceDataService.ValveStatus ? "开启" : "关闭")}");
+
+ return status.ToString();
+ }
+
+ ///
+ /// 获取通信统计信息
+ ///
+ public string GetCommunicationStatistics()
+ {
+ return _statisticsService.GetStatisticsReport();
+ }
+
+ ///
+ /// 设置重连参数
+ ///
+ public void SetReconnectParameters(bool autoReconnect, int maxAttempts, int delaySeconds)
+ {
+ _reconnectionService.SetReconnectParameters(autoReconnect, maxAttempts, delaySeconds);
+ }
+
+ ///
+ /// 接收消息处理
+ ///
+ private void OnMessageReceived(object sender, byte[] message)
+ {
+ try
+ {
+ // 触发接收消息事件
+ MessageReceived?.Invoke(this, message);
+
+ // 处理接收到的消息
+ ProcessReceivedMessage(message);
+
+ // 统计信息记录
+ _statisticsService.RecordMessageReceived();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 处理接收消息时发生错误: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError($"消息处理错误: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 处理接收到的消息
+ ///
+ private void ProcessReceivedMessage(byte[] message)
+ {
+ try
+ {
+ // 解析消息
+ var receivedMessage = Protocol376Message.ParseFromBytes(message);
+
+ // 检查是否是登录确认消息
+ CheckLoginConfirmation(receivedMessage);
+
+ // 如果启用了自动响应,处理自动响应
+ if (_autoResponse)
+ {
+ _ = HandleAutoResponse(receivedMessage);
+ }
+
+ Log.Debug("集中器 (地址: {Address}) 收到消息: {MessageInfo}",
+ _concentratorAddress, receivedMessage.GetMessageInfo());
+ }
+ catch (Exception ex)
+ {
+ Log.Warning("集中器 (地址: {Address}) 消息解析失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ }
+ }
+
+ ///
+ /// 检查是否是登录确认消息
+ ///
+ private void CheckLoginConfirmation(Protocol376Message message)
+ {
+ // 检查是否是响应类型消息,且AFN为登录响应
+ if (message.Type == MessageType.Response && message.Data.Length >= 4 && message.Data[0] == 0x00)
+ {
+ // 检查是否包含数据单元标识
+ if (message.Data[1] == 0x02 && message.Data[2] == 0x70)
+ {
+ // 检查结果码
+ if (message.Data[3] == 0x00)
+ {
+ _loginConfirmed = true;
+ Log.Information("集中器 (地址: {Address}) 登录确认成功", _concentratorAddress);
+ StatusChanged?.Invoke(this, "登录成功");
+ }
+ else
+ {
+ _loginConfirmed = false;
+ Log.Warning("集中器 (地址: {Address}) 登录确认失败, 结果码: {ResultCode}",
+ _concentratorAddress, message.Data[3]);
+ StatusChanged?.Invoke(this, $"登录失败, 结果码: {message.Data[3]}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// 处理自动响应
+ ///
+ private async Task HandleAutoResponse(Protocol376Message receivedMessage)
+ {
+ try
+ {
+ // 根据接收到的消息类型,生成对应的响应消息
+ Protocol376Message responseMessage = null;
+
+ switch (receivedMessage.Type)
+ {
+ case MessageType.Login:
+ // 登录请求的响应
+ byte[] loginResponseData = new byte[] { 0x00, 0x02, 0x70, 0x00 }; // AFN=0, 成功
+ responseMessage = new Protocol376Message
+ {
+ ControlCode = 0x00,
+ Address = receivedMessage.Address,
+ Data = loginResponseData,
+ Type = MessageType.Response
+ };
+ break;
+
+ case MessageType.Heartbeat:
+ // 心跳消息的响应
+ byte[] heartbeatResponseData = new byte[] { 0x00, 0x02, 0x70, 0x00 }; // AFN=0, 成功
+ responseMessage = new Protocol376Message
+ {
+ ControlCode = 0x00,
+ Address = receivedMessage.Address,
+ Data = heartbeatResponseData,
+ Type = MessageType.Response
+ };
+ break;
+
+ case MessageType.ValveControl:
+ // 阀控操作的响应
+ byte[] valveResponseData = new byte[] { 0x00, 0x02, 0x70, 0x00 }; // AFN=0, 成功
+ responseMessage = new Protocol376Message
+ {
+ ControlCode = 0x00,
+ Address = receivedMessage.Address,
+ Data = valveResponseData,
+ Type = MessageType.Response
+ };
+ break;
+
+ case MessageType.DataUpload:
+ // 数据上传的响应
+ byte[] dataUploadResponseData = new byte[] { 0x00, 0x02, 0x70, 0x00 }; // AFN=0, 成功
+ responseMessage = new Protocol376Message
+ {
+ ControlCode = 0x00,
+ Address = receivedMessage.Address,
+ Data = dataUploadResponseData,
+ Type = MessageType.Response
+ };
+ break;
+
+ case MessageType.ReadData:
+ // 读数据请求的响应
+ if (receivedMessage.Data.Length >= 6)
+ {
+ byte dataType = receivedMessage.Data[5];
+ byte[] meterData = _deviceDataService.GetMeterData(dataType);
+
+ if (meterData.Length > 0)
+ {
+ // 构造响应数据
+ byte[] readDataResponseData = new byte[5 + meterData.Length];
+ readDataResponseData[0] = 0x0B; // AFN
+ readDataResponseData[1] = 0x02; // 数据单元标识1
+ readDataResponseData[2] = 0x70; // 数据单元标识2
+ readDataResponseData[3] = 0x00; // 结果码, 成功
+ readDataResponseData[4] = dataType; // 数据类型
+
+ // 拷贝表计数据
+ Array.Copy(meterData, 0, readDataResponseData, 5, meterData.Length);
+
+ responseMessage = new Protocol376Message
+ {
+ ControlCode = 0x00,
+ Address = receivedMessage.Address,
+ Data = readDataResponseData,
+ Type = MessageType.Response
+ };
+ }
+ }
+ break;
+ }
+
+ // 发送响应消息
+ if (responseMessage != null)
+ {
+ await SendMessageAsync(responseMessage);
+ Log.Debug("集中器 (地址: {Address}) 自动响应: {MessageInfo}",
+ _concentratorAddress, responseMessage.GetMessageInfo());
+ }
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 处理自动响应时发生错误: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ _statisticsService.RecordError($"自动响应错误: {ex.Message}");
+ }
+ }
+
+ ///
+ /// 阀门状态变更处理
+ ///
+ private void OnValveStatusChanged(object sender, bool isOpen)
+ {
+ StatusChanged?.Invoke(this, $"阀门状态已变更为: {(isOpen ? "开启" : "关闭")}");
+ }
+
+ ///
+ /// 连接状态变更处理
+ ///
+ private void OnConnectionStatusChanged(object sender, NetworkService.ConnectionStatus status)
+ {
+ switch (status)
+ {
+ case NetworkService.ConnectionStatus.Connected:
+ StatusChanged?.Invoke(this, "已连接到服务器");
+ break;
+
+ case NetworkService.ConnectionStatus.Disconnected:
+ _loginConfirmed = false;
+ StatusChanged?.Invoke(this, "与服务器断开连接");
+ break;
+
+ case NetworkService.ConnectionStatus.Failed:
+ _loginConfirmed = false;
+ StatusChanged?.Invoke(this, "连接失败");
+ break;
+
+ case NetworkService.ConnectionStatus.Reconnecting:
+ StatusChanged?.Invoke(this, "正在重新连接");
+ break;
+ }
+ }
+
+ ///
+ /// 网络错误处理
+ ///
+ private void OnNetworkError(object sender, Exception ex)
+ {
+ _statisticsService.RecordError($"网络错误: {ex.Message}");
+ StatusChanged?.Invoke(this, $"网络错误: {ex.Message}");
+ }
+
+ ///
+ /// 重连完成处理
+ ///
+ private async void OnReconnectAttemptCompleted(object sender, bool success)
+ {
+ if (success)
+ {
+ StatusChanged?.Invoke(this, "重连成功");
+
+ // 重新登录
+ try
+ {
+ await Task.Delay(1000); // 等待1秒确保连接稳定
+ await SendLoginMessageAsync();
+ Log.Information("集中器 (地址: {Address}) 重连后自动登录", _concentratorAddress);
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "集中器 (地址: {Address}) 重连后登录失败: {ErrorMessage}",
+ _concentratorAddress, ex.Message);
+ }
+ }
+ else
+ {
+ StatusChanged?.Invoke(this, $"重连失败 (尝试次数: {ReconnectAttempts})");
+ }
+ }
+ }
+}
\ No newline at end of file