Skip to content

BLE protocol & data layer

This is a technical reference for the communication protocol and data architecture.

The app supports three transports, all sharing the same command/response protocol:

TransportMethodImplementation
Bluetooth LENordic UART Service (NUS) GATTflutter_blue_plus
USB SerialPacket-framed serialMeshCoreUsbManager
TCPPacket-framed socketMeshCoreTcpConnector
  • Service UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
  • RX Characteristic (write to device): 6e400002-b5a3-f393-e0a9-e50e24dcca9e
  • TX Characteristic (notify from device): 6e400003-b5a3-f393-e0a9-e50e24dcca9e

Raw Uint8List payloads are written directly to the RX characteristic. Writes use “write without response” if supported, falling back to “write with response”.

Both use a lightweight packet framing codec:

TX (host → device): [0x3C][len_lo][len_hi][payload...]
RX (device → host): [0x3E][len_lo][len_hi][payload...]
  • Frame start: 0x3C (<) for outgoing, 0x3E (>) for incoming
  • Length: 2-byte little-endian, payload only
  • Max payload: 172 bytes
  • TCP: tcpNoDelay: true (Nagle disabled), writes serialized to prevent interleaving
  • USB: 10ms post-write delay between frames
enum MeshCoreConnectionState {
disconnected,
scanning,
connecting,
connected,
disconnecting,
}
  1. Scan with known name prefixes (defined in MeshCoreUuids.deviceNamePrefixes):
    • MeshCore-
    • Whisper-
    • WisCore-
    • HT-
    • LowMesh_MC_
  2. Connect with 15-second timeout
  3. Request MTU 185 bytes (non-web only)
  4. Discover services and locate NUS
  5. Enable TX notifications (up to 3 attempts on native)
  6. Subscribe to TX characteristic for incoming frames
  7. Initial sync: device info query, time sync, channel sync

On unexpected disconnection, auto-reconnect with exponential backoff:

  • Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s…
  • Resets on successful connection
  • Disabled for manual disconnects
  • Not available for USB or TCP
ConstantValueDescription
Max frame size172 bytesBLE/USB/TCP payload limit
Public key size32 bytesEd25519 public key
Max path size64 bytesMaximum path data
Max name size32 bytesMaximum node name
Max text payload160 bytesFirmware MAX_TEXT_LEN
App protocol version3Sent in device query
Contact frame size148 bytesFixed-size contact record
CodeNameDescription
1CMD_APP_STARTAnnounce app connection
2CMD_SEND_TXT_MSGSend direct text message
3CMD_SEND_CHANNEL_TXT_MSGSend channel text message
4CMD_GET_CONTACTSRequest contact list
5CMD_GET_DEVICE_TIMEQuery device clock
6CMD_SET_DEVICE_TIMESet device clock
7CMD_SEND_SELF_ADVERTBroadcast own advertisement
8CMD_SET_ADVERT_NAMESet node name
9CMD_ADD_UPDATE_CONTACTAdd or update a contact
10CMD_SYNC_NEXT_MESSAGERequest next queued message
11CMD_SET_RADIO_PARAMSSet radio parameters
12CMD_SET_RADIO_TX_POWERSet TX power
13CMD_RESET_PATHReset contact path
14CMD_SET_ADVERT_LATLONSet advertised location
15CMD_REMOVE_CONTACTRemove a contact
16CMD_SHARE_CONTACTShare contact to mesh
17CMD_EXPORT_CONTACTExport contact as bytes
18CMD_IMPORT_CONTACTImport contact from bytes
19CMD_REBOOTReboot device
20CMD_GET_BATT_AND_STORAGEQuery battery and storage
22CMD_DEVICE_QUERYQuery device info
26CMD_SEND_LOGINLogin to repeater/room
27CMD_SEND_STATUS_REQRequest repeater status
30CMD_GET_CONTACT_BY_KEYGet contact by public key
31CMD_GET_CHANNELGet channel definition
32CMD_SET_CHANNELSet channel name and PSK
36CMD_SEND_TRACE_PATHRequest path trace
38CMD_SET_OTHER_PARAMSSet misc parameters
39CMD_GET_TELEMETRY_REQRequest sensor telemetry
40CMD_GET_CUSTOM_VARGet custom variables
41CMD_SET_CUSTOM_VARSet a custom variable
50CMD_SEND_BINARY_REQSend binary request
57CMD_SEND_ANON_REQSend anonymous request
58CMD_SET_AUTO_ADD_CONFIGSet auto-add configuration
59CMD_GET_AUTO_ADD_CONFIGGet auto-add configuration
CodeNameDescription
0RESP_CODE_OKGeneric success
1RESP_CODE_ERRGeneric error
2RESP_CODE_CONTACTS_STARTContact list begins
3RESP_CODE_CONTACTSingle contact data
4RESP_CODE_END_OF_CONTACTSContact list complete
5RESP_CODE_SELF_INFODevice self-info response
6RESP_CODE_SENTMessage transmitted; carries [1]=is_flood, [2–5]=ack_hash, [6–9]=estimated_timeout_ms
7RESP_CODE_CONTACT_MSG_RECVIncoming direct message (v2)
8RESP_CODE_CHANNEL_MSG_RECVIncoming channel message (v2)
10RESP_CODE_NO_MORE_MESSAGESNo more queued messages
11RESP_CODE_EXPORT_CONTACTExported contact data
9RESP_CODE_CURR_TIMECurrent device time
12RESP_CODE_BATT_AND_STORAGEBattery mV (uint16 LE) + storage used/total (uint32 LE each)
13RESP_CODE_DEVICE_INFOFirmware info
16RESP_CODE_CONTACT_MSG_RECV_V3Incoming direct message (v3)
17RESP_CODE_CHANNEL_MSG_RECV_V3Incoming channel message (v3)
18RESP_CODE_CHANNEL_INFOChannel definition
21RESP_CODE_CUSTOM_VARSCustom variables
25RESP_CODE_AUTO_ADD_CONFIGAuto-add flags
0x80PUSH_CODE_ADVERTKnown contact re-seen
0x81PUSH_CODE_PATH_UPDATEDBetter path found; carries the 32-byte public key of the updated contact
0x82PUSH_CODE_SEND_CONFIRMEDDelivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes)
0x83PUSH_CODE_MSG_WAITINGOffline messages queued
0x85PUSH_CODE_LOGIN_SUCCESSRepeater/room login succeeded
0x86PUSH_CODE_LOGIN_FAILRepeater/room login failed
0x87PUSH_CODE_STATUS_RESPONSERepeater status response
0x88PUSH_CODE_LOG_RX_DATARadio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet
0x89PUSH_CODE_TRACE_DATAPath trace result
0x8APUSH_CODE_NEW_ADVERTNew node discovered
0x8BPUSH_CODE_TELEMETRY_RESPONSESensor telemetry data
0x8CPUSH_CODE_BINARY_RESPONSEBinary data response

