From 5ab183af90e7a8c41c891480a22cf4def5d2fb38 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 3 Jun 2026 00:26:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E7=A0=81=E5=8A=9F=E8=83=BD=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 72 +++++++++++++++++++++++++++++++++++------- 2 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6840f40 --- /dev/null +++ b/README.md @@ -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} +``` diff --git a/main.go b/main.go index 2737368..e93c1c1 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "os/signal" "syscall" "time" + "unicode/utf8" mqtt "github.com/eclipse/paho.mqtt.golang" "google.golang.org/protobuf/encoding/protowire" @@ -25,14 +26,23 @@ const ( defaultPSK = "AQ==" defaultTopic = "msh/US/#" - ansiGreenBGWhiteText = "\033[42;37m" - ansiBlueBGWhiteText = "\033[44;37m" - ansiRedBGWhiteText = "\033[41;37m" - ansiReset = "\033[0m" + ansiGreenBGWhiteText = "\033[42;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" + ansiReset = "\033[0m" - unknownApp = 0 - nodeInfoApp = 4 - mapReportApp = 73 + unknownApp = 0 + textMessageApp = 1 + positionApp = 3 + nodeInfoApp = 4 + routingApp = 5 + telemetryApp = 67 + tracerouteApp = 70 + mapReportApp = 73 ) var defaultMeshtasticPSK = []byte{ @@ -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())}) return } + if record["type"] == "empty_packet" { + return + } printJSON(record) } } @@ -511,6 +524,16 @@ func describePacket(topic string, env *serviceEnvelope, key []byte) (map[string] return nil, err } 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: return merge(decodedBase, map[string]any{"type": "decoded_packet"}), nil } @@ -602,6 +625,21 @@ func decodeMapReport(packet *meshPacket) (map[string]any, error) { }, 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 { out := make(map[string]any, len(base)+len(extra)) for k, v := range base { @@ -619,14 +657,24 @@ func printJSON(record map[string]any) { text, _ = json.Marshal(map[string]any{"error": err.Error()}) } - switch { - case record["decrypt_success"] == true: + switch record["type"] { + case "nodeinfo": fmt.Printf("%s%s%s\n", ansiGreenBGWhiteText, text, ansiReset) - case record["error"] != nil: - fmt.Printf("%s%s%s\n", ansiRedBGWhiteText, text, ansiReset) - case record["type"] == "decoded_packet": + case "map_report": 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: + if record["error"] != nil { + fmt.Printf("%s%s%s\n", ansiRedBGWhiteText, text, ansiReset) + return + } fmt.Println(string(text)) } }