添加项目文件。

This commit is contained in:
cli 2025-05-08 17:26:10 +08:00
parent a9199bd905
commit e3ad85e1f5
20 changed files with 4778 additions and 0 deletions

View File

@ -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
{
/// <summary>
/// 命令处理器类,负责处理控制台命令
/// </summary>
public class CommandHandler
{
private readonly RandomDataGenerator _dataGenerator = new RandomDataGenerator();
private bool _useRandomData = false;
private bool _displayHexLog = false;
/// <summary>
/// 处理命令
/// </summary>
/// <param name="commandParts">命令及参数</param>
/// <returns>处理任务</returns>
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 <address> <port>");
}
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 <address>");
}
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 <count>");
}
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 <address>");
}
break;
case "login":
// 发送登录消息
if (commandParts.Length >= 2)
{
await SendLoginCommand(commandParts[1]);
}
else
{
Log.Warning("语法: login <address>");
}
break;
case "heartbeat":
// 发送心跳消息
if (commandParts.Length >= 2)
{
await SendHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: heartbeat <address>");
}
break;
case "valve":
// 发送阀控消息
if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte operation))
{
await SendValveControlCommand(commandParts[1], operation);
}
else
{
Log.Warning("语法: valve <address> <operation> (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 <address> <dataType> (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 <address> <dataType> (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 <address> <paramType> <paramValue>");
}
break;
case "startbeat":
// 启动自动心跳
if (commandParts.Length >= 2)
{
await StartAutoHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: startbeat <address>");
}
break;
case "stopbeat":
// 停止自动心跳
if (commandParts.Length >= 2)
{
await StopAutoHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: stopbeat <address>");
}
break;
case "status":
// 显示集中器状态
if (commandParts.Length >= 2)
{
ShowConcentratorStatusCommand(commandParts[1]);
}
else
{
Log.Warning("语法: status <address>");
}
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 <address> <dataType> <dataValue>");
}
break;
case "autoresponse":
// 设置自动响应
if (commandParts.Length >= 3 && bool.TryParse(commandParts[2], out bool enabled))
{
SetAutoResponseCommand(commandParts[1], enabled);
}
else
{
Log.Warning("语法: autoresponse <address> <enabled>");
}
break;
case "disconnect":
// 断开集中器
if (commandParts.Length >= 2)
{
await DisconnectConcentratorCommand(commandParts[1]);
}
else
{
Log.Warning("语法: disconnect <address>");
}
break;
case "list":
// 列出所有集中器
ListAllConcentratorsCommand();
break;
case "batchlogin":
// 批量登录
if (commandParts.Length >= 2)
{
await BatchLoginCommand(commandParts[1]);
}
else
{
Log.Warning("语法: batchlogin <range>");
}
break;
case "batchbeat":
// 批量发送心跳
if (commandParts.Length >= 2)
{
await BatchSendHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: batchbeat <range>");
}
break;
case "batchvalve":
// 批量阀控
if (commandParts.Length >= 3 && byte.TryParse(commandParts[2], out byte batchOperation))
{
await BatchValveControlCommand(commandParts[1], batchOperation);
}
else
{
Log.Warning("语法: batchvalve <range> <operation>");
}
break;
case "batchstartbeat":
// 批量启动自动心跳
if (commandParts.Length >= 2)
{
await BatchStartAutoHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: batchstartbeat <range>");
}
break;
case "batchstopbeat":
// 批量停止自动心跳
if (commandParts.Length >= 2)
{
await BatchStopAutoHeartbeatCommand(commandParts[1]);
}
else
{
Log.Warning("语法: batchstopbeat <range>");
}
break;
case "stats":
// 显示通信统计
if (commandParts.Length >= 2)
{
ShowCommunicationStatisticsCommand(commandParts[1]);
}
else
{
Log.Warning("语法: stats <address>");
}
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 <address> <autoReconnect> <maxAttempts> <delaySeconds>");
}
break;
case "userand":
// 设置是否使用随机数据
if (commandParts.Length >= 2 && bool.TryParse(commandParts[1], out bool useRandom))
{
_useRandomData = useRandom;
Log.Information("随机数据生成已{Status}", useRandom ? "启用" : "禁用");
}
else
{
Log.Warning("语法: userand <true|false>");
}
break;
case "hexlog":
// 设置是否显示十六进制日志
if (commandParts.Length >= 2 && bool.TryParse(commandParts[1], out bool displayHex))
{
_displayHexLog = displayHex;
Log.Information("十六进制日志已{Status}", displayHex ? "启用" : "禁用");
}
else
{
Log.Warning("语法: hexlog <true|false>");
}
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 <address> <port> - 设置服务器地址和端口");
Console.WriteLine(" create <address> - 创建集中器");
Console.WriteLine(" auto - 自动创建集中器(自动生成地址)");
Console.WriteLine(" batch <count> - 批量创建集中器");
Console.WriteLine(" connect <address> - 连接集中器到服务器");
Console.WriteLine(" login <address> - 发送登录消息");
Console.WriteLine(" heartbeat <address> - 发送心跳消息");
Console.WriteLine(" valve <address> <operation> - 发送阀控消息 (1=开阀, 2=关阀, 3=查询)");
Console.WriteLine(" upload <address> <dataType> - 发送数据上传消息 (1=水表, 2=电表, 3=气表, 4=状态)");
Console.WriteLine(" read <address> <dataType> - 发送数据读取消息");
Console.WriteLine(" setparam <address> <type> <val> - 发送设置参数消息");
Console.WriteLine(" startbeat <address> - 启动自动心跳");
Console.WriteLine(" stopbeat <address> - 停止自动心跳");
Console.WriteLine(" status <address> - 显示集中器状态");
Console.WriteLine(" setdata <address> <type> <val> - 设置表计数据");
Console.WriteLine(" autoresponse <address> <bool> - 设置自动响应");
Console.WriteLine(" disconnect <address> - 断开集中器连接");
Console.WriteLine(" list - 显示所有集中器");
Console.WriteLine(" batchlogin <range> - 批量登录");
Console.WriteLine(" batchbeat <range> - 批量发送心跳");
Console.WriteLine(" batchvalve <range> <op> - 批量阀控");
Console.WriteLine(" batchstartbeat <range> - 批量启动自动心跳");
Console.WriteLine(" batchstopbeat <range> - 批量停止自动心跳");
Console.WriteLine(" stats <address> - 显示通信统计");
Console.WriteLine(" reconnect <addr> <auto> <max> <delay> - 设置重连参数");
Console.WriteLine(" userand <true|false> - 设置是否使用随机数据");
Console.WriteLine(" hexlog <true|false> - 设置是否显示十六进制日志");
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 - 指定的集中器列表");
}
}
}

