@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 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;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "meshtastic",
3
+ "channels": ["meshtastic"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
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
+ }
@@ -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
+ }