Skip to content

fix: align MQTT JSON transport with the Meshtastic protocol#18

Open
1Croydan1 wants to merge 2 commits into
Seeed-Solution:mainfrom
1Croydan1:fix/mqtt-json-protocol
Open

fix: align MQTT JSON transport with the Meshtastic protocol#18
1Croydan1 wants to merge 2 commits into
Seeed-Solution:mainfrom
1Croydan1:fix/mqtt-json-protocol

Conversation

@1Croydan1
Copy link
Copy Markdown

@1Croydan1 1Croydan1 commented Jun 4, 2026

Problem

The mqtt transport documents the standard Meshtastic broker as its default
(mqtt.meshtastic.org, meshdev / large4cats, msh/US/2/json/#), but the
JSON it produced and consumed did not match the Meshtastic JSON
protocol
. As shipped
in v0.2.2 the transport cannot work against that broker:

  • every received text message is filtered out, and
  • every reply it publishes is ignored by the gateway (wrong topic + wrong shape).

What was wrong (vs. the spec)

Meshtastic JSON main (v0.2.2) This PR
Inbound text type type:"text", payload:{text} filters type:"sendtext" type:"text"
Inbound author from=origin, sender=gateway prefers sender (gateway) ❌ prefers from
Downlink payload string { text } string ✅
Downlink fields from (num), channel (idx) sender (str), channel_name from, channel
Downlink topic …/2/json/mqtt/ …/mqtt (no slash) …/mqtt/

"sendtext" is the downlink verb, so filtering inbound on it dropped all
real traffic. On uplink, sender is the gateway that republished to MQTT while
from is the actual author — preferring sender collapses every node in a
multi-hop mesh to the gateway (and breaks DM/allowlist matching). sender is a
node ID, not a display name, so it's no longer surfaced as senderName.

For downlinks the firmware expects {from, type:"sendtext", payload:"<string>", channel?, to?}
on …/2/json/mqtt/. The numeric channelIndex is now threaded through
(mirroring the serial transport's sendText) instead of being parsed out of the
channel name.

Verification

No build/lint/test exists (per AGENTS.md the host loads TS via esbuild), so I
verified end-to-end against a real Meshtastic gateway + OpenClaw. Injecting a
simulated uplink and capturing the broker:

↓ uplink (group text, channel index 1)
msh/RU/2/json/claw/!12345678  {"type":"text","from":305419896,"channel":1,"payload":{"text":"7×6?"}}

↑ reply downlink produced by this PR
msh/RU/2/json/mqtt/  {"from":67725112,"type":"sendtext","payload":"42","channel":1}

📡 gateway accepts it and transmits on the mesh
msh/RU/2/json/claw/!04096738  {"channel":1,"from":67725112,"payload":42,"sender":"!04096738","type":"text"}

Before this change the uplink was dropped and nothing was transmitted.

Operational note (worth a line in the README, happy to add): for replies to
be transmitted, the gateway needs a channel named mqtt with downlink
enabled
and JSON output on, and mqtt.myNodeId must be that gateway's
node ID (used as the downlink from).

Scope

Code-only, 3 files. I kept it to the protocol fix. Separately I'll open an issue
for the openclaw/plugin-sdk/irc + /matrix import paths that break loading on
current OpenClaw (2026.5.x) — that's an SDK-version concern, not a Meshtastic
one.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Improved MQTT message routing by transitioning from channel name-based to channel index-based addressing for more reliable message delivery.
    • Updated MQTT message parsing to align with Meshtastic protocol standards for better protocol compatibility.
    • Streamlined internal message-sending APIs for consistent parameter handling across serial, HTTP, and MQTT transports.

The MQTT transport's JSON did not match the Meshtastic JSON format it
documents (default broker mqtt.meshtastic.org / msh/REGION/2/json), so
as shipped it could neither receive text from the broker nor have its
downlinks transmitted by a gateway.

Inbound (msh/REGION/2/json/...):
- received text packets have type "text", not "sendtext" ("sendtext" is
  the downlink verb and never appears on uplink), so the filter dropped
  every real message
- attribute the message to `from` (the originating node), not `sender`
  (the gateway that uploaded it to MQTT) — otherwise every sender in a
  multi-node mesh collapses to the gateway
- `sender` is a node ID, not a display name, so stop surfacing it as one

Downlink (publish to msh/REGION/2/json/mqtt/):
- `payload` must be a plain string, not { text }
- use `from` (numeric gateway node ID) and `channel` (index), not
  `sender` / `channel_name`
- publish to the documented ".../2/json/mqtt/" topic
- thread the numeric channelIndex through (mirroring the serial
  transport) instead of parsing it out of the channel name

Verified end-to-end against a real Meshtastic gateway: an injected
uplink is now received and answered, and the reply downlink
{"from":<gw>,"type":"sendtext","payload":"42","channel":1} is accepted
by the gateway and transmitted on the mesh.

Ref: https://meshtastic.org/docs/software/integrations/mqtt/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

Warning

Review limit reached

@1Croydan1, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 18 minutes and 47 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecc58538-2dbc-4300-af52-96dc128c0035

📥 Commits

Reviewing files that changed from the base of the PR and between 54c9b73 and 8164a95.

📒 Files selected for processing (2)
  • src/inbound.ts
  • src/send.ts
📝 Walkthrough

Walkthrough

Channel selection is standardized from string-based names to numeric indices throughout the MQTT messaging pipeline. The MQTT client uplink parser now recognizes type: "text" messages and derives sender node IDs from numeric from fields. Downlink publishing creates JSON envelopes with numeric channel and to fields. Callback signatures and monitor routing are updated to carry channelIndex consistently.

Changes

MQTT Channel Index Migration and Message Format Upgrade

Layer / File(s) Summary
MQTT client API contract and message envelope types
src/mqtt-client.ts
Exported sendText method signature changes from channelName?: string to channelIndex?: number. derivePublishTopic is updated to use trailing /mqtt/ path. New internal MqttJsonDownlink type models downlinks with numeric channel and to. Node-ID conversion imports added.
MQTT uplink message parsing and event construction
src/mqtt-client.ts
Incoming message type guard updated to accept type: "text" with payload.text structure. Sender node ID derivation now prefers numeric from field (converted to hex), falling back to sender. MeshtasticMqttTextEvent construction removes senderName assignment and normalizes senderNodeId with optional leading !.
MQTT downlink JSON publishing
src/mqtt-client.ts
sendText implementation rewritten to publish a downlink envelope containing from (gateway node ID or 0), type: "sendtext", payload as plain string, and optional numeric channel/to. No longer derives outbound topic leaf from channel names.
Active send callback type standardization
src/send.ts
setActiveSerialSend and setActiveMqttSend callback type signatures updated to accept channelIndex?: number instead of channelName?: string. MQTT send invocation changed to pass opts.channelIndex as the third argument to active send.
Monitor MQTT reply routing and handler wiring
src/monitor.ts
sendReply MQTT path updated to route based on message.isGroup: group messages provide message.channelIndex, direct messages provide target. Monitor's registered openclaw message send handler callback now accepts channelIndex and forwards it to mqttClient.sendText.

🎯 3 (Moderate) | ⏱️ ~22 minutes

🐰 Channels numbered now, names a thing of the past,
Uplinks and downlinks in JSON, built to last,
From strings to indices, the migration is clear,
MQTT routes messages without a single fear! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: aligning MQTT JSON transport implementation with the Meshtastic protocol specification across three files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@1Croydan1
Copy link
Copy Markdown
Author

The OpenClaw 2026.5.x SDK-load incompatibility I mentioned is now filed separately as #19 (with the verified import mapping + an offer to do that migration PR). This PR is purely the Meshtastic JSON protocol fix and is independent of it.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/send.ts (1)

10-14: ⚠️ Potential issue | 🟠 Major

Fix: channelName option is passed but ignored in sendMessageMeshtastic

  • src/send.ts defines SendMeshtasticOptions.channelName, but sendMessageMeshtastic only uses opts.channelIndex for both MQTT (line ~82) and serial/HTTP (line ~90); opts.channelName is never read.
  • A caller still passes channelName (e.g., src/inbound.ts lines ~104-108: sendMessageMeshtastic(..., { ..., channelName: params.channelName })), so channel selection can be silently ignored when channelIndex is unset.
  • Remove/deprecate channelName from SendMeshtasticOptions, or implement channelNamechannelIndex resolution (or throw when channelName is provided without channelIndex).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/send.ts` around lines 10 - 14, sendMessageMeshtastic currently ignores
SendMeshtasticOptions.channelName; update sendMessageMeshtastic to handle
channelName when channelIndex is not provided by resolving
channelName→channelIndex (or throwing if resolution fails). Add a small helper
(e.g., getChannelIndexByName) that looks up channel names from the Meshtastic
device/config and returns the numeric index, then use that resolved index in the
MQTT and serial/HTTP send branches inside sendMessageMeshtastic; ensure callers
like inbound.ts that pass channelName continue to work. Alternatively, if you
prefer deprecation, remove channelName from SendMeshtasticOptions and update all
call sites (e.g., sendMessageMeshtastic and inbound.ts) to require channelIndex
and throw a clear error when channelName is provided without channelIndex.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/send.ts`:
- Around line 10-14: sendMessageMeshtastic currently ignores
SendMeshtasticOptions.channelName; update sendMessageMeshtastic to handle
channelName when channelIndex is not provided by resolving
channelName→channelIndex (or throwing if resolution fails). Add a small helper
(e.g., getChannelIndexByName) that looks up channel names from the Meshtastic
device/config and returns the numeric index, then use that resolved index in the
MQTT and serial/HTTP send branches inside sendMessageMeshtastic; ensure callers
like inbound.ts that pass channelName continue to work. Alternatively, if you
prefer deprecation, remove channelName from SendMeshtasticOptions and update all
call sites (e.g., sendMessageMeshtastic and inbound.ts) to require channelIndex
and throw a clear error when channelName is provided without channelIndex.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 557ad033-6ce8-4b77-8c52-0ef8ccc2a2e0

📥 Commits

Reviewing files that changed from the base of the PR and between 379da00 and 54c9b73.

📒 Files selected for processing (3)
  • src/monitor.ts
  • src/mqtt-client.ts
  • src/send.ts

Address review (PR Seeed-Solution#18): once the send path standardized on numeric
channelIndex, SendMeshtasticOptions.channelName was no longer read anywhere —
both the MQTT and serial branches use channelIndex. Leaving it invited silent
channel-selection drops when only channelName was set. Remove it together with
the now-redundant deliverMeshtasticReply plumbing in inbound.ts. The inbound
message's own channelName (group routing) is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@1Croydan1
Copy link
Copy Markdown
Author

Good catch — fixed in the latest commit. After this PR standardized the send path on numeric channelIndex, SendMeshtasticOptions.channelName was no longer read by either transport branch, so it was dead/misleading plumbing. Removed it from SendMeshtasticOptions and the redundant pass-through in inbound.ts (deliverMeshtasticReply). The inbound message's own channelName (used for group routing) is unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant