12、从0到1实现SECS协议之SECS-I协议编码与解码

发布时间 2023-08-27 14:25:26作者: 画个一样的我

12、从0到1实现SECS协议之SECS-I协议编码与解码

1、SECS-I 协议编码与解码实现

这个感觉没有啥特别好说的,根据协议慢慢理解就好了,代码实现如下:

package packets

import (
	"encoding/binary"
	"fmt"
)

/*-------------------------------------
	secs头
-------------------------------------*/

type SecsHeader struct {
	// Header的第一字节中的第一个比特: R-bit,其作用是指出消息传输的方向。
	// IsToHost=true -->  equipment -> host
	// IsToHost=false -->  equipment <- host
	IsToHost bool
	// Header的第一和第二字节, 标识一个通信设备
	SessionID uint16
	// Header的第三字节中的第一个比特: Wait-bit,用于指示消息的发送者是否需要回复。
	RequireResponse bool
	// 第一位用于标识 R-bit
	Stream   uint8
	Function uint8
	// End-bit, 用于标识是否是最后一个块
	IsLastBlock bool
	// 第一个比特为 End-bit
	// 0: 还有数据传输
	// 1: 最后一个块
	BlockNumber uint16
	// Header的最后四个字节
	// 第七和第八字节表示 source ID,用于表示消息的发送者,
	// 第九和第十字节表示 transaction ID,用于唯一标示每个发送的消息。
	// 但是代码中实现并没有按照这种格式来?
	System uint32

	// 一个两个字节校验和
	CheckSum uint16
	// 校验是否成功
	CheckSumPassed bool
}

// 方便打印结构体时,以人性化的方式显示
func (sh SecsHeader) String() string {
	return fmt.Sprintf(
		"{SessionID:%d, Stream:%d, Function:%d, isLastBlock:%v, BlockNumber:%d,"+
			" System:0x%x, RequireResponse:%v, IsToHost:%v}",
		sh.SessionID,
		sh.Stream,
		sh.Function,
		sh.IsLastBlock,
		sh.BlockNumber,
		sh.System,
		sh.RequireResponse,
		sh.IsToHost)
}

// Encode SecsHeader 的编码方式
func (sh *SecsHeader) Encode() []byte {
	var result []byte

	//session id
	var sessionIdBytes = make([]byte, 2)
	if sh.IsToHost {
		// Header的第一字节中的第一个比特: R-bit,其作用是指出消息传输的方向。
		binary.BigEndian.PutUint16(sessionIdBytes, sh.SessionID|0b1000000000000000)
	} else {
		// 这里语义清晰可以这样写,不过 0 或 x(任何数) 的结果都是 x
		//binary.BigEndian.PutUint16(sessionIdBytes, sh.SessionID|0b0000000000000000)
		binary.BigEndian.PutUint16(sessionIdBytes, sh.SessionID)
	}
	result = append(result, sessionIdBytes...)

	//stream
	if sh.RequireResponse {
		// Header的第三字节中的第一个比特: Wait-bit,用于指示消息的发送者是否需要回复。
		// 1 表示需要 回复
		result = append(result, sh.Stream|0b10000000)
	} else {
		result = append(result, sh.Stream)
	}
	//function
	result = append(result, sh.Function)

	//block number
	var blockNumberBytes = make([]byte, 2)
	if sh.IsLastBlock {
		// End-bit, 用于标识是否是最后一个块
		// 1 表示最后一个块
		binary.BigEndian.PutUint16(blockNumberBytes, sh.BlockNumber|0b1000000000000000)
	} else {
		binary.BigEndian.PutUint16(blockNumberBytes, sh.BlockNumber)
	}
	result = append(result, blockNumberBytes...)

	//system
	var systemBytes = make([]byte, 4)
	binary.BigEndian.PutUint32(systemBytes, sh.System)
	result = append(result, systemBytes...)

	return result
}

/*-------------------------------------
	secs包
-------------------------------------*/

type SecsPacket struct {
	Header *SecsHeader
	Data   []byte
}

func (sp SecsPacket) String() string {
	return fmt.Sprintf("%+v", sp.Header)
}

// Encode SecsPacket的编码函数
func (sp *SecsPacket) Encode() []byte {
	var result []byte
	//header 编码后的数据
	headerData := sp.Header.Encode()

	//length 消息的长度=headerData + data bytes
	// 这里需要注意, 需要在最后面 加上两位 check sum(2字节), 但是这2字节不算在 length 中
	length := byte(len(headerData) + len(sp.Data))

	//check sum
	checkSum := CalCheckSum(append(headerData, sp.Data...))

	// block 组成格式:
	// length + N-data bytes + check sum(2 bytes)
	// N-data bytes= 10 header + message data
	result = append(result, length)
	result = append(result, headerData...)
	result = append(result, sp.Data...)
	result = append(result, checkSum...)
	return result
}

