meshcli: Meshtastic MQTT 纯 CLI 聊天客户端(数据包解码参考 meshtastic_mqtt_server)
This commit is contained in:
7 files changed
+1131
No files matched your search
+208
@@ -0,0 +1,208 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
// BuildOptions 是构建 MeshPacket 的公共选项。
|
||||
type BuildOptions struct {
|
||||
FromNodeNum uint32
|
||||
ToNodeNum uint32
|
||||
PacketID uint32
|
||||
ChannelID string
|
||||
GatewayID string
|
||||
PSK []byte
|
||||
Encrypt bool
|
||||
ViaMQTT bool
|
||||
}
|
||||
|
||||
// TextBuildOptions 是构建文本消息的选项。
|
||||
type TextBuildOptions struct {
|
||||
BuildOptions
|
||||
Text string
|
||||
}
|
||||
|
||||
// NodeInfoBuildOptions 是构建节点信息广播的选项。
|
||||
type NodeInfoBuildOptions struct {
|
||||
BuildOptions
|
||||
NodeID string
|
||||
LongName string
|
||||
ShortName string
|
||||
HWModel uint32
|
||||
Role uint32
|
||||
IsLicensed bool
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
// RandomPacketID 生成一个非零随机 packet id。
|
||||
func RandomPacketID() uint32 {
|
||||
var buf [4]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return 1
|
||||
}
|
||||
id := binary.LittleEndian.Uint32(buf[:])
|
||||
if id == 0 {
|
||||
id = 1
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// BuildTextServiceEnvelope 构建一条加密的文本消息 ServiceEnvelope。
|
||||
func BuildTextServiceEnvelope(opts TextBuildOptions) ([]byte, error) {
|
||||
if opts.FromNodeNum == 0 {
|
||||
return nil, fmt.Errorf("from node number is required")
|
||||
}
|
||||
if opts.PacketID == 0 {
|
||||
return nil, fmt.Errorf("packet id is required")
|
||||
}
|
||||
if opts.ChannelID == "" {
|
||||
return nil, fmt.Errorf("channel id is required")
|
||||
}
|
||||
if opts.GatewayID == "" {
|
||||
opts.GatewayID = NodeNumToID(opts.FromNodeNum)
|
||||
}
|
||||
if opts.Text == "" {
|
||||
return nil, fmt.Errorf("text is required")
|
||||
}
|
||||
if !utf8.ValidString(opts.Text) {
|
||||
return nil, fmt.Errorf("text must be valid utf-8")
|
||||
}
|
||||
data := buildData(PortNumTextMessage, []byte(opts.Text))
|
||||
packet, err := buildMeshPacket(opts.BuildOptions, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildServiceEnvelope(packet, opts.ChannelID, opts.GatewayID), nil
|
||||
}
|
||||
|
||||
// BuildNodeInfoServiceEnvelope 构建一条节点信息广播 ServiceEnvelope。
|
||||
func BuildNodeInfoServiceEnvelope(opts NodeInfoBuildOptions) ([]byte, error) {
|
||||
if opts.FromNodeNum == 0 {
|
||||
return nil, fmt.Errorf("from node number is required")
|
||||
}
|
||||
if opts.NodeID == "" {
|
||||
opts.NodeID = NodeNumToID(opts.FromNodeNum)
|
||||
}
|
||||
if opts.GatewayID == "" {
|
||||
opts.GatewayID = NodeNumToID(opts.FromNodeNum)
|
||||
}
|
||||
if opts.ChannelID == "" {
|
||||
return nil, fmt.Errorf("channel id is required")
|
||||
}
|
||||
if opts.LongName == "" {
|
||||
opts.LongName = NodeNumToID(opts.FromNodeNum)
|
||||
}
|
||||
if opts.ShortName == "" {
|
||||
opts.ShortName = strings.ToUpper(opts.LongName)
|
||||
if len(opts.ShortName) > 4 {
|
||||
opts.ShortName = opts.ShortName[:4]
|
||||
}
|
||||
}
|
||||
user := buildUser(opts)
|
||||
data := buildData(PortNumNodeInfo, user)
|
||||
packet, err := buildMeshPacket(opts.BuildOptions, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildServiceEnvelope(packet, opts.ChannelID, opts.GatewayID), nil
|
||||
}
|
||||
|
||||
func buildData(portnum uint32, payload []byte) []byte {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(portnum))
|
||||
out = protowire.AppendTag(out, 2, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, payload)
|
||||
return out
|
||||
}
|
||||
|
||||
func buildUser(opts NodeInfoBuildOptions) []byte {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(opts.NodeID))
|
||||
out = protowire.AppendTag(out, 2, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(opts.LongName))
|
||||
out = protowire.AppendTag(out, 3, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(opts.ShortName))
|
||||
if opts.HWModel != 0 {
|
||||
out = protowire.AppendTag(out, 5, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(opts.HWModel))
|
||||
}
|
||||
out = protowire.AppendTag(out, 6, protowire.VarintType)
|
||||
if opts.IsLicensed {
|
||||
out = protowire.AppendVarint(out, 1)
|
||||
} else {
|
||||
out = protowire.AppendVarint(out, 0)
|
||||
}
|
||||
out = protowire.AppendTag(out, 7, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(opts.Role))
|
||||
if len(opts.PublicKey) > 0 {
|
||||
out = protowire.AppendTag(out, 8, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, opts.PublicKey)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func buildMeshPacket(opts BuildOptions, data []byte) ([]byte, error) {
|
||||
if opts.FromNodeNum == 0 {
|
||||
return nil, fmt.Errorf("from node number is required")
|
||||
}
|
||||
if opts.PacketID == 0 {
|
||||
return nil, fmt.Errorf("packet id is required")
|
||||
}
|
||||
if opts.ChannelID == "" {
|
||||
return nil, fmt.Errorf("channel id is required")
|
||||
}
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.FromNodeNum)
|
||||
out = protowire.AppendTag(out, 2, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.ToNodeNum)
|
||||
|
||||
if opts.Encrypt {
|
||||
if len(opts.PSK) == 0 {
|
||||
return nil, fmt.Errorf("psk is required for encrypted packet")
|
||||
}
|
||||
ciphertext, err := cryptAESCTR(opts.PSK, opts.FromNodeNum, opts.PacketID, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = protowire.AppendTag(out, 3, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, uint64(channelHash(opts.ChannelID, opts.PSK)))
|
||||
out = protowire.AppendTag(out, 5, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, ciphertext)
|
||||
} else {
|
||||
out = protowire.AppendTag(out, 4, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, data)
|
||||
}
|
||||
|
||||
out = protowire.AppendTag(out, 6, protowire.Fixed32Type)
|
||||
out = protowire.AppendFixed32(out, opts.PacketID)
|
||||
if opts.ViaMQTT {
|
||||
out = protowire.AppendTag(out, 14, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, 1)
|
||||
}
|
||||
// hop_limit = 7(默认)
|
||||
out = protowire.AppendTag(out, 9, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, 7)
|
||||
out = protowire.AppendTag(out, 15, protowire.VarintType)
|
||||
out = protowire.AppendVarint(out, 7)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func buildServiceEnvelope(packet []byte, channelID string, gatewayID string) []byte {
|
||||
var out []byte
|
||||
out = protowire.AppendTag(out, 1, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, packet)
|
||||
out = protowire.AppendTag(out, 2, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(channelID))
|
||||
out = protowire.AppendTag(out, 3, protowire.BytesType)
|
||||
out = protowire.AppendBytes(out, []byte(gatewayID))
|
||||
return out
|
||||
}
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
// Package mesh 实现 Meshtastic MQTT 数据包的解码与构建。
|
||||
// 协议实现参考 meshtastic_mqtt_server 工程(MIT License):
|
||||
// - ServiceEnvelope / MeshPacket / Data 使用 protobuf wire 格式手写解析(google.golang.org/protobuf/encoding/protowire)
|
||||
// - 加密包使用频道 PSK + AES-CTR,nonce = little-endian(packetID 8B) + little-endian(fromNum 4B) + 4 字节零
|
||||
// - channel hash = xor(channelName) ^ xor(psk)
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protowire"
|
||||
)
|
||||
|
||||
// Portnum 常量(与 meshtastic.proto PortNum 一致)。
|
||||
const (
|
||||
PortNumUnknown = 0
|
||||
PortNumTextMessage = 1
|
||||
PortNumPosition = 3
|
||||
PortNumNodeInfo = 4
|
||||
PortNumRouting = 5
|
||||
PortNumTelemetry = 67
|
||||
PortNumMapReport = 73
|
||||
)
|
||||
|
||||
// NodeNumBroadcast 是广播目标节点号。
|
||||
const NodeNumBroadcast uint32 = 0xffffffff
|
||||
|
||||
// defaultMeshtasticPSK 是 Meshtastic 默认频道密钥(PSK 索引 1)。
|
||||
var defaultMeshtasticPSK = []byte{
|
||||
0xD4, 0xF1, 0xBB, 0x3A,
|
||||
0x20, 0x29, 0x07, 0x59,
|
||||
0xF0, 0xBC, 0xFF, 0xAB,
|
||||
0xCF, 0x4E, 0x69, 0x01,
|
||||
}
|
||||
|
||||
// Packet 是解码后的 MeshPacket 关键字段。
|
||||
type Packet struct {
|
||||
From uint32
|
||||
To uint32
|
||||
Channel uint32
|
||||
ID uint32
|
||||
WantAck bool
|
||||
ViaMQTT bool
|
||||
PKIEncrypted bool
|
||||
Decoded *Data
|
||||
Encrypted []byte
|
||||
}
|
||||
|
||||
// Data 是 MeshPacket.decoded 中的 Data 子包。
|
||||
type Data struct {
|
||||
Portnum uint32
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// TextMessage 是一条解码后的文本消息。
|
||||
type TextMessage struct {
|
||||
From uint32
|
||||
To uint32
|
||||
Text string
|
||||
Hex string // 非 UTF-8 时的十六进制表示
|
||||
}
|
||||
|
||||
// NodeInfo 是解码后的节点信息(NODEINFO_APP)。
|
||||
type NodeInfo struct {
|
||||
From uint32
|
||||
ID string
|
||||
LongName string
|
||||
ShortName string
|
||||
HWModel uint64
|
||||
Role uint64
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
// Position 是解码后的位置信息(POSITION_APP)。
|
||||
type Position struct {
|
||||
From uint32
|
||||
Latitude *float64
|
||||
Longitude *float64
|
||||
Altitude *int32
|
||||
}
|
||||
|
||||
// ExpandPSK 展开 Base64 PSK,兼容 Meshtastic 默认索引 PSK 和短 key 补零规则。
|
||||
func ExpandPSK(pskBase64 string) ([]byte, error) {
|
||||
psk, err := base64.StdEncoding.DecodeString(strings.TrimSpace(pskBase64))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid psk: %w", err)
|
||||
}
|
||||
if len(psk) == 1 {
|
||||
idx := psk[0]
|
||||
if idx == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
key := append([]byte(nil), defaultMeshtasticPSK...)
|
||||
key[len(key)-1] = byte((int(key[len(key)-1]) + int(idx) - 1) & 0xff)
|
||||
return key, nil
|
||||
}
|
||||
if len(psk) > 0 && len(psk) < 16 {
|
||||
return append(psk, make([]byte, 16-len(psk))...), nil
|
||||
}
|
||||
if len(psk) > 16 && len(psk) < 32 {
|
||||
return append(psk, make([]byte, 32-len(psk))...), nil
|
||||
}
|
||||
if len(psk) != 0 && len(psk) != 16 && len(psk) != 24 && len(psk) != 32 {
|
||||
return nil, fmt.Errorf("invalid psk length %d: AES keys must be 16, 24, or 32 bytes", len(psk))
|
||||
}
|
||||
return psk, nil
|
||||
}
|
||||
|
||||
// Decode 解码一个 MQTT 消息 payload(ServiceEnvelope),
|
||||
// 返回解码结果(*TextMessage / *NodeInfo / *Position / *GenericPacket)与错误。
|
||||
// 加密包会用频道 PSK 尝试解密;PKI 加密包(无密钥)返回错误。
|
||||
func Decode(topic string, raw []byte, key []byte) (any, error) {
|
||||
env, err := parseServiceEnvelope(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service envelope: %w", err)
|
||||
}
|
||||
if env.Packet == nil {
|
||||
return nil, fmt.Errorf("no packet in envelope")
|
||||
}
|
||||
pkt := env.Packet
|
||||
|
||||
if pkt.Decoded == nil && len(pkt.Encrypted) > 0 {
|
||||
decoded, status := tryDecrypt(pkt, env.ChannelID, key)
|
||||
if decoded == nil {
|
||||
return nil, fmt.Errorf("decrypt failed (%s)", status)
|
||||
}
|
||||
pkt.Decoded = decoded
|
||||
}
|
||||
|
||||
if pkt.Decoded == nil {
|
||||
return nil, fmt.Errorf("empty packet")
|
||||
}
|
||||
|
||||
switch pkt.Decoded.Portnum {
|
||||
case PortNumTextMessage:
|
||||
text := string(pkt.Decoded.Payload)
|
||||
msg := &TextMessage{From: pkt.From, To: pkt.To, Text: text}
|
||||
if !utf8.Valid(pkt.Decoded.Payload) {
|
||||
msg.Text = ""
|
||||
msg.Hex = fmt.Sprintf("%x", pkt.Decoded.Payload)
|
||||
}
|
||||
return msg, nil
|
||||
case PortNumNodeInfo:
|
||||
info, err := parseUser(pkt.Decoded.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info.From = pkt.From
|
||||
return info, nil
|
||||
case PortNumPosition:
|
||||
pos, err := parsePosition(pkt.Decoded.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pos.From = pkt.From
|
||||
return pos, nil
|
||||
case PortNumTelemetry:
|
||||
return &GenericPacket{Portnum: PortNumTelemetry, From: pkt.From, PayloadLen: len(pkt.Decoded.Payload)}, nil
|
||||
default:
|
||||
return &GenericPacket{Portnum: pkt.Decoded.Portnum, From: pkt.From, PayloadLen: len(pkt.Decoded.Payload)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GenericPacket 是未详细解码的其他类型包。
|
||||
type GenericPacket struct {
|
||||
Portnum uint32
|
||||
From uint32
|
||||
PayloadLen int
|
||||
}
|
||||
|
||||
// PortnumName 返回 portnum 的可读名称。
|
||||
func PortnumName(portnum uint32) string {
|
||||
if name, ok := portNumNames[portnum]; ok {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("PORTNUM_%d", portnum)
|
||||
}
|
||||
|
||||
// NodeNumToID 把节点号格式化为 !xxxxxxxx。
|
||||
func NodeNumToID(nodeNum uint32) string {
|
||||
return fmt.Sprintf("!%08x", nodeNum)
|
||||
}
|
||||
|
||||
// ParseNodeID 解析 !xxxxxxxx 为节点号。
|
||||
func ParseNodeID(nodeID string) (uint32, error) {
|
||||
value := strings.TrimSpace(nodeID)
|
||||
value = strings.TrimPrefix(value, "!")
|
||||
if len(value) != 8 {
|
||||
return 0, fmt.Errorf("node id must be !xxxxxxxx")
|
||||
}
|
||||
var num uint32
|
||||
if _, err := fmt.Sscanf(value, "%08x", &num); err != nil {
|
||||
return 0, fmt.Errorf("invalid node id: %w", err)
|
||||
}
|
||||
return num, nil
|
||||
}
|
||||
|
||||
// tryDecrypt 用频道 PSK 解密 encrypted 载荷(AES-CTR),并解析出 Data 子包。
|
||||
func tryDecrypt(pkt *Packet, channelID string, key []byte) (*Data, string) {
|
||||
if len(key) == 0 {
|
||||
return nil, "psk disables encryption"
|
||||
}
|
||||
if pkt.Channel != uint32(channelHash(channelID, key)) {
|
||||
return nil, "channel hash mismatch"
|
||||
}
|
||||
plaintext, err := cryptAESCTR(key, pkt.From, pkt.ID, pkt.Encrypted)
|
||||
if err != nil {
|
||||
return nil, err.Error()
|
||||
}
|
||||
decoded, err := parseData(plaintext)
|
||||
if err != nil {
|
||||
return nil, "decrypted bytes are not Data protobuf"
|
||||
}
|
||||
if decoded.Portnum == PortNumUnknown {
|
||||
return nil, "decrypted protobuf has UNKNOWN_APP portnum"
|
||||
}
|
||||
return decoded, "success"
|
||||
}
|
||||
|
||||
type serviceEnvelope struct {
|
||||
Packet *Packet
|
||||
ChannelID string
|
||||
GatewayID string
|
||||
}
|
||||
|
||||
func parseServiceEnvelope(payload []byte) (*serviceEnvelope, error) {
|
||||
env := &serviceEnvelope{}
|
||||
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
|
||||
switch num {
|
||||
case 1:
|
||||
b, ok := value.([]byte)
|
||||
if !ok || typ != protowire.BytesType {
|
||||
return nil
|
||||
}
|
||||
packet, err := parseMeshPacket(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env.Packet = packet
|
||||
case 2:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
env.ChannelID = string(b)
|
||||
}
|
||||
case 3:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
env.GatewayID = string(b)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return env, err
|
||||
}
|
||||
|
||||
func parseMeshPacket(payload []byte) (*Packet, error) {
|
||||
pkt := &Packet{}
|
||||
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
|
||||
switch num {
|
||||
case 1:
|
||||
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
|
||||
pkt.From = v
|
||||
}
|
||||
case 2:
|
||||
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
|
||||
pkt.To = v
|
||||
}
|
||||
case 3:
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
pkt.Channel = uint32(v)
|
||||
}
|
||||
case 4:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
decoded, err := parseData(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pkt.Decoded = decoded
|
||||
}
|
||||
case 5:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
pkt.Encrypted = append([]byte(nil), b...)
|
||||
}
|
||||
case 6:
|
||||
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
|
||||
pkt.ID = v
|
||||
}
|
||||
case 10:
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
pkt.WantAck = v != 0
|
||||
}
|
||||
case 14:
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
pkt.ViaMQTT = v != 0
|
||||
}
|
||||
case 17:
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
pkt.PKIEncrypted = v != 0
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return pkt, err
|
||||
}
|
||||
|
||||
func parseData(payload []byte) (*Data, error) {
|
||||
data := &Data{}
|
||||
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
|
||||
switch num {
|
||||
case 1:
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
data.Portnum = uint32(v)
|
||||
}
|
||||
case 2:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
data.Payload = append([]byte(nil), b...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return data, err
|
||||
}
|
||||
|
||||
func parseUser(payload []byte) (*NodeInfo, error) {
|
||||
user := &NodeInfo{}
|
||||
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
|
||||
switch num {
|
||||
case 1:
|
||||
user.ID = stringBytes(typ, value)
|
||||
case 2:
|
||||
user.LongName = stringBytes(typ, value)
|
||||
case 3:
|
||||
user.ShortName = stringBytes(typ, value)
|
||||
case 5:
|
||||
user.HWModel = varintValue(typ, value)
|
||||
case 7:
|
||||
user.Role = varintValue(typ, value)
|
||||
case 8:
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
user.PublicKey = append([]byte(nil), b...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return user, err
|
||||
}
|
||||
|
||||
func parsePosition(payload []byte) (*Position, error) {
|
||||
pos := &Position{}
|
||||
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
|
||||
switch num {
|
||||
case 1:
|
||||
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
|
||||
lat := float64(int32(v)) * 1e-7
|
||||
pos.Latitude = &lat
|
||||
}
|
||||
case 2:
|
||||
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
|
||||
lon := float64(int32(v)) * 1e-7
|
||||
pos.Longitude = &lon
|
||||
}
|
||||
case 3:
|
||||
if typ == protowire.VarintType {
|
||||
alt := int32(varintValue(typ, value))
|
||||
pos.Altitude = &alt
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return pos, err
|
||||
}
|
||||
|
||||
// walkFields 遍历 protobuf wire 字段。
|
||||
func walkFields(payload []byte, handle func(protowire.Number, protowire.Type, any) error) error {
|
||||
for len(payload) > 0 {
|
||||
num, typ, n := protowire.ConsumeTag(payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
payload = payload[n:]
|
||||
|
||||
var value any
|
||||
switch typ {
|
||||
case protowire.VarintType:
|
||||
v, n := protowire.ConsumeVarint(payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
value = v
|
||||
payload = payload[n:]
|
||||
case protowire.Fixed32Type:
|
||||
v, n := protowire.ConsumeFixed32(payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
value = v
|
||||
payload = payload[n:]
|
||||
case protowire.Fixed64Type:
|
||||
v, n := protowire.ConsumeFixed64(payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
value = v
|
||||
payload = payload[n:]
|
||||
case protowire.BytesType:
|
||||
v, n := protowire.ConsumeBytes(payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
value = v
|
||||
payload = payload[n:]
|
||||
default:
|
||||
n := protowire.ConsumeFieldValue(num, typ, payload)
|
||||
if n < 0 {
|
||||
return protowire.ParseError(n)
|
||||
}
|
||||
payload = payload[n:]
|
||||
}
|
||||
|
||||
if err := handle(num, typ, value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringBytes(typ protowire.Type, value any) string {
|
||||
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
|
||||
return string(b)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func varintValue(typ protowire.Type, value any) uint64 {
|
||||
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func xorHash(data []byte) byte {
|
||||
var result byte
|
||||
for _, b := range data {
|
||||
result ^= b
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func channelHash(channelName string, key []byte) byte {
|
||||
return xorHash([]byte(channelName)) ^ xorHash(key)
|
||||
}
|
||||
|
||||
// cryptAESCTR 按 Meshtastic nonce 规则执行 AES-CTR;CTR 加密和解密是同一个 XOR 流操作。
|
||||
func cryptAESCTR(key []byte, fromNum, packetID uint32, input []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, aes.BlockSize)
|
||||
binary.LittleEndian.PutUint64(nonce[0:8], uint64(packetID))
|
||||
binary.LittleEndian.PutUint32(nonce[8:12], fromNum)
|
||||
output := make([]byte, len(input))
|
||||
cipher.NewCTR(block, nonce).XORKeyStream(output, input)
|
||||
return output, nil
|
||||
}
|
||||
|
||||
var portNumNames = map[uint32]string{
|
||||
0: "UNKNOWN_APP", 1: "TEXT_MESSAGE_APP", 2: "REMOTE_HARDWARE_APP", 3: "POSITION_APP", 4: "NODEINFO_APP",
|
||||
5: "ROUTING_APP", 6: "ADMIN_APP", 7: "TEXT_MESSAGE_COMPRESSED_APP", 8: "WAYPOINT_APP", 9: "AUDIO_APP",
|
||||
10: "DETECTION_SENSOR_APP", 11: "ALERT_APP", 12: "KEY_VERIFICATION_APP", 13: "REMOTE_SHELL_APP", 32: "REPLY_APP",
|
||||
33: "IP_TUNNEL_APP", 34: "PAXCOUNTER_APP", 35: "STORE_FORWARD_PLUSPLUS_APP", 36: "NODE_STATUS_APP", 64: "SERIAL_APP",
|
||||
65: "STORE_FORWARD_APP", 66: "RANGE_TEST_APP", 67: "TELEMETRY_APP", 68: "ZPS_APP", 69: "SIMULATOR_APP",
|
||||
70: "TRACEROUTE_APP", 71: "NEIGHBORINFO_APP", 72: "ATAK_PLUGIN", 73: "MAP_REPORT_APP", 74: "POWERSTRESS_APP",
|
||||
75: "LORAWAN_BRIDGE", 76: "RETICULUM_TUNNEL_APP", 77: "CAYENNE_APP", 78: "ATAK_PLUGIN_V2", 112: "GROUPALARM_APP",
|
||||
256: "PRIVATE_APP", 257: "ATAK_FORWARDER", 511: "MAX",
|
||||
}
|
||||
Reference in New Issue
Block a user