26 KiB
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 useappName(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 timestamppubKeyPrefix(6 bytes): First 6 bytes of recipient's public keytext(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 typechannelIdx(uint8): Channel index (0-255)timestamp(uint32 LE): Unix timestamptext(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:
ContactsStart(with count)- Multiple
Contactresponses 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 typeFlood: Broadcast to entire networkZeroHop: 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 keytype(uint8): Contact type (0=none, 1=chat, 2=repeater, 3=room)flags(uint8): Contact flagsoutPathLen(int8): Length of routing pathoutPath(64 bytes): Routing path (padded)advName(32 bytes): Name as C-stringlastAdvert(uint32 LE): Last advertisement timestampadvLat(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 contactChannelMsgRecv- Message from channelNoMoreMessages- 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 × 10000lon(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 pathpath(N bytes): Routing pathrawData(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 keypassword(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 contactrequestCodeAndParams(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 indexname(32 bytes): Channel name as C-stringsecret(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 responseauth(uint32 LE): Authentication code (usually 0)flags(uint8): Trace flagspath(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 returnoffset(uint16 LE): Pagination offsetorderBy(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
estTimeoutfromSentresponse - 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
- meshcore.js Connection class
- MeshCore Firmware
- MESHCORE_PROTOCOL.md - Mesh packet protocol
- MESHCORE_QUICK_REFERENCE.md - Quick reference card
Document Version: 1.0 Last Updated: 2025-10-14 Compatible with: MeshCore firmware v1.9.0+, meshcore.js v1.x