/*-------------------------------------
	解析
	header=10
注意
	1. data是slice类型会被覆盖 ???
-------------------------------------*/

func Decode(data []byte) *SecsPacket {
	var pkt SecsPacket
	var header SecsHeader
	header.CheckSumPassed = false // 语义更加清晰, 默认失败
	pkt.Header = &header

	if len(data) < 12 {
		// 最少有 10个字节的 header + 2 字节的 check sum
		return &pkt
	}

	//检查checksum
	var checkSum uint16
	for _, i := range data[:len(data)-2] {
		checkSum += uint16(i)
	}
	header.CheckSum = binary.BigEndian.Uint16(data[len(data)-2:])
	header.CheckSumPassed = header.CheckSum == checkSum // 检查校验是否正确
	if !header.CheckSumPassed {
		return &pkt
	}

	header.IsToHost = binary.BigEndian.Uint16(data[:2])>>15 == 1
	// & 与运算, 1 & x 的结果都是 x
	header.SessionID = binary.BigEndian.Uint16(data[:2]) & 0b0111111111111111
	header.RequireResponse = ((data[2] & 0b10000000) >> 7) == 1
	header.Stream = data[2] & 0b01111111
	header.Function = data[3]
	header.IsLastBlock = binary.BigEndian.Uint16(data[4:6])>>15 == 1
	header.BlockNumber = binary.BigEndian.Uint16(data[4:6]) & 0b0111111111111111
	header.System = binary.BigEndian.Uint32(data[6:10])

	pkt.Header = &header
	// 前 10 个字节是 header
	pkt.Data = data[10 : len(data)-2]
	return &pkt
}

// CalCheckSum 校验码 将 data 所有的数值求和
func CalCheckSum(data []byte) []byte {
	var total uint16
	total = 0
	for _, d := range data {
		total += uint16(d)
	}

	var result = make([]byte, 2)
	binary.BigEndian.PutUint16(result, total)
	return result
}

2、单元测试用例

package packets

import (
	"fmt"
	"testing"
)

func TestDecode(t *testing.T) {
	// 定义一个测试用例类型
	type test struct {
		input []byte
		want  *SecsPacket
	}
	s1f1H := &SecsHeader{
		IsToHost:        false,
		SessionID:       0,
		RequireResponse: true,
		Stream:          1,
		Function:        1,
		IsLastBlock:     true,
		// end-bit
		// 0: 还有数据传输
		// 1: 最后一个块
		BlockNumber: 1,
		System:      0x269fbf56,
	}
	w1 := &SecsPacket{
		Header: s1f1H,
		Data:   nil,
	}

	s1f14H := &SecsHeader{
		IsToHost:        true,
		SessionID:       0,
		RequireResponse: false,
		Stream:          1,
		Function:        14,
		IsLastBlock:     true,
		BlockNumber:     1,
		System:          0x269fbf59,
	}
	w2 := &SecsPacket{
		Header: s1f14H,
		Data:   []byte{1, 0, 1, 2, 65, 10, 115, 105, 109, 117, 108, 97, 116, 105, 111, 110, 65, 5, 50, 46, 48, 46, 56, 8, 225},
	}
	// 定义一个存储测试用例的切片
	tests := []test{
		// 此数据是 host -> eqp
		// 发送方发送的数据, 解码时为什么不需要第一个字节?
		// 因为根据协议, 第一个字节表示 block data 的长度, 那后面的才是 sces-i 协议数据
		// 因为日志中是编码过后在打印的数据, 所以包含了 block length(即第一个字节)
		// {input: []byte{10, 0, 0, 129, 1, 128, 1, 38, 159, 191, 86, 2, 221}, want: w1},
		{input: []byte{0, 0, 129, 1, 128, 1, 38, 159, 191, 86, 2, 221}, want: w1},

		// 31 128 0 1 14 128 1 38 159 191 89 1 2 33 1 0 1 2 65 10 115 105 109 117 108 97 116 105 111 110 65 5 50 46 48 46 56 8 225
		{input: []byte{128, 0, 1, 14, 128, 1, 38, 159, 191, 89, 1, 2, 33, 1, 0, 1, 2, 65, 10, 115, 105, 109, 117, 108, 97, 116, 105, 111, 110, 65, 5, 50, 46, 48, 46, 56, 8, 225}, want: w2},
	}

	for _, v := range tests {
		get := Decode(v.input)
		//fmt.Println(fmt.Sprintf("%v", get))
		//fmt.Println(fmt.Sprintf("%v", v.want))
		if fmt.Sprintf("%v", get) != fmt.Sprintf("%v", v.want) {
			t.Fatalf("secs-i packets decode failed, get: %+v, want: %+v", get, v.want)
		}
	}
}