Skip to content

Meshtastic vs MeshCore feature parity

This document summarizes which client features are Meshtastic-only, MeshCore-only, or shared, and whether gaps are app wiring, post-MQTT, or blocked by protocol.

See also CONTRIBUTING.md (dual-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) BLE, Web Serial, TCP bridge (5000) 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 only; meshcore.js limits) App
MQTT broker UI Full (with transport selection) Same broker fields; transport protocol selected when connecting; MeshCore-only LetsMesh / Ripple / Colorado Mesh / 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; hops via outPathLen from device trace App (implemented)
RF diagnostics (LocalStats) From protobuf Not available Blocked
Routing diagnostics (hop-based) RoutingDiagnosticEngine with hop count Skipped when hasHopCount === false Blocked until hop metric exists
Neighbor UI neighborInfo protobuf getNeighbours (repeaters) 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 Advert lat/lon + setAdvertLatLong Partial
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 getTelemetryenvironmentTelemetry 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; 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 (App 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 App 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 Same ChatComposer in ChatPanel and RoomsPanel App (shared)
Repeater CLI Not applicable Per-repeater expandable CLI in RepeatersPanel; prefix-token correlation, retry, flood/auto routing toggle (RepeaterCommandService); Flood Advert and Sync Clock buttons moved to Radio panel (Device Actions section); 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, hashtag presets, app_settings reapply on connect) 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; protocol-neutral with Meshtastic built-ins
Log analyzer LogPanelAnalyze (logAnalyzer.ts, protocol-aware) Same shared UI App (implemented)
Room servers (BBS) Not applicable Rooms tab: login/post/admin CLI; optional Remember password (app_settings); Auto-sync periodic re-login while radio connected (meshcoreRoomSyncScheduler.ts, useMeshcoreRuntime.ts); RF-only (not MQTT) 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: Guest read-only uses zero password bytes when the server guest password is empty (Continue read-only on the login overlay). Admin login uses the configured password. 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. Inbound SignedPlain pushes include a four-byte author prefix; the Rooms UI strips it. Posts appear in the Rooms tab (channel -2), not Chat channel pills.

Sync: Firmware only pushes new posts after login (no history backfill). 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: identity-scoped UI stores

Live Chat and Nodes read identity-scoped nodeStore / messageStore (keyed by identityId). Hook-local refs in useMeshcoreRuntime (nodesRef, pubkeys) remain for send/RPC until contact rebuild syncs. 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.

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 in 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 App → 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 App settings (fallback to keyless @[Name] emoji when the picker emoji is not in the Open index table). See meshcoreOpenReaction.ts.

MeshCore Open compatibility (App 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 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 MQTT :1883, Liam's, and Custom preset buttons. They populate MQTTSettings used by mqtt-manager.ts.

Preset Broker host Port Notes
MQTT :1883 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=base64 for MQTT-only channels (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.ts uses last RF myNodeNum when 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.

MeshCore MQTT network presets

In MeshCore mode only, ConnectionPanel.tsx shows LetsMesh, Ripple Networks, Colorado Mesh, and Custom preset buttons. They populate the same MQTTSettings the main process uses for meshcore-mqtt-adapter.ts (with mqttTransportProtocol: 'meshcore').

Preset Broker host Port Notes
LetsMesh mqtt-us-v1.letsmesh.net or mqtt-eu-v1.letsmesh.net 443 WebSocket (wss). JWT auth; see Authentication below. Optional Packet logger publishes to meshcore/packets. See letsmesh-mqtt-auth.md.
Ripple Networks mqtt.ripplenetworks.com.au 8883 TLS; preset fills default shared credentials and insecure TLS for self-signed / non–public CA chains
Colorado Mesh meshcore_mqtt.coloradomesh.org 8883 / 443 TLS; JWT auth with custom audience mapping (coloradomesh in token generation). Same topic prefix meshcore.
Custom (user) ; No automatic changes; use for private brokers

Topic prefix is set to meshcore for both public presets; users can still edit fields after choosing a preset.

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

LetsMesh JWT authentication

LetsMesh uses WebSocket (wss) with JWT authentication. The implementation matches meshcore-mqtt-broker:

  • MQTT username: v1_<64-hex public key> (uppercase hex)
  • MQTT password: A token from @michaelhart/meshcore-decoder createAuthToken with:
  • publicKey: 64-character hex public key
  • iat: Issued-at timestamp
  • exp: Expiration timestamp
  • JWT aud (audience): The regional broker hostname (same as the Server field)

The JWT audience must match the regional broker hostname (mqtt-us-v1.letsmesh.net or mqtt-eu-v1.letsmesh.net).

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).

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.