269
Factory/SimulatorFactory.cs Normal file
View File

@ -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
{
/// <summary>
/// 模拟器工厂类,负责创建和管理模拟器实例
/// </summary>
public class SimulatorFactory
{
private static readonly Dictionary<string, ISimulator> _simulators = new Dictionary<string, ISimulator>();
private static string _serverAddress = "127.0.0.1";
private static int _serverPort = 10502;
private static int _addressCounter = 1;
/// <summary>
/// 设置服务器地址和端口
/// </summary>
/// <param name="serverAddress">服务器地址</param>
/// <param name="serverPort">服务器端口</param>
public static void SetServerConfig(string serverAddress, int serverPort)
{
_serverAddress = serverAddress;
_serverPort = serverPort;
Log.Information("服务器配置已更新: {ServerAddress}:{ServerPort}", _serverAddress, _serverPort);
}
/// <summary>
/// 创建集中器模拟器
/// </summary>
/// <param name="address">集中器地址</param>
/// <returns>创建的模拟器实例</returns>
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;
}
/// <summary>
/// 创建带自动生成地址的集中器模拟器
/// </summary>
/// <returns>创建的模拟器实例</returns>
public static ISimulator CreateConcentratorWithAutoAddress()
{
string address = GenerateNextAddress();
return CreateConcentrator(address);
}
/// <summary>
/// 批量创建集中器模拟器
/// </summary>
/// <param name="count">创建数量</param>
/// <returns>创建的模拟器地址列表</returns>
public static List<string> BatchCreateConcentrators(int count)
{
var addresses = new List<string>();
for (int i = 0; i < count; i++)
{
string address = GenerateNextAddress();
CreateConcentrator(address);
addresses.Add(address);
}
Log.Information("已批量创建 {Count} 个集中器", count);
return addresses;
}
/// <summary>
/// 生成下一个集中器地址
/// </summary>
/// <returns>生成的地址</returns>
private static string GenerateNextAddress()
{
// 生成9位十六进制地址格式如312001001
string address = $"31{_addressCounter:D7}";
_addressCounter++;
return address;
}
/// <summary>
/// 获取模拟器实例
/// </summary>
/// <param name="address">集中器地址</param>
/// <returns>模拟器实例如果不存在则返回null</returns>
public static ISimulator GetSimulator(string address)
{
if (_simulators.TryGetValue(address, out var simulator))
{
return simulator;
}
Log.Warning("集中器地址 {Address} 不存在", address);
return null;
}
/// <summary>
/// 获取所有模拟器实例
/// </summary>
/// <returns>模拟器实例列表</returns>
public static Dictionary<string, ISimulator> GetAllSimulators()
{
return _simulators;
}
/// <summary>
/// 按范围获取模拟器地址
/// </summary>
/// <param name="range">范围表达式</param>
/// <returns>模拟器地址列表</returns>
public static List<string> GetAddressesInRange(string range)
{
var addresses = new List<string>();
// 如果是"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;
}
/// <summary>
/// 按范围批量操作模拟器
/// </summary>
/// <param name="range">范围表达式</param>
/// <param name="action">操作委托</param>
/// <returns>操作任务</returns>
public static async Task BatchOperationAsync(string range, Func<ISimulator, Task> 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);
}
}
}
}
/// <summary>
/// 删除模拟器
/// </summary>
/// <param name="address">集中器地址</param>
/// <returns>是否成功删除</returns>
public static async Task<bool> 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;
}
/// <summary>
/// 清空所有模拟器
/// </summary>
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("已清空所有模拟器");
}
}
}

View File

