443 lines
16 KiB
C#
443 lines
16 KiB
C#
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;
|
||
}
|
||
}
|
||
} |