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