解码功能完成
This commit is contained in:
@@ -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}
|
||||||
|
```
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user