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