This document describes the Simple Management Protocol (SMP) implementation used by MCUManager for communicating with devices over Bluetooth Low Energy.
- Overview
- SMP Protocol Structure
- Message Format
- Management Groups
- Image Management Group
- OS Management Group
- CBOR Encoding
- MCUboot Image Format
- Bluetooth Transport
- External References
MCU Manager uses the Simple Management Protocol (SMP) for device management operations. SMP is a binary protocol that uses CBOR (Concise Binary Object Representation) for efficient encoding of structured data.
Key Characteristics:
- Binary protocol with 8-byte header + CBOR payload
- Request/response model
- Organized into management groups (OS, Image, Stats, etc.)
- Transport-agnostic (this implementation uses Bluetooth LE)
- Sequence numbers for matching requests and responses
Every SMP message consists of an 8-byte header followed by a CBOR-encoded payload:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Op Code | Flags | Length (16-bit) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Group ID (16-bit) | Sequence Num | Command ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| CBOR Payload (variable) |
| ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Header Fields:
| Byte | Field | Description |
|---|---|---|
| 0 | Op Code | Operation type (read, write, etc.) |
| 1 | Flags | Reserved for future use (currently 0) |
| 2-3 | Length | Payload length in bytes (big-endian) |
| 4-5 | Group ID | Management group identifier (big-endian) |
| 6 | Sequence | Sequence number (0-255, wraps around) |
| 7 | Command ID | Command within the group |
| 8+ | Payload | CBOR-encoded data |
MGMT_OP_READ = 0 // Request to read data
MGMT_OP_READ_RSP = 1 // Response to read request
MGMT_OP_WRITE = 2 // Request to write/modify data
MGMT_OP_WRITE_RSP = 3 // Response to write requestUsage:
- Use
MGMT_OP_READto query device state (e.g., get image list) - Use
MGMT_OP_WRITEto modify device state (e.g., upload image, reset device) - Responses use the corresponding
_RSPoperation code
Sequence numbers (0-255) help match responses to requests:
- Incremented by sender for each new request
- Wraps to 0 after 255
- Receiver echoes the sequence number in the response
- Not used for matching in this implementation (single request at a time)
SMP organizes commands into functional groups. Each group has its own set of commands.
MGMT_GROUP_ID_OS = 0 // Operating system commands
MGMT_GROUP_ID_IMAGE = 1 // Image/firmware management
MGMT_GROUP_ID_STAT = 2 // Statistics
MGMT_GROUP_ID_CONFIG = 3 // Runtime configuration
MGMT_GROUP_ID_LOG = 4 // Log management
MGMT_GROUP_ID_CRASH = 5 // Crash dump management
MGMT_GROUP_ID_SPLIT = 6 // Split image management
MGMT_GROUP_ID_RUN = 7 // Runtime information
MGMT_GROUP_ID_FS = 8 // File system operations
MGMT_GROUP_ID_SHELL = 9 // Shell command executionImplementation Status:
- Fully implemented: OS (group 0), Image (group 1)
- Not implemented: Other groups (can be added as needed)
Group ID: 1 (MGMT_GROUP_ID_IMAGE)
Commands for managing firmware images on the device.
IMG_MGMT_ID_STATE = 0 // Get/set image state
IMG_MGMT_ID_UPLOAD = 1 // Upload image data
IMG_MGMT_ID_FILE = 2 // File operations (not implemented)
IMG_MGMT_ID_CORELIST = 3 // Core dump list (not implemented)
IMG_MGMT_ID_CORELOAD = 4 // Load core dump (not implemented)
IMG_MGMT_ID_ERASE = 5 // Erase secondary slotRead Request:
Op: MGMT_OP_READ (0)
Group: MGMT_GROUP_ID_IMAGE (1)
ID: IMG_MGMT_ID_STATE (0)
Payload: {} (empty or omitted)
Response:
{
"images": [
{
"slot": 0, // Slot number
"version": "1.0.0", // Firmware version string
"hash": <bytes>, // SHA-256 hash (32 bytes)
"bootable": true, // Image is bootable
"pending": false, // Pending swap
"confirmed": true, // Permanently confirmed
"active": true, // Currently running
"permanent": false // Permanent flag
},
// Additional slots...
],
"splitStatus": 0 // Split image status (optional)
}Write Request (Test Image):
{
"hash": <Uint8Array>, // Image hash to test
"confirm": false // false = test, true = confirm
}Write Request (Confirm Image):
{
"hash": <Uint8Array>, // Image hash to confirm
"confirm": true
}Uploads firmware image in chunks.
Request (First Chunk):
{
"data": <Uint8Array>, // Image chunk data
"len": 123456, // Total image size (first chunk only)
"off": 0, // Offset in image (starts at 0)
"sha": <Uint8Array> // SHA-256 of complete image (first chunk only)
}Request (Subsequent Chunks):
{
"data": <Uint8Array>, // Image chunk data
"off": 512 // Current offset
}Response:
{
"rc": 0, // Return code (0 = success)
"off": 512 // Next expected offset
}Upload Process:
- Client sends first chunk with
len,sha,off=0, anddata - Device responds with next expected
off - Client sends subsequent chunks with
offanddata - Repeat until
off >= len - Device validates SHA-256 hash
Chunk Size Calculation:
// Maximum chunk size based on MTU
const nmpOverhead = 8; // SMP header
const maxChunkSize = MTU - CBOR_encode(message).length - nmpOverhead;Erases the secondary image slot.
Request:
{} // Empty object or omittedResponse:
{
"rc": 0 // Return code
}Note: This operation can take 500ms or more on some devices. The implementation uses a 500ms timeout to prevent unnecessary retries.
Group ID: 0 (MGMT_GROUP_ID_OS)
Commands for operating system control and diagnostics.
OS_MGMT_ID_ECHO = 0 // Echo test
OS_MGMT_ID_CONS_ECHO_CTRL = 1 // Console echo control (not implemented)
OS_MGMT_ID_TASKSTAT = 2 // Task statistics (not implemented)
OS_MGMT_ID_MPSTAT = 3 // Memory pool stats (not implemented)
OS_MGMT_ID_DATETIME_STR = 4 // Date/time string (not implemented)
OS_MGMT_ID_RESET = 5 // Reset deviceTests SMP communication by echoing back a message.
Request:
{
"d": "Hello, device!" // Message to echo
}Response:
{
"r": "Hello, device!" // Echoed message
}Resets the device.
Request:
{} // Empty object or omittedResponse:
{
"rc": 0 // Return code
}Note: Device will disconnect shortly after sending response.
SMP uses CBOR (RFC 7049) for efficient binary encoding of structured data.
Advantages:
- Compact binary representation
- Self-describing format
- Supports common data types (integers, strings, arrays, maps, byte strings)
- Efficient for embedded systems
Implementation:
This project uses the cbor.js library (by Patrick Gansterer, MIT license) for encoding and decoding.
Example:
// Encode JavaScript object to CBOR bytes
const data = { off: 512, data: new Uint8Array([1, 2, 3]) };
const encoded = CBOR.encode(data);
// Decode CBOR bytes to JavaScript object
const decoded = CBOR.decode(encoded.buffer);MCU Manager expects firmware images in the MCUboot format. MCUboot is a secure bootloader for 32-bit microcontrollers.
+-------------------+
| MCUboot Header | 32 bytes minimum
+-------------------+
| Application Code | Variable size
+-------------------+
| TLV Area | Variable size (metadata)
+-------------------+
| Offset | Size | Field | Value | Description |
|---|---|---|---|---|
| 0 | 4 | Magic | 0x96f3b83d | MCUboot magic number (little-endian) |
| 4 | 4 | Load Address | 0x00000000 | Base address for loading |
| 8 | 2 | Header Size | Variable | Size of this header |
| 10 | 2 | Protected TLV | 0x0000 | Must be 0 |
| 12 | 4 | Image Size | Variable | Size of application code (excludes header) |
| 16 | 4 | Flags | 0x00000000 | Must be 0 |
| 20 | 1 | Version Major | 0-255 | Major version number |
| 21 | 1 | Version Minor | 0-255 | Minor version number |
| 22 | 2 | Version Revision | 0-65535 | Revision number (little-endian) |
| 24 | 4 | Build Number | Variable | Build number (not parsed) |
| 28+ | Variable | Padding | 0x00 | Padding to header size |
The imageInfo() method validates these fields:
// Check magic bytes (little-endian 0x96f3b83d)
if (view[0] !== 0x3d || view[1] !== 0xb8 ||
view[2] !== 0xf3 || view[3] !== 0x96) {
throw new Error('Invalid image (wrong magic bytes)');
}
// Check load address is 0x00000000
if (view[4] !== 0x00 || view[5] !== 0x00 ||
view[6] !== 0x00 || view[7] !== 0x00) {
throw new Error('Invalid image (wrong load address)');
}
// Check protected TLV area size is 0
if (view[10] !== 0x00 || view[11] !== 0x00) {
throw new Error('Invalid image (wrong protected TLV area size)');
}
// Check flags is 0x00000000
if (view[16] !== 0x00 || view[17] !== 0x00 ||
view[18] !== 0x00 || view[19] !== 0x00) {
throw new Error('Invalid image (wrong flags)');
}const major = view[20];
const minor = view[21];
const revision = view[22] + view[23] * 256; // little-endian
const version = `${major}.${minor}.${revision}`;The hash includes only the header and image data (not the TLV area):
const headerSize = view[8] + view[9] * 256;
const imageSize = view[12] + view[13] * 256 +
view[14] * 65536 + view[15] * 16777216;
const hashData = image.slice(0, imageSize + headerSize);
const hash = await crypto.subtle.digest('SHA-256', hashData);MCU Manager uses standard UUIDs for the SMP service:
SERVICE_UUID = '8d53dc1d-1db7-4cd3-868b-8a527460aa84'
CHARACTERISTIC_UUID = 'da2e7828-fbce-4e01-ae9e-261174997c48'Connection:
- User selects device via
navigator.bluetooth.requestDevice() - Connect to GATT server:
device.gatt.connect() - Get SMP service:
server.getPrimaryService(SERVICE_UUID) - Get SMP characteristic:
service.getCharacteristic(CHARACTERISTIC_UUID) - Enable notifications:
characteristic.startNotifications()
Sending Commands:
- Construct SMP header (8 bytes)
- Encode payload with CBOR
- Concatenate header + payload
- Write to characteristic:
characteristic.writeValueWithoutResponse(data)
Receiving Responses:
- Listen for
characteristicvaluechangedevents - Accumulate data in buffer (responses may be fragmented)
- Check if complete message received (based on Length field)
- Decode CBOR payload
- Process response
Default MTU: 23 bytes (Bluetooth LE minimum)
- Usable payload: 20 bytes (23 - 3 byte ATT overhead)
- Too small for efficient firmware upload
Negotiated MTU: 400+ bytes (typical for modern devices)
- Allows larger chunks
- Improves upload speed
- Automatically negotiated by browser
This implementation assumes: MTU of 400 bytes (configurable via _mtu property)
Automatic Reconnection:
- On unexpected disconnect, library automatically reconnects after 1 second (configurable)
- Firmware upload resumes from last acknowledged offset
- User-initiated disconnects do not trigger reconnection
Benefits:
- Resilient to temporary connection issues
- Transparent to user during long uploads
- No data loss on reconnection
All SMP responses include an rc (return code) field indicating success or failure.
| Code | Name | Description |
|---|---|---|
| 0 | MGMT_ERR_EOK | Success |
| 1 | MGMT_ERR_EUNKNOWN | Unknown error |
| 2 | MGMT_ERR_ENOMEM | Out of memory |
| 3 | MGMT_ERR_EINVAL | Invalid value/parameter |
| 4 | MGMT_ERR_ETIMEOUT | Operation timed out |
| 5 | MGMT_ERR_ENOENT | No such entry/file |
| 6 | MGMT_ERR_EBADSTATE | Bad state for operation |
| 7 | MGMT_ERR_EMSGSIZE | Response too large |
| 8 | MGMT_ERR_ENOTSUP | Operation not supported |
| 9 | MGMT_ERR_ECORRUPT | Data corruption detected |
| 10 | MGMT_ERR_EBUSY | Resource busy |
| 11 | MGMT_ERR_EACCESSDENIED | Access denied |
| 12 | MGMT_ERR_UNSUPPORTED_TOO_OLD | Unsupported format (too old) |
| 13 | MGMT_ERR_UNSUPPORTED_TOO_NEW | Unsupported format (too new) |
NRF52 Quirk: Some NRF52.4 devices return rc: undefined instead of rc: 0 for successful operations.
Handling:
if (data.rc === 0 || data.rc === undefined) {
// Success
} else {
// Error
}- MCUboot Documentation: https://www.mcuboot.com/
- MCUboot GitHub: https://github.com/mcu-tools/mcuboot
- Apache Mynewt: https://mynewt.apache.org/
- Mynewt Newtmgr (CLI tool): https://mynewt.apache.org/latest/newtmgr/index.html
- MCUboot Image Format: https://interrupt.memfault.com/blog/mcuboot-overview#mcuboot-image-binaries
- CBOR Specification (RFC 7049): https://tools.ietf.org/html/rfc7049
- Web Bluetooth API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
- Web Bluetooth Specification: https://webbluetoothcg.github.io/web-bluetooth/
- mcumgr CLI: https://github.com/apache/mynewt-mcumgr-cli (Go-based CLI tool)
- nRF Connect: https://www.nordicsemi.com/Products/Development-tools/nrf-connect-for-desktop (Nordic's device management tool)
- MCUmgr Android: https://github.com/JuulLabs-OSS/mcumgr-android (Android library)
- MCUmgr iOS: https://github.com/JuulLabs-OSS/mcumgr-ios (iOS library)
The SMP protocol is defined in the Mynewt/Zephyr projects:
- Zephyr SMP Server: https://docs.zephyrproject.org/latest/services/device_mgmt/smp_protocol.html
- Mynewt Image Manager: https://github.com/apache/mynewt-core/tree/master/mgmt/imgmgr
For testing, you'll need firmware built with MCUboot support:
- Mynewt Examples: https://github.com/apache/mynewt-core/tree/master/apps
- Zephyr SMP Sample: https://github.com/zephyrproject-rtos/zephyr/tree/main/samples/subsys/mgmt/mcumgr
This implementation focuses on firmware update use cases (Image and OS groups). The SMP protocol supports additional groups that could be implemented:
- Statistics (group 2): Query device statistics
- Configuration (group 3): Runtime configuration
- Logging (group 4): Remote log access
- File System (group 8): File upload/download
- Shell (group 9): Remote shell commands
See CONTRIBUTING.md for information on adding new protocol features.