@ -0,0 +1,41 @@
using System;
namespace Protocol376Simulator.Interfaces
{
/// <summary>
/// 定义协议消息的基本接口
/// </summary>
public interface IProtocolMessage
{
/// <summary>
/// 将消息转换为字节数组
/// </summary>
/// <returns>表示消息的字节数组</returns>
byte[] ToBytes();
/// <summary>
/// 获取消息的详细信息
/// </summary>
/// <returns>格式化的消息信息字符串</returns>
string GetMessageInfo();
/// <summary>
/// 消息类型
/// </summary>
MessageType Type { get; }
}
/// <summary>
/// 定义消息类型枚举
/// </summary>
public enum MessageType
{
Login = 1,
Heartbeat = 2,
ValveControl = 3,
DataUpload = 4,
ReadData = 5,
SetParameter = 6,
Response = 99
}
}

69
Interfaces/ISimulator.cs Normal file
View File

@ -0,0 +1,69 @@
using System;
using System.Threading.Tasks;
namespace Protocol376Simulator.Interfaces
{
/// <summary>
/// 定义模拟器的基本接口
/// </summary>
public interface ISimulator
{
/// <summary>
/// 启动模拟器
/// </summary>
/// <param name="autoLogin">是否自动登录</param>
/// <param name="autoHeartbeat">是否自动发送心跳</param>
Task StartAsync(bool autoLogin = false, bool autoHeartbeat = false);
/// <summary>
/// 停止模拟器
/// </summary>
Task StopAsync();
/// <summary>
/// 发送登录消息
/// </summary>
Task SendLoginMessageAsync();
/// <summary>
/// 发送心跳消息
/// </summary>
Task SendHeartbeatMessageAsync();
/// <summary>
/// 启动心跳发送
/// </summary>
void StartHeartbeat();
/// <summary>
/// 停止心跳发送
/// </summary>
void StopHeartbeat();
/// <summary>
/// 获取模拟器状态
/// </summary>
/// <returns>格式化的状态信息字符串</returns>
string GetStatus();
/// <summary>
/// 当状态变更时触发的事件
/// </summary>
event EventHandler<string> StatusChanged;
/// <summary>
/// 当接收到消息时触发的事件
/// </summary>
event EventHandler<byte[]> MessageReceived;
/// <summary>
/// 模拟器是否已连接
/// </summary>
bool IsConnected { get; }
/// <summary>
/// 模拟器是否已登录
/// </summary>
bool IsLoggedIn { get; }
}
}

374
Models/MessageAnalyzer.cs Normal file
View File

@ -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<byte, string> AfnDescriptions = new Dictionary<byte, string>
{
{ 0x00, "确认/否认" },
{ 0x01, "复位" },
{ 0x02, "链路接口检测" },
{ 0x04, "设置参数" },
{ 0x05, "控制命令" },
{ 0x06, "身份认证及密钥协商" },
{ 0x07, "自定义数据请求" },
{ 0x08, "主动上报" },
{ 0x09, "读取数据" },
{ 0x0A, "查询参数" },
{ 0x0B, "软件升级" },
{ 0x0F, "文件传输" }
};
/// <summary>
/// 分析报文内容并生成详细解释
/// </summary>
/// <param name="message">原始报文字节数组</param>
/// <returns>报文分析结果</returns>
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();
}
/// <summary>
/// 解析控制码
/// </summary>
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();
}
/// <summary>
/// 获取启动站功能码描述
/// </summary>
private static string GetPrimaryFunctionCodeDesc(byte code)
{
return code switch
{
0 => "复位远方链路",
1 => "读取状态",
3 => "发送/确认用户数据",
4 => "发送/无需确认用户数据",
9 => "请求/响应链路状态",
10 => "请求/响应用户数据1",
11 => "请求/响应用户数据2",
_ => "未知功能"
};
}
/// <summary>
/// 获取从动站功能码描述
/// </summary>
private static string GetSecondaryFunctionCodeDesc(byte code)
{
return code switch
{
0 => "确认",
1 => "链路忙",
9 => "从站状态",
11 => "响应用户数据",
_ => "未知功能"
};
}
/// <summary>
/// 根据不同的AFN值分析数据域
/// </summary>
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();
}
/// <summary>
/// 计算校验和
/// </summary>
private static byte CalculateChecksum(byte[] message)
{
byte sum = 0;
// 从第二个68H开始计算校验和
for (int i = 5; i < message.Length - 2; i++)
{
sum += message[i];
}
return sum;
}
/// <summary>
/// 将BCD码转换为字符串
/// </summary>
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');
}
}
}

18
Models/Parameter.cs Normal file
View File

@ -0,0 +1,18 @@
using System;
namespace Protocol376Simulator.Models
{
/// <summary>
/// 参数生成辅助类
/// </summary>
public static class Parameter
{
/// <summary>
/// 创建参数数据
/// </summary>
public static byte[] Create(byte paramType, string value)
{
return System.Text.Encoding.UTF8.GetBytes(value);
}
}
}

View File

@ -0,0 +1,443 @@
using System;
using System.Text;
using Protocol376Simulator.Interfaces;
namespace Protocol376Simulator.Models
{
/// <summary>
/// 376.1协议消息类
/// </summary>
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; }
/// <summary>
/// 将消息转换为字节数组
/// </summary>
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;
}
/// <summary>
/// 获取消息详细信息
/// </summary>
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();
}
/// <summary>
/// 转换集中器地址为字节数组
/// </summary>
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);
}
}
/// <summary>
/// 创建登录消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
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;
}
/// <summary>
/// 创建心跳消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
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;
}
/// <summary>
/// 创建阀控消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
/// <param name="valveOperation">阀门操作1=开阀2=关阀3=查询状态</param>
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;
}
/// <summary>
/// 创建数据上传消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
/// <param name="data">表计数据</param>
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;
}
/// <summary>
/// 创建读数据消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
/// <param name="dataType">数据类型1=水表2=电表3=气表4=状态</param>
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;
}
/// <summary>
/// 创建设置参数消息
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
/// <param name="paramType">参数类型</param>
/// <param name="paramData">参数数据</param>
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;
}
/// <summary>
/// 从字节数组解析消息
/// </summary>
/// <param name="messageBytes">消息字节数组</param>
/// <returns>解析后的消息对象</returns>
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;
}
}
}

View File

@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
namespace Protocol376Simulator.Models
{
/// <summary>
/// 模拟数据随机生成器
/// </summary>
public class RandomDataGenerator
{
private readonly Random _random = new Random();
private readonly Dictionary<byte, uint> _lastValues = new Dictionary<byte, uint>();
// 不同时段用水模式
private static readonly Dictionary<int, (int Min, int Max)> WaterUsagePatterns = new Dictionary<int, (int Min, int Max)>
{
{ 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<int, (int Min, int Max)> ElectricityUsagePatterns = new Dictionary<int, (int Min, int Max)>
{
{ 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<int, (int Min, int Max)> GasUsagePatterns = new Dictionary<int, (int Min, int Max)>
{
{ 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点
};
/// <summary>
/// 生成递增的表计数据
/// </summary>
/// <param name="dataType">表计类型: 1=水表, 2=电表, 3=气表</param>
/// <param name="previousData">上一次的数据如果没有则为null</param>
/// <returns>新的表计数据</returns>
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;
}
/// <summary>
/// 生成状态数据
/// </summary>
/// <returns>状态数据</returns>
public byte[] GenerateStatusData()
{
// 生成随机的状态数据
byte[] statusData = new byte[2];
statusData[0] = (byte)_random.Next(0, 3); // 状态码
statusData[1] = (byte)_random.Next(0, 256); // 状态标志
return statusData;
}
/// <summary>
/// 根据参数类型生成参数数据
/// </summary>
/// <param name="paramType">参数类型</param>
/// <returns>参数数据</returns>
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;
}
}
/// <summary>
/// 根据表计类型和时间计算合理的增量
/// </summary>
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);
}
}
}

191
Models/TestScenario.cs Normal file
View File

@ -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
{
/// <summary>
/// 测试场景,用于保存和加载测试配置
/// </summary>
public class TestScenario
{
/// <summary>
/// 场景名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 场景描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 服务器地址
/// </summary>
public string ServerAddress { get; set; }
/// <summary>
/// 服务器端口
/// </summary>
public int ServerPort { get; set; }
/// <summary>
/// 集中器配置列表
/// </summary>
public List<ConcentratorConfig> Concentrators { get; set; } = new List<ConcentratorConfig>();
/// <summary>
/// 测试步骤列表
/// </summary>
public List<TestStep> Steps { get; set; } = new List<TestStep>();
/// <summary>
/// 将测试场景保存到文件
/// </summary>
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);
}
/// <summary>
/// 从文件加载测试场景
/// </summary>
public static TestScenario LoadFromFile(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"测试场景文件不存在: {filePath}");
}
string json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<TestScenario>(json);
}
/// <summary>
/// 获取所有保存的测试场景文件
/// </summary>
public static List<string> GetAllScenarioFiles(string directory = "Scenarios")
{
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
return new List<string>();
}
var files = Directory.GetFiles(directory, "*.json");
return new List<string>(files);
}
}
/// <summary>
/// 集中器配置
/// </summary>
public class ConcentratorConfig
{
/// <summary>
/// 集中器地址
/// </summary>
public string Address { get; set; }
/// <summary>
/// 是否自动登录
/// </summary>
public bool AutoLogin { get; set; } = true;
/// <summary>
/// 是否自动心跳
/// </summary>
public bool AutoHeartbeat { get; set; } = true;
/// <summary>
/// 是否自动响应
/// </summary>
public bool AutoResponse { get; set; } = true;
/// <summary>
/// 是否启用断线重连
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// 最大重连尝试次数
/// </summary>
public int MaxReconnectAttempts { get; set; } = 5;
/// <summary>
/// 重连延迟时间(秒)
/// </summary>
public int ReconnectDelaySeconds { get; set; } = 5;
/// <summary>
/// 初始表计数据
/// </summary>
public Dictionary<byte, string> InitialMeterData { get; set; } = new Dictionary<byte, string>();
}
/// <summary>
/// 测试步骤
/// </summary>
public class TestStep
{
/// <summary>
/// 步骤类型枚举
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum StepType
{
Login,
Heartbeat,
ValveControl,
DataUpload,
ReadData,
SetParameter,
Wait,
SetMeterData
}
/// <summary>
/// 步骤类型
/// </summary>
public StepType Type { get; set; }
/// <summary>
/// 目标集中器地址,"all"表示所有集中器
/// </summary>
public string ConcentratorAddress { get; set; }
/// <summary>
/// 步骤描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 步骤参数,根据不同步骤类型使用不同的参数
/// </summary>
public Dictionary<string, object> Parameters { get; set; } = new Dictionary<string, object>();
/// <summary>
/// 执行本步骤前的延迟时间(毫秒)
/// </summary>
public int DelayBeforeStepMs { get; set; } = 0;
/// <summary>
/// 执行下一步骤前的延迟时间(毫秒)
/// </summary>
public int DelayAfterStepMs { get; set; } = 1000;
}
}

