Files
meshcore-sar_android/MESHCORE_BLE_PROTOCOL.md
2026-02-28 10:11:33 +01:00

26 KiB
Raw Blame History

MeshCore BLE Protocol Specification

Complete BLE command/response protocol extracted from meshcore.js Connection class.

Overview

The MeshCore BLE protocol provides a command/response interface for smartphone applications to interact with MeshCore devices over Bluetooth Low Energy. This is a separate protocol from the mesh packet protocol used for LoRa radio communication.

Protocol Characteristics:

  • Uses Nordic UART Service (NUS) profile
  • Simple command/response model
  • App acts as BLE central, device acts as peripheral
  • Commands sent over RX characteristic
  • Responses/events received over TX characteristic
  • Supports asynchronous push notifications

Service Specification

Service UUID: 6E400001-B5A3-F393-E0A9-E50E24DCCA9E

Characteristic UUID Properties Direction Description
RX 6E400002-B5A3-F393-E0A9-E50E24DCCA9E Write, Write Without Response App → Device Commands
TX 6E400003-B5A3-F393-E0A9-E50E24DCCA9E Notify Device → App Responses & Events

Protocol Structure

Command Frame Format

[Command Code: 1B] [Parameters...]

Response Frame Format

[Response Code: 1B] [Data...]

Push Notification Format

[Push Code: 1B] [Data...]

Command Codes

Commands sent from app to device over RX characteristic:

Code Name Description
- AppStart Initialize connection, get device info
- SendTxtMsg Send text message to contact
- SendChannelTxtMsg Send text message to channel
- GetContacts Request list of contacts
- GetDeviceTime Get device's current time
- SetDeviceTime Set device's current time
- SendSelfAdvert Broadcast advertisement
- SetAdvertName Set device's advertised name
- AddUpdateContact Add or update contact details
- SyncNextMessage Retrieve next queued message
- SetRadioParams Configure LoRa radio parameters
- SetTxPower Set transmit power
- ResetPath Reset routing path for contact
- SetAdvertLatLon Set device's GPS coordinates
- RemoveContact Delete contact from device
- ShareContact Send contact to mesh network
- ExportContact Export contact as packet bytes
- ImportContact Import contact from packet bytes
- Reboot Reboot device
- GetBatteryVoltage Read battery voltage
- DeviceQuery Query device firmware info
- ExportPrivateKey Export device's private key
- ImportPrivateKey Import private key to device
- SendRawData Send raw mesh packet
- SendLogin Login to repeater/room
- SendStatusReq Request repeater status
- SendTelemetryReq Request telemetry from contact
- SendBinaryReq Send binary request
- GetChannel Get channel configuration
- SetChannel Set channel configuration
- SignStart Start signing data
- SignData Send data chunk to sign
- SignFinish Finish signing, get signature
- SendTracePath Trace network path
- SetOtherParams Set miscellaneous parameters

Response Codes

Responses sent from device to app over TX characteristic:

Code Name Description
- Ok Command succeeded
- Err Command failed
- SelfInfo Device information
- CurrTime Current device time
- NoMoreMessages Message queue empty
- ContactMsgRecv Contact message received
- ChannelMsgRecv Channel message received
- ContactsStart Start of contacts list
- Contact Contact entry
- EndOfContacts End of contacts list
- Sent Message sent to mesh
- ExportContact Exported contact data
- BatteryVoltage Battery voltage reading
- DeviceInfo Firmware version info
- PrivateKey Exported private key
- Disabled Feature disabled
- ChannelInfo Channel configuration
- SignStart Signing session started
- Signature Ed25519 signature

Push Codes

Asynchronous events pushed from device to app:

Code Name Description
- Advert Advertisement broadcast
- PathUpdated Routing path updated
- SendConfirmed Message ACK received
- MsgWaiting Messages queued
- RawData Raw packet received
- LoginSuccess Login succeeded
- StatusResponse Status data received
- LogRxData Raw RX data (debug)
- TelemetryResponse Telemetry data received
- TraceData Path trace completed
- NewAdvert New contact advertised
- BinaryResponse Binary request response

Command Details

AppStart

Initialize BLE connection and retrieve device information.

Format:

[AppStart] [1B: appVer] [6B: reserved] [string: appName]

Parameters:

  • appVer (uint8): App protocol version (e.g., 1)
  • reserved (6 bytes): Reserved for future use
  • appName (string): Application name (null-terminated)

Response: SelfInfo

Example:

await connection.sendCommandAppStart();
// Sends: [CMD][0x01][00 00 00 00 00 00]["test\0"]

SendTxtMsg

Send a text message to a contact.

Format:

[SendTxtMsg] [1B: txtType] [1B: attempt] [4B: timestamp] [6B: pubKeyPrefix] [string: text]

Parameters:

  • txtType (uint8): Message type (0=Plain, 1=Emergency, etc.)
  • attempt (uint8): Retry attempt number (usually 0)
  • timestamp (uint32 LE): Unix timestamp
  • pubKeyPrefix (6 bytes): First 6 bytes of recipient's public key
  • text (string): UTF-8 message text (null-terminated)

Response: Sent

Example:

const txtType = 0;  // Plain
const attempt = 0;
const timestamp = Math.floor(Date.now() / 1000);
await connection.sendCommandSendTxtMsg(txtType, attempt, timestamp, contactPublicKey, "Hello!");

SendChannelTxtMsg

Send a text message to a group channel.

Format:

[SendChannelTxtMsg] [1B: txtType] [1B: channelIdx] [4B: timestamp] [string: text]

Parameters:

  • txtType (uint8): Message type
  • channelIdx (uint8): Channel index (0-255)
  • timestamp (uint32 LE): Unix timestamp
  • text (string): UTF-8 message text

Response: Ok or Err


GetContacts

Request list of all contacts from device.

Format:

[GetContacts] [4B: since]?

Parameters:

  • since (uint32 LE, optional): Only return contacts modified after this timestamp

Response Sequence:

  1. ContactsStart (with count)
  2. Multiple Contact responses
  3. EndOfContacts

Example:

const contacts = await connection.getContacts();
// Returns array of contact objects

GetDeviceTime

Get current time from device.

Format:

[GetDeviceTime]

Response: CurrTime

Response Format:

[CurrTime] [4B: epochSecs]

SetDeviceTime

Set device's current time.

Format:

[SetDeviceTime] [4B: epochSecs]

Parameters:

  • epochSecs (uint32 LE): Unix timestamp

Response: Ok or Err

Example:

await connection.syncDeviceTime();  // Sets to current system time

SendSelfAdvert

Broadcast advertisement to mesh network.

Format:

[SendSelfAdvert] [1B: type]

Parameters:

  • type (uint8): Advertisement type
    • Flood: Broadcast to entire network
    • ZeroHop: Only adjacent nodes

Response: Ok or Err

Push: Advert (when broadcast begins)

Example:

await connection.sendFloodAdvert();    // Network-wide
await connection.sendZeroHopAdvert();  // Adjacent only

SetAdvertName

Set device's advertised name.

Format:

[SetAdvertName] [string: name]

Parameters:

  • name (string): Display name (null-terminated, max 31 chars)

Response: Ok or Err

Example:

await connection.setAdvertName("Alice");

AddUpdateContact

Add new contact or update existing contact.

Format:

[AddUpdateContact] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen]
                   [64B: outPath] [32B: advName] [4B: lastAdvert]
                   [4B: advLat] [4B: advLon]

Parameters:

  • publicKey (32 bytes): Ed25519 public key
  • type (uint8): Contact type (0=none, 1=chat, 2=repeater, 3=room)
  • flags (uint8): Contact flags
  • outPathLen (int8): Length of routing path
  • outPath (64 bytes): Routing path (padded)
  • advName (32 bytes): Name as C-string
  • lastAdvert (uint32 LE): Last advertisement timestamp
  • advLat (uint32 LE): GPS latitude (×10000)
  • advLon (uint32 LE): GPS longitude (×10000)

Response: Ok or Err


SyncNextMessage

Retrieve next message from device's queue.

Format:

[SyncNextMessage]

Response: One of:

  • ContactMsgRecv - Message from contact
  • ChannelMsgRecv - Message from channel
  • NoMoreMessages - Queue empty

Example:

