首页 > 其他分享 >12、从0到1实现SECS协议之SECS-I协议编码与解码

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

时间:2023-08-27 14:35:37浏览次数:38  
标签:协议 12 fmt header SECS want byte data

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)
		}
	}
}

标签:协议,12,fmt,header,SECS,want,byte,data
From: https://www.cnblogs.com/huageyiyangdewo/p/17660255.html

相关文章

  • 11、从0到1实现SECS协议之HSMS协议中的handler
    11、从0到1实现SECS协议之HSMS协议中的handler前面实现了发送事件的处理机制,接下来我们即将实现ISecsHandler接口,将前面实现的各种功能组合起来,从而提供一个完整可用的服务。1、handler的具体实现packagehandlerimport( "context" "errors" "fmt" "github.com/loopl......
  • 13、从0到1实现SECS协议之优先级队列(SECS-I)
    13、从0到1实现SECS协议之优先级队列(SECS-I)逻辑和HSMS协议中的优先级队列一样,只不过存储的数据变了而已。1、并发安全的优先级队列packagequeueimport( "secs-gem/common" "secs-gem/secs/packets" "secs-gem/secsgem" "container/heap" "context" "sync......
  • 12、整合Mybatis
    12、整合Mybatis导包<dependencies><!--https://mvnrepository.com/artifact/org.springframework/spring-webmvc--><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifact......
  • Modbus协议详解1:Modbus的简介
    Modbus协议是应用非常广泛的一种通信协议规约,也是最早应用在工业现场的协议。早在1979年出现工业串行链路以来,Modbus的应用就开始普遍起来,它使得各种自动化设备之间的互相通信得到实现,做嵌入式产品开发的朋友应该不会陌生,Modbus的身影是随处可见的。并且在中国Modbus都已经形成了规......
  • oracle学习笔记(12)——数据库服务器工作模式与数据字典
    1、 专用服务器工作模式    1)概念:       专用服务器模式是指Oracle为每个用户进程启动一个专门的服务器进程,该服务器进程仅为该用户进程提供服务,直到用户进程断开连接时,对应的服务器进程才终止。    2)特点:       服务器进程与客户进......
  • ⛳ TCP 协议面试题
    ⛳TCP协议面试题......
  • CS0012: 类型“System.Data.Objects.DataClasses.EntityObject”在未被引用的程序集中
    CS0012:类型“System.Data.Objects.DataClasses.EntityObject”在未被引用的程序集中定义。必须添加对程序集“System.Data.Entity,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”的引用。网址:yii666.com<网址:yii666.com处理办法,在web.config-》syst......
  • LLM设置-AI基础系列文章第12篇
    您的关注是对我最大的支持......
  • hdu:悼念512汶川大地震遇难同胞——珍惜现在,感恩生活
    ProblemDescription急!灾区的食物依然短缺!为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾区,现在假设你一共有资金n元,而市场有m种大米,每种大米都是袋装产品,其价格不等,并且只能整袋购买。请问:你用有限的资金最多能采购多少公斤粮食呢?后记:人生是一个充满了......
  • 【12.0】Flask框架之flask-script
    【一】Django中的命令【1】引入django中,有命令pythonmanage.pyrunserver:这个命令用于启动Django开发服务器,让我们能够在本地运行我们的应用程序。它会默认在本地的8000端口上启动服务器,我们可以在浏览器中访问http://localhost:8000来查看应用程序。pythonmanage.......