Meshtastic vs MeshCore feature parity
This document summarizes which client features are Meshtastic-only, MeshCore-only, or shared between the two LoRa companion-radio stacks, and whether gaps are app wiring, post-MQTT, or blocked by protocol.
Reticulum is the third protocol tab (amber pill, AGPL Rust sidecar, LXMF DMs). It is documented separately in reticulum.md and reticulum-sidecar-ipc.md — not in the matrix below.
See also CONTRIBUTING.md (multi-protocol architecture).
Capability flags
Shared UI gates use ProtocolCapabilities in src/renderer/lib/radio/BaseRadioProvider.ts. Prefer new gates there instead of protocol === 'meshcore' string checks.
Feature matrix
| Area | Meshtastic | MeshCore | Gap type |
|---|---|---|---|
| Transports | BLE, Serial, HTTP (@meshtastic/core), WiFi/TCP fast path (TransportTcpIpc, port 4403); launch auto-connect via useProtocolRfAutoConnect / ProtocolAutoConnectCoordinator + protocolRfAutoConnectGate |
BLE, Web Serial, TCP bridge (5000; main meshcore:tcp-*, tolerates peer FIN after contact burst, TCP_NODELAY + keepalive); same RF auto-connect coordinator; TCP reconnect owned by useMeshcoreRuntime + rfReconnectController (conn side effects skip handleConnectionLost for TCP) |
Different stacks |
| Tab “Modules” / “Repeaters” | ModulePanel (protobuf modules; Remote Hardware GPIO, IP Tunnel status) |
RepeatersPanel (trace, status, neighbors) |
Product split |
| Tab “Administration” | AdminPanel (reboot, shutdown, factory reset, NodeDB reset, OTA/DFU) |
AdminPanel (reboot via companion; meshcore.js limits for shutdown/factory/OTA) |
App (implemented; reboot wired; extended admin capability-limited) |
| MQTT broker UI | Full (with transport selection) | Same broker fields; transport protocol selected when connecting; MeshCore-only LetsMesh / MeshMapper / Colorado Mesh / Waev / Meshat.se / MeshCore.CA / EastMesh / Ripple / Custom presets fill known public brokers | Post-MQTT codec on broker path |
| MQTT wire format | ServiceEnvelope / MeshPacket (mqtt-manager.ts) |
JSON v1 chat on {topicPrefix}/meshcore/chat (non-LetsMesh / private brokers); LetsMesh: optional meshcoretomqtt-style packet JSON on {topicPrefix}/meshcore/packets (meshcore-mqtt-adapter.ts); chat parser in meshcoreMqttEnvelope.ts |
Adapter vs protobuf |
| MQTT channel crypto / uplink | AES-128/256-CTR, channelPsks, TLS (mqttTls.ts), per-channel publish (meshtasticMqttPublish.ts); mqtt-manager.ts |
JSON v1 path unchanged | App (Meshtastic wire) |
| Node list hops / MQTT columns | hops_away, via_mqtt from device |
Contact model; node-list hops_away derives from contact outPathLen (meshcoreInferHopsFromOutPath); per-message chat hop pills instead use the companion pathLen on RX events 7/8 (meshcoreCompanionRxPathLenToHopCount) — see docs/agents/chat.md §Hop badges |
App (implemented) |
| RF diagnostics (LocalStats) | From protobuf | Different data model: Repeater Status meshcore_local_stats packet-stats feed Elevated Noise Floor / Excessive Flooding findings only (no CU/TX-based findings) |
App (implemented, different metrics) |
| Routing diagnostics (hop-based) | RoutingDiagnosticEngine with hop count |
hasHopCount is true (hops via outPathLen); same RoutingDiagnosticEngine hop anomalies run, plus MeshCore-only weak_link (per-hop trace SNR) |
App (implemented) |
| Foreign LoRa overhear UI | Diagnostics tab tables (MeshCore / Reticulum RNS / unknown); Meshtastic decode-fail logs + dual-radio MeshCore RX | Records foreign traffic; Diagnostics foreign-LoRa tables on MeshCore tab (keyed by MeshCore self id) and Meshtastic tab | App (implemented; tables on Meshtastic and MeshCore tabs) |
| Neighbor UI | neighborInfo protobuf |
Paged binary GetNeighbours (MESHCORE_NEIGHBORS_PAGE_SIZE request cap, offset append via mergeMeshcoreNeighborPage); Load more on RepeatersPanel and NodeDetailModal (firmware often returns fewer rows than requested) |
Different primitive |
| Radio config | Full protobuf (role, presets, WiFi, etc.) | setRadioParams, channels, advert name/position |
Blocked for Meshtastic-only admin |
| Channel URL sync | Radio tab import/export via meshtasticUrlEncoder.ts + meshtasticChannelApply.ts (https://meshtastic.org/e/#…, meshtastic://) |
Not available | App (Meshtastic-only) |
| Position | Full GPS protobuf + request position | Radio Position / GPS: advertised readout + lat/lon + setAdvertLatLong via Send Position; no GPS mode / broadcast intervals / altitude / request-position |
App (implemented; advert lat/lon only — protocol) |
| Waypoints | Supported | Not in protocol surface | Blocked |
| Favorites | nodes table |
meshcore_contacts.favorited + db:updateMeshcoreContactFavorited |
App (implemented) |
| Environment telemetry charts | Device telemetry module | Cayenne LPP via getTelemetry → environmentTelemetry |
App (implemented) |
| Chat transport badges / history | received_via (rf / mqtt / both) plus via_store_forward for S&F replays; router heartbeat triggers CLIENT_HISTORY via meshtasticBacklogUtils.ts |
meshcore_messages.received_via (rf / mqtt / both) |
App (implemented) |
| Chat search | searchMessages |
searchMeshcoreMessages; UI search modal supports user: / channel: filters for cross-channel lookup |
Parallel DB tables |
Chat @[Display Name] tokens |
Same on-wire pattern for replies / reactions / path-style lines | Same | App (implemented); chat body renders tokens as inline labels (see below) |
| Emoji reactions / tapbacks | reactions.ts decodes protobuf tapbacks (emoji flag + UTF-8 payload, legacy index 1–12); ChatPanel quick picker + sendReaction |
Default outbound keyless @[Name] emoji / @[Name] body; optional MeshCore Open compatibility (Radio toggle) enables keyed replies, r:HASH:INDEX, and g:GIFID send — buildMeshcoreOutboundTapbackWire, buildMeshcoreOutboundSendText, meshcoreOpenReaction.ts, meshcoreGifWire.ts; inbound keyed/keyless + Open wire always parsed; emoji-only replies promoted via meshcorePromoteEmojiOnlyReplyToTapback; echo dedup in meshcoreStoreDedup.ts |
App (shared UI, protocol-specific wire) |
| MeshCore Open wire (experimental) | N/A | Radio toggle meshcoreOpenWireCompatEnabled (defaultAppSettings.ts): keyed replies, r: reactions, g: GIF send; default off (companion keyless wire) |
App (MeshCore-only) |
| Chat composer | ChatComposer.tsx in ChatPanel; long text auto-splits into [i/N] chunks (up to 9, getMaxChunks) |
Same ChatComposer in ChatPanel and RoomsPanel, but single-packet: getMaxChunks('meshcore') === 1, so over-limit text is blocked with an explanatory notice (no multi-part split — busy repeaters drop parts, meshcore-dev/MeshCore#1502 / #2820). Non-blocking fast-send advisory (meshcoreSendRateNotice.ts) when sending within ~5s. Inbound multi-part still merged. |
App (shared UI; MeshCore single-packet parity gap) |
| Repeater CLI | Not applicable | Expandable CLI in RepeatersPanel for repeater and room rows; prefix-token correlation (RepeaterCommandService); auto Ping before the first multi-hop CLI command when no trace exists this session; destructive-command confirm (reboot / erase / factory-reset patterns via meshcoreRepeaterCliDanger.ts); ping-first guidance for multi-hop CLI; quick pills include clock, clock sync, clear stats, advert, board (+ room get acl / allow.read.only); Flood Advert and Sync Clock toolbar actions live on Radio panel (Device Actions) — distinct from the CLI clock sync pill; auto flood advert scheduling available in App Settings (disabled / 12h / 24h) |
App (MeshCore-only) |
| Regional flood scope | Meshtastic region via LoRa config | Radio tab flood scope (setFloodScope / clearFloodScope); user-managed saved hashtags (meshcoreFloodScopePresets) + Chat split-Send override remembered per channel view; app_settings reapply on connect. Community region/scope guide: RegionMesh MeshCore region configuration |
App (MeshCore v8+ transport keys) |
| Meshtastic MQTT downlink | Firmware MQTT module + MqttClientProxyMessage bridge when proxy_to_client_enabled (BLE/serial); per-channel downlink on Radio tab |
N/A (JSON MQTT ingest only) | App (Meshtastic) |
| Security / PKI admin | SecurityPanel when hasSecurityPanel; DM backup/restore per nodeNum (full public + private pair) — see key-backup-and-crypto.md |
SecurityPanel (partial): backup/restore per nodeId, sign, export/import; no Meshtastic PKI admin. Active MQTT cache: mesh-client:meshcoreIdentity — see key-backup-and-crypto.md |
Partial — shared tab; protocol-specific backup + MC MQTT cache |
| PKC remote admin | ConfigureNodeSelector, meshtasticRemoteAdmin.ts, meshtasticRemoteAdminKeyStorage.ts; local radio (2.5+) |
Not available | App (Meshtastic-only) |
| Contact groups | Built-in groups (GPS, RF+MQTT) via meshtasticContactGroupUtils; user-managed via ContactGroupsModal |
SQLite-backed groups + Nodes toolbar (useContactGroups, ContactGroupsModal); built-in Room filter |
App (implemented); protocol-neutral with Meshtastic built-ins |
| Log analyzer | LogPanel → Analyze (logAnalyzer.ts, protocol-aware) |
Same shared UI | App (implemented) |
| Room servers (BBS) | Not applicable | Rooms tab: login/post; optional Remember password (app_settings); Auto-sync periodic re-login while radio connected (meshcoreRoomSyncScheduler.ts, useMeshcoreRuntime.ts); room admin CLI / ACL setperm on Repeaters (room rows); RF-only (not MQTT) |
App (MeshCore-only) |
| Repeater admin passwords | Not applicable | Per-repeater Remember (meshcoreRepeaterCredential:<nodeId> in app_settings); shared factory meshcorePerNodeCredentialStorage.ts with meshcoreRepeaterCredentialStorage.ts / meshcoreRoomCredentialStorage.ts; useMeshcoreRepeaterRemoteAuth.tsx, MeshcoreRepeaterPasswordControls.tsx; Repeaters sidebar Saved admin passwords + Forget |
App (MeshCore-only) |
| MsgWaiting background drain | Not applicable | Event 131 silent drain (meshcoreWaitingMessagesDrain.ts): bulk getWaitingMessages first (header X / Y), syncNextMessage fallback on timeout without disconnect (Fetched N…); header status indicator (queued backlog and active sync on any protocol tab; paused/deferred only on MeshCore tab); manual Sync now with determinate progress |
App (MeshCore-only) |
MeshCore: Room servers
Room servers (hw_model === 'Room', contact type 3) are BBS nodes on the mesh. The companion radio must be connected over RF (BLE, serial, or TCP); MQTT does not carry room login/post.
Login: Blank guest Login sends zero password bytes (read-only when allow.read.only is on; same wire as Continue read-only). hello is the default read/write guest password. Admin login uses the configured password. LoginSuccess ACL is the companion v7+ permissions byte (PERM_ACL_*); reserved is only the legacy admin/guest hint (do not treat as ACL). Login RPC, queue, and path sync live under src/renderer/lib/meshcoreRoom*.ts (e.g. meshcoreRoomLoginRpc.ts, meshcoreRoomLoginQueue.ts); timeouts are shorter on TCP and 0-hop paths (timeConstants.ts).
Posts: Outbound room posts use plain UTF-8 (TXT_TYPE_PLAIN) after login and are single-packet — mesh-client does not emit multi-part [i/N] room posts; over-limit text is blocked in the composer (same rationale as chat/DM). Inbound SignedPlain pushes include a four-byte author prefix; the Rooms UI strips it, and inbound multi-part from other clients is still merged for display. Posts appear in the Rooms tab (channel -2), not Chat channel pills.
Sync: After login, the room server pushes posts newer than the companion sync_since watermark (ring buffer, typically ~32). mesh-client resets that watermark when the device has no local last-post time yet, then drains waiting messages. Auto-sync re-logs in on a timer while the radio stays connected (minimum 60 minutes per room, meshcoreRoomSyncScheduler.ts). Saved passwords: SQLite app_settings (same pattern as Meshtastic remote admin keys). Session clears on disconnect.
Unread: Room BBS traffic increments the Rooms sidebar badge (meshcoreRoomsUnread.ts) and system-tray unread when backgrounded; it does not increment the Chat tab badge.
Dedup: meshcoreStoreDedup.ts merges duplicate RF/MQTT and tapback echoes for chat and rooms (cross-transport and channel RF 5 min; room/tapback 60 s).
MeshCore: regional flood scope
MeshCore regions (on repeaters) and scopes (on outbound flood messages) limit how far flooded traffic is forwarded. Community naming, exact matching, hierarchy (region put child parent), and the wildcard * for unscoped traffic are documented by RegionMesh — MeshCore region configuration.
In mesh-client:
- Radio → Regional flood scope sets the companion radio-wide default (
meshcoreFloodScopeHashtaginapp_settings; reapplied on connect). - Chat split-Send override chooses Default (follow Radio), Unscoped (mesh-wide / clear scope for that channel’s sends), or a named hashtag (e.g.
#metro,#us-co). Default and Unscoped stay distinct choices. - Chat remembers the override per channel view (
mesh-client:floodScopeOverrides:meshcore, keyed like drafts bych:N) so a mesh-wide Public channel and a scoped shared-key channel (e.g. metro containment) do not bleed into each other when switching pills. - Channel PSK still controls who can decode; region/scope controls which repeaters forward. Hierarchy and
region allowflive on repeater CLI, not in Chat.
MeshCore: identity-scoped UI stores
Live Chat and Nodes read identity-scoped nodeStore / messageStore (keyed by identityId) via identityStoreReads (getIdentityNode / getIdentityChatMessages). DM/trace pubkeys live in meshcorePubKeyRegistry (mirrored into runtime maps for send/RPC; Nodes also keep meshcorePubKeyHexByNodeId for offline short ids). Nodes table shows Node health (not a MeshCore ID column) and pubkey short id (! + 8 hex) with a key icon. Hydration: hydrateIdentityStoresFromDb.ts. Chat-driven last_heard (meshcoreIngest, ensureMeshcoreChatSenderInNodeStore) updates node freshness on text traffic, not only adverts.
MeshCore: Rooms scroll UX
Rooms tab scroll layout matches Chat: outer scroll container, unread divider, jump-to-unread button, and persisted last-read via meshcoreRoomsUnread / localStorage. Uses chatScrollUtils.ts (getDistFromChatBottom).
Operational troubleshooting: troubleshooting.md.
MeshCore: Trace Route and Ping trace
Trace Route (node detail) and Ping trace (Repeaters panel) use the firmware tracePath flow. Remote nodes often answer only when they have your node in their contact list. Heard-only or one-way peers may produce no response until the client times out. See troubleshooting.md.
Serialized traceroutes (protocol requirement)
MeshCore companion firmware handles one SendTracePath / TraceData cycle at a time on a given RF link. Parallel traceroutes are not permitted — the radio will not reliably accept overlapping trace commands.
mesh-client enforces this in two layers:
- Per-radio trace queue (
meshcoreRepeaterRpcInFlight.ts) — ping clicks for different repeaters run one after another, not concurrently. Duplicate clicks on the same repeater share one in-flight promise. - Companion RPC queue (
repeaterRemoteRpcQueue.ts) — serializes sends only. AfterRESP_SENT, status/neighbors/telemetry/binary responses are matched by pubkey prefix orexpectedAckCrctag while the queue serves other work (runMeshcoreRepeaterQueuedSend). - Admin deferral — before admin sends: wait for active TraceData (
awaitMeshcoreRepeaterAdminRfIdle); before admin on the same repeater: wait for that node's ping wrapper to finish (awaitMeshcoreRepeaterPingSettleForNode, up to 360s). - 0-hop ping — first attempt uses 1-byte pubkey prefix; failure triggers cancel + full-pubkey direct retry (0-hop only). Multi-hop requires hash-segment outPath (≥2 bytes).
Practical guidance: Run one ping at a time when possible; let it finish (Hops column updates) before starting Status on the same repeater. Queued pings may take up to 180s each (including 0-hop direct retry). Status/Neighbors/Telemetry use 120s flat timeouts.
Multi-hop route priming: When outbound path bytes are missing but the UI shows multi-hop, ping/trace first runs a passive priming pass (wait for PathUpdated 129 and refresh contacts, hop-scaled 15s + 5s × hops, capped at 45s). If that still yields no usable hash-segment path and hops ≥ 2, mesh-client may run up to two flood-advert rounds as a fallback (listener registered before each advert). Path synthesis: 1-hop — [relayPrefix, destPrefix] via a known 0-hop repeater; 2-hop — prepend relay byte to a stored 2-byte path; 3+ — only stored paths with enough segments (no blind pubkey guessing). Multi-hop cached full destination pubkeys are discarded. One-way contacts may still time out with no TraceData — see troubleshooting.md.
Repeater admin RPC wire shape: Status and Telemetry use pubkey-framed companion commands (meshcoreRepeaterStatusRpc.ts, meshcoreRepeaterTelemetryRpc.ts). Neighbors uses runMeshcoreRepeaterBinaryRequest with queued send and paged GetNeighbours (count + offset; append into meshcoreNeighbors via mergeMeshcoreNeighborPage). Status/Telemetry/Neighbors throw on disconnect so UI toasts fire. Login is optional for CLI/telemetry when a password is saved; Status/Neighbors typically work without login on direct (0-hop) repeaters.
Implementation reference: trace priming constants and PathUpdated wait helpers in meshcoreHookPreamble.ts; flood-advert priming rounds in meshcoreTraceRoutePrime.ts; runMeshcoreTracePathMultiplexed in meshcoreTracePathMultiplex.ts; pubkey-framed Status/Telemetry via meshcoreRepeaterPrefixPushRpc.ts; traceRoute / ping in useMeshcoreRuntime.ts.
Windows: MeshCore over BLE
Pair the radio in Settings → Bluetooth & devices before connecting from the app; WinRT is much more reliable with a bonded device. The client may retry once after transient GATT discovery failures, and canceling mid-connect should not surface a misleading long-running channel timeout. User-facing copy lives in the Connection tab on Windows; contributor details are in CONTRIBUTING.md (MeshCore internals, BLE) and README.md (MeshCore Transport Notes).
Linux: MeshCore over BLE
Linux uses Web Bluetooth in the renderer (not Noble). After you pick a device, the client reads bluetoothctl info <MAC>. If the radio is not paired in BlueZ, the UI asks for the PIN shown on the device and runs bluetooth-pair before resolving the pending Web Bluetooth requestDevice() selection. If a handshake times out, a single retry reuses the granted device via getDevices() so requestDevice() is not called again without a click. See development-environment.md and troubleshooting.md.
Chat mention tokens
Meshtastic and MeshCore use the literal form @[Display Name] in channel payloads for thread replies, legacy emoji tapbacks, path / hop summaries, and inline references. The client may keep the raw string in storage when a reply parent cannot be matched; the Chat tab still parses these segments for display only: brackets are hidden and the name is shown as a compact inline label (ChatPayloadText.tsx, used from ChatPanel.tsx; parser in chatMentionSegments.ts). Threading / replyId behavior is unchanged; this is purely presentational.
MeshCore Open: GIF wire (g:GIFID)
MeshCore Open sends GIFs as compact wire text g:{giphyId} (Giphy CDN). mesh-client renders these inline in chat via meshcoreGifWire.ts and ChatPayloadText.tsx. Full Giphy media/page URLs are also recognized. Outbound GIF send (paste URL/ID or GIF button in Chat composer) is available when MeshCore Open compatibility is enabled in Radio → MeshCore Open wire (experimental).
MeshCore: emoji reactions (tapbacks)
UI: MeshCore Chat uses the same reaction picker as Meshtastic — native macOS/Windows emoji panel (showEmojiPanel()) or Linux emoji-picker-element — plus 12 quick reactions on hover. Tapbacks render as reaction badges on the parent message (emoji + replyId on the stored row), not as reply bubbles.
Wire (MeshCore companion): Tapbacks and text replies use the same keyless bracket prefix (official companion shape):
@[Display Name] emoji
@[Display Name] message
Outbound tapbacks use formatMeshcoreWireTapbackPrefix + emoji via buildMeshcoreOutboundTapbackWire in useMeshcoreRuntime sendReaction. Text replies use the same keyless prefix via buildMeshcoreOutboundSendText (useSendMessage / useMeshcoreRuntime sendMessage); plain body when the parent is not in store. Display names are sanitized via sanitizeMeshcoreWireName.
Local model: Stored rows use clean payload plus replyId / quote preview when applicable — never the bracket wire string. Inbound replies with a single-emoji body are classified as tapbacks via meshcorePromoteEmojiOnlyReplyToTapback (live ingest, repair, and display hydration).
MeshCore Open (r:HASH:INDEX): Parsed inbound for display. Outbound r: reactions are sent when MeshCore Open compatibility is enabled in Radio settings (fallback to keyless @[Name] emoji when the picker emoji is not in the Open index table). See meshcoreOpenReaction.ts.
MeshCore Open compatibility (Radio toggle, default off): Enables keyed outbound text replies (@[Name#replyKey] body), r: reactions, and g: GIF send for meshes with MeshCore Open clients. Default wire remains official companion keyless @[Name] … for replies and tapbacks.
Limitations:
| Topic | Behavior |
|---|---|
| Emoji choice | Any glyph from the native/Linux picker; not limited to the Open index table. |
| Text replies | Default outbound keyless @[Name] body via buildMeshcoreOutboundSendText; with Open compat, keyed @[Name#key] body. Plain body when parent not in store. Inbound keyed @[Name#key] (seconds or ms) resolved via meshcoreMessageMatchesReplyKey. |
| Meshtastic | Unaffected — protobuf tapbacks (replyId + emoji flag), not MeshCore text lines. |
| Dedup | mesh-client merges tapback RF/MQTT echoes (meshcoreStoreDedup.ts, 60 s window). Default outbound uses keyless @[Name] …; Open compat may send keyed/r:/g: wire on the same channel. |
Inbound: parse r:HASH:INDEX (MeshCore Open), keyed @[Name#key] body or emoji, and keyless @[Name] body / emoji from other clients. See troubleshooting.md — MeshCore reply misquote / duplicate chat messages.
MeshCore MQTT JSON envelope (v1)
Interim broker format until a binary/official MeshCore MQTT layout ships:
{
"v": 1,
"text": "message body",
"channelIdx": 0,
"senderName": "optional",
"senderNodeId": 305419896,
"timestamp": 1700000000000
}
Subscribes under {topicPrefix}/#. Outbound optional publish uses mqtt:publishMeshcore → {topicPrefix}/meshcore/chat (JSON same shape). LetsMesh public brokers do not use that path for MQTT-only chat without a radio; optional Packet logger (mqtt:publishMeshcorePacketLog) publishes RX packet summaries to {topicPrefix}/meshcore/packets using meshcoretomqtt-shaped JSON; implemented in MeshcoreMqttAdapter.publishPacketLog (see letsmesh-mqtt-auth.md § Packet logger). Debug logging is sampled to suppress repeated decode failures and noise (traceroute, empty-type JSON).
Meshtastic MQTT network presets
In Meshtastic mode, ConnectionPanel.tsx shows a Network Preset picker (shared MqttNetworkPresetSelect) with Official, Liam's, and Custom options. They populate MQTTSettings used by mqtt-manager.ts.
| Preset | Broker host | Port | Notes |
|---|---|---|---|
| Official | mqtt.meshtastic.org |
1883 | Plaintext; may be blocked on some networks |
| Liam's | mqtt.meshtastic.liamcottle.net |
1883 | Uplink-only (puts your node on Liam Cottle's map; no downlink). No TLS. Useful when mqtt.meshtastic.org is unreachable |
| Custom | (user) | — | No automatic changes; use for private brokers |
Topic prefix defaults to msh/US/; users can edit fields after choosing a preset. Defined in meshtasticMqttTlsMigration.ts.
Private brokers (Meshtastic)
For ham or private MQTT brokers (typically Custom preset), the Connection tab adds:
- Channel PSKs: AES-128 (16-byte) or AES-256 (32-byte) base64 keys, one per line; optional
ChannelName=base64orChannelName@index=base64for MQTT-only channels when your local slot differs from the topic name (multiple lines per name allowed). LongFast default is always tried. Keys from the Radio tab sync when the radio is connected; custom named keys are preserved when sync would only send the default public PSK. Line parsing:meshtasticChannelPskLine.ts. - MQTT-only sender id:
meshtasticMqttIdentity.tsuses last RFmyNodeNumwhen known, else a stable virtual id for chat/MQTT publish without a radio. - Enable TLS (mqtts / wss): explicit TLS toggle via
mqttTls.ts(port 8883/443 no longer required to imply TLS). Allow insecure TLS for self-signed or non–public CA chains. - Per-channel uplink: outbound RF → MQTT uses each channel’s name and PSK via
meshtasticMqttPublish.ts. - Inbound text channel: MQTT downlink text prefers the topic channel name mapped through
channelNameToIndex(receiver-local slot) inmqtt-manager.ts;MeshPacket.channelis sender-local and used only as fallback when topic is absent (sampled log when topic and packet disagree).
MeshCore MQTT network presets
In MeshCore mode only, ConnectionPanel.tsx shows a Network Preset picker (shared MqttNetworkPresetSelect) with, in order: LetsMesh, MeshMapper, Colorado Mesh, Waev, Meshat.se, MeshCore.CA, EastMesh, Ripple Networks, Custom. New installs default to LetsMesh. They populate the same MQTTSettings the main process uses for meshcore-mqtt-adapter.ts (with mqttTransportProtocol: 'meshcore'). Preset fields live in meshcoreMqttPresets.ts.
| Preset | Broker host | Port | Notes |
|---|---|---|---|
| LetsMesh | mqtt-us-v1.letsmesh.net or mqtt-eu-v1.letsmesh.net |
443 | Default for new users. WebSocket (wss, path /ws). Topic prefix meshcore/test. JWT auth; see Authentication below. Optional Packet logger publishes to meshcore/packets. See letsmesh-mqtt-auth.md. |
| MeshMapper | mqtt.meshmapper.net |
443 | WebSocket (wss, path /ws). Topic prefix meshcore/test. |
| Colorado Mesh | mqtt.meshcore.coloradomesh.org |
443 | Colorado residents only. WebSocket (wss, path /ws). Topic prefix meshcore/DEN. Confirm on select; one-time stay-or-switch gate for existing Colorado users. JWT device-signing auth. |
| Waev | mqtt.waev.app |
443 | WebSocket (wss, path /mqtt). Topic prefix meshcore/test. JWT device-signing auth (broker enforces a short auth-token lifetime). |
| Meshat.se | meshcore-mqtt.meshat.se |
443 | WebSocket (wss, path /mqtt). Topic prefix meshcore/test. JWT device-signing auth. |
| MeshCore.CA | mqtt1.meshcore.ca (Primary) or mqtt2.meshcore.ca (Backup) |
443 | WebSocket (wss, path /mqtt). Topic prefix meshcore/test. JWT device-signing auth. Primary/Backup broker toggle appears under the picker. |
| EastMesh | mqtt2.eastmesh.au |
443 | WebSocket (wss, path /mqtt). Topic prefix meshcore/test. JWT device-signing auth. |
| Ripple Networks | mqtt.ripplenetworks.com.au |
8883 | TLS; preset fills default shared credentials and insecure TLS for self-signed / non–public CA chains. Topic prefix meshcore. |
| Custom | (user) | — | No automatic changes; use for private brokers |
Topic prefixes for the device-signing presets (LetsMesh / MeshMapper / Colorado / Waev / Meshat.se / MeshCore.CA / EastMesh — and Custom when pointed at those hosts) must be meshcore/{IATA} (3 letters) or meshcore/test — validated in meshcoreMqttTopicPrefix.ts. Users can still edit fields after choosing a preset. On upgrade, connectionPanelStorageMigrations.ts migrates stale Colorado Mesh port 1883 → 443, bare topic meshcore → IATA, repairs invalid IATA shapes, seeds LetsMesh for new installs, and reconciles preset-owned fields before MQTT auto-launch (also runs from main.tsx before React mount).
Log filtering
MQTT log messages are prefixed for easy filtering: [Meshtastic MQTT] in mqtt-manager.ts and [MeshCore MQTT] in meshcore-mqtt-adapter.ts. The Log panel filter and analyzer patterns recognize these tags.
Maintenance
When MeshCore firmware/SDK defines official MQTT topics and payloads, replace or extend MeshcoreMqttAdapter and update this document.
MeshCore MQTT Authentication
Device-signing JWT authentication
The LetsMesh, MeshMapper, Colorado Mesh, Waev, Meshat.se, MeshCore.CA, and EastMesh presets all use WebSocket (wss) with the same device-signed JWT authentication (Custom settings pointed at those hosts use it too). The implementation matches meshcore-mqtt-broker:
- MQTT username:
v1_<64-hex public key>(uppercase hex) - MQTT password: A token from
@michaelhart/meshcore-decodercreateAuthTokenwith: publicKey: 64-character hex public keyiat: Issued-at timestampexp: Expiration timestamp- JWT
aud(audience): The broker hostname you connect to (same as the Server field), vialetsMeshJwtAudience()
The JWT audience always matches the connect hostname — e.g. mqtt-us-v1.letsmesh.net, mqtt.waev.app, mqtt1.meshcore.ca. The broker allowlist and each broker's WebSocket path (/ws for LetsMesh/MeshMapper/Colorado, /mqtt for Waev/Meshat.se/MeshCore.CA/EastMesh) live together in letsMeshJwt.ts (DEVICE_SIGNING_HOST_WS_PATHS); connect/deviation guards enforce port 443, TLS, and the expected wsPath in letsMeshConnectionGuards.ts.
Signing uses cached private key material from either a Radio-tab MeshCore JSON import or automatic persistence after a successful MeshCore radio session (same storage shape as import).
On upgrade, the legacy MeshMapper host mqtt.meshmapper.cc is rewritten to mqtt.meshmapper.net (TLS on .cc fails with alert 80) by connectionPanelStorageMigrations.ts.
Configuration
Import a MeshCore config JSON file (Radio tab) when you need credentials before connecting a radio, or to replace missing data; otherwise connecting the MeshCore radio first fills the same cache. The implementation is in letsMeshJwt.ts.
Packet logger (optional)
The optional Packet logger publishes RX packet summaries to meshcore/packets under the topic prefix using meshcoretomqtt-shaped JSON. See letsmesh-mqtt-auth.md for details.