6
NuGet.Config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

152
Program.cs Normal file
View File

@ -0,0 +1,152 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Protocol376Simulator.CommandProcessor;
using Protocol376Simulator.Factory;
using Serilog;
namespace Protocol376Simulator
{
/// <summary>
/// 程序入口类
/// </summary>
class Program
{
private static string _serverAddress = "127.0.0.1";
private static int _serverPort = 10502; // 默认端口
private static readonly CommandHandler _commandHandler = new CommandHandler();
/// <summary>
/// 程序入口点
/// </summary>
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();
}
/// <summary>
/// 配置日志记录器
/// </summary>
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();
}
/// <summary>
/// 处理命令行参数
/// </summary>
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);
}
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>disable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
</ItemGroup>
</Project>

25
Protocol376Simulator.sln Normal file
View File

@ -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

295
README.md Normal file
View File

@ -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: 增加测试场景导出为脚本功能,支持自动化批量测试

View File

@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using Serilog;
namespace Protocol376Simulator.Services
{
/// <summary>
/// 设备数据服务类,负责管理设备的业务数据
/// </summary>
public class DeviceDataService
{
private readonly string _deviceIdentifier;
private readonly Dictionary<byte, byte[]> _meterData = new Dictionary<byte, byte[]>();
private bool _valveStatus = false; // 阀门状态false表示关闭true表示打开
/// <summary>
/// 当数据更新时触发
/// </summary>
public event EventHandler<DataUpdateEventArgs> DataUpdated;
/// <summary>
/// 当阀门状态改变时触发
/// </summary>
public event EventHandler<bool> ValveStatusChanged;
/// <summary>
/// 阀门状态
/// </summary>
public bool ValveStatus
{
get => _valveStatus;
private set
{
if (_valveStatus != value)
{
_valveStatus = value;
ValveStatusChanged?.Invoke(this, _valveStatus);
}
}
}
/// <summary>
/// 数据更新事件参数
/// </summary>
public class DataUpdateEventArgs : EventArgs
{
public byte DataType { get; }
public byte[] Data { get; }
public DataUpdateEventArgs(byte dataType, byte[] data)
{
DataType = dataType;
Data = data;
}
}
/// <summary>
/// 构造函数
/// </summary>
/// <param name="deviceIdentifier">设备标识(用于日志)</param>
public DeviceDataService(string deviceIdentifier)
{
_deviceIdentifier = deviceIdentifier;
// 初始化模拟数据
InitializeSimulatedData();
}
/// <summary>
/// 初始化模拟数据
/// </summary>
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 }; // 状态码
}
/// <summary>
/// 设置表计数据
/// </summary>
/// <param name="dataType">数据类型</param>
/// <param name="data">数据内容</param>
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));
}
/// <summary>
/// 获取表计数据
/// </summary>
/// <param name="dataType">数据类型</param>
/// <returns>数据内容</returns>
public byte[] GetMeterData(byte dataType)
{
if (_meterData.TryGetValue(dataType, out byte[] data))
{
return data;
}
// 如果没有指定类型的数据,返回空数组
Log.Warning("{DeviceId} 请求的数据类型 {DataType} 不存在", _deviceIdentifier, dataType);
return new byte[0];
}
/// <summary>
/// 更新阀门状态
/// </summary>
/// <param name="isOpen">阀门是否打开</param>
public void UpdateValveStatus(bool isOpen)
{
ValveStatus = isOpen;
Log.Information("{DeviceId} 阀门状态已更新为: {Status}",
_deviceIdentifier, isOpen ? "打开" : "关闭");
}
/// <summary>
/// 获取所有表计数据类型
/// </summary>
/// <returns>数据类型列表</returns>
public IEnumerable<byte> GetAllDataTypes()
{
return _meterData.Keys;
}
/// <summary>
/// 检查是否存在指定类型的数据
/// </summary>
/// <param name="dataType">数据类型</param>
/// <returns>是否存在</returns>
public bool HasData(byte dataType)
{
return _meterData.ContainsKey(dataType);
}
/// <summary>
/// 获取设备数据状态的字符串表示
/// </summary>
/// <returns>格式化的数据状态</returns>
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;
}
/// <summary>
/// 获取数据类型的名称
/// </summary>
/// <param name="dataType">数据类型</param>
/// <returns>类型名称</returns>
private string GetDataTypeName(byte dataType)
{
return dataType switch
{
0x01 => "水表数据",
0x02 => "电表数据",
0x03 => "气表数据",
0x04 => "状态数据",
_ => "未知类型"
};
}
}
}

View File

@ -0,0 +1,216 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace Protocol376Simulator.Services
{
/// <summary>
/// 心跳服务类,负责处理定时发送心跳
/// </summary>
public class HeartbeatService
{
private readonly string _deviceIdentifier;
private readonly Func<Task> _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;
/// <summary>
/// 当心跳发送成功时触发
/// </summary>
public event EventHandler<DateTime> HeartbeatSent;
/// <summary>
/// 成功的心跳次数
/// </summary>
public int SuccessfulHeartbeats => _successfulHeartbeats;
/// <summary>
/// 最后一次心跳时间
/// </summary>
public DateTime LastHeartbeatTime => _lastHeartbeatTime;
/// <summary>
/// 心跳间隔
/// </summary>
public TimeSpan HeartbeatInterval
{
get => _heartbeatInterval;
set => _heartbeatInterval = value;
}
/// <summary>
/// 心跳是否正在运行
/// </summary>
public bool IsRunning => _isRunning;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="deviceIdentifier">设备标识(用于日志)</param>
/// <param name="heartbeatAction">执行心跳的委托函数</param>
public HeartbeatService(string deviceIdentifier, Func<Task> heartbeatAction)
{
_deviceIdentifier = deviceIdentifier;
_heartbeatAction = heartbeatAction ?? throw new ArgumentNullException(nameof(heartbeatAction));
}
/// <summary>
/// 启动心跳服务
/// </summary>
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);
}
}
/// <summary>
/// 停止心跳服务
/// </summary>
public void Stop()
{
lock (_heartbeatLock)
{
if (!_isRunning)
{
return;
}
_heartbeatCancellationTokenSource?.Cancel();
_isRunning = false;
Log.Information("{DeviceId} 已停止心跳服务", _deviceIdentifier);
}
}
/// <summary>
/// 重置心跳计数
/// </summary>
public void ResetHeartbeatCount()
{
_successfulHeartbeats = 0;
}
/// <summary>
/// 记录心跳成功
/// </summary>
public void RecordHeartbeatSuccess()
{
_successfulHeartbeats++;
_lastHeartbeatTime = DateTime.Now;
HeartbeatSent?.Invoke(this, _lastHeartbeatTime);
}
/// <summary>
/// 设置心跳间隔
/// </summary>
/// <param name="minutes">间隔分钟数</param>
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();
}
}
/// <summary>
/// 运行心跳任务
/// </summary>
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;
}
}
}
/// <summary>
/// 立即发送一次心跳
/// </summary>
public async Task SendHeartbeatImmediatelyAsync()
{
try
{
await _heartbeatAction();
RecordHeartbeatSuccess();
Log.Information("{DeviceId} 已立即发送心跳", _deviceIdentifier);
}
catch (Exception ex)
{
Log.Error(ex, "{DeviceId} 立即发送心跳时发生错误: {ErrorMessage}",
_deviceIdentifier, ex.Message);
throw;
}
}
}
}

269
Services/NetworkService.cs Normal file
View File

@ -0,0 +1,269 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace Protocol376Simulator.Services
{
/// <summary>
/// 网络服务类负责TCP连接和网络通信
/// </summary>
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;
/// <summary>
/// 当接收到消息时触发
/// </summary>
public event EventHandler<byte[]> MessageReceived;
/// <summary>
/// 当连接状态改变时触发
/// </summary>
public event EventHandler<ConnectionStatus> ConnectionStatusChanged;
/// <summary>
/// 当发生错误时触发
/// </summary>
public event EventHandler<Exception> ErrorOccurred;
/// <summary>
/// 连接状态枚举
/// </summary>
public enum ConnectionStatus
{
Disconnected,
Connecting,
Connected,
Failed,
Reconnecting
}
/// <summary>
/// 是否连接到服务器
/// </summary>
public bool IsConnected => _client != null && _client.Connected;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serverAddress">服务器地址</param>
/// <param name="serverPort">服务器端口</param>
/// <param name="deviceIdentifier">设备标识(用于日志)</param>
public NetworkService(string serverAddress, int serverPort, string deviceIdentifier)
{
_serverAddress = serverAddress;
_serverPort = serverPort;
_deviceIdentifier = deviceIdentifier;
_cancellationTokenSource = new CancellationTokenSource();
}
/// <summary>
/// 连接到服务器
/// </summary>
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;
}
}
/// <summary>
/// 断开连接
/// </summary>
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;
}
}
/// <summary>
/// 发送消息
/// </summary>
/// <param name="message">要发送的消息字节数组</param>
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;
}
}
/// <summary>
/// 开始接收消息的后台任务
/// </summary>
/// <param name="cancellationToken">取消标记</param>
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);
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_stream?.Dispose();
_client?.Dispose();
_cancellationTokenSource = null;
_stream = null;
_client = null;
}
}
}

View File

