Files
meshtastic_mqtt_server/mqtpp/mqtpp.go
T
2026-06-12 18:07:53 +08:00

1163 lines
37 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mqtpp
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"unicode/utf8"
"google.golang.org/protobuf/encoding/protowire"
)
const (
unknownApp = 0
textMessageApp = 1
positionApp = 3
nodeInfoApp = 4
routingApp = 5
telemetryApp = 67
tracerouteApp = 70
mapReportApp = 73
)
var defaultMeshtasticPSK = []byte{
0xD4, 0xF1, 0xBB, 0x3A,
0x20, 0x29, 0x07, 0x59,
0xF0, 0xBC, 0xFF, 0xAB,
0xCF, 0x4E, 0x69, 0x01,
}
type Options struct {
AllowEncryptedForwarding bool
}
type serviceEnvelope struct {
Packet *meshPacket
ChannelID string
GatewayID string
}
type meshPacket struct {
From uint32
To uint32
Channel uint32
Decoded *dataPacket
Encrypted []byte
ID uint32
ViaMQTT bool
PKIEncrypted bool
PayloadVariant string
}
type dataPacket struct {
Portnum uint32
Payload []byte
}
type userInfo struct {
ID string
LongName string
ShortName string
HWModel uint64
IsLicensed bool
Role uint64
PublicKey []byte
}
type mapReport struct {
LongName string
ShortName string
Role uint64
HWModel uint64
FirmwareVersion string
Region uint64
ModemPreset uint64
LatitudeI int32
LongitudeI int32
Altitude int32
PositionPrecision uint32
NumOnlineLocalNodes uint32
HasOptedReportLocation bool
}
type positionInfo struct {
LatitudeI *int32
LongitudeI *int32
Altitude *int32
Time uint32
LocationSource uint64
AltitudeSource uint64
Timestamp uint32
TimestampMillisAdjust int32
AltitudeHAE *int32
AltitudeGeoidalSeparation *int32
PDOP uint32
HDOP uint32
VDOP uint32
GPSAccuracy uint32
GroundSpeed *uint32
GroundTrack *uint32
FixQuality uint32
FixType uint32
SatsInView uint32
SensorID uint32
NextUpdate uint32
SeqNumber uint32
PrecisionBits uint32
}
type telemetryInfo struct {
Time uint32
Type string
Metrics map[string]any
}
// MQTTPP 处理一个 MQTT 原始 payload,返回合规状态、原始数据和解码后的记录。
// 第一个返回值表示数据是否合规;第二个返回值在不合规时为 nil;第三个返回值是解码结果记录。
func MQTTPP(topic string, raw []byte, key []byte, opts Options) (bool, []byte, map[string]any) {
env, err := parseServiceEnvelope(raw)
if err != nil {
//解包失败
return false, nil, map[string]any{"topic": topic, "error": "protobuf decode failed: " + err.Error(), "payload_len": len(raw)}
}
record, err := describePacket(topic, env, key)
if err != nil {
//解码失败
return false, nil, map[string]any{"topic": topic, "error": err.Error(), "payload_len": len(raw)}
}
if record["type"] == "encrypted_packet" && !opts.AllowEncryptedForwarding {
record["error"] = "cannot be decrypted"
return false, nil, record
}
return true, raw, record
}
// ExpandPSK 展开 Base64 PSK,兼容 Meshtastic 默认索引 PSK 和短 key 补零规则。
func ExpandPSK(pskBase64 string) ([]byte, error) {
psk, err := base64.StdEncoding.DecodeString(pskBase64)
if err != nil {
return nil, fmt.Errorf("invalid psk: %w", err)
}
if len(psk) == 1 {
pskIndex := psk[0]
if pskIndex == 0 {
return []byte{}, nil
}
key := append([]byte(nil), defaultMeshtasticPSK...)
key[len(key)-1] = byte((int(key[len(key)-1]) + int(pskIndex) - 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 after expansion: AES keys must be 16, 24, or 32 bytes", len(psk))
}
return psk, nil
}
// isCompliantMQTTPacket 判断 MQTT 原始数据是否合规;当前预留判断位置,暂时始终返回 true。
func isCompliantMQTTPacket(_ []byte) bool {
// TODO: Add packet compliance checks here.
return true
}
// MustJSON 将记录编码成 JSON;编码失败时返回包含错误信息的 JSON。
func MustJSON(record map[string]any) []byte {
text, err := json.Marshal(record)
if err != nil {
text, _ = json.Marshal(map[string]any{"error": err.Error()})
}
return text
}
// parseServiceEnvelope 从 protobuf wire 数据中解析 MQTT ServiceEnvelope。
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
})
if err != nil {
return nil, err
}
if env.Packet == nil {
env.Packet = &meshPacket{}
}
return env, nil
}
// parseMeshPacket 从 protobuf wire 数据中解析 MeshPacket 的关键字段。
func parseMeshPacket(payload []byte) (*meshPacket, error) {
packet := &meshPacket{}
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 {
packet.From = v
}
case 2:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
packet.To = v
}
case 3:
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
packet.Channel = uint32(v)
}
case 4:
b, ok := value.([]byte)
if !ok || typ != protowire.BytesType {
return nil
}
decoded, err := parseDataPacket(b)
if err != nil {
return err
}
packet.Decoded = decoded
packet.Encrypted = nil
packet.PayloadVariant = "decoded"
case 5:
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
packet.Encrypted = append([]byte(nil), b...)
packet.Decoded = nil
packet.PayloadVariant = "encrypted"
}
case 6:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
packet.ID = v
}
case 14:
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
packet.ViaMQTT = v != 0
}
case 17:
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
packet.PKIEncrypted = v != 0
}
}
return nil
})
return packet, err
}
// parseDataPacket 解析 MeshPacket decoded 字段中的 Data 子包。
func parseDataPacket(payload []byte) (*dataPacket, error) {
data := &dataPacket{}
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
}
// parseUser 解析 NODEINFO_APP 的 User payload。
func parseUser(payload []byte) (*userInfo, error) {
user := &userInfo{}
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 6:
user.IsLicensed = varintValue(typ, value) != 0
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
}
// parseMapReport 解析 MAP_REPORT_APP 的地图报告 payload。
func parseMapReport(payload []byte) (*mapReport, error) {
report := &mapReport{}
err := walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
switch num {
case 1:
report.LongName = stringBytes(typ, value)
case 2:
report.ShortName = stringBytes(typ, value)
case 3:
report.Role = varintValue(typ, value)
case 4:
report.HWModel = varintValue(typ, value)
case 5:
report.FirmwareVersion = stringBytes(typ, value)
case 6:
report.Region = varintValue(typ, value)
case 7:
report.ModemPreset = varintValue(typ, value)
case 9:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
report.LatitudeI = int32(v)
}
case 10:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
report.LongitudeI = int32(v)
}
case 11:
report.Altitude = int32(varintValue(typ, value))
case 12:
report.PositionPrecision = uint32(varintValue(typ, value))
case 13:
report.NumOnlineLocalNodes = uint32(varintValue(typ, value))
case 14:
report.HasOptedReportLocation = varintValue(typ, value) != 0
}
return nil
})
return report, err
}
// parsePosition 解析 POSITION_APP 的 Position payload。
func parsePosition(payload []byte) (*positionInfo, error) {
position := &positionInfo{}
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 {
position.LatitudeI = int32Ptr(int32(v))
}
case 2:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
position.LongitudeI = int32Ptr(int32(v))
}
case 3:
if typ == protowire.VarintType {
position.Altitude = int32Ptr(int32(varintValue(typ, value)))
}
case 4:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
position.Time = v
}
case 5:
position.LocationSource = varintValue(typ, value)
case 6:
position.AltitudeSource = varintValue(typ, value)
case 7:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
position.Timestamp = v
}
case 8:
if typ == protowire.VarintType {
position.TimestampMillisAdjust = int32(varintValue(typ, value))
}
case 9:
if typ == protowire.VarintType {
position.AltitudeHAE = int32Ptr(decodeZigZag32(varintValue(typ, value)))
}
case 10:
if typ == protowire.VarintType {
position.AltitudeGeoidalSeparation = int32Ptr(decodeZigZag32(varintValue(typ, value)))
}
case 11:
position.PDOP = uint32(varintValue(typ, value))
case 12:
position.HDOP = uint32(varintValue(typ, value))
case 13:
position.VDOP = uint32(varintValue(typ, value))
case 14:
position.GPSAccuracy = uint32(varintValue(typ, value))
case 15:
position.GroundSpeed = uint32Ptr(uint32(varintValue(typ, value)))
case 16:
position.GroundTrack = uint32Ptr(uint32(varintValue(typ, value)))
case 17:
position.FixQuality = uint32(varintValue(typ, value))
case 18:
position.FixType = uint32(varintValue(typ, value))
case 19:
position.SatsInView = uint32(varintValue(typ, value))
case 20:
position.SensorID = uint32(varintValue(typ, value))
case 21:
position.NextUpdate = uint32(varintValue(typ, value))
case 22:
position.SeqNumber = uint32(varintValue(typ, value))
case 23:
position.PrecisionBits = uint32(varintValue(typ, value))
}
return nil
})
return position, err
}
// parseTelemetry 解析 TELEMETRY_APP 的 Telemetry payload 和具体 telemetry variant。
func parseTelemetry(payload []byte) (*telemetryInfo, error) {
telemetry := &telemetryInfo{}
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 {
telemetry.Time = v
}
case 2:
telemetry.Type = "device_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, deviceMetricFields)
case 3:
telemetry.Type = "environment_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, environmentMetricFields)
case 4:
telemetry.Type = "air_quality_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, airQualityMetricFields)
case 5:
telemetry.Type = "power_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, powerMetricFields)
case 6:
telemetry.Type = "local_stats"
telemetry.Metrics = parseMetricBytes(typ, value, localStatsFields)
case 7:
telemetry.Type = "health_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, healthMetricFields)
case 8:
telemetry.Type = "host_metrics"
telemetry.Metrics = parseMetricBytes(typ, value, hostMetricFields)
case 9:
telemetry.Type = "traffic_management_stats"
telemetry.Metrics = parseMetricBytes(typ, value, trafficManagementFields)
}
return nil
})
return telemetry, err
}
type metricKind int
const (
metricUint32 metricKind = iota
metricUint64
metricInt32
metricFloat32
metricString
metricRepeatedFloat32
)
type metricField struct {
Name string
Kind metricKind
}
// parseMetricBytes 按字段定义表解析 telemetry variant 的指标字段。
func parseMetricBytes(typ protowire.Type, value any, fields map[protowire.Number]metricField) map[string]any {
metrics := map[string]any{}
payload, ok := value.([]byte)
if !ok || typ != protowire.BytesType {
return metrics
}
_ = walkFields(payload, func(num protowire.Number, typ protowire.Type, value any) error {
field, ok := fields[num]
if !ok {
return nil
}
switch field.Kind {
case metricUint32:
metrics[field.Name] = uint32(varintValue(typ, value))
case metricUint64:
metrics[field.Name] = varintValue(typ, value)
case metricInt32:
metrics[field.Name] = int32(varintValue(typ, value))
case metricFloat32:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
metrics[field.Name] = float64(math.Float32frombits(v))
}
case metricString:
metrics[field.Name] = stringBytes(typ, value)
case metricRepeatedFloat32:
if v, ok := value.(uint32); ok && typ == protowire.Fixed32Type {
appendMetric(metrics, field.Name, float64(math.Float32frombits(v)))
}
if payload, ok := value.([]byte); ok && typ == protowire.BytesType {
for len(payload) > 0 {
v, n := protowire.ConsumeFixed32(payload)
if n < 0 {
break
}
appendMetric(metrics, field.Name, float64(math.Float32frombits(v)))
payload = payload[n:]
}
}
}
return nil
})
return metrics
}
// appendMetric 追加 repeated telemetry 字段值。
func appendMetric(metrics map[string]any, name string, value any) {
if existing, ok := metrics[name]; ok {
metrics[name] = append(existing.([]any), value)
return
}
metrics[name] = []any{value}
}
// int32Ptr 返回 int32 指针,用于记录 proto optional 字段是否出现。
func int32Ptr(v int32) *int32 {
return &v
}
// uint32Ptr 返回 uint32 指针,用于记录 proto optional 字段是否出现。
func uint32Ptr(v uint32) *uint32 {
return &v
}
// decodeZigZag32 解码 protobuf sint32 的 zig-zag 编码。
func decodeZigZag32(v uint64) int32 {
return int32((v >> 1) ^ uint64(-int64(v&1)))
}
// optionalInt32 把 optional int32 指针转换成 JSON 可表达的值。
func optionalInt32(v *int32) any {
if v == nil {
return nil
}
return *v
}
// optionalUint32 把 optional uint32 指针转换成 JSON 可表达的值。
func optionalUint32(v *uint32) any {
if v == nil {
return nil
}
return *v
}
// optionalCoordinate 把 Meshtastic 1e-7 度坐标转换成浮点经纬度。
func optionalCoordinate(v *int32) any {
if v == nil {
return nil
}
return float64(*v) * 1e-7
}
// optionalDegrees100 把 1/100 度单位转换成度。
func optionalDegrees100(v *uint32) any {
if v == nil {
return nil
}
return float64(*v) / 100
}
// dopValue 把 1/100 精度因子转换成浮点值,未设置时返回 nil。
func dopValue(v uint32) any {
if v == 0 {
return nil
}
return float64(v) / 100
}
// 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
}
// stringBytes 在字段类型为 bytes 时把字段值转换为字符串。
func stringBytes(typ protowire.Type, value any) string {
if b, ok := value.([]byte); ok && typ == protowire.BytesType {
return string(b)
}
return ""
}
// varintValue 在字段类型为 varint 时提取无符号整数值。
func varintValue(typ protowire.Type, value any) uint64 {
if v, ok := value.(uint64); ok && typ == protowire.VarintType {
return v
}
return 0
}
// describePacket 根据 ServiceEnvelope 和 PSK 生成统一的 JSON 记录字段。
func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]any, error) {
packet := env.Packet
if packet == nil {
packet = &meshPacket{}
}
base := map[string]any{
"topic": topic,
"channel_id": env.ChannelID,
"gateway_id": env.GatewayID,
"packet_from": nodeNumToID(packet.From),
"packet_from_num": packet.From,
"packet_to": nodeNumToID(packet.To),
"packet_to_num": packet.To,
"packet_id": packet.ID,
"payload_variant": packet.PayloadVariant,
"via_mqtt": packet.ViaMQTT,
"pki_encrypted": packet.PKIEncrypted,
}
if packet.PayloadVariant == "encrypted" {
decryptedPacket, decryptStatus := tryDecryptPacket(packet, env.ChannelID, key)
if decryptedPacket == nil {
return merge(base, map[string]any{
"type": "encrypted_packet",
"encrypted_len": len(packet.Encrypted),
"decrypt_success": false,
"decrypt_status": decryptStatus,
}), nil
}
decryptedEnv := *env
decryptedEnv.Packet = decryptedPacket
decrypted, err := describePacket(topic, &decryptedEnv, key)
if err != nil {
return nil, err
}
decrypted["payload_variant"] = "decoded"
decrypted["decrypt_success"] = true
decrypted["decrypt_status"] = decryptStatus
return decrypted, nil
}
if packet.PayloadVariant != "decoded" || packet.Decoded == nil {
return merge(base, map[string]any{"type": "empty_packet"}), nil
}
decodedBase := merge(base, map[string]any{
"portnum": enumName(portNumNames, uint64(packet.Decoded.Portnum)),
"payload_len": len(packet.Decoded.Payload),
})
switch packet.Decoded.Portnum {
case nodeInfoApp:
record, err := decodeUser(packet)
if err != nil {
return nil, err
}
return merge(decodedBase, record), nil
case mapReportApp:
record, err := decodeMapReport(packet)
if err != nil {
return nil, err
}
return merge(decodedBase, record), nil
case textMessageApp:
return merge(decodedBase, decodeTextMessage(packet)), nil
case positionApp:
record, err := decodePosition(packet)
if err != nil {
return nil, err
}
return merge(decodedBase, record), nil
case telemetryApp:
record, err := decodeTelemetry(packet)
if err != nil {
return nil, err
}
return merge(decodedBase, record), nil
case routingApp:
return merge(decodedBase, map[string]any{"type": "routing"}), nil
case tracerouteApp:
return merge(decodedBase, map[string]any{"type": "traceroute"}), nil
default:
return merge(decodedBase, map[string]any{"type": "decoded_packet"}), nil
}
}
// tryDecryptPacket 尝试用 channel PSK 解密 encrypted MeshPacket,并返回解密状态。
func tryDecryptPacket(packet *meshPacket, channelID string, key []byte) (*meshPacket, string) {
if len(key) == 0 {
return nil, "psk disables encryption"
}
if packet.Channel != uint32(channelHash(channelID, key)) {
return nil, "channel hash mismatch"
}
plaintext, err := decryptAESCTR(key, packet.From, packet.ID, packet.Encrypted)
if err != nil {
return nil, err.Error()
}
decoded, err := parseDataPacket(plaintext)
if err != nil {
return nil, fmt.Sprintf("decrypted bytes are not Data protobuf: %v", err)
}
if decoded.Portnum == unknownApp {
return nil, "decrypted protobuf has UNKNOWN_APP portnum"
}
decrypted := *packet
decrypted.Encrypted = nil
decrypted.Decoded = decoded
decrypted.PayloadVariant = "decoded"
return &decrypted, "success"
}
// decodeUser 将 NODEINFO_APP payload 解码为节点信息 JSON 字段。
func decodeUser(packet *meshPacket) (map[string]any, error) {
user, err := parseUser(packet.Decoded.Payload)
if err != nil {
return nil, err
}
var publicKey any
if len(user.PublicKey) > 0 {
publicKey = hex.EncodeToString(user.PublicKey)
}
return map[string]any{
"type": "nodeinfo",
"from": nodeNumToID(packet.From),
"from_num": packet.From,
"user_id": user.ID,
"long_name": user.LongName,
"short_name": user.ShortName,
"hw_model": enumName(hardwareModelNames, user.HWModel),
"role": enumName(roleNames, user.Role),
"is_licensed": user.IsLicensed,
"public_key": publicKey,
}, nil
}
// decodeMapReport 将 MAP_REPORT_APP payload 解码为地图报告 JSON 字段。
func decodeMapReport(packet *meshPacket) (map[string]any, error) {
report, err := parseMapReport(packet.Decoded.Payload)
if err != nil {
return nil, err
}
var latitude, longitude any
if report.LatitudeI != 0 {
latitude = float64(report.LatitudeI) * 1e-7
}
if report.LongitudeI != 0 {
longitude = float64(report.LongitudeI) * 1e-7
}
return map[string]any{
"type": "map_report",
"from": nodeNumToID(packet.From),
"from_num": packet.From,
"long_name": report.LongName,
"short_name": report.ShortName,
"role": enumName(roleNames, report.Role),
"hw_model": enumName(hardwareModelNames, report.HWModel),
"firmware_version": report.FirmwareVersion,
"region": enumName(regionCodeNames, report.Region),
"modem_preset": enumName(modemPresetNames, report.ModemPreset),
"latitude": latitude,
"longitude": longitude,
"altitude": report.Altitude,
"position_precision": report.PositionPrecision,
"num_online_local_nodes": report.NumOnlineLocalNodes,
"has_opted_report_location": report.HasOptedReportLocation,
}, nil
}
// decodePosition 将 POSITION_APP payload 解码为位置 JSON 字段。
func decodePosition(packet *meshPacket) (map[string]any, error) {
position, err := parsePosition(packet.Decoded.Payload)
if err != nil {
return nil, err
}
return map[string]any{
"type": "position",
"from": nodeNumToID(packet.From),
"from_num": packet.From,
"latitude": optionalCoordinate(position.LatitudeI),
"longitude": optionalCoordinate(position.LongitudeI),
"altitude": optionalInt32(position.Altitude),
"time": position.Time,
"location_source": enumName(locationSourceNames, position.LocationSource),
"altitude_source": enumName(altitudeSourceNames, position.AltitudeSource),
"timestamp": position.Timestamp,
"timestamp_millis_adjust": position.TimestampMillisAdjust,
"altitude_hae": optionalInt32(position.AltitudeHAE),
"altitude_geoidal_separation": optionalInt32(position.AltitudeGeoidalSeparation),
"pdop": dopValue(position.PDOP),
"hdop": dopValue(position.HDOP),
"vdop": dopValue(position.VDOP),
"gps_accuracy": position.GPSAccuracy,
"ground_speed": optionalUint32(position.GroundSpeed),
"ground_track": optionalDegrees100(position.GroundTrack),
"fix_quality": position.FixQuality,
"fix_type": position.FixType,
"sats_in_view": position.SatsInView,
"sensor_id": position.SensorID,
"next_update": position.NextUpdate,
"seq_number": position.SeqNumber,
"precision_bits": position.PrecisionBits,
}, nil
}
// decodeTelemetry 将 TELEMETRY_APP payload 解码为遥测 JSON 字段。
func decodeTelemetry(packet *meshPacket) (map[string]any, error) {
telemetry, err := parseTelemetry(packet.Decoded.Payload)
if err != nil {
return nil, err
}
return map[string]any{
"type": "telemetry",
"from": nodeNumToID(packet.From),
"from_num": packet.From,
"time": telemetry.Time,
"telemetry_type": telemetry.Type,
"metrics": telemetry.Metrics,
}, nil
}
// decodeTextMessage 将 TEXT_MESSAGE_APP payload 解码为聊天文本 JSON 字段。
func decodeTextMessage(packet *meshPacket) map[string]any {
text := string(packet.Decoded.Payload)
record := map[string]any{
"type": "text_message",
"from": nodeNumToID(packet.From),
"from_num": packet.From,
"text": text,
}
if !utf8.Valid(packet.Decoded.Payload) {
record["text"] = nil
record["payload_hex"] = hex.EncodeToString(packet.Decoded.Payload)
}
return record
}
// merge 合并两个 JSON 字段 mapextra 中的同名字段会覆盖 base。
func merge(base map[string]any, extra map[string]any) map[string]any {
out := make(map[string]any, len(base)+len(extra))
for k, v := range base {
out[k] = v
}
for k, v := range extra {
out[k] = v
}
return out
}
// nodeNumToID 将 Meshtastic 数字节点号格式化为 !xxxxxxxx 字符串。
func nodeNumToID(nodeNum uint32) string {
return fmt.Sprintf("!%08x", nodeNum)
}
// xorHash 计算 Meshtastic channel hash 使用的逐字节异或值。
func xorHash(data []byte) byte {
var result byte
for _, b := range data {
result ^= b
}
return result
}
// channelHash 根据 channel 名称和 PSK 计算 Meshtastic 加密包中的 channel hash。
func channelHash(channelName string, key []byte) byte {
return xorHash([]byte(channelName)) ^ xorHash(key)
}
// decryptAESCTR 按 Meshtastic nonce 规则使用 AES-CTR 解密 payload。
func decryptAESCTR(key []byte, fromNum, packetID uint32, ciphertext []byte) ([]byte, error) {
return cryptAESCTR(key, fromNum, packetID, ciphertext)
}
// cryptAESCTR 按 Meshtastic nonce 规则执行 AES-CTRCTR 加密和解密是同一个 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
}
// enumName 把已知枚举值转换成名称,未知值保留为数字。
func enumName(names map[uint64]string, value uint64) any {
if name, ok := names[value]; ok {
return name
}
return value
}
var locationSourceNames = map[uint64]string{
0: "LOC_UNSET",
1: "LOC_MANUAL",
2: "LOC_INTERNAL",
3: "LOC_EXTERNAL",
}
var altitudeSourceNames = map[uint64]string{
0: "ALT_UNSET",
1: "ALT_MANUAL",
2: "ALT_INTERNAL",
3: "ALT_EXTERNAL",
4: "ALT_BAROMETRIC",
}
var deviceMetricFields = map[protowire.Number]metricField{
1: {"battery_level", metricUint32},
2: {"voltage", metricFloat32},
3: {"channel_utilization", metricFloat32},
4: {"air_util_tx", metricFloat32},
5: {"uptime_seconds", metricUint32},
}
var environmentMetricFields = map[protowire.Number]metricField{
1: {"temperature", metricFloat32},
2: {"relative_humidity", metricFloat32},
3: {"barometric_pressure", metricFloat32},
4: {"gas_resistance", metricFloat32},
5: {"voltage", metricFloat32},
6: {"current", metricFloat32},
7: {"iaq", metricUint32},
8: {"distance", metricFloat32},
9: {"lux", metricFloat32},
10: {"white_lux", metricFloat32},
11: {"ir_lux", metricFloat32},
12: {"uv_lux", metricFloat32},
13: {"wind_direction", metricUint32},
14: {"wind_speed", metricFloat32},
15: {"weight", metricFloat32},
16: {"wind_gust", metricFloat32},
17: {"wind_lull", metricFloat32},
18: {"radiation", metricFloat32},
19: {"rainfall_1h", metricFloat32},
20: {"rainfall_24h", metricFloat32},
21: {"soil_moisture", metricUint32},
22: {"soil_temperature", metricFloat32},
23: {"one_wire_temperature", metricRepeatedFloat32},
}
var airQualityMetricFields = map[protowire.Number]metricField{
1: {"pm10_standard", metricUint32},
2: {"pm25_standard", metricUint32},
3: {"pm100_standard", metricUint32},
4: {"pm10_environmental", metricUint32},
5: {"pm25_environmental", metricUint32},
6: {"pm100_environmental", metricUint32},
7: {"particles_03um", metricUint32},
8: {"particles_05um", metricUint32},
9: {"particles_10um", metricUint32},
10: {"particles_25um", metricUint32},
11: {"particles_50um", metricUint32},
12: {"particles_100um", metricUint32},
13: {"co2", metricUint32},
14: {"co2_temperature", metricFloat32},
15: {"co2_humidity", metricFloat32},
16: {"form_formaldehyde", metricFloat32},
17: {"form_humidity", metricFloat32},
18: {"form_temperature", metricFloat32},
19: {"pm40_standard", metricUint32},
20: {"particles_40um", metricUint32},
21: {"pm_temperature", metricFloat32},
22: {"pm_humidity", metricFloat32},
23: {"pm_voc_idx", metricFloat32},
24: {"pm_nox_idx", metricFloat32},
25: {"particles_tps", metricFloat32},
}
var powerMetricFields = map[protowire.Number]metricField{
1: {"ch1_voltage", metricFloat32}, 2: {"ch1_current", metricFloat32},
3: {"ch2_voltage", metricFloat32}, 4: {"ch2_current", metricFloat32},
5: {"ch3_voltage", metricFloat32}, 6: {"ch3_current", metricFloat32},
7: {"ch4_voltage", metricFloat32}, 8: {"ch4_current", metricFloat32},
9: {"ch5_voltage", metricFloat32}, 10: {"ch5_current", metricFloat32},
11: {"ch6_voltage", metricFloat32}, 12: {"ch6_current", metricFloat32},
13: {"ch7_voltage", metricFloat32}, 14: {"ch7_current", metricFloat32},
15: {"ch8_voltage", metricFloat32}, 16: {"ch8_current", metricFloat32},
}
var localStatsFields = map[protowire.Number]metricField{
1: {"uptime_seconds", metricUint32},
2: {"channel_utilization", metricFloat32},
3: {"air_util_tx", metricFloat32},
4: {"num_packets_tx", metricUint32},
5: {"num_packets_rx", metricUint32},
6: {"num_packets_rx_bad", metricUint32},
7: {"num_online_nodes", metricUint32},
8: {"num_total_nodes", metricUint32},
9: {"num_rx_dupe", metricUint32},
10: {"num_tx_relay", metricUint32},
11: {"num_tx_relay_canceled", metricUint32},
12: {"heap_total_bytes", metricUint32},
13: {"heap_free_bytes", metricUint32},
14: {"num_tx_dropped", metricUint32},
15: {"noise_floor", metricInt32},
}
var healthMetricFields = map[protowire.Number]metricField{
1: {"heart_bpm", metricUint32},
2: {"spO2", metricUint32},
3: {"temperature", metricFloat32},
}
var hostMetricFields = map[protowire.Number]metricField{
1: {"uptime_seconds", metricUint32},
2: {"freemem_bytes", metricUint64},
3: {"diskfree1_bytes", metricUint64},
4: {"diskfree2_bytes", metricUint64},
5: {"diskfree3_bytes", metricUint64},
6: {"load1", metricUint32},
7: {"load5", metricUint32},
8: {"load15", metricUint32},
9: {"user_string", metricString},
}
var trafficManagementFields = map[protowire.Number]metricField{
1: {"packets_inspected", metricUint32},
2: {"position_dedup_drops", metricUint32},
3: {"nodeinfo_cache_hits", metricUint32},
4: {"rate_limit_drops", metricUint32},
5: {"unknown_packet_drops", metricUint32},
6: {"hop_exhausted_packets", metricUint32},
7: {"router_hops_preserved", metricUint32},
}
var portNumNames = map[uint64]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",
}
var roleNames = map[uint64]string{
0: "CLIENT", 1: "CLIENT_MUTE", 2: "ROUTER", 3: "ROUTER_CLIENT", 4: "REPEATER", 5: "TRACKER", 6: "SENSOR",
7: "TAK", 8: "CLIENT_HIDDEN", 9: "LOST_AND_FOUND", 10: "TAK_TRACKER", 11: "ROUTER_LATE", 12: "CLIENT_BASE",
}
var regionCodeNames = map[uint64]string{
0: "UNSET", 1: "US", 2: "EU_433", 3: "EU_868", 4: "CN", 5: "JP", 6: "ANZ", 7: "KR", 8: "TW", 9: "RU",
10: "IN", 11: "NZ_865", 12: "TH", 13: "LORA_24", 14: "UA_433", 15: "UA_868", 16: "MY_433", 17: "MY_919",
18: "SG_923", 19: "PH_433", 20: "PH_868", 21: "PH_915", 22: "ANZ_433", 23: "KZ_433", 24: "KZ_863",
25: "NP_865", 26: "BR_902", 27: "ITU1_2M", 28: "ITU2_2M", 29: "EU_866", 30: "EU_874", 31: "EU_917",
32: "EU_N_868", 33: "ITU3_2M",
}
var modemPresetNames = map[uint64]string{
0: "LONG_FAST", 1: "LONG_SLOW", 2: "VERY_LONG_SLOW", 3: "MEDIUM_SLOW", 4: "MEDIUM_FAST", 5: "SHORT_SLOW", 6: "SHORT_FAST",
7: "LONG_MODERATE", 8: "SHORT_TURBO", 9: "LONG_TURBO", 10: "LITE_FAST", 11: "LITE_SLOW", 12: "NARROW_FAST", 13: "NARROW_SLOW",
}
var hardwareModelNames = map[uint64]string{
0: "UNSET", 1: "TLORA_V2", 2: "TLORA_V1", 3: "TLORA_V2_1_1P6", 4: "TBEAM", 5: "HELTEC_V2_0",
6: "TBEAM_V0P7", 7: "T_ECHO", 8: "TLORA_V1_1P3", 9: "RAK4631", 10: "HELTEC_V2_1", 11: "HELTEC_V1",
12: "LILYGO_TBEAM_S3_CORE", 13: "RAK11200", 14: "NANO_G1", 15: "TLORA_V2_1_1P8", 16: "TLORA_T3_S3",
17: "NANO_G1_EXPLORER", 18: "NANO_G2_ULTRA", 19: "LORA_TYPE", 20: "WIPHONE", 21: "WIO_WM1110", 22: "RAK2560",
23: "HELTEC_HRU_3601", 24: "HELTEC_WIRELESS_BRIDGE", 25: "STATION_G1", 26: "RAK11310", 27: "SENSELORA_RP2040",
28: "SENSELORA_S3", 29: "CANARYONE", 30: "RP2040_LORA", 31: "STATION_G2", 32: "LORA_RELAY_V1", 33: "T_ECHO_PLUS",
34: "PPR", 35: "GENIEBLOCKS", 36: "NRF52_UNKNOWN", 37: "PORTDUINO", 38: "ANDROID_SIM", 39: "DIY_V1",
40: "NRF52840_PCA10059", 41: "DR_DEV", 42: "M5STACK", 43: "HELTEC_V3", 44: "HELTEC_WSL_V3", 45: "BETAFPV_2400_TX",
46: "BETAFPV_900_NANO_TX", 47: "RPI_PICO", 48: "HELTEC_WIRELESS_TRACKER", 49: "HELTEC_WIRELESS_PAPER", 50: "T_DECK",
51: "T_WATCH_S3", 52: "PICOMPUTER_S3", 53: "HELTEC_HT62", 54: "EBYTE_ESP32_S3", 55: "ESP32_S3_PICO", 56: "CHATTER_2",
57: "HELTEC_WIRELESS_PAPER_V1_0", 58: "HELTEC_WIRELESS_TRACKER_V1_0", 59: "UNPHONE", 60: "TD_LORAC", 61: "CDEBYTE_EORA_S3",
62: "TWC_MESH_V4", 63: "NRF52_PROMICRO_DIY", 64: "RADIOMASTER_900_BANDIT_NANO", 65: "HELTEC_CAPSULE_SENSOR_V3",
66: "HELTEC_VISION_MASTER_T190", 67: "HELTEC_VISION_MASTER_E213", 68: "HELTEC_VISION_MASTER_E290", 69: "HELTEC_MESH_NODE_T114",
70: "SENSECAP_INDICATOR", 71: "TRACKER_T1000_E", 72: "RAK3172", 73: "WIO_E5", 74: "RADIOMASTER_900_BANDIT",
75: "ME25LS01_4Y10TD", 76: "RP2040_FEATHER_RFM95", 77: "M5STACK_COREBASIC", 78: "M5STACK_CORE2", 79: "RPI_PICO2",
80: "M5STACK_CORES3", 81: "SEEED_XIAO_S3", 82: "MS24SF1", 83: "TLORA_C6", 84: "WISMESH_TAP", 85: "ROUTASTIC",
86: "MESH_TAB", 87: "MESHLINK", 88: "XIAO_NRF52_KIT", 89: "THINKNODE_M1", 90: "THINKNODE_M2", 91: "T_ETH_ELITE",
92: "HELTEC_SENSOR_HUB", 93: "MUZI_BASE", 94: "HELTEC_MESH_POCKET", 95: "SEEED_SOLAR_NODE", 96: "NOMADSTAR_METEOR_PRO",
97: "CROWPANEL", 98: "LINK_32", 99: "SEEED_WIO_TRACKER_L1", 100: "SEEED_WIO_TRACKER_L1_EINK", 101: "MUZI_R1_NEO",
102: "T_DECK_PRO", 103: "T_LORA_PAGER", 104: "M5STACK_RESERVED", 105: "WISMESH_TAG", 106: "RAK3312", 107: "THINKNODE_M5",
108: "HELTEC_MESH_SOLAR", 109: "T_ECHO_LITE", 110: "HELTEC_V4", 111: "M5STACK_C6L", 112: "M5STACK_CARDPUTER_ADV",
113: "HELTEC_WIRELESS_TRACKER_V2", 114: "T_WATCH_ULTRA", 115: "THINKNODE_M3", 116: "WISMESH_TAP_V2", 117: "RAK3401",
118: "RAK6421", 119: "THINKNODE_M4", 120: "THINKNODE_M6", 121: "MESHSTICK_1262", 122: "TBEAM_1_WATT", 123: "T5_S3_EPAPER_PRO",
124: "TBEAM_BPF", 125: "MINI_EPAPER_S3", 126: "TDISPLAY_S3_PRO", 127: "HELTEC_MESH_NODE_T096", 128: "TRACKER_T1000_E_PRO",
129: "THINKNODE_M7", 130: "THINKNODE_M8", 131: "THINKNODE_M9", 132: "HELTEC_V4_R8", 133: "HELTEC_MESH_NODE_T1",
134: "STATION_G3", 135: "T_IMPULSE_PLUS", 136: "T_ECHO_CARD", 255: "PRIVATE_HW",
}