32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:

[0] = resp_code
[1–32] = public key (32 bytes)
[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
[34] = flags (bit 0 = favorite)
[35] = path_length
[36–99] = path (64 bytes)
[100–131] = name (32 bytes, null-padded)
[132–135] = timestamp (uint32 LE)
[136–139] = latitude (int32 LE, × 1e-6 degrees)
[140–143] = longitude (int32 LE, × 1e-6 degrees)
[144–147] = last_modified (uint32 LE)

Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.

Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.

Index (0–7), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.

UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.

All data is stored via SharedPreferences (JSON-serialized). No SQLite or other database.

DataStorage Key PatternScope
Contactscontacts<pubKey10>Per device identity
Messagesmessages_<pubKey10><contactKey>Per device + contact
Channel Messageschannel_messages_<pubKey10><index>Per device + channel
Channelschannels<pubKey10>Per device identity
Channel Orderchannel_order_<pubKey10>Per device identity
Contact Groupscontact_groups<pubKey10>Per device identity
Communitiescommunities_v1<pubKey10>Per device identity
Unread Countscontact_unread_count<pubKey10>Per device identity
Discovered Contactsdiscovered_contactsGlobal
App Settingsapp_settingsGlobal
Path Historypath_history_<contactKey>Per contact

Used by CMD_SET_AUTO_ADD_CONFIG (58) and RESP_CODE_AUTO_ADD_CONFIG (25):

BitFlagDescription
00x01Overwrite oldest contact when list is full
10x02Auto-add chat users
20x04Auto-add repeaters
30x08Auto-add room servers
40x10Auto-add sensors

Seen inside PUSH_CODE_LOG_RX_DATA raw packets:

CodeType
0x00REQ (request)
0x01RESPONSE
0x02TXTMSG (text message)
0x03ACK
0x04ADVERT
0x05GRPTXT (group/channel text)
0x06GRPDATA (group data)
0x07ANONREQ (anonymous request)
0x08PATH
0x09TRACE
0x0AMULTIPART
0x0BCONTROL
0x0FRAW_CUSTOM

Uses Flutter Provider with ChangeNotifier. The central state holder is MeshCoreConnector, which owns all in-memory collections and fires debounced (50ms) notifyListeners() to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.

  1. Raw frames arrive over BLE/USB/TCP
  2. First byte is parsed as response/push code
  3. Appropriate model factory (fromFrame()) parses the data
  4. In-memory collections are updated
  5. Storage stores are persisted (async)
  6. notifyListeners() triggers UI rebuilds
  7. Screens read current state via getters