添加项目文件。
This commit is contained in:
parent
a9199bd905
commit
e3ad85e1f5
739
CommandProcessor/CommandHandler.cs
Normal file
739
CommandProcessor/CommandHandler.cs
Normal 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
269
Factory/SimulatorFactory.cs
Normal 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("已清空所有模拟器");
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Interfaces/IProtocolMessage.cs
Normal file
41
Interfaces/IProtocolMessage.cs
Normal 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
69
Interfaces/ISimulator.cs
Normal 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
374
Models/MessageAnalyzer.cs
Normal 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
18
Models/Parameter.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
443
Models/Protocol376Message.cs
Normal file
443
Models/Protocol376Message.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
249
Models/RandomDataGenerator.cs
Normal file
249
Models/RandomDataGenerator.cs
Normal 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
191
Models/TestScenario.cs
Normal 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
6
NuGet.Config
Normal 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
152
Program.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Protocol376Simulator.csproj
Normal file
16
Protocol376Simulator.csproj
Normal 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
25
Protocol376Simulator.sln
Normal 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
295
README.md
Normal 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: 增加测试场景导出为脚本功能,支持自动化批量测试
|
||||
191
Services/DeviceDataService.cs
Normal file
191
Services/DeviceDataService.cs
Normal 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 => "状态数据",
|
||||
_ => "未知类型"
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
216
Services/HeartbeatService.cs
Normal file
216
Services/HeartbeatService.cs
Normal 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
269
Services/NetworkService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
263
Services/ReconnectionService.cs
Normal file
263
Services/ReconnectionService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
214
Services/StatisticsService.cs
Normal file
214
Services/StatisticsService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
738
Simulators/ConcentratorSimulator.cs
Normal file
738
Simulators/ConcentratorSimulator.cs
Normal 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})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user