while (true) {
    const msg = await connection.syncNextMessage();
    if (!msg) break;  // No more messages
    console.log(msg);
}

SetRadioParams

Configure LoRa radio parameters.

Format:

[SetRadioParams] [4B: radioFreq] [4B: radioBw] [1B: radioSf] [1B: radioCr]

Parameters:

  • radioFreq (uint32 LE): Frequency in Hz (e.g., 915000000)
  • radioBw (uint32 LE): Bandwidth in Hz (e.g., 125000)
  • radioSf (uint8): Spreading factor (7-12)
  • radioCr (uint8): Coding rate (5-8)

Response: Ok or Err

Example:

await connection.setRadioParams(
    915000000,  // 915 MHz
    125000,     // 125 kHz bandwidth
    7,          // SF7
    5           // CR 4/5
);

SetTxPower

Set transmit power level.

Format:

[SetTxPower] [1B: txPower]

Parameters:

  • txPower (uint8): Power level in dBm (device-specific range)

Response: Ok or Err


ResetPath

Clear routing path for a contact (force rediscovery).

Format:

[ResetPath] [32B: pubKey]

Parameters:

  • pubKey (32 bytes): Contact's public key

Response: Ok or Err


SetAdvertLatLon

Set device's GPS coordinates for advertisements.

Format:

[SetAdvertLatLon] [4B: lat] [4B: lon]

Parameters:

  • lat (int32 LE): Latitude × 10000
  • lon (int32 LE): Longitude × 10000

Response: Ok or Err

Example:

// Set location to 46.0569°N, 14.5058°E
await connection.setAdvertLatLong(460569, 145058);

RemoveContact

Delete contact from device.

Format:

[RemoveContact] [32B: pubKey]

Parameters:

  • pubKey (32 bytes): Contact's public key

Response: Ok or Err


ShareContact

Broadcast contact to mesh network.

Format:

[ShareContact] [32B: pubKey]

Parameters:

  • pubKey (32 bytes): Contact to share

Response: Ok or Err


ExportContact

Export contact as advertisement packet bytes.

Format:

[ExportContact] [32B: pubKey]?

Parameters:

  • pubKey (32 bytes, optional): Contact to export, or omit for self

Response: ExportContact

Response Format:

[ExportContact] [N bytes: advertPacketBytes]

ImportContact

Import contact from advertisement packet bytes.

Format:

[ImportContact] [N bytes: advertPacketBytes]

Parameters:

  • advertPacketBytes: Complete advertisement packet

Response: Ok or Err


Reboot

Reboot the device.

Format:

[Reboot] [string: "reboot"]

Response: None (device reboots)

Example:

await connection.reboot();
// Device will disconnect and reboot

GetBatteryVoltage

Read device's battery voltage.

Format:

[GetBatteryVoltage]

Response: BatteryVoltage

Response Format:

[BatteryVoltage] [2B: batteryMilliVolts]

Example:

const { batteryMilliVolts } = await connection.getBatteryVoltage();
console.log(`Battery: ${batteryMilliVolts / 1000}V`);

DeviceQuery

Query device firmware information.

Format:

[DeviceQuery] [1B: appTargetVer]

Parameters:

  • appTargetVer (uint8): Protocol version app expects (e.g., 1)

Response: DeviceInfo

Response Format:

[DeviceInfo] [1B: firmwareVer] [6B: reserved] [12B: buildDate] [string: model]

Example:

const info = await connection.deviceQuery(1);
console.log(`Firmware v${info.firmwareVer}, ${info.manufacturerModel}`);

ExportPrivateKey

Export device's Ed25519 private key.

Format:

[ExportPrivateKey]

Response: PrivateKey or Disabled

Response Format (PrivateKey):

[PrivateKey] [64B: privateKey]

Security Note: May be disabled in firmware for security.


ImportPrivateKey

Import Ed25519 private key to device.

Format:

[ImportPrivateKey] [64B: privateKey]

Parameters:

  • privateKey (64 bytes): Ed25519 private key

Response: Ok, Err, or Disabled

Security Note: May be disabled in firmware.


SendRawData

Send raw custom data through mesh.

Format:

