Files
meshtastic_mqtt_server/README.md
T
2026-06-03 23:58:17 +08:00

379 lines
14 KiB
Markdown
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.
# Meshtastic MQTT Server
本程序启动一个本地 MQTT broker,并在转发客户端发布的消息前校验 Meshtastic MQTT payload。
每条传入的 `PUBLISH` 都会先进入:
```go
valid, _, record := mqtpp.MQTTPP(topic, payload, key)
```
- `valid == true`:保留原始 topic、payload、QoS、retain 等字段,正常转发给订阅匹配 topic 的客户端
- `valid == false`:丢弃该消息,不转发给订阅客户端
当前不桥接到 `mqtt.meshtastic.org` 等上游 broker。
## 运行
```bash
go run .
```
默认监听:
- host`0.0.0.0`
- port`1883`
- PSK`AQ==`
- TLS:关闭
- Web`0.0.0.0:8080`,静态目录 `./dist`
- 数据库:SQLite
- SQLite 文件:Unix/Linux 为 `/srv/mesh_mqtt_go/mesh_mqtt_go.db`Windows 测试为 `./win/etc/mesh_mqtt_go/mesh_mqtt_go.db`
首次启动会自动生成配置文件;之后每次启动都会检查配置项,缺失项会自动补全并写回。
配置文件路径:
- Unix/Linux`/etc/mesh_mqtt_go/config.yaml`
- Windows 测试:`./win/etc/mesh_mqtt_go/config.yaml`
默认配置内容:
```yaml
mqtt:
host: 0.0.0.0
port: 1883
tls:
enabled: false
cert_file: ""
key_file: ""
meshtastic:
psk: AQ==
database:
driver: sqlite
sqlite:
path: /srv/mesh_mqtt_go/mesh_mqtt_go.db
mysql:
dsn: ""
web:
enabled: true
host: 0.0.0.0
port: 8080
static_dir: ./dist
admin:
username: admin
password: admin
session_secret: ""
session_secure: false
```
配置优先级:
```text
内置默认值 < 配置文件 < 环境变量 < 命令行参数
```
也可以用命令行临时覆盖监听地址、PSK 和 TLS 设置:
```bash
go run . --host 127.0.0.1 --port 1883 --psk AQ==
```
## 参数
```text
--host MQTT broker listen host
--port MQTT broker listen port
--psk Base64 channel PSK used to try decrypting encrypted packets
--tls Enable MQTT TLS listener
--tls-cert MQTT TLS certificate file
--tls-key MQTT TLS private key file
--db-driver Database driver: sqlite or mysql
--sqlite-path SQLite database file path
--mysql-dsn MySQL database DSN
--web Enable Gin web server
--web-host Web server listen host
--web-port Web server listen port
--web-static-dir Web frontend static files directory
```
## Web 前端
开发模式:
```bash
go run . --web-host 127.0.0.1 --web-port 8080
cd meshmap_frontend
npm run dev
```
生产构建:
```bash
cd meshmap_frontend
npm run build
cd ..
go run .
```
构建后的文件位于项目根目录 `dist/`Gin 会提供静态文件服务;`/api` 路径保留给后端接口。
管理页面位于 `/admin`,默认管理员账号为 `admin` / `admin`。生产环境请修改 `web.admin.password` 或设置 `MESH_ADMIN_PASSWORD`,并配置固定的 `web.admin.session_secret``MESH_ADMIN_SESSION_SECRET`;如果 `session_secret` 为空,程序会在启动时生成临时签名密钥,重启后需要重新登录。后台页面包括 `/admin` 服务状态、`/admin/users` 用户管理、`/admin/log/login` 登录日志。后台支持新增管理员用户和修改用户密码;密码使用 bcrypt hash 保存,API 不会返回密码 hash。修改密码不会立即使已签发 Session 失效,当前 Session 到期或退出登录后才需要使用新密码。登录成功和失败都会记录到登录日志,包含用户名、结果、原因、来源地址、User-Agent 和时间。
常用 API
```text
GET /api/health
POST /api/admin/login
POST /api/admin/logout
GET /api/admin/me
GET /api/admin/mqtt/status
GET /api/admin/log/login
GET /api/admin/users
POST /api/admin/users
PUT /api/admin/users/:id/password
GET /api/nodeinfo
GET /api/nodeinfo/:id
GET /api/map-reports
GET /api/map-reports/:id
GET /api/nodes # /api/nodeinfo 的兼容别名
GET /api/nodes/:id # /api/nodeinfo/:id 的兼容别名
GET /api/text-messages
GET /api/positions
GET /api/telemetry
GET /api/routing
GET /api/traceroute
```
## TLS 配置示例
```yaml
mqtt:
host: 0.0.0.0
port: 8883
tls:
enabled: true
cert_file: ./certs/server.crt
key_file: ./certs/server.key
meshtastic:
psk: AQ==
```
启用 TLS 后,`cert_file``key_file` 必须指向可读取的证书和私钥文件。
## 数据库持久化
程序默认启用 SQLite,数据库表迁移和操作由 GORM 执行,并持久化以下数据:
- `login_log`:追加保存后台登录成功和失败日志
- `nodeinfo`:保存 `type == "nodeinfo"` 的节点身份和设备信息
- `map_report`:保存 `type == "map_report"` 的地图报告信息,前端地图从该表读取
- `text_message`:追加保存 `type == "text_message"` 的聊天消息
- `position`:追加保存 `type == "position"` 的位置包
- `telemetry`:追加保存 `type == "telemetry"` 的遥测包
- `routing`:追加保存 `type == "routing"` 的路由控制包
- `traceroute`:追加保存 `type == "traceroute"` 的路径追踪包
`nodeinfo` / `map_report` 规则:
- 两张表都以 `node_id`(即解析结果中的 `from`,例如 `!a8dfd867`)作为主键
- `nodeinfo` 只保存节点身份和设备字段,例如 `user_id`、名称、硬件型号、角色、授权状态和公钥
- `map_report` 只保存地图报告字段,例如名称、硬件型号、角色、固件版本、区域、调制预设、经纬度、海拔、位置精度和在线节点数
- 重复收到同一节点时不会插入重复行,只更新 `updated_at``content_json` 和本次记录中有值的字段
- `first_seen_at` 保留第一次写入时间
- `content_json` 分别保存最新一次 `nodeinfo``map_report` 的完整解析结果 JSON
- 旧版本创建的 `nodeinfo_map` 融合表不会被自动删除,新版本不再写入该表;新表会从新收到的数据开始填充
`text_message` 规则:
- 使用自增 `id` 作为主键
- 每条聊天消息都会新增一行,不做去重
- 保存 `from_id``from_num``text``payload_hex`、topic、packet 元数据和完整 `content_json`
- 保存 MQTT 客户端信息:`mqtt_client_id``mqtt_username``mqtt_listener``mqtt_remote_addr``mqtt_remote_host``mqtt_remote_port`
`position` / `telemetry` / `routing` / `traceroute` 规则:
- 都使用自增 `id` 作为主键
- 每条有效记录都会新增一行,不做去重
- 保存通用 packet 元数据、MQTT 客户端信息和完整 `content_json`
- `position` 额外保存经纬度、海拔、时间、定位来源、精度、速度、卫星数等字段
- `telemetry` 额外保存 `telemetry_type`,并把动态 `metrics` 对象保存为 `metrics_json`
- `routing``traceroute` 当前保存通用元数据和完整 JSON;后续如果解析更多 payload 字段,可继续扩展列
查询最近聊天消息示例:
```sql
SELECT id, created_at, from_id, text, mqtt_remote_host
FROM text_message
ORDER BY id DESC
LIMIT 20;
```
查询位置包示例:
```sql
SELECT id, created_at, from_id, latitude, longitude, altitude
FROM position
ORDER BY id DESC
LIMIT 20;
```
查询遥测包示例:
```sql
SELECT id, created_at, from_id, telemetry_type, metrics_json
FROM telemetry
ORDER BY id DESC
LIMIT 20;
```
SQLite 默认路径:
- Unix/Linux`/srv/mesh_mqtt_go/mesh_mqtt_go.db`
- Windows 测试:`./win/etc/mesh_mqtt_go/mesh_mqtt_go.db`
MySQL 配置示例:
```yaml
database:
driver: mysql
sqlite:
path: /srv/mesh_mqtt_go/mesh_mqtt_go.db
mysql:
dsn: mesh_user:mesh_pass@tcp(127.0.0.1:3306)/mesh_mqtt_go?parseTime=true&charset=utf8mb4,utf8
```
使用 MySQL 时,需要提前创建好 database/schema。
## 转发规则
程序监听所有传入 publish。payload 能被 `mqtpp.MQTTPP` 解析时,认为 `valid == true`broker 会继续把原始 MQTT 消息转发给订阅者;解析失败时,认为 `valid == false`broker 会拒绝并丢弃该 publish。
`empty_packet` 仍然属于 `valid == true`,会被转发;只是控制台默认不显示它。
无法解密但能解析的加密包通常会输出为 `encrypted_packet`,仍然属于 `valid == true`,因此会被转发。
## 本地验证
一个终端启动 broker
```bash
go run . --host 127.0.0.1 --port 1883 --psk AQ==
```
另一个终端订阅:
```bash
mosquitto_sub -h 127.0.0.1 -p 1883 -t '#'
```
发布非法 payload
```bash
mosquitto_pub -h 127.0.0.1 -p 1883 -t 'msh/US/test' -m 'not protobuf'
```
订阅端应该收不到该消息。
要验证 valid 消息转发,请使用真实 Meshtastic MQTT payload 发布到本 broker;订阅匹配 topic 的客户端应收到原始消息,broker 控制台会打印解析后的 `record`
## 控制台颜色说明
程序会按数据包类型使用不同背景色,方便快速区分消息类型。
| 背景色 | type | portnum | 含义 |
|---|---|---|---|
| 绿色 | `nodeinfo` | `NODEINFO_APP` | 节点信息包,包含节点 ID、长名称、短名称、硬件型号、角色、公钥等 |
| 蓝色 | `map_report` | `MAP_REPORT_APP` | 地图报告包,包含节点名称、硬件、固件版本、区域、调制预设、位置等地图信息 |
| 紫色 | `text_message` | `TEXT_MESSAGE_APP` | 聊天文本消息 |
| 青色 | `position` | `POSITION_APP` | 位置包,会展开解析经纬度、海拔、时间、定位来源、精度、速度、卫星数等字段 |
| 黄色 | `telemetry` | `TELEMETRY_APP` | 遥测包,会展开解析设备、电源、环境、空气质量、本地统计、健康、主机和流量管理指标 |
| 灰色 | `routing` | `ROUTING_APP` | 路由控制包,常见于 ACK、NAK、路由错误等控制信息 |
| 灰色 | `traceroute` | `TRACEROUTE_APP` | 路径追踪包,用于 mesh 网络路径探测 |
| 红色 | error record | - | protobuf 解析失败、payload 解码失败或其他处理错误 |
| 无颜色 | `encrypted_packet` | - | 加密包但当前 PSK/频道 hash 无法解密;这不一定是错误 |
| 无颜色 | `decoded_packet` | 其他 portnum | 已解码/已解密,但程序尚未细分的其他应用包 |
## 已展开解析的数据包
### `position` / `POSITION_APP`
位置包会从 Meshtastic `Position` payload 中展开常用字段,包括:
- `latitude` / `longitude`:经纬度,已从 `latitude_i` / `longitude_i` 转换为浮点角度
- `altitude`:海拔,单位米
- `time` / `timestamp`:位置相关时间戳
- `location_source`:定位来源,例如 `LOC_MANUAL``LOC_INTERNAL``LOC_EXTERNAL`
- `altitude_source`:海拔来源,例如 `ALT_MANUAL``ALT_INTERNAL``ALT_BAROMETRIC`
- `altitude_hae` / `altitude_geoidal_separation`HAE 海拔和大地水准面分离值
- `pdop` / `hdop` / `vdop`:定位精度因子,已从 1/100 单位转换为浮点值
- `gps_accuracy`GPS 精度,单位 mm
- `ground_speed`:地面速度,单位 m/s
- `ground_track`:地面航迹角,已从 1/100 度转换为度
- `fix_quality` / `fix_type` / `sats_in_view`:GPS fix 质量、类型和可见卫星数
- `sensor_id` / `next_update` / `seq_number` / `precision_bits`:传感器、更新间隔、序列号和位置精度位数
### `telemetry` / `TELEMETRY_APP`
遥测包会输出:
- `time`:遥测时间戳
- `telemetry_type`:具体 telemetry variant
- `metrics`:展开后的指标对象
当前支持的 `telemetry_type`
| telemetry_type | 含义 | 常见 metrics |
|---|---|---|
| `device_metrics` | 设备状态 | `battery_level``voltage``channel_utilization``air_util_tx``uptime_seconds` |
| `environment_metrics` | 环境传感器 | `temperature``relative_humidity``barometric_pressure``gas_resistance``lux``wind_speed``rainfall_1h` 等 |
| `air_quality_metrics` | 空气质量 | `pm25_standard``pm100_standard``co2``pm_temperature``pm_humidity``pm_voc_idx` 等 |
| `power_metrics` | 多通道电源数据 | `ch1_voltage``ch1_current``ch8_voltage``ch8_current` |
| `local_stats` | 本地 mesh 统计 | `num_packets_tx``num_packets_rx``num_online_nodes``heap_free_bytes``noise_floor` 等 |
| `health_metrics` | 健康数据 | `heart_bpm``spO2``temperature` |
| `host_metrics` | Linux/Portduino 主机指标 | `uptime_seconds``freemem_bytes``diskfree1_bytes``load1``load5``load15``user_string` |
| `traffic_management_stats` | 流量管理统计 | `packets_inspected``position_dedup_drops``rate_limit_drops``unknown_packet_drops` 等 |
## 过滤规则
程序默认不显示 `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":"position","portnum":"POSITION_APP","from":"!12345678","latitude":42.51043,"longitude":-83.08625,"altitude":192,"location_source":"LOC_INTERNAL","sats_in_view":8}
```
遥测包:
```json
{"type":"telemetry","portnum":"TELEMETRY_APP","from":"!12345678","telemetry_type":"device_metrics","metrics":{"battery_level":85,"voltage":4.1,"channel_utilization":2.3,"air_util_tx":0.5,"uptime_seconds":12345}}
```
解密失败的加密包:
```json
{"type":"encrypted_packet","decrypt_success":false,"decrypt_status":"channel hash mismatch","encrypted_len":43}
```