174 lines
5.2 KiB
Go
174 lines
5.2 KiB
Go
// Package protocol 实现《气体探测器通讯协议》V1.8 的二进制帧编解码。
|
|
package protocol
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
StartByte = byte(0x5E)
|
|
EndByte = byte(0x5B)
|
|
FixedBodyBytes = 29 // key 到有效数据长度,不含载荷和校验。
|
|
)
|
|
|
|
var (
|
|
ErrFrameTooShort = errors.New("数据帧长度不足")
|
|
ErrBoundary = errors.New("起始符或结束符无效")
|
|
ErrLength = errors.New("帧长度不匹配")
|
|
ErrChecksum = errors.New("LRC8 校验失败")
|
|
ErrPayloadLength = errors.New("数据包有效长度无效")
|
|
ErrEncryptedLength = errors.New("密文长度不是 16 字节的倍数")
|
|
)
|
|
|
|
// Frame 是设备原始帧的强类型表示;多字节整数均按大端序传输。
|
|
type Frame struct {
|
|
KeyID byte
|
|
Version byte
|
|
Control byte
|
|
DeviceKind byte
|
|
DeviceType byte
|
|
DeviceModel [3]byte
|
|
DeviceID [8]byte
|
|
PacketNumber uint16
|
|
Sequence byte
|
|
Final bool
|
|
DeviceTime time.Time
|
|
Payload []byte
|
|
}
|
|
|
|
// Keyring 按协议支持 0 号明文和 1/2/3 号 AES-128 密钥。
|
|
type Keyring map[byte][]byte
|
|
|
|
// Encode 构造可直接作为 MQTT payload 发布的厂商二进制帧。
|
|
func Encode(frame Frame, keys Keyring) ([]byte, error) {
|
|
payload, err := cryptPayload(frame.KeyID, frame.Payload, keys, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body := make([]byte, FixedBodyBytes+len(payload))
|
|
body[0], body[1], body[2] = frame.KeyID, frame.Version, frame.Control
|
|
body[3], body[4] = frame.DeviceKind, frame.DeviceType
|
|
copy(body[5:8], frame.DeviceModel[:])
|
|
copy(body[8:16], frame.DeviceID[:])
|
|
binary.BigEndian.PutUint16(body[16:18], frame.PacketNumber)
|
|
body[18] = frame.Sequence
|
|
if frame.Final {
|
|
body[19] = 1
|
|
}
|
|
encodeBCDTime(body[20:27], frame.DeviceTime)
|
|
binary.BigEndian.PutUint16(body[27:29], uint16(len(frame.Payload)))
|
|
copy(body[29:], payload)
|
|
|
|
frameLength := len(body) + 1 // 加上校验字节,不含起始符、长度字段和结束符。
|
|
if frameLength > 0xffff {
|
|
return nil, fmt.Errorf("帧过长: %d", frameLength)
|
|
}
|
|
result := make([]byte, 0, frameLength+4)
|
|
result = append(result, StartByte, byte(frameLength>>8), byte(frameLength))
|
|
result = append(result, body...)
|
|
result = append(result, LRC8(result[1:]), EndByte)
|
|
return result, nil
|
|
}
|
|
|
|
// Decode 校验边界、长度、LRC8 和 AES 后返回有效载荷。
|
|
func Decode(raw []byte, keys Keyring) (Frame, error) {
|
|
var frame Frame
|
|
if len(raw) < FixedBodyBytes+5 {
|
|
return frame, ErrFrameTooShort
|
|
}
|
|
if raw[0] != StartByte || raw[len(raw)-1] != EndByte {
|
|
return frame, ErrBoundary
|
|
}
|
|
declared := int(binary.BigEndian.Uint16(raw[1:3]))
|
|
if declared+4 != len(raw) {
|
|
return frame, ErrLength
|
|
}
|
|
if LRC8(raw[1:len(raw)-2]) != raw[len(raw)-2] {
|
|
return frame, ErrChecksum
|
|
}
|
|
body := raw[3 : len(raw)-2]
|
|
frame.KeyID, frame.Version, frame.Control = body[0], body[1], body[2]
|
|
frame.DeviceKind, frame.DeviceType = body[3], body[4]
|
|
copy(frame.DeviceModel[:], body[5:8])
|
|
copy(frame.DeviceID[:], body[8:16])
|
|
frame.PacketNumber = binary.BigEndian.Uint16(body[16:18])
|
|
frame.Sequence, frame.Final = body[18], body[19] == 1
|
|
frame.DeviceTime = decodeBCDTime(body[20:27])
|
|
validLength := int(binary.BigEndian.Uint16(body[27:29]))
|
|
plain, err := cryptPayload(frame.KeyID, body[29:], keys, true)
|
|
if err != nil {
|
|
return frame, err
|
|
}
|
|
if validLength > len(plain) {
|
|
return frame, ErrPayloadLength
|
|
}
|
|
frame.Payload = append([]byte(nil), plain[:validLength]...)
|
|
return frame, nil
|
|
}
|
|
|
|
// LRC8 返回连续字节和的二进制补码低字节。
|
|
func LRC8(data []byte) byte {
|
|
var sum byte
|
|
for _, value := range data {
|
|
sum += value
|
|
}
|
|
return ^sum + 1
|
|
}
|
|
|
|
func cryptPayload(keyID byte, input []byte, keys Keyring, decrypt bool) ([]byte, error) {
|
|
if keyID == 0 {
|
|
size := len(input)
|
|
if !decrypt && size%aes.BlockSize != 0 {
|
|
size += aes.BlockSize - size%aes.BlockSize
|
|
}
|
|
result := make([]byte, size)
|
|
copy(result, input)
|
|
return result, nil
|
|
}
|
|
key := keys[keyID]
|
|
if len(key) != aes.BlockSize {
|
|
return nil, fmt.Errorf("%d 号 AES 密钥必须为 16 字节", keyID)
|
|
}
|
|
if decrypt && len(input)%aes.BlockSize != 0 {
|
|
return nil, ErrEncryptedLength
|
|
}
|
|
size := len(input)
|
|
if !decrypt && size%aes.BlockSize != 0 {
|
|
size += aes.BlockSize - size%aes.BlockSize
|
|
}
|
|
output := make([]byte, size)
|
|
copy(output, input)
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for offset := 0; offset < len(output); offset += aes.BlockSize {
|
|
if decrypt {
|
|
block.Decrypt(output[offset:offset+aes.BlockSize], input[offset:offset+aes.BlockSize])
|
|
} else {
|
|
block.Encrypt(output[offset:offset+aes.BlockSize], output[offset:offset+aes.BlockSize])
|
|
}
|
|
}
|
|
return output, nil
|
|
}
|
|
|
|
func encodeBCDTime(dst []byte, value time.Time) {
|
|
if value.IsZero() {
|
|
value = time.Now()
|
|
}
|
|
parts := []int{value.Year() / 100, value.Year() % 100, int(value.Month()), value.Day(), value.Hour(), value.Minute(), value.Second()}
|
|
for index, part := range parts {
|
|
dst[index] = byte((part/10)<<4 | part%10)
|
|
}
|
|
}
|
|
|
|
func decodeBCDTime(src []byte) time.Time {
|
|
n := func(value byte) int { return int(value>>4)*10 + int(value&0x0f) }
|
|
year := n(src[0])*100 + n(src[1])
|
|
return time.Date(year, time.Month(n(src[2])), n(src[3]), n(src[4]), n(src[5]), n(src[6]), 0, time.Local)
|
|
}
|