[SendRawData] [1B: pathLen] [N bytes: path] [M bytes: rawData]

Parameters:

  • pathLen (uint8): Length of routing path
  • path (N bytes): Routing path
  • rawData (M bytes): Custom payload

Response: Ok or Err


SendLogin

Authenticate with repeater or room server.

Format:

[SendLogin] [32B: publicKey] [string: password]

Parameters:

  • publicKey (32 bytes): Server's public key
  • password (string): Login password (max 15 chars)

Response: Sent

Push: LoginSuccess (when authenticated)

Example:

await connection.login(repeaterPublicKey, "mypassword");
// Waits for LoginSuccess push

SendStatusReq

Request status information from repeater.

Format:

[SendStatusReq] [32B: publicKey]

Parameters:

  • publicKey (32 bytes): Repeater's public key

Response: Sent

Push: StatusResponse (with repeater stats)

Status Response Format:

{
    batt_milli_volts: uint16,      // Battery voltage (mV)
    curr_tx_queue_len: uint16,     // Transmit queue length
    noise_floor: int16,             // Noise floor (dBm)
    last_rssi: int16,               // Last RSSI (dBm)
    n_packets_recv: uint32,         // Total packets received
    n_packets_sent: uint32,         // Total packets sent
    total_air_time_secs: uint32,    // Total air time (seconds)
    total_up_time_secs: uint32,     // Uptime (seconds)
    n_sent_flood: uint32,           // Flood packets sent
    n_sent_direct: uint32,          // Direct packets sent
    n_recv_flood: uint32,           // Flood packets received
    n_recv_direct: uint32,          // Direct packets received
    err_events: uint16,             // Error events
    last_snr: int16,                // Last SNR (×4)
    n_direct_dups: uint16,          // Duplicate direct packets
    n_flood_dups: uint16,           // Duplicate flood packets
}

SendTelemetryReq

Request telemetry data from contact.

Format:

[SendTelemetryReq] [3B: reserved] [32B: publicKey]

Parameters:

  • reserved (3 bytes): Reserved (set to 0)
  • publicKey (32 bytes): Contact's public key

Response: Sent

Push: TelemetryResponse (with Cayenne LPP data)

Example:

const telemetry = await connection.getTelemetry(contactPublicKey);
console.log(telemetry.lppSensorData);  // Parse with Cayenne LPP parser

SendBinaryReq

Send custom binary request to contact.

Format:

[SendBinaryReq] [32B: publicKey] [N bytes: requestCodeAndParams]

Parameters:

  • publicKey (32 bytes): Target contact
  • requestCodeAndParams (variable): Application-specific request

Response: Sent

Push: BinaryResponse (with tag and response data)

Binary Request Types:

  • GetNeighbours (0x00): Query repeater for neighbor list

GetChannel

Get channel configuration by index.

Format:

[GetChannel] [1B: channelIdx]

Parameters:

  • channelIdx (uint8): Channel index (0-N)

Response: ChannelInfo or Err

Response Format:

[ChannelInfo] [1B: idx] [32B: name] [16B: secret]

Example:

const channel = await connection.getChannel(0);
console.log(`Channel: ${channel.name}`);

SetChannel

Set channel configuration.

Format:

[SetChannel] [1B: channelIdx] [32B: name] [16B: secret]

Parameters:

  • channelIdx (uint8): Channel index
  • name (32 bytes): Channel name as C-string
  • secret (16 bytes): AES-128 shared key

Response: Ok or Err

Example:

// Delete channel
await connection.deleteChannel(0);
// Internally: setChannel(0, "", new Uint8Array(16))

SignStart

Start signing session.

Format:

[SignStart]

Response: SignStart

Response Format:

[SignStart] [1B: reserved] [4B: maxSignDataLen]

SignData

Send data chunk to sign.

Format:

[SignData] [N bytes: dataToSign]

Parameters:

  • dataToSign: Data chunk (max 128 bytes)

Response: Ok (send next chunk)


SignFinish

Finish signing and retrieve signature.

Format:

[SignFinish]

Response: Signature

Response Format:

[Signature] [64B: signature]

Example:

const signature = await connection.sign(data);
// Automatically handles chunking

SendTracePath

