解码功能完成

This commit is contained in:
2026-06-03 00:26:25 +08:00
parent 0a9f54f2c3
commit 5ab183af90
2 changed files with 153 additions and 12 deletions
+93
View File
@@ -0,0 +1,93 @@
# Meshtastic MQTT Server
这是一个 Meshtastic MQTT 订阅工具,用 Go 实现 [py/mqtt_nodeinfo_subscriber.py](py/mqtt_nodeinfo_subscriber.py) 的主要功能。
程序会连接 Meshtastic MQTT broker,订阅指定 topic,解析 `ServiceEnvelope` / `MeshPacket`,并将重点数据包以 JSONL 形式输出到控制台。
## 运行
```bash
go run .
```
默认连接:
- broker`mqtt.meshtastic.org:1883`
- username`meshdev`
- password`large4cats`
- topic`msh/US/#`
- PSK`AQ==`
也可以指定 topic
```bash
go run . --topic 'msh/US/#'
```
多个 topic 可重复传入:
```bash
go run . --topic 'msh/US/#' --topic 'msh/EU_868/#'
```
## 参数
```text
--host MQTT broker hostname
--port MQTT broker port
--username MQTT username
--password MQTT password
--psk Base64 channel PSK used to try decrypting encrypted packets
--topic Topic to subscribe; may be repeated
--qos MQTT subscription QoS: 0, 1, or 2
--client-id MQTT client id
```
## 控制台颜色说明
程序会按数据包类型使用不同背景色,方便快速区分消息类型。
| 背景色 | type | portnum | 含义 |
|---|---|---|---|
| 绿色 | `nodeinfo` | `NODEINFO_APP` | 节点信息包,包含节点 ID、长名称、短名称、硬件型号、角色、公钥等 |
| 蓝色 | `map_report` | `MAP_REPORT_APP` | 地图报告包,包含节点名称、硬件、固件版本、区域、调制预设、位置等地图信息 |
| 紫色 | `text_message` | `TEXT_MESSAGE_APP` | 聊天文本消息 |
| 青色 | `position` | `POSITION_APP` | 位置包,表示节点位置相关数据;当前只标记类型,不展开解析 payload |
| 黄色 | `telemetry` | `TELEMETRY_APP` | 遥测包,表示电池、信道、设备或环境传感器相关数据;当前只标记类型,不展开解析 payload |
| 灰色 | `routing` | `ROUTING_APP` | 路由控制包,常见于 ACK、NAK、路由错误等控制信息 |
| 灰色 | `traceroute` | `TRACEROUTE_APP` | 路径追踪包,用于 mesh 网络路径探测 |
| 红色 | error record | - | protobuf 解析失败、payload 解码失败或其他处理错误 |
| 无颜色 | `encrypted_packet` | - | 加密包但当前 PSK/频道 hash 无法解密;这不一定是错误 |
| 无颜色 | `decoded_packet` | 其他 portnum | 已解码/已解密,但程序尚未细分的其他应用包 |
## 过滤规则
程序默认不显示 `empty_packet`
`empty_packet``MeshPacket` 中没有 `decoded``encrypted` payload 的包,只包含类似 `from``to``id``via_mqtt` 等包头信息。根据固件源码分析,这类包通常不是普通业务数据,更多是 MQTT 回显/隐式 ACK 相关的元信息,对查看节点信息、地图报告和聊天内容价值较低。
## 输出示例
节点信息包:
```json
{"type":"nodeinfo","portnum":"NODEINFO_APP","from":"!a8dfd867","long_name":"Kabi Matrix 🖥️","short_name":"KaMX","hw_model":"PRIVATE_HW","role":"CLIENT_MUTE"}
```
地图报告包:
```json
{"type":"map_report","portnum":"MAP_REPORT_APP","from":"!675c9803","long_name":"PaulHome","latitude":42.51043,"longitude":-83.08624999999999,"hw_model":"PORTDUINO"}
```
聊天消息包:
```json
{"type":"text_message","portnum":"TEXT_MESSAGE_APP","from":"!12345678","text":"hello mesh"}
```
解密失败的加密包:
```json
{"type":"encrypted_packet","decrypt_success":false,"decrypt_status":"channel hash mismatch","encrypted_len":43}
```
+53 -5
View File
@@ -13,6 +13,7 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"time" "time"
"unicode/utf8"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"google.golang.org/protobuf/encoding/protowire" "google.golang.org/protobuf/encoding/protowire"
@@ -27,11 +28,20 @@ const (
ansiGreenBGWhiteText = "\033[42;37m" ansiGreenBGWhiteText = "\033[42;37m"
ansiBlueBGWhiteText = "\033[44;37m" ansiBlueBGWhiteText = "\033[44;37m"
ansiPurpleBGWhiteText = "\033[45;37m"
ansiCyanBGBlackText = "\033[46;30m"
ansiYellowBGBlackText = "\033[43;30m"
ansiGrayBGWhiteText = "\033[100;37m"
ansiRedBGWhiteText = "\033[41;37m" ansiRedBGWhiteText = "\033[41;37m"
ansiReset = "\033[0m" ansiReset = "\033[0m"
unknownApp = 0 unknownApp = 0
textMessageApp = 1
positionApp = 3
nodeInfoApp = 4 nodeInfoApp = 4
routingApp = 5
telemetryApp = 67
tracerouteApp = 70
mapReportApp = 73 mapReportApp = 73
) )
@@ -205,6 +215,9 @@ func handleMessage(key []byte) mqtt.MessageHandler {
printJSON(map[string]any{"topic": msg.Topic(), "error": err.Error(), "payload_len": len(msg.Payload())}) printJSON(map[string]any{"topic": msg.Topic(), "error": err.Error(), "payload_len": len(msg.Payload())})
return return
} }
if record["type"] == "empty_packet" {
return
}
printJSON(record) printJSON(record)
} }
} }
@@ -511,6 +524,16 @@ func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string]
return nil, err return nil, err
} }
return merge(decodedBase, record), nil return merge(decodedBase, record), nil
case textMessageApp:
return merge(decodedBase, decodeTextMessage(packet)), nil
case positionApp:
return merge(decodedBase, map[string]any{"type": "position"}), nil
case telemetryApp:
return merge(decodedBase, map[string]any{"type": "telemetry"}), nil
case routingApp:
return merge(decodedBase, map[string]any{"type": "routing"}), nil
case tracerouteApp:
return merge(decodedBase, map[string]any{"type": "traceroute"}), nil
default: default:
return merge(decodedBase, map[string]any{"type": "decoded_packet"}), nil return merge(decodedBase, map[string]any{"type": "decoded_packet"}), nil
} }
@@ -602,6 +625,21 @@ func decodeMapReport(packet *meshPacket) (map[string]any, error) {
}, nil }, nil
} }
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
}
func merge(base map[string]any, extra map[string]any) map[string]any { func merge(base map[string]any, extra map[string]any) map[string]any {
out := make(map[string]any, len(base)+len(extra)) out := make(map[string]any, len(base)+len(extra))
for k, v := range base { for k, v := range base {
@@ -619,14 +657,24 @@ func printJSON(record map[string]any) {
text, _ = json.Marshal(map[string]any{"error": err.Error()}) text, _ = json.Marshal(map[string]any{"error": err.Error()})
} }
switch { switch record["type"] {
case record["decrypt_success"] == true: case "nodeinfo":
fmt.Printf("%s%s%s\n", ansiGreenBGWhiteText, text, ansiReset) fmt.Printf("%s%s%s\n", ansiGreenBGWhiteText, text, ansiReset)
case record["error"] != nil: case "map_report":
fmt.Printf("%s%s%s\n", ansiRedBGWhiteText, text, ansiReset)
case record["type"] == "decoded_packet":
fmt.Printf("%s%s%s\n", ansiBlueBGWhiteText, text, ansiReset) fmt.Printf("%s%s%s\n", ansiBlueBGWhiteText, text, ansiReset)
case "text_message":
fmt.Printf("%s%s%s\n", ansiPurpleBGWhiteText, text, ansiReset)
case "position":
fmt.Printf("%s%s%s\n", ansiCyanBGBlackText, text, ansiReset)
case "telemetry":
fmt.Printf("%s%s%s\n", ansiYellowBGBlackText, text, ansiReset)
case "routing", "traceroute":
fmt.Printf("%s%s%s\n", ansiGrayBGWhiteText, text, ansiReset)
default: default:
if record["error"] != nil {
fmt.Printf("%s%s%s\n", ansiRedBGWhiteText, text, ansiReset)
return
}
fmt.Println(string(text)) fmt.Println(string(text))
} }
} }