C#应用 - 实现SECS协议
前言
本文部分实现了HSMS,SECS-II协议。只关注协议核心部分,用尽量少的代码写一个简化版的SECS端点,能够与第三方SECS工具进行通信。
1,SECS标准介绍
简单介绍一下SECS标准,此处我们只关注HSMS和SECS-II(其中E5的意思是SEMI标准里编号为E5的标准)。

- HSMS(High-Speed SECS Message Services):属于传输层,在TCP/IP协议基础上做封装
- SECS-II(SEMI Equipment Communications Standard II):属于应用层,在HSMS协议基础上做封装。
本文重点在于协议的代码实现,介绍部分只是列出了协议的关键部分,如果你对协议并不熟悉,建议参考官方文档。
2,HSMS协议
2.1 基本概念
HSMS连接分4种状态:
- NOT CONNECTED:还没有建立任何TCP连接
- CONNECTED:已经建立了TCP连接。此状态有两个子状态,NOT SELECTED 和 SELECTED
- NOT SELECTED:还没有建立HSMS会话
- SELECTED:建立了HSMS会话,此状态下可以正式通信,交换数据消息(Data Message)
HSMS分两种模式:
- Passive:被动模式,内部封装TcpListener,监听端口
- Active:主动模式,内部封装TcpClient,主动发起连接
HSMS分两种消息:
- Control Message:控制消息,用于管理TCP/IP连接上的会话状态,总共有8种
- Select.req / Select.rsp:建立HSMS会话
- Linktest.req / Linktest.rsp:检测会话存活,作用类似心跳
- Deselect.req / Deselect.rsp / Reject.req / Separate.req:简化版不管这些
- Data Message:数据消息,携带实际业务数据,属于SECS-II的范畴
HSMS消息顺序:
- Primary message:主动发送的消息
- Reply message :对Primary message的回复,不一定需要
Passive模式状态转移过程:
- 从NOT CONNECTED到CONNECTED:监听端口,有Active端(TcpClient)连接到此端口
- 从CONNECTED到SELECTED:收到Active端(TcpClient)发送的Select.req,给Active端(TcpClient)回复Select.rsp
Active模式状态转移过程:
- 从NOT CONNECTED到CONNECTED:连接到Passive端(TcpListener)
- 从CONNECTED到SELECTED:向Passive端(TcpListener)发送Select.req,收到Passive端(TcpListener)回复的Select.rsp
超时时间(所有超时时间简化版都不管):
- T3:Reply Timeout,数据消息回复超时,等待数据消息回复的最长时间
- T5:Connect Separation Time,连接超时,两个连接请求之间的最小时间间隔
- T6:Control Transactions Timeout,控制消息超时,等待控制消息回复的最长时间
- T7:Not Selected Timeout,未选择超时,CONNECTED后没有转移到SELECTED的超时时间
2.2 协议格式
- 消息整体格式如下表
| 字节数 | 描述 |
|---|---|
| 4 bytes | Message Length,表示Message Header+Message Text的字节数 |
| 10 bytes | Message Header |
| N bytes | Message Text,格式由消息报头的 PType 字段进一步指定 |
- Message Header格式如下表
| 字节序号 | 描述 |
|---|---|
| 0-1 | Session ID (Device ID),用来区分会话,回复消息时保持一致就行了(Linktest.req和Linktest.rsp是特殊的,必须为0xFFFF) |
| 2 | Header Byte 2,对于控制消息,表示状态码。对于数据消息,表示SECS Stream和是否需要回复(后面SECS-II再介绍) |
| 3 | Header Byte 3,对于控制消息,表示状态码。对于数据消息,表示SECS Function(后面SECS-II再介绍) |
| 4 | PType,一般为0,表示应用层为SECS-II |
| 5 | SType,表示消息类型,0=数据消息,1-9=控制消息(1=Select.req 2=Select.rsp 5=Linktest.req 6=Linktest.rsp) |
| 6-9 | System Bytes,用来区分消息,回复消息时保持一致就行了 |
2.2.1 Control Message
基于上述协议格式,可以写出Select.req和Select.rsp的具体格式,Linktest.req和Linktest.rsp也是类似就不列举了。
| 字段 | 值(16#) | 说明 |
|---|---|---|
| Message Length | 00 00 00 0A | 固定10 bytes,Header(10 bytes)+Text(0 bytes)=10 bytes |
| Session ID | XX XX | 发送方自定义,比如00 01 |
| Header Byte 2 | 00 | 固定为0 |
| Header Byte 3 | 00 | 固定为0 |
| PType | 00 | 固定为0 |
| SType | 01 | 1=Select.req |
| System Bytes | XX XX XX XX | 发送方自定义,比如00 00 00 01 |
| 字段 | 值(16#) | 说明 |
|---|---|---|
| Message Length | 00 00 00 0A | 固定10 bytes,Header(10 bytes)+Text(0 bytes)=10 bytes |
| Session ID | XX XX | 必须与Select.req的Session ID相同 |
| Header Byte 2 | 00 | 固定为0 |
| Header Byte 3 | 00 | 状态,0=成功 其他=失败 |
| PType | 00 | 固定为0 |
| SType | 02 | 2=Select.rsp |
| System Bytes | XX XX XX XX | 必须与Select.req的System Bytes相同 |
2.2.2 Data Message
Data Message的Message Text解析属于SECS-II部分暂时不管,此处只涉及HSMS相关部分。以S1F1(Stream=1 Function=1)为例
| 字段 | 值(16#) | 说明 |
|---|---|---|
| Message Length | 00 00 00 XX | Header(10 bytes)+Text(N bytes)=10+N bytes |
| Session ID | XX XX | 发送方自定义,比如00 01 |
| Header Byte 2 | 81 | bit7表示W-bit=true此消息需要回复,bit6-bit0表示SECS Stream=1 |
| Header Byte 3 | 01 | SECS Function=1 |
| PType | 00 | 固定为0 |
| SType | 02 | 0=Data Message |
| System Bytes | XX XX XX XX | 发送方自定义,比如00 00 00 01 |
| Message Text | Unkwon | N bytes |
2.3 协议实现
用2个类实现HSMS协议,class HSMSMessage和class HSMSConnection
2.3.1 HSMSMessage类
此类定义了HSMS消息本身
namespace 简单SECS
{
public class HSMSMessage
{
// 实际数据由Length(4byte)+Header(10byte)+Data(Nbyte)组成
public byte[] Header { get; } = new byte[10]; // 10-byte HSMS header
public byte[] Data { get; set; }
// Structured accessors
public int SessionId => (Header[0] << 8) | Header[1];
public HSMSSessionType SType => (HSMSSessionType)Header[5];
public int SystemBytes => (Header[6] << 24) | (Header[7] << 16) | (Header[8] << 8) | Header[9];
// Data message properties
public bool ReplyExpected => (Header[2] & 0x80) != 0;
public byte Stream => (byte)(Header[2] & 0x7F);
public byte Function => Header[3];
// Control Message properties
public byte StatusCode => Header[3]; // 用于Select.rsp等
// 从字节流解析(包含4字节长度头)
public static HSMSMessage FromBytes(byte[] data)
{
if (data.Length < 14) // 4-byte length + 10-byte header
throw new ArgumentException("Invalid HSMS message");
var message = new HSMSMessage();
// Verify length
int declaredLength = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
if (declaredLength + 4 != data.Length)
throw new ArgumentException("Length mismatch");
// Copy header (skip 4-byte length)
Array.Copy(data, 4, message.Header, 0, 10);
// Copy data if present
if (declaredLength > 10)
{
message.Data = new byte[declaredLength - 10];
Array.Copy(data, 14, message.Data, 0, declaredLength - 10);
}
return message;
}
// 生成字节流(包含4字节长度头)
public byte[] ToBytes()
{
// 4 byte + 10 byte + N byte
int dataLength = Data?.Length ?? 0;
int totalLength = 10 + dataLength;
byte[] result = new byte[4 + totalLength]; // 4-byte length + message
// Write length (big-endian)
result[0] = (byte)((totalLength >> 24) & 0xFF);
result[1] = (byte)((totalLength >> 16) & 0xFF);
result[2] = (byte)((totalLength >> 8) & 0xFF);
result[3] = (byte)(totalLength & 0xFF);
// Write header
Array.Copy(Header, 0, result, 4, 10);
// Write data
if (dataLength > 0)
{
Array.Copy(Data, 0, result, 14, dataLength);
}
return result;
}
public static HSMSMessage CreateSelectReq()
{
HSMSMessage msg = new HSMSMessage();
msg.Header[0] = 0x00; // 0-1是SessionID
msg.Header[1] = 0x00;
msg.Header[2] = 0x00; // Header Byte 2,必须为0
msg.Header[3] = 0x00; // Header Byte 3,必须为0
msg.Header[4] = 0x00; // PType,固定为0(SECS-II编码)
msg.Header[5] = (byte)HSMSSessionType.SelectReq; // SType
msg.Header[6] = 0x00; // 6-9是SystemBytes
msg.Header[7] = 0x00;
msg.Header[8] = 0x00;
msg.Header[9] = 0x01;
return msg;
}
public static HSMSMessage CreateLinktestReq()
{
HSMSMessage msg = new HSMSMessage();
msg.Header[0] = 0xFF; // 0-1是SessionID=0xFFFF
msg.Header[1] = 0xFF;
msg.Header[2] = 0x00; // Header Byte 2,必须为0
msg.Header[3] = 0x00; // Header Byte 3,必须为0
msg.Header[4] = 0x00; // PType,固定为0(SECS-II编码)
msg.Header[5] = (byte)HSMSSessionType.LinktestReq; // SType
msg.Header[6] = 0x00; // 6-9是SystemBytes
msg.Header[7] = 0x00;
msg.Header[8] = 0x00;
msg.Header[9] = 0x01;
return msg;
}
public static HSMSMessage CreateSelectRsp(HSMSMessage hsmsMessage)
{
HSMSMessage msg = new HSMSMessage();
Array.Copy(hsmsMessage.Header, 0, msg.Header, 0, 10);
msg.Header[3] = 0x00; // Header Byte 3,0表示成功
msg.Header[5] = (byte)HSMSSessionType.SelectRsp; // SType
return msg;
}
public static HSMSMessage CreateLinktestRsp(HSMSMessage hsmsMessage)
{
HSMSMessage msg = new HSMSMessage();
Array.Copy(hsmsMessage.Header, 0, msg.Header, 0, 10);
msg.Header[5] = (byte)HSMSSessionType.LinktestRsp; // SType
return msg;
}
public override string ToString()
{
switch (SType)
{
case HSMSSessionType.DataMessage:
return $"HSMS Message S{Stream}F{Function} SystemBytes: {SystemBytes} Reply Expected: {ReplyExpected}";
default:
return $"{SType} (Session:{SessionId})";
}
}
}
public enum HSMSSessionType : byte
{
DataMessage = 0,
SelectReq = 1,
SelectRsp = 2,
DeselectReq = 3,
DeselectRsp = 4,
LinktestReq = 5,
LinktestRsp = 6,
RejectReq = 7,
SeparateReq = 9
}
2.3.2 HSMSConnection类
此类定义了模式,连接,消息收发,状态转移等
namespace 简单SECS
{
public enum ConnectionMode { Passive, Active }
public enum ConnectionState { NotConnected, Connecting, Connected, Selected }
public class HSMSConnection
{
private TcpClient _tcpClient;
private TcpListener _tcpListener;
private NetworkStream _networkStream;
private ConnectionMode _mode;
private ConnectionState _state;
private IPEndPoint _endPoint;
private CancellationTokenSource _cts;
// 添加缓冲区用于处理粘包
private byte[] _receiveBuffer = new byte[4096];
private int _bufferOffset = 0;
public event EventHandler<HSMSMessage> ControlMessageSend;
public event EventHandler<HSMSMessage> DataMessageReceived;
public event EventHandler<ConnectionState> StateChanged;
public event EventHandler<string> InternalMessage;
public ConnectionState State
{
get => _state;
set
{
if (_state != value)
{
_state = value;
StateChanged?.Invoke(this, _state);
}
}
}
public HSMSConnection(IPEndPoint endPoint, ConnectionMode mode)
{
_endPoint = endPoint ?? throw new ArgumentNullException(nameof(endPoint));
_mode = mode;
_cts = new CancellationTokenSource();
State = ConnectionState.NotConnected;
}
public async Task ConnectAsync()
{
try
{
if (_mode == ConnectionMode.Active)
{
_tcpClient = new TcpClient();
State = ConnectionState.Connecting;
await _tcpClient.ConnectAsync(_endPoint.Address, _endPoint.Port);
_networkStream = _tcpClient.GetStream();
State = ConnectionState.Connected;
}
else
{
_tcpListener = new TcpListener(_endPoint);
_tcpListener.Start();
State = ConnectionState.Connecting;
_tcpClient = await _tcpListener.AcceptTcpClientAsync();
_networkStream = _tcpClient.GetStream();
State = ConnectionState.Connected;
}
StartReceiving();
}
catch(Exception ex)
{
InternalMessage?.Invoke(this, $"[{_mode}] Connect error: {ex.Message}");
}
}
private void StartReceiving()
{
Task.Run(async () =>
{
while (!_cts.IsCancellationRequested && _tcpClient.Connected)
{
try
{
// 读取数据到缓冲区
int bytesRead = await _networkStream.ReadAsync(_receiveBuffer, _bufferOffset, _receiveBuffer.Length - _bufferOffset, _cts.Token);
if (bytesRead > 0)
{
_bufferOffset += bytesRead;
ProcessBuffer();
}
}
catch (Exception ex)
{
InternalMessage?.Invoke(this, $"[{_mode}] Receive error: {ex.Message}");
}
}
Disconnect();
}, _cts.Token);
async void ProcessBuffer()
{
while (_bufferOffset >= 4) // 至少能读取长度头
{
// 读取消息总长度(大端序)
int totalLength = (_receiveBuffer[0] << 24) | (_receiveBuffer[1] << 16) | (_receiveBuffer[2] << 8) | _receiveBuffer[3];
// 检查是否收到完整消息
if (_bufferOffset < totalLength + 4)
{
break; // 等待更多数据
}
// 提取完整消息(包含4字节长度头)
byte[] messageData = new byte[totalLength + 4];
Array.Copy(_receiveBuffer, 0, messageData, 0, totalLength + 4);
// 获取消息(跳过长度头)
HSMSMessage hsmsMsg = HSMSMessage.FromBytes(messageData);
// 移动缓冲区剩余数据
int remaining = _bufferOffset - (totalLength + 4);
if (remaining > 0)
{
Array.Copy(_receiveBuffer, totalLength + 4, _receiveBuffer, 0, remaining);
}
_bufferOffset = remaining;
// 处理消息
try
{
switch (hsmsMsg.SType)
{
case HSMSSessionType.SelectReq:
// HandleSelectReq
InternalMessage?.Invoke(this, $"[{_mode}] Received: {hsmsMsg}");
HSMSMessage reply = HSMSMessage.CreateSelectRsp(hsmsMsg);
await SendMessageAsync(reply);
InternalMessage?.Invoke(this, $"[{_mode}] Send: {reply}");
State = ConnectionState.Selected;
break;
case HSMSSessionType.SelectRsp:
// HandleSelectRsp
InternalMessage?.Invoke(this, $"[{_mode}] Received: {hsmsMsg}");
State = ConnectionState.Selected;
break;
case HSMSSessionType.LinktestReq:
// HandleLinktestReq
InternalMessage?.Invoke(this, $"[{_mode}] Received: {hsmsMsg}");
HSMSMessage reply1 = HSMSMessage.CreateLinktestRsp(hsmsMsg);
await SendMessageAsync(reply1);
InternalMessage?.Invoke(this, $"[{_mode}] Send: {reply1}");
break;
case HSMSSessionType.DataMessage:
// 交给SECS-II处理
DataMessageReceived?.Invoke(this, hsmsMsg);
break;
}
}
catch (Exception ex)
{
InternalMessage?.Invoke(this, $"[{_mode}] Error processing message: {ex}");
}
}
}
}
public async Task SendSelectReqAsync()
{
HSMSMessage selectReq = HSMSMessage.CreateSelectReq();
ControlMessageSend?.Invoke(this, selectReq);
await SendMessageAsync(selectReq);
}
public async Task SendMessageAsync(HSMSMessage message)
{
if (_networkStream == null || !_tcpClient.Connected)
{
InternalMessage?.Invoke(this, $"[{_mode}] Send Message Failed: Not connected");
return;
}
try
{
byte[] data = message.ToBytes();
if (_networkStream != null)
{
await _networkStream.WriteAsync(data, 0, data.Length);
}
}
catch (Exception ex)
{
InternalMessage?.Invoke(this, $"[{_mode}] Send Message error {ex}");
Disconnect();
}
}
public void Disconnect()
{
_cts.Cancel();
_networkStream?.Close();
_tcpClient?.Close();
_tcpListener?.Stop();
State = ConnectionState.NotConnected;
}
}
}
3,SECS-II协议
3.1 基本概念
消息组成
- Stream:表示消息的主要类型
- Function:表示消息的特定功能,Primary Message的Function为奇数,Reply Message的Function为对应的Primary Message的Function加1
- Message Text:表示消息的内容,即Data Items,TVL(Type-Length-Value)格式
Data Item组成
- Type(1 byte):bit7-bit2表示数据类型,bit1-bit0表示Length byte长度
- Length(1-3 bytes):表示Value的长度,到底用几个byte由Type的bit1-bit0决定
- Value:实际数据内容
数据类型
- List:类似于C#的List<object>,可以包含一系列Data Item
- Binary, Boolean, ASCII, I8, I1, I2, I4, F8, F4, U8, U1, U2, U4:类似于C#的基本数据类型byte[], bool, string, long, sbyte, short, int, double, float, ulong, byte, ushort, uint,可以单独存在,也可以被List包含
3.2 HSMS与SECS-II转换
以HSMS消息转换到SECS消息为例
3.2.1 Stream和Function
- 根据Message Header的Header Byte 2,bit6-bit0解析出Stream。比如S1F1 W的Header Byte 2为16#81,bit6-bit0为1,表示S1
- 根据Message Header的Header Byte 3,解析出Function。比如S1F1 W的Header Byte 3为16#01,表示F1
3.2.2 Data Items
数据类型定义如下图

-
例子1,Message Text内容为41 0A 68 65 6C 6C 6F 00 73 65 63 73
- Type(1 byte)为16#41,bit7-bit2为16#40,根据定义16#40对应ASCII。bit1-bit0为16#1,16#1表示Length byte长度为1,即用1个byte表示Length
- Length(1 byte)为16#0A,表示Value的长度为10 bytes
- Value(10 bytes)为68 65 6C 6C 6F 00 73 65 63 73,从ASCII码转换成字符串就是"hello_secs"
- 此Text只有1段,可简写为A[10] “hello_secs”
-
例子2,Message Text内容为01 02 A9 04 00 64 00 C8 01 02 41 04 32 30 30 31 41 04 32 30 30 32
- Type(1 byte)为16#01,bit7-bit2为16#40,根据定义16#40对应List。bit1-bit0为16#1,16#1表示Length byte长度为1,即用1个byte表示Length
- Length(1 byte)为16#02,表示Value的长度为2,即List有2个元素。注意List和基本数据类型不同,List只是包含Data Item,自身并没有Value部分
- Text的第1段,可简写为L[2]
- Type(1 byte)为16#A9,bit7-bit2为16#A8,根据定义16#A8对应U2。bit1-bit0为16#1,16#1表示Length byte长度为1,即用1个byte表示Length
- Length(1 byte)为16#04,表示Value的长度为4 bytes
- Value(4 bytes)为00 64 00 C8,转换成ushort就是100,200
- Text的第2段,可简写为U2[2] 100 200
- Type(1 byte)为16#01
- Length(1 byte)为16#02
- Text的第3段,可简写为L[2]
- Type(1 byte)为16#41
- Length(1 byte)为16#04
- Value(4 bytes)为32 30 30 31,从ASCII码转换成字符串就是"2001"
- Text的第4段,可简写为A[4] "2001"
- Type(1 byte)为16#41
- Length(1 byte)为16#04
- Value(4 bytes)为32 30 30 32,从ASCII码转换成字符串就是"2002"
- Text的第5段,可简写为A[4] "2002"
综上,例子2的消息简写如下
L[2]
U2[2] 100 200
L[2]
A[4] “2001”
A[4] “2002”用C#来类比如下,很清晰吧
new List<object>() {new ushort[] {100, 200}, new List<object>() { "2001", "2002"}}
3.2.3 完整案例
这次以SECS消息转换到HSMS消息为例
-
SECS消息如下
S1F3 W
L[2]
U4[1] 4115
U4[1] 4116 -
对应的HSMS消息
字段 值(16#) 说明 Message Length 00 00 00 18 Header(10 bytes)+Text(14 bytes)=24 bytes Session ID XX XX 发送方自定义 Header Byte 2 81 bit7表示W-bit=true此消息需要回复,bit6-bit0表示SECS Stream=1 Header Byte 3 03 SECS Function=3 PType 00 固定为0 SType 00 0=Data Message System Bytes XX XX XX XX 发送方自定义 Message Text 01 02
B1 04 00 00 10 13
B1 04 00 00 10 14L[2]
U4[1] 4115
U4[1] 4116
3.3 协议实现
用3个类实现SECS-II协议,class SECSItem,class SECSMessage和class SECSEndpoint。用1个类做大小端转换,class ByteArrayConverter
3.3.1 SECSItem类
此类定义了所有数据类型,List和基本数据类型
namespace 简单SECS
{
public abstract class SECSItem
{
public SECSItemFormat Format { get; protected set; }
public abstract byte[] ToBytes();
public static List<SECSItem> ParseItems(byte[] data, HSMSMessage hsmsMsg = null)
{
var items = new List<SECSItem>();
int index = 0;
while (index < data.Length)
{
byte formatByte = data[index++];
SECSItemFormat format = (SECSItemFormat)(formatByte & 0xFC);
int lengthBytes = formatByte & 0x03; // 表示长度的字节数
// List类型保存长度,其他类型保存内容
if (format == SECSItemFormat.List)
{
byte[] itemData = new byte[lengthBytes];
Array.Copy(data, index, itemData, 0, lengthBytes);
index += lengthBytes;
items.Add(CreateItem(format, itemData));
}
else
{
// Read length
int length = 0;
for (int i = 0; i < lengthBytes; i++)
{
if (index >= data.Length)
{
throw new Exception("长度不对");
}
length = (length << 8) | data[index++];
}
// Read data
if (index + length > data.Length)
{
throw new Exception("越界");
}
byte[] itemData = new byte[length];
Array.Copy(data, index, itemData, 0, length);
index += length;
items.Add(CreateItem(format, itemData));
}
}
return items;
}
private static SECSItem CreateItem(SECSItemFormat format, byte[] data)
{
switch (format)
{
case SECSItemFormat.List:
return new SECSListItem(data);
case SECSItemFormat.ASCII:
return new SECSASCIIItem(data);
case SECSItemFormat.Binary:
return new SECSBinaryItem(data);
case SECSItemFormat.Boolean:
return new SECSBooleanItem(data);
case SECSItemFormat.I1:
return new SECSI1Item(data);
case SECSItemFormat.I2:
return new SECSI2Item(data);
case SECSItemFormat.I4:
return new SECSI4Item(data);
case SECSItemFormat.I8:
return new SECSI8Item(data);
case SECSItemFormat.U1:
return new SECSU1Item(data);
case SECSItemFormat.U2:
return new SECSU2Item(data);
case SECSItemFormat.U4:
return new SECSU4Item(data);
case SECSItemFormat.U8:
return new SECSU8Item(data);
case SECSItemFormat.F4:
return new SECSF4Item(data);
case SECSItemFormat.F8:
return new SECSF8Item(data);
default:
throw new NotSupportedException($"Format {format} not supported");
}
}
// 辅助方法
protected int BytesToInt(byte[] value, bool changeEndian = true)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (value.Length >= 4)
{
return BitConverter.ToInt32(value, 0);
}
// 检查最高位是否设置(是否为负数)
bool isNegative = (value[0] & 0x80) != 0;
byte[] paddedBytes = new byte[4];
int startIndex = 4 - value.Length;
// 填充字节(如果负数,填充 0xFF;否则填充 0x00)
byte fillByte = isNegative ? (byte)0xFF : (byte)0x00;
for (int i = 0; i < startIndex; i++)
{
paddedBytes[i] = fillByte;
}
Array.Copy(value, 0, paddedBytes, startIndex, value.Length);
if (changeEndian)
{
Array.Reverse(paddedBytes);
}
int result = BitConverter.ToInt32(paddedBytes, 0);
return result;
}
protected byte[] IntToBytes(int value, bool changeEndian = true)
{
byte[] bytes = BitConverter.GetBytes(value);
if (changeEndian)
{
Array.Reverse(bytes);
}
// 找到第一个非零字节的位置
int startIndex = 0;
while (startIndex < bytes.Length && bytes[startIndex] == 0)
{
startIndex++;
}
// 如果全部是零,返回单个零字节
if (startIndex == bytes.Length)
{
return new byte[] { 0 };
}
// 截取非零部分
byte[] result = new byte[bytes.Length - startIndex];
Array.Copy(bytes, startIndex, result, 0, result.Length);
return result;
}
}
public class SECSListItem : SECSItem
{
public int Length { get; set; } // List类型保存长度,其他类型保存内容
public SECSListItem(int length)
{
Format = SECSItemFormat.List;
Length = length;
}
public SECSListItem(byte[] data)
{
Format = SECSItemFormat.List;
Length = BytesToInt(data);
}
public override byte[] ToBytes()
{
byte[] data = IntToBytes(Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (data.Length & 0x03)));
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append($"L[{Length}]");
return sb.ToString();
}
}
public class SECSASCIIItem : SECSItem
{
public string Value { get; set; }
public SECSASCIIItem(string value)
{
Format = SECSItemFormat.ASCII;
Value = value;
}
public SECSASCIIItem(byte[] data)
{
Format = SECSItemFormat.ASCII;
Value = Encoding.ASCII.GetString(data);
}
public override byte[] ToBytes()
{
byte[] data = Encoding.ASCII.GetBytes(Value);
byte[] lengthBytes = IntToBytes(data.Length);
List<byte> result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"A[{Value.Length}]\"{Value}\"";
}
}
public class SECSBinaryItem : SECSItem
{
public byte[] Value { get; set; }
public SECSBinaryItem(byte[] value)
{
Format = SECSItemFormat.Binary;
Value = value;
}
public override byte[] ToBytes()
{
byte[] lengthBytes = IntToBytes(Value.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(Value);
return result.ToArray();
}
public override string ToString()
{
return $"B[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSBooleanItem : SECSItem
{
public bool[] Value { get; set; }
public SECSBooleanItem(bool[] value)
{
Format = SECSItemFormat.Boolean;
Value = value;
}
public SECSBooleanItem(byte[] data)
{
Format = SECSItemFormat.Boolean;
Value = new bool[data.Length];
for (int i = 0; i < data.Length; i++)
{
Value[i] = data[i] != 0;
}
}
public override byte[] ToBytes()
{
byte[] data = new byte[Value.Length];
for (int i = 0; i < Value.Length; i++)
{
data[i] = (byte)(Value[i] ? 1 : 0);
}
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"Bool[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSI1Item : SECSItem
{
public sbyte[] Value { get; set; }
public SECSI1Item(sbyte[] value)
{
Format = SECSItemFormat.I1;
Value = value;
}
public SECSI1Item(byte[] data)
{
Format = SECSItemFormat.I1;
Value = ByteArrayConverter.FromByteArray<sbyte>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<sbyte>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"I1[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSI2Item : SECSItem
{
public short[] Value { get; set; }
public SECSI2Item(short[] value)
{
Format = SECSItemFormat.I2;
Value = value;
}
public SECSI2Item(byte[] data)
{
Format = SECSItemFormat.I2;
Value = ByteArrayConverter.FromByteArray<short>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<short>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"I2[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSI4Item : SECSItem
{
public int[] Value { get; set; }
public SECSI4Item(int[] value)
{
Format = SECSItemFormat.I4;
Value = value;
}
public SECSI4Item(byte[] data)
{
Format = SECSItemFormat.I4;
Value = ByteArrayConverter.FromByteArray<int>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<int>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"I4[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSI8Item : SECSItem
{
public long[] Value { get; set; }
public SECSI8Item(long[] value)
{
Format = SECSItemFormat.I8;
Value = value;
}
public SECSI8Item(byte[] data)
{
Format = SECSItemFormat.I8;
Value = ByteArrayConverter.FromByteArray<long>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<long>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"I8[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSU1Item : SECSItem
{
public byte[] Value { get; set; }
public SECSU1Item(byte[] value)
{
Format = SECSItemFormat.U1;
Value = value;
}
public override byte[] ToBytes()
{
byte[] lengthBytes = IntToBytes(Value.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(Value);
return result.ToArray();
}
public override string ToString()
{
return $"U1[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSU2Item : SECSItem
{
public ushort[] Value { get; set; }
public SECSU2Item(ushort[] value)
{
Format = SECSItemFormat.U2;
Value = value;
}
public SECSU2Item(byte[] data)
{
Format = SECSItemFormat.U2;
Value = ByteArrayConverter.FromByteArray<ushort>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<ushort>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"U2[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSU4Item : SECSItem
{
public uint[] Value { get; set; }
public SECSU4Item(uint[] value)
{
Format = SECSItemFormat.U4;
Value = value;
}
public SECSU4Item(byte[] data)
{
Format = SECSItemFormat.U4;
Value = ByteArrayConverter.FromByteArray<uint>(data, true); // 00 00 03 E9 => 1001
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<uint>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"U4[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSU8Item : SECSItem
{
public ulong[] Value { get; set; }
public SECSU8Item(ulong[] value)
{
Format = SECSItemFormat.U8;
Value = value;
}
public SECSU8Item(byte[] data)
{
Format = SECSItemFormat.U8;
Value = ByteArrayConverter.FromByteArray<ulong>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<ulong>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"U8[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSF4Item : SECSItem
{
public float[] Value { get; set; }
public SECSF4Item(float[] value)
{
Format = SECSItemFormat.F4;
Value = value;
}
public SECSF4Item(byte[] data)
{
Format = SECSItemFormat.F4;
Value = ByteArrayConverter.FromByteArray<float>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<float>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"F4[{Value.Length}]{string.Join(",", Value)}";
}
}
public class SECSF8Item : SECSItem
{
public double[] Value { get; set; }
public SECSF8Item(double[] value)
{
Format = SECSItemFormat.F8;
Value = value;
}
public SECSF8Item(byte[] data)
{
Format = SECSItemFormat.F8;
Value = ByteArrayConverter.FromByteArray<double>(data, true);
}
public override byte[] ToBytes()
{
byte[] data = ByteArrayConverter.ToByteArray<double>(Value, true);
byte[] lengthBytes = IntToBytes(data.Length);
var result = new List<byte>();
result.Add((byte)((byte)Format | (lengthBytes.Length & 0x03)));
result.AddRange(lengthBytes);
result.AddRange(data);
return result.ToArray();
}
public override string ToString()
{
return $"F8[{Value.Length}]{string.Join(",", Value)}";
}
}
public enum SECSItemFormat : byte
{
List = 0x00,
Binary = 0x20,
Boolean = 0x24,
ASCII = 0x40,
JIS8 = 0x42,
I8 = 0x60,
I1 = 0x64,
I2 = 0x68,
I4 = 0x70,
F8 = 0x80,
F4 = 0x90,
U8 = 0xA0,
U1 = 0xA4,
U2 = 0xA8,
U4 = 0xB0
}
}
3.3.2 SECSMessage类
此类定义了SECS消息本身,内部封装了SECSItem
namespace 简单SECS
{
public class SECSMessage
{
public byte Stream { get; set; }
public byte Function { get; set; }
public bool ReplyExpected { get; set; }
public int SystemBytes { get; set; }
public List<SECSItem> Items { get; set; } = new List<SECSItem>();
public static SECSMessage FromHSMS(HSMSMessage hsmsMsg)
{
var secsMsg = new SECSMessage
{
Stream = hsmsMsg.Stream,
Function = hsmsMsg.Function,
ReplyExpected = hsmsMsg.ReplyExpected,
SystemBytes = hsmsMsg.SystemBytes
};
if (hsmsMsg.Data != null && hsmsMsg.Data.Length > 2)
{
secsMsg.Items = SECSItem.ParseItems(hsmsMsg.Data, hsmsMsg);
}
return secsMsg;
}
public HSMSMessage ToHSMS()
{
HSMSMessage hsmsMsg = new HSMSMessage();
// Header
hsmsMsg.Header[2] = (byte)((ReplyExpected ? 0x80 : 0x00) | (Stream & 0x7F));
hsmsMsg.Header[3] = Function;
hsmsMsg.Header[5] = (byte)HSMSSessionType.DataMessage;
// SystemBytes
hsmsMsg.Header[6] = (byte)((SystemBytes >> 24) & 0xFF);
hsmsMsg.Header[7] = (byte)((SystemBytes >> 16) & 0xFF);
hsmsMsg.Header[8] = (byte)((SystemBytes >> 8) & 0xFF);
hsmsMsg.Header[9] = (byte)(SystemBytes & 0xFF);
// Serialize items
if (Items.Count > 0)
{
List<byte> dataBytes = new List<byte>();
foreach (SECSItem item in Items)
{
dataBytes.AddRange(item.ToBytes());
}
hsmsMsg.Data = dataBytes.ToArray();
}
return hsmsMsg;
}
public SECSMessage CreateReply(byte function)
{
return new SECSMessage
{
Stream = Stream,
Function = function,
SystemBytes = SystemBytes,
ReplyExpected = false
};
}
public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine($"SECS Message S{Stream}F{Function} SystemBytes: {SystemBytes} Reply Expected: {ReplyExpected}");
List<int> LengthList = new List<int>(); // 用于计算缩进
foreach (SECSItem item in Items)
{
sb.Append(' ', LengthList.Count * 2);
sb.AppendLine($"{item}\n");
if (LengthList.Count > 0 &&LengthList.Last() > 0)
{
LengthList[LengthList.Count - 1] -= 1;
}
if (item is SECSListItem)
{
LengthList.Add(((SECSListItem)item).Length);
}
while (LengthList.Count > 0 && LengthList[LengthList.Count - 1] <= 0)
{
LengthList.RemoveAt(LengthList.Count - 1);
}
}
return sb.ToString();
}
}
}
3.3.3 SECSEndpoint类
此类面向应用,定义了一系列事件,内部封装了HSMSConnection
namespace 简单SECS
{
public class SECSEndpoint
{
private readonly HSMSConnection _connection;
private ConnectionMode _mode;
public ConnectionMode Mode
{
get => _mode;
private set
{
_mode = value;
}
}
public string Id { get; } = Guid.NewGuid().ToString("N").Substring(0, 8);
public ConnectionState ConnectionState => _connection.State;
public event EventHandler<HSMSMessage> ControlMessageSend;
public event EventHandler<SECSMessage> DataMessageSend;
public event EventHandler<SECSMessage> DataMessageReceived;
public event EventHandler<ConnectionState> StateChanged;
public event EventHandler<string> InternalMessage;
public SECSEndpoint(IPEndPoint endPoint, ConnectionMode mode)
{
_mode = mode;
_connection = new HSMSConnection(endPoint, mode);
_connection.ControlMessageSend += OnControlMessageSend;
_connection.DataMessageReceived += OnDataMessageReceived;
_connection.StateChanged += OnStateChanged;
_connection.InternalMessage += OnInternalMessage;
}
public async Task ConnectAsync()
{
await _connection.ConnectAsync();
if (_connection.State == ConnectionState.Connected && Mode == ConnectionMode.Active)
{
await _connection.SendSelectReqAsync();
}
}
public async Task SendMessageAsync(SECSMessage message)
{
DataMessageSend?.Invoke(this, message);
await _connection.SendMessageAsync(message.ToHSMS());
}
public void Disconnect()
{
_connection.Disconnect();
}
private void OnControlMessageSend(object sender, HSMSMessage hsmsMsg)
{
ControlMessageSend?.Invoke(this, hsmsMsg);
}
private void OnDataMessageReceived(object sender, HSMSMessage hsmsMsg)
{
SECSMessage secsMessage = SECSMessage.FromHSMS(hsmsMsg);
DataMessageReceived?.Invoke(this, secsMessage);
}
private void OnStateChanged(object sender, ConnectionState state)
{
StateChanged?.Invoke(this, state);
}
private void OnInternalMessage(object sender, string message)
{
InternalMessage?.Invoke(this, message);
}
}
}
3.3.4 ByteArrayConverter类
由于BitConverter的默认行为,定义了几个辅助方法
namespace 简单SECS
{
// BitConverter.IsLittleEndian=true还不让改
// 它认为系统是小端,网络是大端,所以BitConverter所有的方法都自动会做大小端转换,但我并不希望它转换,因此才有了ByteArrayConverter类
public static class ByteArrayConverter
{
/// <summary>
/// 将基本类型数组转换为byte数组,x86/x64是小端,网络传输是大端,小端就是低对低,大端就是低对高
/// </summary>
/// <typeparam name="T">支持的类型: sbyte, short, int, long, byte, ushort, uint, ulong, float, double</typeparam>
/// <param name="array">源数组</param>
/// <param name="changeEndian">改变字节序</param>
public static byte[] ToByteArray<T>(this T[] array, bool changeEndian) where T : struct
{
if (array == null || array.Length == 0)
{
return Array.Empty<byte>();
}
int elementSize = GetElementSize<T>();
byte[] result = new byte[array.Length * elementSize];
for (int i = 0; i < array.Length; i++)
{
int offset = i * elementSize;
switch (array[i])
{
case sbyte sb:
result[offset] = (byte)sb;
break;
case byte b:
result[offset] = b;
break;
case short s:
WriteBytes(BitConverter.GetBytes(s), result, offset, changeEndian);
break;
case ushort us:
WriteBytes(BitConverter.GetBytes(us), result, offset, changeEndian);
break;
case int i32:
WriteBytes(BitConverter.GetBytes(i32), result, offset, changeEndian);
break;
case uint ui32:
WriteBytes(BitConverter.GetBytes(ui32), result, offset, changeEndian);
break;
case long l64:
WriteBytes(BitConverter.GetBytes(l64), result, offset, changeEndian);
break;
case ulong ul64:
WriteBytes(BitConverter.GetBytes(ul64), result, offset, changeEndian);
break;
case float f:
WriteBytes(BitConverter.GetBytes(f), result, offset, changeEndian);
break;
case double d:
WriteBytes(BitConverter.GetBytes(d), result, offset, changeEndian);
break;
default:
throw new NotSupportedException($"不支持类型{typeof(T)}");
}
}
return result;
}
/// <summary>
/// 将byte数组转换为基本类型数组,x86/x64是小端,网络传输是大端,小端就是低对低,大端就是低对高
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="bytes">源字节数组</param>
/// <param name="changeEndian">改变字节序</param>
public static T[] FromByteArray<T>(this byte[] bytes, bool changeEndian) where T : struct
{
if (bytes == null || bytes.Length == 0)
{
return Array.Empty<T>();
}
int elementSize = GetElementSize<T>();
if (bytes.Length % elementSize != 0)
{
throw new ArgumentException($"byte数组长度必须是{elementSize}的整数倍");
}
int count = bytes.Length / elementSize;
T[] result = new T[count];
for (int i = 0; i < count; i++)
{
int offset = i * elementSize;
if (typeof(T) == typeof(sbyte))
{
result[i] = (T)(object)(sbyte)bytes[offset];
}
else if (typeof(T) == typeof(byte))
{
result[i] = (T)(object)bytes[offset];
}
else
{
byte[] temp = new byte[elementSize];
Array.Copy(bytes, offset, temp, 0, elementSize);
if (changeEndian)
{
Array.Reverse(temp);
}
result[i] = GetValueFromBytes<T>(temp);
}
}
return result;
}
#region 辅助方法
private static int GetElementSize<T>() where T : struct
{
if (typeof(T) == typeof(sbyte) || typeof(T) == typeof(byte))
return 1;
if (typeof(T) == typeof(short) || typeof(T) == typeof(ushort))
return 2;
if (typeof(T) == typeof(int) || typeof(T) == typeof(uint) || typeof(T) == typeof(float))
return 4;
if (typeof(T) == typeof(long) || typeof(T) == typeof(ulong) || typeof(T) == typeof(double))
return 8;
throw new NotSupportedException($"不支持类型{typeof(T)}");
}
private static void WriteBytes(byte[] source, byte[] destination, int offset, bool changeEndian)
{
if (changeEndian)
{
for (int i = 0; i < source.Length; i++)
{
destination[offset + i] = source[source.Length - 1 - i];
}
}
else
{
Array.Copy(source, 0, destination, offset, source.Length);
}
}
private static T GetValueFromBytes<T>(byte[] bytes) where T : struct
{
if (typeof(T) == typeof(short))
return (T)(object)BitConverter.ToInt16(bytes, 0);
if (typeof(T) == typeof(ushort))
return (T)(object)BitConverter.ToUInt16(bytes, 0);
if (typeof(T) == typeof(int))
return (T)(object)BitConverter.ToInt32(bytes, 0);
if (typeof(T) == typeof(uint))
return (T)(object)BitConverter.ToUInt32(bytes, 0);
if (typeof(T) == typeof(long))
return (T)(object)BitConverter.ToInt64(bytes, 0);
if (typeof(T) == typeof(ulong))
return (T)(object)BitConverter.ToUInt64(bytes, 0);
if (typeof(T) == typeof(float))
return (T)(object)BitConverter.ToSingle(bytes, 0);
if (typeof(T) == typeof(double))
return (T)(object)BitConverter.ToDouble(bytes, 0);
throw new NotSupportedException($"不支持类型{typeof(T)}");
}
#endregion
}
}
4,应用
4.1 Passive端
新建一个winform项目,引用简单SECS.dll,建立起Passive端。里面定义了一些收发方法,纯粹是为了测试,与实际业务数据不一样。
namespace SECSPassive端
{
public partial class Form1 : Form
{
SECSEndpoint passive;
private readonly Dictionary<int, Func<SECSMessage, Task>> _messageHandlers = new Dictionary<int, Func<SECSMessage, Task>>();
public Form1()
{
InitializeComponent();
}
#region 控件方法
private async void 连接_Click(object sender, EventArgs e)
{
SetTextbox("SECS Passive Simulator");
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555);
passive = new SECSEndpoint(endPoint, ConnectionMode.Passive);
InitializeMessageHandlers();
passive.ControlMessageSend += Passive_ControlMessageSend;
passive.DataMessageSend += Passive_DataMessageSend;
passive.DataMessageReceived += Passive_DataMessageReceived;
passive.StateChanged += Passive_StateChanged;
passive.InternalMessage += Passive_InternalMessage;
await passive.ConnectAsync();
}
private async void 发送_Click(object sender, EventArgs e)
{
string stream = Stream.Text;
string function = Function.Text;
MethodInfo info = this.GetType().GetMethod($"SendS{stream}F{function}");
Task task = (Task)info?.Invoke(this, null);
await task;
}
private void 断开_Click(object sender, EventArgs e)
{
passive.Disconnect();
}
#endregion
private void InitializeMessageHandlers()
{
_messageHandlers[257] = HandleS1F1; // Are You There Request 1*256+1=257
_messageHandlers[269] = HandleS1F13; // Establish Communications Request
_messageHandlers[525] = HandleS2F13; // Equipment Constant Request
_messageHandlers[527] = HandleS2F15; // New Equipment Constant Send
_messageHandlers[529] = HandleS2F17; // Date and Time Request 2*256+17=529
_messageHandlers[1547] = HandleS6F11; // Event Report Send 6*256+11=1547
}
private void Passive_ControlMessageSend(object sender, HSMSMessage hsmsMessage)
{
SetTextbox($"[{passive.Mode}] Send: {hsmsMessage}");
}
private void Passive_DataMessageSend(object sender, SECSMessage secsMessage)
{
SetTextbox($"[{passive.Mode}] Send: {secsMessage}");
}
private async void Passive_DataMessageReceived(object sender, SECSMessage secsMessage)
{
try
{
SetTextbox($"[{passive.Mode}] Received: {secsMessage}");
int streamFunction = (secsMessage.Stream << 8) | secsMessage.Function;
if (_messageHandlers.TryGetValue(streamFunction, out var handler))
{
await handler(secsMessage);
}
else
{
SetTextbox($"[{passive.Mode}] No handler for S{secsMessage.Stream}F{secsMessage.Function}");
}
}
catch (Exception ex)
{
SetTextbox($"[{passive.Mode}] Error processing message: {ex}");
}
}
private void Passive_StateChanged(object sender, ConnectionState state)
{
if (this.InvokeRequired)
{
if (state == ConnectionState.Connected || state == ConnectionState.Selected)
{
Invoke(new Action(() =>
{
连接.Enabled = false;
断开.Enabled = true;
}));
}
else
{
Invoke(new Action(() =>
{
连接.Enabled = true;
断开.Enabled = false;
}));
}
}
else
{
if (state == ConnectionState.Connected || state == ConnectionState.Selected)
{
连接.Enabled = false;
断开.Enabled = true;
}
else
{
连接.Enabled = true;
断开.Enabled = false;
}
}
SetTextbox($"[{passive.Mode}] Connection state changed: {state}");
}
private void Passive_InternalMessage(object sender, string message)
{
SetTextbox(message);
}
private void SetTextbox(string str)
{
if (!str.EndsWith("\r\n"))
{
str += "\r\n";
}
if (this.InvokeRequired)
{
Invoke(new Action(() => textBox1.AppendText(str)));
}
else
{
textBox1.AppendText(str);
}
}
#region Handle
private async Task HandleS1F1(SECSMessage msg)
{
SECSMessage reply = msg.CreateReply(2);
reply.Items.Add(new SECSASCIIItem($"SECSEndpoint_{passive.Id}"));
await passive.SendMessageAsync(reply);
}
private async Task HandleS1F13(SECSMessage msg)
{
SECSMessage reply = msg.CreateReply(14);
reply.Items.Add(new SECSASCIIItem($"SECSEndpoint_{passive.Id}"));
await passive.SendMessageAsync(reply);
}
private async Task HandleS2F13(SECSMessage msg)
{
// 处理Equipment Constant Request
SECSMessage reply = msg.CreateReply(14);
if (msg.Items.Count <= 0) // 返回所有Equipment Constant
{
reply.Items.Add(new SECSListItem(5));
for (int i = 0; i < 5; i++)
{
reply.Items.Add(new SECSU4Item(new uint[] { (uint)(100 + i) }));
}
}
else // 返回指定Equipment Constant
{
int length = ((SECSListItem)msg.Items[0]).Length;
reply.Items.Add(new SECSListItem(length));
for (int i = 1; i < msg.Items.Count; i++)
{
uint[] ecId = ((SECSU4Item)msg.Items[i]).Value;
reply.Items.Add(new SECSU4Item(ecId.Select(item => item + 100).ToArray()));
}
}
await passive.SendMessageAsync(reply);
}
private async Task HandleS2F15(SECSMessage msg)
{
SECSMessage reply = msg.CreateReply(16);
await passive.SendMessageAsync(reply);
}
private async Task HandleS2F17(SECSMessage msg)
{
SECSMessage reply = msg.CreateReply(18);
reply.Items.Add(new SECSASCIIItem(DateTime.Now.ToString("yyyyMMdd-HH:mm:ss")));
await passive.SendMessageAsync(reply);
}
private async Task HandleS6F11(SECSMessage msg)
{
SECSMessage reply = msg.CreateReply(12);
await passive.SendMessageAsync(reply);
}
#endregion
#region Send
public async Task SendS1F1()
{
var message = new SECSMessage
{
Stream = 1,
Function = 1,
ReplyExpected = true
};
await passive.SendMessageAsync(message);
}
public async Task SendS1F13() //EstablishCommunication
{
var message = new SECSMessage
{
Stream = 1,
Function = 13,
ReplyExpected = true
};
await passive.SendMessageAsync(message);
}
public async Task SendS2F13(int ecId) //RequestEquipmentConstant
{
var message = new SECSMessage
{
Stream = 2,
Function = 13,
ReplyExpected = true
};
message.Items.Add(new SECSI4Item(new int[] { ecId }));
await passive.SendMessageAsync(message);
}
public async Task SendS2F15(int eventId)
{
var message = new SECSMessage
{
Stream = 2,
Function = 15,
ReplyExpected = true
};
List<SECSItem> list = new List<SECSItem>();
list.Add(new SECSListItem(2));
list.Add(new SECSI4Item(new int[] { eventId }));
list.Add(new SECSASCIIItem("Enable"));
message.Items.AddRange(list);
await passive.SendMessageAsync(message);
}
public async Task SendS2F17() //RequestDateTime
{
var message = new SECSMessage
{
Stream = 2,
Function = 17,
ReplyExpected = true
};
await passive.SendMessageAsync(message);
}
public async Task SendS6F11()
{
var message = new SECSMessage
{
Stream = 6,
Function = 11,
ReplyExpected = false
};
List<SECSItem> list = new List<SECSItem>();
list.Add(new SECSListItem(2));
list.Add(new SECSI4Item(new int[] { 1001 }));
list.Add(new SECSListItem(2));
list.Add(new SECSASCIIItem("SampleEvent"));
list.Add(new SECSI2Item(new short[] { 123 }));
message.Items.AddRange(list);
await passive.SendMessageAsync(message);
}
public async Task SendS10F11() //TEST
{
var message = new SECSMessage
{
Stream = 10,
Function = 11,
ReplyExpected = false
};
List<SECSItem> list = new List<SECSItem>();
list.Add(new SECSListItem(2));
list.Add(new SECSU2Item(new ushort[] { 100, 200 }));
list.Add(new SECSListItem(2));
list.Add(new SECSASCIIItem("2001"));
list.Add(new SECSASCIIItem("2002"));
message.Items.AddRange(list);
await passive.SendMessageAsync(message);
}
#endregion
}
}
4.2 Active端
新建一个winform项目,引用简单SECS.dll,建立起Passive端。与Passive端只有一行代码有区别,其余完全一致。
active = new SECSEndpoint(endPoint, ConnectionMode.Active);
至此代码部分都写完了,解决方案结构如下

4.3 第三方工具
市面上第三方工具很多,我选了两种来测试,一种是ITRI SECS Emulator,另一种是FASTSim。
4.3.1 ITRI SECS Emulator
下面让Emulator做Passive,简单SECS做Active,建立连接,然后互发几句测试消息。
-
打开SECS Emulator,设置为Passive端,设置好HSMS的IP地址和端口

-
读取准备好的SML文件,然后开启通信

-
把Active端winform项目跑起来,点击连接,然后让Active发送S1F1

-
让Passive发送复杂测试消息,实际业务中消息不是这样的,此处纯粹是为了测试

没毛病,符合预期。
4.3.2 FASTSim
下面让FASTSim做Active,简单SECS做Passive,建立连接,然后互发几句测试消息。
-
把Passive端winform项目跑起来,点击连接

-
打开FASTSim,设置为Active端,设置好HSMS的IP地址和端口

-
建立通信,然后让Active和Passive相互发送S1F1

-
让Passive发送复杂测试消息,实际业务中消息不是这样的,此处纯粹是为了测试

没毛病,符合预期。

浙公网安备 33010602011771号