Trace path through mesh network.

Format:

[SendTracePath] [4B: tag] [4B: auth] [1B: flags] [N bytes: path]

Parameters:

  • tag (uint32 LE): Random tag for matching response
  • auth (uint32 LE): Authentication code (usually 0)
  • flags (uint8): Trace flags
  • path (variable): Routing path to trace

Response: Sent

Push: TraceData

Trace Data Format:

{
    reserved: uint8,
    pathLen: uint8,
    flags: uint8,
    tag: uint32,
    authCode: uint32,
    pathHashes: Uint8Array,  // Node IDs
    pathSnrs: Uint8Array,    // SNR at each hop
    lastSnr: float,          // Final SNR (÷4)
}

Example:

const trace = await connection.tracePath(path);
console.log(`Path length: ${trace.pathLen}`);
console.log(`SNRs: ${trace.pathSnrs}`);

SetOtherParams

Set miscellaneous device parameters.

Format:

[SetOtherParams] [1B: manualAddContacts]

Parameters:

  • manualAddContacts (uint8): 0=auto-add, 1=manual-add

Response: Ok or Err

Example:

await connection.setAutoAddContacts();    // Auto-add from advertisements
await connection.setManualAddContacts();  // Require manual addition

Binary Request Types

Sent via SendBinaryReq command:

GetNeighbours (0x00)

Query repeater for neighbor list.

Request Format:

[0x00] [1B: version] [1B: count] [2B: offset] [1B: orderBy] [1B: prefixLen] [4B: random]

Parameters:

  • version (uint8): Request version (0)
  • count (uint8): Max neighbors to return
  • offset (uint16 LE): Pagination offset
  • orderBy (uint8): Sort order
    • 0: Newest to oldest
    • 1: Oldest to newest
    • 2: Strongest to weakest (SNR)
    • 3: Weakest to strongest
  • prefixLen (uint8): Public key prefix length (1-32)
  • random (uint32 LE): Random blob for hash uniqueness

Response Format:

[2B: totalCount] [2B: resultsCount] [repeated: neighbor entries]

Neighbor Entry:

[N bytes: pubKeyPrefix] [4B: heardSecondsAgo] [1B: snr]

Example:

const result = await connection.getNeighbours(
    repeaterPublicKey,
    10,    // count
    0,     // offset
    2,     // order by strongest
    8      // 8-byte prefix
);
console.log(`Total neighbors: ${result.totalNeighboursCount}`);
result.neighbours.forEach(n => {
    console.log(`  ${n.publicKeyPrefix.toString('hex')} - SNR: ${n.snr} dB`);
});

Response Details

SelfInfo

Device information response.

Format:

[SelfInfo] [1B: type] [1B: txPower] [1B: maxTxPower] [32B: publicKey]
           [4B: advLat] [4B: advLon] [3B: reserved] [1B: manualAddContacts]
           [4B: radioFreq] [4B: radioBw] [1B: radioSf] [1B: radioCr] [string: name]

Fields:

{
    type: uint8,               // Device type
    txPower: uint8,            // Current TX power (dBm)
    maxTxPower: uint8,         // Maximum TX power
    publicKey: Uint8Array,     // 32-byte public key
    advLat: int32,             // GPS latitude (×10000)
    advLon: int32,             // GPS longitude (×10000)
    reserved: Uint8Array,      // 3 bytes reserved
    manualAddContacts: uint8,  // 0=auto, 1=manual
    radioFreq: uint32,         // Frequency (Hz)
    radioBw: uint32,           // Bandwidth (Hz)
    radioSf: uint8,            // Spreading factor
    radioCr: uint8,            // Coding rate
    name: string,              // Device name
}

Contact

Contact entry response.

Format:

[Contact] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen] [64B: outPath]
          [32B: advName] [4B: lastAdvert] [4B: advLat] [4B: advLon] [4B: lastMod]

Fields:

{
    publicKey: Uint8Array,     // 32-byte public key
    type: uint8,               // 0=none, 1=chat, 2=repeater, 3=room
    flags: uint8,              // Contact flags
    outPathLen: int8,          // Path length
    outPath: Uint8Array,       // 64-byte path (padded)
    advName: string,           // Name (32-byte C-string)
    lastAdvert: uint32,        // Last advertisement time
    advLat: uint32,            // GPS latitude (×10000)
    advLon: uint32,            // GPS longitude (×10000)
    lastMod: uint32,           // Last modification time
}

