@seeed-studio/meshtastic 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +170 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +30 -0
- package/src/accounts.ts +196 -0
- package/src/channel.ts +347 -0
- package/src/client.ts +296 -0
- package/src/config-schema.ts +106 -0
- package/src/inbound.ts +397 -0
- package/src/monitor.ts +300 -0
- package/src/mqtt-client.ts +162 -0
- package/src/normalize.ts +112 -0
- package/src/onboarding.ts +421 -0
- package/src/policy.ts +153 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +100 -0
- package/src/types.ts +108 -0
package/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# openclaw-meshtastic
|
|
2
|
+
|
|
3
|
+
OpenClaw channel plugin for [Meshtastic](https://meshtastic.org/) LoRa mesh networks.
|
|
4
|
+
|
|
5
|
+
Lets your OpenClaw gateway send and receive messages over Meshtastic devices — via USB serial, HTTP (WiFi), or MQTT broker.
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<img src="media/hardware.jpg" width="400" alt="Meshtastic LoRa hardware with Seeed WM1302 module" />
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
https://github.com/user-attachments/assets/demo.mp4
|
|
14
|
+
|
|
15
|
+
> The video above shows OpenClaw communicating over a Meshtastic LoRa mesh network. If it doesn't load, see [media/demo.mp4](media/demo.mp4).
|
|
16
|
+
|
|
17
|
+
## Features
|
|
18
|
+
|
|
19
|
+
- **Three transport modes**
|
|
20
|
+
- **Serial** — connect a Meshtastic device via USB (e.g. `/dev/ttyUSB0`)
|
|
21
|
+
- **HTTP** — connect over WiFi to a device's HTTP API (e.g. `meshtastic.local`)
|
|
22
|
+
- **MQTT** — connect through an MQTT broker (e.g. `mqtt.meshtastic.org`), no local hardware needed
|
|
23
|
+
- **Direct messages and group channels** — supports both DM and mesh channel conversations
|
|
24
|
+
- **Access control** — DM policy (open / pairing / allowlist), per-channel allowlists, mention-gating for group channels
|
|
25
|
+
- **Multi-account** — run multiple Meshtastic connections with independent configs
|
|
26
|
+
- **LoRa region selection** — configure device region on connect (US, EU_868, CN, JP, etc.)
|
|
27
|
+
- **Device display name** — set node longName, also used as @mention trigger in channels
|
|
28
|
+
- **Interactive onboarding** — guided setup wizard via `openclaw setup`
|
|
29
|
+
- **Auto-reconnect** — resilient connection handling with configurable retry
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- [OpenClaw](https://github.com/openclaw/openclaw) installed and running
|
|
34
|
+
- Node.js 22+
|
|
35
|
+
- For serial transport: a Meshtastic device connected via USB
|
|
36
|
+
- For HTTP transport: a Meshtastic device on the same network
|
|
37
|
+
- For MQTT transport: access to an MQTT broker (public `mqtt.meshtastic.org` works out of the box)
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
openclaw plugins install @seeed-studio/openclaw-meshtastic
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or install from a local directory during development:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/suharvest/openclaw-meshtastic.git
|
|
49
|
+
openclaw plugins install -l ./openclaw-meshtastic
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Interactive setup
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
openclaw setup
|
|
58
|
+
# Select "Meshtastic" when prompted for channel
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The wizard walks you through transport selection, connection details, region, access policy, and channel config.
|
|
62
|
+
|
|
63
|
+
<p align="center">
|
|
64
|
+
<img src="media/setup-screenshot.png" width="600" alt="OpenClaw setup wizard with Meshtastic channel configured" />
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
### Manual configuration
|
|
68
|
+
|
|
69
|
+
Add to your OpenClaw config (`openclaw config edit`):
|
|
70
|
+
|
|
71
|
+
**Serial (USB device):**
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
channels:
|
|
75
|
+
meshtastic:
|
|
76
|
+
enabled: true
|
|
77
|
+
transport: serial
|
|
78
|
+
serialPort: /dev/ttyUSB0
|
|
79
|
+
nodeName: OpenClaw
|
|
80
|
+
dmPolicy: pairing
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**HTTP (WiFi device):**
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
channels:
|
|
87
|
+
meshtastic:
|
|
88
|
+
enabled: true
|
|
89
|
+
transport: http
|
|
90
|
+
httpAddress: meshtastic.local
|
|
91
|
+
httpTls: false
|
|
92
|
+
nodeName: OpenClaw
|
|
93
|
+
dmPolicy: pairing
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**MQTT (broker):**
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
channels:
|
|
100
|
+
meshtastic:
|
|
101
|
+
enabled: true
|
|
102
|
+
transport: mqtt
|
|
103
|
+
mqtt:
|
|
104
|
+
broker: mqtt.meshtastic.org
|
|
105
|
+
port: 1883
|
|
106
|
+
username: meshdev
|
|
107
|
+
password: large4cats
|
|
108
|
+
topic: "msh/US/2/json/#"
|
|
109
|
+
tls: false
|
|
110
|
+
dmPolicy: pairing
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Configuration reference
|
|
114
|
+
|
|
115
|
+
| Key | Type | Default | Description |
|
|
116
|
+
|-----|------|---------|-------------|
|
|
117
|
+
| `transport` | `serial` \| `http` \| `mqtt` | `serial` | Connection method |
|
|
118
|
+
| `serialPort` | string | — | Serial device path |
|
|
119
|
+
| `httpAddress` | string | `meshtastic.local` | Device IP or hostname |
|
|
120
|
+
| `httpTls` | boolean | `false` | Use HTTPS for HTTP transport |
|
|
121
|
+
| `mqtt.broker` | string | `mqtt.meshtastic.org` | MQTT broker hostname |
|
|
122
|
+
| `mqtt.port` | number | `1883` | MQTT broker port |
|
|
123
|
+
| `mqtt.username` | string | `meshdev` | MQTT username |
|
|
124
|
+
| `mqtt.password` | string | `large4cats` | MQTT password |
|
|
125
|
+
| `mqtt.topic` | string | `msh/US/2/json/#` | MQTT subscribe topic |
|
|
126
|
+
| `mqtt.tls` | boolean | `false` | Use TLS for MQTT |
|
|
127
|
+
| `region` | string | `UNSET` | LoRa region (serial/HTTP only) |
|
|
128
|
+
| `nodeName` | string | — | Device display name and @mention trigger |
|
|
129
|
+
| `dmPolicy` | `open` \| `pairing` \| `allowlist` | `pairing` | DM access policy |
|
|
130
|
+
| `allowFrom` | string[] | — | Allowed node IDs (e.g. `["!aabbccdd"]`) |
|
|
131
|
+
| `groupPolicy` | `open` \| `allowlist` \| `disabled` | `disabled` | Group channel policy |
|
|
132
|
+
| `channels` | object | — | Per-channel config (requireMention, tools, allowFrom) |
|
|
133
|
+
|
|
134
|
+
### Multi-account
|
|
135
|
+
|
|
136
|
+
```yaml
|
|
137
|
+
channels:
|
|
138
|
+
meshtastic:
|
|
139
|
+
accounts:
|
|
140
|
+
home:
|
|
141
|
+
transport: serial
|
|
142
|
+
serialPort: /dev/ttyUSB0
|
|
143
|
+
remote:
|
|
144
|
+
transport: mqtt
|
|
145
|
+
mqtt:
|
|
146
|
+
broker: mqtt.meshtastic.org
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Verify
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
openclaw channels status --probe
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Environment variables
|
|
156
|
+
|
|
157
|
+
These can be used as alternatives to config file settings:
|
|
158
|
+
|
|
159
|
+
- `MESHTASTIC_TRANSPORT` — `serial`, `http`, or `mqtt`
|
|
160
|
+
- `MESHTASTIC_SERIAL_PORT` — serial device path
|
|
161
|
+
- `MESHTASTIC_HTTP_ADDRESS` — device IP or hostname
|
|
162
|
+
- `MESHTASTIC_MQTT_BROKER` — MQTT broker hostname
|
|
163
|
+
|
|
164
|
+
## Supported LoRa regions
|
|
165
|
+
|
|
166
|
+
US, EU_433, EU_868, CN, JP, ANZ, KR, TW, RU, IN, NZ_865, TH, UA_433, UA_868, MY_433, MY_919, SG_923, LORA_24
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { meshtasticPlugin } from "./src/channel.js";
|
|
4
|
+
import { setMeshtasticRuntime } from "./src/runtime.js";
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: "meshtastic",
|
|
8
|
+
name: "Meshtastic",
|
|
9
|
+
description: "Meshtastic LoRa mesh channel plugin",
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi) {
|
|
12
|
+
setMeshtasticRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: meshtasticPlugin as ChannelPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seeed-studio/meshtastic",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenClaw Meshtastic LoRa mesh channel plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Seeed-Solution/openclaw-meshtastic"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"openclaw",
|
|
13
|
+
"meshtastic",
|
|
14
|
+
"lora",
|
|
15
|
+
"mesh",
|
|
16
|
+
"iot"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@meshtastic/core": "^2.6.7",
|
|
20
|
+
"@meshtastic/transport-http": "^0.2.5",
|
|
21
|
+
"@meshtastic/transport-node-serial": "^0.0.2",
|
|
22
|
+
"mqtt": "^5.10.0",
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
|
+
},
|
|
25
|
+
"openclaw": {
|
|
26
|
+
"extensions": [
|
|
27
|
+
"./index.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import type { CoreConfig, MeshtasticAccountConfig, MeshtasticTransport } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type ResolvedMeshtasticAccount = {
|
|
5
|
+
accountId: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
configured: boolean;
|
|
9
|
+
transport: MeshtasticTransport;
|
|
10
|
+
serialPort: string;
|
|
11
|
+
httpAddress: string;
|
|
12
|
+
httpTls: boolean;
|
|
13
|
+
config: MeshtasticAccountConfig;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function listConfiguredAccountIds(cfg: CoreConfig): string[] {
|
|
17
|
+
const accounts = cfg.channels?.meshtastic?.accounts;
|
|
18
|
+
if (!accounts || typeof accounts !== "object") {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const ids = new Set<string>();
|
|
22
|
+
for (const key of Object.keys(accounts)) {
|
|
23
|
+
if (key.trim()) {
|
|
24
|
+
ids.add(normalizeAccountId(key));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...ids];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveAccountConfig(
|
|
31
|
+
cfg: CoreConfig,
|
|
32
|
+
accountId: string,
|
|
33
|
+
): MeshtasticAccountConfig | undefined {
|
|
34
|
+
const accounts = cfg.channels?.meshtastic?.accounts;
|
|
35
|
+
if (!accounts || typeof accounts !== "object") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const direct = accounts[accountId] as MeshtasticAccountConfig | undefined;
|
|
39
|
+
if (direct) {
|
|
40
|
+
return direct;
|
|
41
|
+
}
|
|
42
|
+
const normalized = normalizeAccountId(accountId);
|
|
43
|
+
const matchKey = Object.keys(accounts).find((key) => normalizeAccountId(key) === normalized);
|
|
44
|
+
return matchKey ? (accounts[matchKey] as MeshtasticAccountConfig | undefined) : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mergeMeshtasticAccountConfig(cfg: CoreConfig, accountId: string): MeshtasticAccountConfig {
|
|
48
|
+
const { accounts: _ignored, ...base } = (cfg.channels?.meshtastic ??
|
|
49
|
+
{}) as MeshtasticAccountConfig & {
|
|
50
|
+
accounts?: unknown;
|
|
51
|
+
};
|
|
52
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
53
|
+
const merged: MeshtasticAccountConfig = { ...base, ...account };
|
|
54
|
+
if (base.mqtt || account.mqtt) {
|
|
55
|
+
merged.mqtt = {
|
|
56
|
+
...base.mqtt,
|
|
57
|
+
...account.mqtt,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return merged;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function listMeshtasticAccountIds(cfg: CoreConfig): string[] {
|
|
64
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
65
|
+
if (ids.length === 0) {
|
|
66
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
67
|
+
}
|
|
68
|
+
return ids.toSorted((a, b) => a.localeCompare(b));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveDefaultMeshtasticAccountId(cfg: CoreConfig): string {
|
|
72
|
+
const ids = listMeshtasticAccountIds(cfg);
|
|
73
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
74
|
+
return DEFAULT_ACCOUNT_ID;
|
|
75
|
+
}
|
|
76
|
+
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveMeshtasticAccount(params: {
|
|
80
|
+
cfg: CoreConfig;
|
|
81
|
+
accountId?: string | null;
|
|
82
|
+
}): ResolvedMeshtasticAccount {
|
|
83
|
+
const hasExplicitAccountId = Boolean(params.accountId?.trim());
|
|
84
|
+
const baseEnabled = params.cfg.channels?.meshtastic?.enabled !== false;
|
|
85
|
+
|
|
86
|
+
const resolve = (accountId: string) => {
|
|
87
|
+
const merged = mergeMeshtasticAccountConfig(params.cfg, accountId);
|
|
88
|
+
const accountEnabled = merged.enabled !== false;
|
|
89
|
+
const enabled = baseEnabled && accountEnabled;
|
|
90
|
+
|
|
91
|
+
const envTransport =
|
|
92
|
+
accountId === DEFAULT_ACCOUNT_ID
|
|
93
|
+
? (process.env.MESHTASTIC_TRANSPORT?.trim() as MeshtasticTransport | undefined)
|
|
94
|
+
: undefined;
|
|
95
|
+
const transport: MeshtasticTransport = merged.transport ?? envTransport ?? "serial";
|
|
96
|
+
|
|
97
|
+
const envSerialPort =
|
|
98
|
+
accountId === DEFAULT_ACCOUNT_ID ? process.env.MESHTASTIC_SERIAL_PORT?.trim() : undefined;
|
|
99
|
+
const serialPort = merged.serialPort?.trim() || envSerialPort || "";
|
|
100
|
+
|
|
101
|
+
const envHttpAddress =
|
|
102
|
+
accountId === DEFAULT_ACCOUNT_ID ? process.env.MESHTASTIC_HTTP_ADDRESS?.trim() : undefined;
|
|
103
|
+
const httpAddress = merged.httpAddress?.trim() || envHttpAddress || "";
|
|
104
|
+
|
|
105
|
+
const httpTls = merged.httpTls ?? false;
|
|
106
|
+
|
|
107
|
+
// Apply env vars to MQTT config
|
|
108
|
+
if (accountId === DEFAULT_ACCOUNT_ID && merged.mqtt) {
|
|
109
|
+
const envBroker = process.env.MESHTASTIC_MQTT_BROKER?.trim();
|
|
110
|
+
const envTopic = process.env.MESHTASTIC_MQTT_TOPIC?.trim();
|
|
111
|
+
if (envBroker && !merged.mqtt.broker) {
|
|
112
|
+
merged.mqtt.broker = envBroker;
|
|
113
|
+
}
|
|
114
|
+
if (envTopic && !merged.mqtt.topic) {
|
|
115
|
+
merged.mqtt.topic = envTopic;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// For MQTT transport, also check env vars even without mqtt config block
|
|
120
|
+
if (transport === "mqtt" && !merged.mqtt && accountId === DEFAULT_ACCOUNT_ID) {
|
|
121
|
+
const envBroker = process.env.MESHTASTIC_MQTT_BROKER?.trim();
|
|
122
|
+
const envTopic = process.env.MESHTASTIC_MQTT_TOPIC?.trim();
|
|
123
|
+
if (envBroker || envTopic) {
|
|
124
|
+
merged.mqtt = {
|
|
125
|
+
broker: envBroker,
|
|
126
|
+
topic: envTopic,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const configured = resolveIsConfigured(transport, serialPort, httpAddress, merged);
|
|
132
|
+
|
|
133
|
+
const config: MeshtasticAccountConfig = {
|
|
134
|
+
...merged,
|
|
135
|
+
transport,
|
|
136
|
+
serialPort: serialPort || undefined,
|
|
137
|
+
httpAddress: httpAddress || undefined,
|
|
138
|
+
httpTls,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
accountId,
|
|
143
|
+
enabled,
|
|
144
|
+
name: merged.name?.trim() || undefined,
|
|
145
|
+
configured,
|
|
146
|
+
transport,
|
|
147
|
+
serialPort,
|
|
148
|
+
httpAddress,
|
|
149
|
+
httpTls,
|
|
150
|
+
config,
|
|
151
|
+
} satisfies ResolvedMeshtasticAccount;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const normalized = normalizeAccountId(params.accountId);
|
|
155
|
+
const primary = resolve(normalized);
|
|
156
|
+
if (hasExplicitAccountId) {
|
|
157
|
+
return primary;
|
|
158
|
+
}
|
|
159
|
+
if (primary.configured) {
|
|
160
|
+
return primary;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fallbackId = resolveDefaultMeshtasticAccountId(params.cfg);
|
|
164
|
+
if (fallbackId === primary.accountId) {
|
|
165
|
+
return primary;
|
|
166
|
+
}
|
|
167
|
+
const fallback = resolve(fallbackId);
|
|
168
|
+
if (!fallback.configured) {
|
|
169
|
+
return primary;
|
|
170
|
+
}
|
|
171
|
+
return fallback;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function resolveIsConfigured(
|
|
175
|
+
transport: MeshtasticTransport,
|
|
176
|
+
serialPort: string,
|
|
177
|
+
httpAddress: string,
|
|
178
|
+
config: MeshtasticAccountConfig,
|
|
179
|
+
): boolean {
|
|
180
|
+
switch (transport) {
|
|
181
|
+
case "serial":
|
|
182
|
+
return Boolean(serialPort);
|
|
183
|
+
case "http":
|
|
184
|
+
return Boolean(httpAddress);
|
|
185
|
+
case "mqtt":
|
|
186
|
+
return Boolean(config.mqtt?.broker);
|
|
187
|
+
default:
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function listEnabledMeshtasticAccounts(cfg: CoreConfig): ResolvedMeshtasticAccount[] {
|
|
193
|
+
return listMeshtasticAccountIds(cfg)
|
|
194
|
+
.map((accountId) => resolveMeshtasticAccount({ cfg, accountId }))
|
|
195
|
+
.filter((account) => account.enabled);
|
|
196
|
+
}
|