@ -0,0 +1,263 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Serilog;
namespace Protocol376Simulator.Services
{
/// <summary>
/// 重连服务类,负责处理自动重连逻辑
/// </summary>
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;
/// <summary>
/// 当重连尝试开始时触发
/// </summary>
public event EventHandler<int> ReconnectAttemptStarted;
/// <summary>
/// 当重连尝试结束时触发
/// </summary>
public event EventHandler<bool> ReconnectAttemptCompleted;
/// <summary>
/// 是否启用自动重连
/// </summary>
public bool AutoReconnect
{
get => _autoReconnect;
set => _autoReconnect = value;
}
/// <summary>
/// 重连尝试次数
/// </summary>
public int ReconnectAttempts => _reconnectAttempts;
/// <summary>
/// 是否正在重连
/// </summary>
public bool IsReconnecting => _isReconnecting;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="networkService">网络服务</param>
/// <param name="deviceIdentifier">设备标识(用于日志)</param>
public ReconnectionService(NetworkService networkService, string deviceIdentifier)
{
_networkService = networkService ?? throw new ArgumentNullException(nameof(networkService));
_deviceIdentifier = deviceIdentifier;
// 订阅网络服务的连接状态变更事件
_networkService.ConnectionStatusChanged += OnConnectionStatusChanged;
}
/// <summary>
/// 设置重连参数
/// </summary>
/// <param name="autoReconnect">是否启用自动重连</param>
/// <param name="maxAttempts">最大重连尝试次数</param>
/// <param name="delaySeconds">重连延迟(秒)</param>
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);
}
/// <summary>
/// 连接状态变更处理
/// </summary>
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();
}
}
/// <summary>
/// 开始重连任务
/// </summary>
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);
}
}
/// <summary>
/// 停止重连任务
/// </summary>
private void StopReconnect()
{
lock (_reconnectLock)
{
_reconnectCancellationTokenSource?.Cancel();
_isReconnecting = false;
}
}
/// <summary>
/// 重连任务实现
/// </summary>
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);
}
}
/// <summary>
/// 强制立即尝试重连
/// </summary>
public async Task ForceReconnectAsync()
{
// 停止当前的重连任务
StopReconnect();
// 等待一段时间确保任务停止
await Task.Delay(100);
// 重置计数并开始新的重连
_reconnectAttempts = 0;
StartReconnectAsync();
}
}
}

View File

@ -0,0 +1,214 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Protocol376Simulator.Interfaces;
namespace Protocol376Simulator.Services
{
/// <summary>
/// 统计服务类,用于收集和报告通信统计信息
/// </summary>
public class StatisticsService
{
private readonly string _deviceIdentifier;
private int _totalMessagesSent = 0;
private int _totalMessagesReceived = 0;
private int _failedMessages = 0;
private readonly Dictionary<MessageType, int> _messageTypeStats = new Dictionary<MessageType, int>();
private readonly List<TimeSpan> _responseTimes = new List<TimeSpan>();
private DateTime _lastMessageSentTime = DateTime.MinValue;
private readonly Dictionary<string, int> _errorStats = new Dictionary<string, int>();
/// <summary>
/// 构造函数
/// </summary>
/// <param name="deviceIdentifier">设备标识(用于日志和显示)</param>
public StatisticsService(string deviceIdentifier)
{
_deviceIdentifier = deviceIdentifier;
// 初始化消息类型统计
foreach (MessageType type in Enum.GetValues(typeof(MessageType)))
{
_messageTypeStats[type] = 0;
}
}
/// <summary>
/// 总发送消息数
/// </summary>
public int TotalMessagesSent => _totalMessagesSent;
/// <summary>
/// 总接收消息数
/// </summary>
public int TotalMessagesReceived => _totalMessagesReceived;
/// <summary>
/// 失败消息数
/// </summary>
public int FailedMessages => _failedMessages;
/// <summary>
/// 消息类型统计
/// </summary>
public Dictionary<MessageType, int> MessageTypeStats => _messageTypeStats;
/// <summary>
/// 平均响应时间
/// </summary>
public TimeSpan AverageResponseTime => _responseTimes.Count > 0 ?
TimeSpan.FromMilliseconds(_responseTimes.Average(t => t.TotalMilliseconds)) :
TimeSpan.Zero;
/// <summary>
/// 最大响应时间
/// </summary>
public TimeSpan MaxResponseTime => _responseTimes.Count > 0 ?
_responseTimes.Max() : TimeSpan.Zero;
/// <summary>
/// 最小响应时间
/// </summary>
public TimeSpan MinResponseTime => _responseTimes.Count > 0 ?
_responseTimes.Min() : TimeSpan.Zero;
/// <summary>
/// 记录消息发送
/// </summary>
public void RecordMessageSent()
{
_totalMessagesSent++;
_lastMessageSentTime = DateTime.Now;
}
/// <summary>
/// 记录消息接收
/// </summary>
public void RecordMessageReceived()
{
_totalMessagesReceived++;
}
/// <summary>
/// 记录消息失败
/// </summary>
public void RecordMessageFailed()
{
_failedMessages++;
}
/// <summary>
/// 记录消息类型统计
/// </summary>
/// <param name="messageType">消息类型</param>
public void RecordMessageType(MessageType messageType)
{
if (_messageTypeStats.ContainsKey(messageType))
{
_messageTypeStats[messageType]++;
}
}
/// <summary>
/// 记录响应时间
/// </summary>
/// <param name="responseTime">响应时间</param>
public void RecordResponseTime(TimeSpan responseTime)
{
if (responseTime.TotalMilliseconds > 0)
{
_responseTimes.Add(responseTime);
// 保持响应时间列表在合理大小范围内,避免内存泄漏
if (_responseTimes.Count > 1000)
{
_responseTimes.RemoveAt(0);
}
}
}
/// <summary>
/// 记录错误统计
/// </summary>
/// <param name="errorType">错误类型</param>
public void RecordError(string errorType)
{
if (string.IsNullOrEmpty(errorType))
{
return;
}
if (_errorStats.ContainsKey(errorType))
{
_errorStats[errorType]++;
}
else
{
_errorStats[errorType] = 1;
}
}
/// <summary>
/// 获取统计信息的字符串表示
/// </summary>
/// <returns>格式化的统计信息</returns>
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();
}
/// <summary>
/// 重置所有统计数据
/// </summary>
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();
}
}
}

View File