ContactMsgRecv

Contact message received.

Format:

[ContactMsgRecv] [6B: pubKeyPrefix] [1B: pathLen] [1B: txtType] [4B: senderTimestamp] [string: text]

Fields:

{
    pubKeyPrefix: Uint8Array,  // 6-byte sender public key prefix
    pathLen: uint8,            // Hop count (0xFF=direct)
    txtType: uint8,            // Message type
    senderTimestamp: uint32,   // Sender's timestamp
    text: string,              // Message text
}

ChannelMsgRecv

Channel message received.

Format:

[ChannelMsgRecv] [1B: channelIdx] [1B: pathLen] [1B: txtType] [4B: senderTimestamp] [string: text]

Fields:

{
    channelIdx: int8,          // Channel index (0=public)
    pathLen: uint8,            // Hop count (0xFF=direct)
    txtType: uint8,            // Message type
    senderTimestamp: uint32,   // Sender's timestamp
    text: string,              // Message text
}

Sent

Message sent to mesh network.

Format:

[Sent] [1B: result] [4B: expectedAckCrc] [4B: estTimeout]

Fields:

{
    result: int8,              // Send result code
    expectedAckCrc: uint32,    // CRC for ACK matching
    estTimeout: uint32,        // Estimated timeout (ms)
}

Push Notifications

PathUpdated

Routing path updated for contact.

Format:

[PathUpdated] [32B: publicKey]

SendConfirmed

Message ACK received from network.

Format:

[SendConfirmed] [4B: ackCode] [4B: roundTrip]

Fields:

{
    ackCode: uint32,           // ACK code (matches expectedAckCrc)
    roundTrip: uint32,         // Round-trip time (ms)
}

MsgWaiting

Messages queued on device.

Format:

[MsgWaiting]

Action: Call SyncNextMessage to retrieve.


NewAdvert

New contact advertised on network.

Format:

[NewAdvert] [32B: publicKey] [1B: type] [1B: flags] [1B: outPathLen] [64B: outPath]
            [32B: advName] [4B: lastAdvert] [4B: advLat] [4B: advLon] [4B: lastMod]

(Same structure as Contact response)


Error Codes

Err Response Format:

[Err] [1B: errCode]?

Error codes are application-specific. Check firmware documentation for specific codes.


Usage Patterns

Initialize Connection

// Called automatically on connect
await connection.onConnected();
// Sends: AppStart with protocol version

Send Message

const contact = await connection.findContactByName("Alice");
await connection.sendTextMessage(contact.publicKey, "Hello!");

Sync Messages

const messages = await connection.getWaitingMessages();
messages.forEach(msg => {
    if (msg.contactMessage) {
        console.log(`From contact: ${msg.contactMessage.text}`);
    } else if (msg.channelMessage) {
        console.log(`From channel: ${msg.channelMessage.text}`);
    }
});

Monitor Events

connection.on('NewAdvert', (data) => {
    console.log(`New contact: ${data.advName}`);
});

connection.on('SendConfirmed', (data) => {
    console.log(`Message confirmed in ${data.roundTrip}ms`);
});

Implementation Notes

Timeouts

Most commands have default timeouts. For requests expecting mesh responses:

  • Use estTimeout from Sent response
  • Add extra buffer time (e.g., +1000ms)
  • Implement exponential backoff for retries

Buffering

BLE packets may arrive fragmented:

  • Buffer incoming data until complete frame received
  • Use frame length headers when available
  • Implement packet boundary detection

Thread Safety

Connection is event-driven:

  • Use promises for request/response pattern
  • Remove event listeners after use
  • Handle concurrent requests carefully

Security

  • Verify signatures on received advertisements
  • Validate public keys before import
  • Sanitize user input (names, messages)
  • Rate limit commands to prevent DoS

References


Document Version: 1.0 Last Updated: 2025-10-14 Compatible with: MeshCore firmware v1.9.0+, meshcore.js v1.x