@ -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
{
/// <summary>
/// 集中器模拟器类
/// </summary>
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;
/// <summary>
/// 当接收到消息时触发
/// </summary>
public event EventHandler<byte[]> MessageReceived;
/// <summary>
/// 当状态变更时触发
/// </summary>
public event EventHandler<string> StatusChanged;
/// <summary>
/// 是否已连接
/// </summary>
public bool IsConnected => _networkService.IsConnected;
/// <summary>
/// 是否已登录
/// </summary>
public bool IsLoggedIn => _lastLoginTime > DateTime.MinValue && _loginConfirmed;
/// <summary>
/// 阀门状态
/// </summary>
public bool ValveStatus => _deviceDataService.ValveStatus;
/// <summary>
/// 成功发送的心跳次数
/// </summary>
public int SuccessfulHeartbeats => _heartbeatService.SuccessfulHeartbeats;
/// <summary>
/// 最后登录时间
/// </summary>
public DateTime LastLoginTime => _lastLoginTime;
/// <summary>
/// 是否启用自动重连
/// </summary>
public bool AutoReconnect
{
get => _reconnectionService.AutoReconnect;
set => _reconnectionService.AutoReconnect = value;
}
/// <summary>
/// 重连尝试次数
/// </summary>
public int ReconnectAttempts => _reconnectionService.ReconnectAttempts;
/// <summary>
/// 是否正在重连
/// </summary>
public bool IsReconnecting => _reconnectionService.IsReconnecting;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="concentratorAddress">集中器地址</param>
/// <param name="serverAddress">服务器地址</param>
/// <param name="serverPort">服务器端口</param>
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;
}
/// <summary>
/// 启动模拟器
/// </summary>
/// <param name="autoLogin">是否自动登录</param>
/// <param name="autoHeartbeat">是否自动发送心跳</param>
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;
}
}
/// <summary>
/// 停止模拟器
/// </summary>
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);
}
}
/// <summary>
/// 发送登录消息
/// </summary>
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;
}
}
/// <summary>
/// 发送心跳消息
/// </summary>
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;
}
}
/// <summary>
/// 发送阀控操作消息
/// </summary>
/// <param name="valveOperation">阀门操作1=开阀2=关阀3=查询状态</param>
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;
}
}
/// <summary>
/// 发送数据上传消息
/// </summary>
/// <param name="dataType">数据类型</param>
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;
}
}
/// <summary>
/// 发送读数据消息
/// </summary>
/// <param name="dataType">数据类型</param>
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;
}
}
/// <summary>
/// 发送设置参数消息
/// </summary>
/// <param name="paramType">参数类型</param>
/// <param name="paramData">参数数据</param>
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;
}
}
/// <summary>
/// 发送消息的通用方法
/// </summary>
/// <param name="message">要发送的消息</param>
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);
}
/// <summary>
/// 启动心跳
/// </summary>
public void StartHeartbeat()
{
_heartbeatService.Start();
StatusChanged?.Invoke(this, "心跳已启动");
Log.Information("集中器 (地址: {Address}) 自动心跳已启动", _concentratorAddress);
}
/// <summary>
/// 停止心跳
/// </summary>
public void StopHeartbeat()
{
_heartbeatService.Stop();
StatusChanged?.Invoke(this, "心跳已停止");
Log.Information("集中器 (地址: {Address}) 自动心跳已停止", _concentratorAddress);
}
/// <summary>
/// 设置是否自动响应
/// </summary>
/// <param name="enabled">是否启用自动响应</param>
public void SetAutoResponse(bool enabled)
{
_autoResponse = enabled;
Log.Information("集中器 (地址: {Address}) 自动响应已{Status}",
_concentratorAddress, enabled ? "启用" : "禁用");
}
/// <summary>
/// 更新表计数据
/// </summary>
public void UpdateMeterData(byte dataType, byte[] data)
{
_deviceDataService.SetMeterData(dataType, data);
}
/// <summary>
/// 获取模拟器状态
/// </summary>
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();
}
/// <summary>
/// 获取通信统计信息
/// </summary>
public string GetCommunicationStatistics()
{
return _statisticsService.GetStatisticsReport();
}
/// <summary>
/// 设置重连参数
/// </summary>
public void SetReconnectParameters(bool autoReconnect, int maxAttempts, int delaySeconds)
{
_reconnectionService.SetReconnectParameters(autoReconnect, maxAttempts, delaySeconds);
}
/// <summary>
/// 接收消息处理
/// </summary>
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}");
}
}
/// <summary>
/// 处理接收到的消息
/// </summary>
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);
}
}
/// <summary>
/// 检查是否是登录确认消息
/// </summary>
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]}");
}
}
}
}
/// <summary>
/// 处理自动响应
/// </summary>
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}");
}
}
/// <summary>
/// 阀门状态变更处理
/// </summary>
private void OnValveStatusChanged(object sender, bool isOpen)
{
StatusChanged?.Invoke(this, $"阀门状态已变更为: {(isOpen ? "" : "")}");
}
/// <summary>
/// 连接状态变更处理
/// </summary>
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;
}
}
/// <summary>
/// 网络错误处理
/// </summary>
private void OnNetworkError(object sender, Exception ex)
{
_statisticsService.RecordError($"网络错误: {ex.Message}");
StatusChanged?.Invoke(this, $"网络错误: {ex.Message}");
}
/// <summary>
/// 重连完成处理
/// </summary>
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})");
}
}
}
}