MetaWear BLE Protocol Reference¶
Every byte-level detail needed to implement the MetaWear BLE serial protocol from scratch in any language.
BLE Transport Layer¶
All commands are written to a single GATT characteristic. All responses (notifications) arrive on a single notify characteristic.
Service UUID: 0x326a9000-85cb-9195-d9dd-464cfbbae75a
Command char UUID: 0x326a9001-85cb-9195-d9dd-464cfbbae75a (write without response, or with response for MACRO commands)
Notify char UUID: 0x326a9006-85cb-9195-d9dd-464cfbbae75a (subscribe for notifications)
Device Info service (standard BLE 0x180A):
Firmware revision: 0x2A26
Model number: 0x2A24
Hardware revision: 0x2A27
Manufacturer: 0x2A29
Serial number: 0x2A25
Packet Format¶
Every command and every notification follows the same two- or three-byte header:
Byte 0: module_id
Byte 1: register_id (bit 7 = 0x80 set means "READ" request/response)
Byte 2: data_id (only present for signals that have an ID, e.g. timer, logger entries)
Bytes 3+: payload
Macros are the only commands written with response; everything else uses write-without-response.
The READ modifier:
Module IDs¶
MBL_MW_MODULE_SWITCH = 0x01
MBL_MW_MODULE_LED = 0x02
MBL_MW_MODULE_ACCELEROMETER = 0x03
MBL_MW_MODULE_TEMPERATURE = 0x04
MBL_MW_MODULE_GPIO = 0x05
MBL_MW_MODULE_NEO_PIXEL = 0x06
MBL_MW_MODULE_IBEACON = 0x07
MBL_MW_MODULE_HAPTIC = 0x08
MBL_MW_MODULE_DATA_PROCESSOR = 0x09
MBL_MW_MODULE_EVENT = 0x0A
MBL_MW_MODULE_LOGGING = 0x0B
MBL_MW_MODULE_TIMER = 0x0C
MBL_MW_MODULE_I2C = 0x0D (Serial Passthrough: I2C + SPI)
MBL_MW_MODULE_MACRO = 0x0F
MBL_MW_MODULE_CONDUCTANCE = 0x10
MBL_MW_MODULE_SETTINGS = 0x11
MBL_MW_MODULE_BAROMETER = 0x12
MBL_MW_MODULE_GYRO = 0x13
MBL_MW_MODULE_AMBIENT_LIGHT = 0x14
MBL_MW_MODULE_MAGNETOMETER = 0x15
MBL_MW_MODULE_HUMIDITY = 0x16
MBL_MW_MODULE_COLOR_DETECTOR = 0x17
MBL_MW_MODULE_PROXIMITY = 0x18
MBL_MW_MODULE_SENSOR_FUSION = 0x19
MBL_MW_MODULE_DEBUG = 0xFE
Board Initialization Sequence¶
- Enable notifications on the notify characteristic.
- Read each Device Info GATT characteristic in order (firmware, model, hardware, manufacturer, serial).
- For each module in
MODULE_DISCOVERY_CMDSlist, send: The board responds with[module_id, 0x80, implementation_byte, revision_byte, ...]. - After all modules respond, call
init_*_module()for each present module. - Read logging time signal (module 0x0B, register 0x84) and set reference epoch.
Module discovery order:
SWITCH, LED, ACCELEROMETER, TEMPERATURE, GPIO, IBEACON, HAPTIC,
DATA_PROCESSOR, EVENT, LOGGING, TIMER, I2C, MACRO, SETTINGS,
BAROMETER, GYRO, AMBIENT_LIGHT, MAGNETOMETER, HUMIDITY,
SENSOR_FUSION, DEBUG
Model numbers (from module_number string in Device Info "model number" characteristic):
"0" -> MetaWear R
"1" -> MetaWear RG (or RPro if barometer + ambient light present)
"2" -> MetaWear C (or CPro if magnetometer present, or MetaEnv if humidity present)
"3" -> MetaHealth
"4" -> MetaTracker
"5" -> MetaMotion R (or RL if no ambient light)
"6" -> MetaMotion C
"8" -> MetaMotion S
Hardware revisions (from hardware_revision string, characteristic 0x2A27) shipped per model:
0.X form without the leading r; treat both forms as equivalent.
Module 0x01 — Switch¶
No register header file found in the public SDK. Module responds to info query only. The switch button state is a data signal; response is a 1-byte value (0=released, 1=pressed).
Module 0x02 — LED¶
Register opcodes:
Write LED Pattern¶
color is an enum: 0=Green, 1=Red, 2=Blue (from MblMwLedColor).
MblMwLedPattern (13 bytes, packed):
uint8_t high_intensity // 0..31
uint8_t low_intensity // 0..31
uint16_t rise_time_ms
uint16_t high_time_ms
uint16_t fall_time_ms
uint16_t pulse_duration_ms
uint16_t delay_time_ms // only if revision >= 1 (DELAYED_REVISION)
uint8_t repeat_count // 0xFF = indefinite (use 0xFF, not 0; 0 causes undefined behaviour on firmware)
Play / Pause / Stop¶
Play: [0x02, 0x01, 0x01]
Autoplay: [0x02, 0x01, 0x02]
Pause: [0x02, 0x01, 0x00]
Stop: [0x02, 0x02, 0x00]
Stop+Clear: [0x02, 0x02, 0x01]
Required sequence¶
Always send Stop+Clear before writing a new pattern. The firmware does not reset LED state on BLE reconnection; stale patterns from a previous session persist until explicitly cleared.
Module 0x03 — Accelerometer¶
Implementation Types¶
BMI160 Register Opcodes¶
POWER_MODE = 0x01
DATA_INTERRUPT_ENABLE = 0x02
DATA_CONFIG = 0x03
DATA_INTERRUPT = 0x04
DATA_INTERRUPT_CONFIG = 0x05
MOTION_INTERRUPT_ENABLE = 0x09
MOTION_CONFIG = 0x0A
MOTION_INTERRUPT = 0x0B
TAP_INTERRUPT_ENABLE = 0x0C
TAP_CONFIG = 0x0D
TAP_INTERRUPT = 0x0E
ORIENT_INTERRUPT_ENABLE = 0x0F
ORIENT_CONFIG = 0x10
ORIENT_INTERRUPT = 0x11
STEP_DETECTOR_INTERRUPT_EN= 0x17
STEP_DETECTOR_CONFIG = 0x18
STEP_DETECTOR_INTERRUPT = 0x19
STEP_COUNTER_DATA = 0x1A
STEP_COUNTER_RESET = 0x1B
PACKED_ACC_DATA = 0x1C
BMI270 Register Opcodes¶
POWER_MODE = 0x01
DATA_INTERRUPT_ENABLE = 0x02
DATA_CONFIG = 0x03
DATA_INTERRUPT = 0x04
PACKED_ACC_DATA = 0x05
FEATURE_ENABLE = 0x06
FEATURE_INTERRUPT_ENABLE = 0x07
FEATURE_CONFIG = 0x08
MOTION_INTERRUPT = 0x09
WRIST_INTERRUPT = 0x0A
STEP_COUNT_INTERRUPT = 0x0B
ACTIVITY_INTERRUPT = 0x0C
TEMP_INTERRUPT = 0x0D
TEMP_ENABLE = 0x0E
TEMP = 0x0F
OFFSET = 0x10
DOWNSAMPLING = 0x11
Commands¶
Start / Stop sampling:
Enable / Disable data stream (BMI160):
Write acceleration config (BMI160 / BMI270):
acc_conf_byte layout — BMI160:
bits 0-3: odr (MblMwAccBmi160Odr + 1, i.e. 1-indexed)
bits 4-6: bwp (2 = normal for ODR >= 12.5 Hz; must be 0 when acc_us is set)
bit 7: us (under-sampling: 1 for ODR < 12.5 Hz, 0 otherwise)
0.78125 Hz -> 0x81 12.5 Hz -> 0x25 200 Hz -> 0x29
1.5625 Hz -> 0x82 25 Hz -> 0x26 400 Hz -> 0x2A
3.125 Hz -> 0x83 50 Hz -> 0x27 800 Hz -> 0x2B
6.25 Hz -> 0x84 100 Hz -> 0x28 1600 Hz -> 0x2C
acc_conf_byte layout — BMI270 (different from BMI160):
bits 0-3: acc_odr (same 1-indexed codes as BMI160)
bits 4-6: acc_bwp (always 2 = normal averaging)
bit 7: acc_filter_perf (1 for ODR >= 12.5 Hz, 0 for ODR < 12.5 Hz)
0.78125 Hz -> 0x21 12.5 Hz -> 0xA5 200 Hz -> 0xA9
1.5625 Hz -> 0x22 25 Hz -> 0xA6 400 Hz -> 0xAA
3.125 Hz -> 0x23 50 Hz -> 0xA7 800 Hz -> 0xAB
6.25 Hz -> 0x24 100 Hz -> 0xA8 1600 Hz -> 0xAC
acc_range_byte (BMI160 FSR bitmasks):
+/-2g -> 0x03 scale = 16384 LSB/g
+/-4g -> 0x05 scale = 8192 LSB/g
+/-8g -> 0x08 scale = 4096 LSB/g
+/-16g -> 0x0C scale = 2048 LSB/g
acc_range_byte (BMI270 FSR bitmasks):
+/-2g -> 0x00 scale = 16384 LSB/g
+/-4g -> 0x01 scale = 8192 LSB/g
+/-8g -> 0x02 scale = 4096 LSB/g
+/-16g -> 0x03 scale = 2048 LSB/g
Enable / Disable motion interrupt (BMI160):
Enable / Disable tap detection (BMI160):
Enable single/double: [0x03, 0x0C, mask, 0x00]
mask bit 0 = double tap, mask bit 1 = single tap
Disable: [0x03, 0x0C, 0x00, 0x03]
Enable / Disable orientation detection (BMI160):
BMI160 Step detector enable/disable:
BMI270 Feature enable/disable (using FEATURE_ENABLE and FEATURE_INTERRUPT_ENABLE):
Step counter enable:
[0x03, 0x07, 0x02, 0x00] <- interrupt enable
[0x03, 0x06, 0x02, 0x00] <- feature enable
Step counter disable:
[0x03, 0x07, 0x00, 0x02]
[0x03, 0x06, 0x00, 0x02]
Step detector enable:
[0x03, 0x07, 0x80, 0x00]
[0x03, 0x06, 0x80, 0x00]
Step detector disable:
[0x03, 0x07, 0x00, 0x80]
[0x03, 0x06, 0x00, 0x80]
BMI270 Feature config (FEATURE_CONFIG = 0x08):
Feature indices used:axis_remap = 0
any_motion = 1
no_motion = 2
sig_motion = 3
step_counter_0..3 = 4..7
wrist_gesture = 8
wrist_wakeup = 9
Notification Headers (what the board sends back)¶
| Register | Description |
|---|---|
| [0x03, 0x04] | Accelerometer XYZ data (no ID byte) |
| [0x03, 0x05] | BMI270 Packed accelerometer data |
| [0x03, 0x0B] | BMI160 Any/Slow/No-motion interrupt |
| [0x03, 0x09] | BMI270 Motion interrupt |
| [0x03, 0x0E] | BMI160 Tap interrupt |
| [0x03, 0x11] | BMI160 Orientation interrupt |
| [0x03, 0x19] | BMI160 Step detector |
| [0x03, 0x1C] | BMI160 Packed accelerometer data |
| [0x03, 0x0B] | BMI270 Step count interrupt |
| [0x03, 0x0A] | BMI270 Wrist gesture interrupt |
| [0x03, 0x0C] | BMI270 Activity interrupt |
Response Parsing¶
Accelerometer XYZ data (6 bytes after header):
val x = (response[2] or (response[3].toInt() shl 8)).toShort() / scale
val y = (response[4] or (response[5].toInt() shl 8)).toShort() / scale
val z = (response[6] or (response[7].toInt() shl 8)).toShort() / scale
// 'scale' from FSR lookup table above
Any-motion response (1 byte, response[2]):
bit 3: x-axis active (0x1 << (0+3))
bit 4: y-axis active (0x1 << (1+3))
bit 5: z-axis active (0x1 << (2+3))
bit 6: sign (0 = positive, 1 = negative)
Tap response (1 byte, response[2]):
Orientation response (1 byte, response[2]):
BMI270 Gesture response (1 byte, response[2]):
BMI270 Activity response (1 byte, response[2]):
Packed accelerometer data (multiple 6-byte XYZ triplets starting at response[2]):
Module 0x13 — Gyroscope¶
Implementation Types¶
BMI160 Register Opcodes¶
BMI270 Register Opcodes¶
POWER_MODE = 0x01
DATA_INTERRUPT_ENABLE = 0x02
CONFIG = 0x03
DATA = 0x04
PACKED_GYRO_DATA = 0x05
OFFSET = 0x06
Config Struct (GyroBoschConfig, 2 bytes)¶
byte 0:
bits 0-3: gyr_odr (use MblMwGyroBoschOdr enum value directly)
bits 4-5: gyr_bwp (2 = normal)
bits 6-7: unused
byte 1:
bits 0-2: gyr_range (0=2000dps, 1=1000dps, 2=500dps, 3=250dps, 4=125dps)
bits 3-7: unused
FSR scales (index = gyr_range):
0 -> 16.4 (2000 dps, 1 LSB = 1/16.4 dps)
1 -> 32.8 (1000 dps)
2 -> 65.6 (500 dps)
3 -> 131.2 (250 dps)
4 -> 262.4 (125 dps)
Commands¶
Start / Stop:
Start BMI160: [0x13, 0x01, 0x01]
Stop BMI160: [0x13, 0x01, 0x00]
Start BMI270: [0x13, 0x01, 0x01]
Stop BMI270: [0x13, 0x01, 0x00]
Enable / Disable data stream:
Enable BMI160: [0x13, 0x02, 0x01, 0x00]
Disable BMI160: [0x13, 0x02, 0x00, 0x01]
Enable BMI270: [0x13, 0x02, 0x01, 0x00]
Disable BMI270: [0x13, 0x02, 0x00, 0x01]
Write config (BMI160):
Write config (BMI270):
Write offsets (BMI270):
Notification Headers¶
| Register | Description |
|---|---|
| [0x13, 0x05] | BMI160 Rotation XYZ data |
| [0x13, 0x07] | BMI160 Packed rotation data |
| [0x13, 0x04] | BMI270 Rotation XYZ data |
| [0x13, 0x05] | BMI270 Packed rotation data |
Response Parsing¶
Rotation XYZ (6 bytes after header, same as accelerometer):
val x = (response[2] or (response[3].toInt() shl 8)).toShort() / scale
val y = (response[4] or (response[5].toInt() shl 8)).toShort() / scale
val z = (response[6] or (response[7].toInt() shl 8)).toShort() / scale
Module 0x15 — Magnetometer (BMM150)¶
Register Opcodes¶
POWER_MODE = 0x01
DATA_INTERRUPT_ENABLE = 0x02
DATA_RATE = 0x03
DATA_REPETITIONS = 0x04
MAG_DATA = 0x05
PACKED_MAG_DATA = 0x09
Packed mag data available when module revision >= 1. Suspend mode available when module revision >= 2.
Commands¶
Power / start / stop / suspend:
Start: [0x15, 0x01, 0x01]
Stop: [0x15, 0x01, 0x00]
Suspend: [0x15, 0x01, 0x02] <- only if revision >= 2
Enable / Disable data stream:
Configure (XY reps, Z reps, ODR):
Presets:
LOW_POWER: xy=3, z=3, ODR=10Hz
REGULAR: xy=9, z=15, ODR=10Hz
ENHANCED_REGULAR: xy=15, z=27, ODR=10Hz
HIGH_ACCURACY: xy=47, z=83, ODR=20Hz
Response Parsing¶
Mag data (6 bytes after header):
val x = (response[2] or (response[3].toInt() shl 8)).toShort() / 16.0f // uT
val y = (response[4] or (response[5].toInt() shl 8)).toShort() / 16.0f
val z = (response[6] or (response[7].toInt() shl 8)).toShort() / 16.0f
Module 0x12 — Barometer (BMP280 / BME280)¶
Implementation Types¶
Register Opcodes¶
Config Struct (BoschBaroConfig, 2 bytes)¶
byte 0:
bits 0-1: unused
bits 2-4: pressure_oversampling
bits 5-7: temperature_oversampling
byte 1:
bits 0-1: unused
bits 2-4: iir_filter
bits 5-7: standby_time
Commands¶
Start / Stop cyclic measurement:
Write config:
Response Parsing¶
Pressure (4 bytes, unsigned, response[2..5]):
val rawPressure = (response[2].toLong() and 0xFF) or
((response[3].toLong() and 0xFF) shl 8) or
((response[4].toLong() and 0xFF) shl 16) or
((response[5].toLong() and 0xFF) shl 24)
val pressurePa = rawPressure / 256.0f // BOSCH_BARO_SCALE = 256.0
Altitude (4 bytes, signed):
val rawAlt = ByteBuffer.wrap(response, 2, 4).order(ByteOrder.LITTLE_ENDIAN).int
val altitudeM = rawAlt / 256.0f
Module 0x19 — Sensor Fusion¶
Register Opcodes¶
ENABLE = 0x01
MODE = 0x02
OUTPUT_ENABLE = 0x03
CORRECTED_ACC = 0x04
CORRECTED_GYRO = 0x05
CORRECTED_MAG = 0x06
QUATERNION = 0x07
EULER_ANGLES = 0x08
GRAVITY_VECTOR = 0x09
LINEAR_ACC = 0x0A
CALIBRATION_STATE = 0x0B
ACC_CAL_DATA = 0x0C
GYRO_CAL_DATA = 0x0D
MAG_CAL_DATA = 0x0E
RESET_ORIENTATION = 0x0F
Config Struct (SensorFusionState.config, 2 bytes)¶
byte 0: mode (MblMwSensorFusionMode: 0=SLEEP, 1=NDOF, 2=IMU_PLUS, 3=COMPASS, 4=M4G)
byte 1:
bits 0-3: acc_range (0=2g, 1=4g, 2=8g, 3=16g)
bits 4-7: gyro_range (enum+1: 1=2000dps, 2=1000dps, 3=500dps, 4=250dps, 5=125dps)
Enable mask bits¶
bit 0: CORRECTED_ACC
bit 1: CORRECTED_GYRO
bit 2: CORRECTED_MAG
bit 3: QUATERNION
bit 4: EULER_ANGLES
bit 5: GRAVITY_VECTOR
bit 6: LINEAR_ACC
Underlying-sensor requirements per mode¶
The fusion algorithm is fed by the on-board acc / gyro / mag modules. Each mode dictates which sensors must be running and at what rate; the host must configure and start each underlying sensor in addition to the fusion module itself.
| Mode | Acc | Gyro | Mag |
|---|---|---|---|
SLEEP |
— | — | — |
NDOF |
100 Hz, host range | 100 Hz, host range | 25 Hz, xy=9, z=15 |
IMU_PLUS |
100 Hz, host range | 100 Hz, host range | — |
COMPASS |
25 Hz, host range | — | 25 Hz, xy=9, z=15 |
M4G |
50 Hz, host range | — | 25 Hz, xy=9, z=15 |
Mag preset is fixed by the firmware: xy_reps = 9, z_reps = 15, ODR = 25 Hz → [0x15, 0x04, 0x04, 0x0E] followed by [0x15, 0x03, 0x06].
Commands¶
The lifecycle for configuring, starting, and stopping sensor fusion follows this sequence of BLE commands:
Write fusion config:
whererange_byte = acc_range | ((gyro_range + 1) << 4).
Enable output mask:
enable_mask is the per-signal bit (see Enable mask bits above). Multiple bits may be set to subscribe to several outputs from the same fusion run.
Start / Stop the fusion algorithm:
Clear output mask (issued during stop, before stopping the underlying sensors):
Full configure sequence (NDOF, BMI160, ±2g / ±2000 dps shown)¶
[0x19, 0x02, 0x01, 0x10] # fusion mode = NDOF, ranges packed
[0x03, 0x03, 0x28, 0x03] # acc 100 Hz, ±2g (BMI160: conf=0x28, range bitmask=0x03)
[0x13, 0x03, 0x28, 0x00] # gyro 100 Hz, ±2000 dps
[0x15, 0x04, 0x04, 0x0E] # mag repetitions: xy=9, z=15
[0x15, 0x03, 0x06] # mag ODR = 25 Hz
For BMI270 the acc command differs: [0x03, 0x03, 0xA8, 0x00] (filter_perf bit 7 set, 0-based range).
For IMU_PLUS the two mag commands are omitted. For COMPASS and M4G the gyro command is omitted (and the acc ODR is 25 Hz / 50 Hz respectively → conf = 0x26 / 0x27 on BMI160, 0xA6 / 0xA7 on BMI270).
Full start sequence (NDOF + quaternion shown)¶
# Underlying sensors first — interrupt enables, then start
[0x03, 0x02, 0x01, 0x00] # acc data interrupt enable
[0x13, 0x02, 0x01, 0x00] # gyro data interrupt enable
[0x15, 0x02, 0x01, 0x00] # mag data interrupt enable
[0x03, 0x01, 0x01] # acc start
[0x13, 0x01, 0x01] # gyro start
[0x15, 0x01, 0x01] # mag start
# Fusion last — enable mask then start the algorithm
[0x19, 0x03, 0x08, 0x00] # output_enable: bit 3 = QUATERNION
[0x19, 0x01, 0x01] # fusion start
IMU_PLUS skips the mag enable + mag start. COMPASS and M4G skip the gyro enable + gyro start.
Full stop sequence (NDOF shown)¶
Reverses the start: fusion off + mask cleared first, then underlying sensors are stopped + interrupts disabled.
[0x19, 0x01, 0x00] # fusion stop
[0x19, 0x03, 0x00, 0x7F] # output_enable: clear all bits
[0x03, 0x01, 0x00] # acc stop
[0x13, 0x01, 0x00] # gyro stop
[0x15, 0x01, 0x00] # mag stop
[0x03, 0x02, 0x00, 0x01] # acc data interrupt disable
[0x13, 0x02, 0x00, 0x01] # gyro data interrupt disable
[0x15, 0x02, 0x00, 0x01] # mag data interrupt disable
A common host bug is to send only the fusion config + start ([0x19, 0x02, …] and [0x19, 0x01, 0x01]) without configuring or starting the underlying acc / gyro / mag — the fusion algorithm runs but produces no output because it never sees any input samples.
Read config:
Read calibration state:
Read calibration data (chain of 3 reads):
[0x19, 0x8C] <- READ_REGISTER(ACC_CAL_DATA)
Response -> [0x19, 0x8C, <10 bytes acc cal>]
Then send:
[0x19, 0x8D] <- READ_REGISTER(GYRO_CAL_DATA)
Response -> [0x19, 0x8D, <10 bytes gyro cal>]
Then send:
[0x19, 0x8E] <- READ_REGISTER(MAG_CAL_DATA)
Response -> [0x19, 0x8E, <10 bytes mag cal>]
Response Parsing¶
Corrected Accelerometer (13 bytes after header):
// First 12 bytes = 3x float32 (x, y, z in m/s^2), last byte = accuracy
val x = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).float / 1000f // SENSOR_FUSION_ACC_SCALE
val y = ByteBuffer.wrap(response, 6, 4).order(LITTLE_ENDIAN).float / 1000f
val z = ByteBuffer.wrap(response, 10, 4).order(LITTLE_ENDIAN).float / 1000f
val accuracy = response[14]
Corrected Gyro / Corrected Mag (13 bytes after header):
// 3x float32, same layout; no divide (direct float), last byte = accuracy
val x = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).float
val y = ByteBuffer.wrap(response, 6, 4).order(LITTLE_ENDIAN).float
val z = ByteBuffer.wrap(response, 10, 4).order(LITTLE_ENDIAN).float
val accuracy = response[14]
Quaternion (16 bytes after header = 4x float32):
val w = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).float
val x = ByteBuffer.wrap(response, 6, 4).order(LITTLE_ENDIAN).float
val y = ByteBuffer.wrap(response, 10, 4).order(LITTLE_ENDIAN).float
val z = ByteBuffer.wrap(response, 14, 4).order(LITTLE_ENDIAN).float
Euler Angles (16 bytes = 4x float32):
val heading = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).float
val pitch = ByteBuffer.wrap(response, 6, 4).order(LITTLE_ENDIAN).float
val roll = ByteBuffer.wrap(response, 10, 4).order(LITTLE_ENDIAN).float
val yaw = ByteBuffer.wrap(response, 14, 4).order(LITTLE_ENDIAN).float
Gravity Vector / Linear Acceleration (12 bytes = 3x float32):
val x = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).float / 9.80665f // MSS_TO_G_SCALE
val y = ByteBuffer.wrap(response, 6, 4).order(LITTLE_ENDIAN).float / 9.80665f
val z = ByteBuffer.wrap(response, 10, 4).order(LITTLE_ENDIAN).float / 9.80665f
Calibration state (3 bytes after header):
val acc_cal = response[2]
val gyro_cal = response[3]
val mag_cal = response[4]
// Values 0-3 (0=uncalibrated, 3=fully calibrated)
Module 0x04 — Temperature¶
Multichannel temperature. Read via the data signal mechanism. Temperature values are signed int32 divided by 8.0 (TEMPERATURE_SCALE = 8.0f).
Module 0x0B — Logging¶
Register Opcodes¶
ENABLE = 0x01
TRIGGER = 0x02
REMOVE = 0x03
TIME = 0x04
LENGTH = 0x05
READOUT = 0x06
READOUT_NOTIFY = 0x07
READOUT_PROGRESS = 0x08
REMOVE_ENTRIES = 0x09
REMOVE_ALL = 0x0A
CIRCULAR_BUFFER = 0x0B
READOUT_PAGE_COMPLETED = 0x0D
READOUT_PAGE_CONFIRM = 0x0E
PAGE_FLUSH = 0x10
Revisions:
Tick-to-ms Conversion¶
Key Constants¶
ENTRY_ID_MASK = 0x1F (lower 5 bits of byte)
RESET_UID_MASK = 0x07 (next 3 bits: bits 5-7)
LOG_ENTRY_SIZE = 8 bytes total (1 id/reset + 3 tick + 4 data)
LOG_ENTRY_DATA_SIZE = 4 bytes (uint32_t payload per entry)
Commands¶
Create a logger for a signal:
Response:[0x0B, 0x02, assigned_entry_id]
Start logging (with optional overwrite):
Stop logging:
Read time signal (get reference epoch):
Response:[0x0B, 0x84, tick_byte0, tick_byte1, tick_byte2, tick_byte3, reset_uid]
val tick = ByteBuffer.wrap(response, 2, 4).order(LITTLE_ENDIAN).int.toLong() and 0xFFFFFFFFL
val epoch = System.currentTimeMillis() - (tick * TICK_TIME_STEP).toLong()
val resetUid = response[6]
Download sequence:
1. Enable readout notify: [0x0B, 0x07, 0x01]
2. For extended logging (revision 2): [0x0B, 0x0D, 0x01]
3. Enable progress: [0x0B, 0x08, 0x01]
4. Read length: [0x0B, 0x85] (READ_REGISTER(LENGTH))
5. On length response, send readout: [0x0B, 0x06, n_entries(4 bytes LE), n_notify(4 bytes LE)]
6. For each page-completed notification, confirm with: [0x0B, 0x0E]
Clear log entries:
Flush page (MMS only, revision 3):
Log Entry Format (from READOUT_NOTIFY = 0x07)¶
Each notification packet is [0x0B, 0x07, entry...] and carries 1 or 2 log
entries of 8 bytes each (so payload = 8 or 16 bytes, packet = 10 or 18 bytes
including the 2-byte header):
Entry at offset 2 (always present):
byte[offset+0]: (reset_uid << 5) | entry_id (reset_uid: bits 5-7, entry_id: bits 0-4)
byte[offset+1..3]: uint24 tick (little-endian, 3 bytes)
byte[offset+4..7]: uint32 data (little-endian, 4 bytes)
Entry at offset 10 (present if packet length == 18):
same format as above
Wall-clock time of an entry: logReferenceDate + tick * TICK_TIME_STEP.
To convert to a signal value, reassemble 4-byte chunks from consecutive entry IDs.
Module 0x0C — Timer¶
Register Opcodes¶
ENABLE = 0x01
TIMER_ENTRY = 0x02
START = 0x03
STOP = 0x04
REMOVE = 0x05
NOTIFY = 0x06
NOTIFY_ENABLE = 0x07
Commands¶
Create timer:
-period in milliseconds
- repetitions = 0xFFFF for indefinite
- immediate_flag = 1 for immediate first fire, 0 for delayed
Response: [0x0C, 0x02, timer_id]
Start / Stop / Remove:
Timer fires notification: [0x0C, 0x06, timer_id]
Module 0x0A — Event¶
Register Opcodes¶
Event Entry Command Format¶
Events bind a source signal to a command that fires when the signal fires.
[0x0A, 0x02, src_module_id, src_register_id, src_data_id, dst_module_id, dst_register_id, param_length]
[data_length_and_offset_byte, dest_offset_byte]
where byte0 = 0x01 | (data_length << 1) | (data_offset << 4)
Then parameters:
Remove specific event commands:
Remove all events:
Module 0x09 — Data Processor¶
Register Opcodes¶
ADD = 0x02
NOTIFY = 0x03
STATE = 0x04
PARAMETER = 0x05
REMOVE = 0x06
NOTIFY_ENABLE = 0x07
REMOVE_ALL = 0x08
Overview¶
The data processor chains on-device signal transformations. Processors are created one at a time; each ADD response assigns an ID that can be used as the source for subsequent processors.
ADD command format¶
| Byte | Field | Notes |
|---|---|---|
| 0 | module | 0x09 |
| 1 | register | 0x02 (ADD) |
| 2 | src_module | Source module ID |
| 3 | src_reg | Source register ID (not OR'd with 0x80) |
| 4 | src_data_id | Source data ID, or 0xFF for "any" |
| 5 | src_config | Encodes sample length and offset (see below) |
| 6 | proc_type | Processor type ID |
| 7+ | config_bytes | Per-processor config (see Processor Types) |
Response (plain notification on (0x09, 0x02)):
Note: this is a plain notification, not a read-response (bit 7 is NOT set).Source config byte formula¶
This encodes the total sample length minus 1 in the upper 3 bits, and the byte offset within the sample in the lower 5 bits.
Common source signals:
| Signal | Module | Reg | ID | Channels | Ch size | src_config |
|---|---|---|---|---|---|---|
| Switch | 0x01 | 0x01 | 0xFF | 1 | 1B | 0x00 |
| GPIO ADC | 0x05 | 0x07 | pin | 1 | 2B | 0x20 |
| GPIO absolute | 0x05 | 0x06 | pin | 1 | 2B | 0x20 |
| Accelerometer | 0x03 | 0x04 | 0xFF | 3 | 2B | 0xA0 |
| Gyroscope | 0x13 | 0x05 | 0xFF | 3 | 2B | 0xA0 |
| Temperature | 0x04 | 0xC1 | ch | 1 | 2B | 0x20 |
| Processor output | 0x09 | 0x03 | proc_id | varies | varies | computed |
Processor streaming¶
Enable notifications:
Disable notifications:
Data notification format:
Multiple processors all share the same (0x09, 0x03) notification; demultiplex by proc_id at byte[2].Remove processors¶
Remove one:
Remove all:
Processor Types¶
0x01 — Passthrough¶
Gates data flow.
Config bytes (3): [mode, count_lo, count_hi]
| Mode | Value |
|---|---|
| ALL | 0 |
| CONDITIONAL | 1 |
| COUNT | 2 |
0x02 — Accumulator / Counter¶
Config byte (1): {output_size-1 : 2, input_size-1 : 2, mode : 3}
| mode | Meaning |
|---|---|
| 0 | Accumulate (SUM) |
| 1 | Count events |
For Counter, input_size field is 0 (ignored). Output is always 1 channel.
Reference test (test_led_controller step 1, Counter outputSize=1):
Config byte 0x10 =(0 & 0x3) | (1 << 4) — outputSize=1, mode=COUNT.
0x03 — Average (Low-pass filter)¶
Config bytes (2): [byte0, sample_size]
byte0 = (output_unit-1 & 0x3) | ((input_unit-1 & 0x3) << 2) (output == input size, mode=0=LPF)
Reference test (test_freefall step 2, Average of 2-byte RSS output, sampleSize=4):
Config bytes[0x05, 0x04] — unit=2, s=1 → 1|(1<<2)=0x05; sample_size=4.
0x06 — Comparator¶
Config bytes (7): [is_signed, operation, padding, ref_b0, ref_b1, ref_b2, ref_b3]
Reference is a signed Int32 in little-endian byte order.
| Operation | Value |
|---|---|
| EQ | 0 |
| NEQ | 1 |
| LT | 2 |
| LTE | 3 |
| GT | 4 |
| GTE | 5 |
Reference test (test_freefall step 4, EQ -1 signed):
0x07 — RMS / RSS Combiner¶
Reduces a multi-axis signal to a scalar magnitude.
Config bytes (2): [byte0, mode]
byte0 = (unit-1 & 0x3) | ((unit-1 & 0x3) << 2) | ((channels-1 & 0x7) << 4) | (is_signed << 7)
| Mode | Value |
|---|---|
| RMS | 0 |
| RSS | 1 |
Output: 1 channel, same byte width as one input channel, unsigned.
Reference test (test_freefall step 1, RSS of accelerometer 3ch×2B signed):
Config bytes[0xA5, 0x01] — unit=2, s=1, ch=3, signed: 1|(1<<2)|(2<<4)|0x80 = 0xA5, mode=RSS=1.
0x08 — Time Delay¶
Passes one sample per period.
Config bytes (5): [byte0, period_b0, period_b1, period_b2, period_b3]
byte0 = ((data_length-1) & 0x7) | ((mode & 0x7) << 3)
Period is in milliseconds, little-endian UInt32.
| Mode | Value |
|---|---|
| ABSOLUTE | 0 |
| DIFFERENTIAL | 1 |
0x09 — Math¶
Arithmetic transform applied per sample.
Config bytes (7): [byte0, operation, rhs_b0, rhs_b1, rhs_b2, rhs_b3, n_channels]
byte0 = (output_unit-1 & 0x3) | ((input_unit-1 & 0x3) << 2) | (is_signed << 4)
n_channels = inputChannels - 1 when multichannel, else 0.
| Operation | Value |
|---|---|
| ADD | 0 |
| SUBTRACT | 1 |
| MULTIPLY | 2 |
| DIVIDE | 3 |
| MODULO | 4 |
| EXPONENT | 5 |
| SQRT | 6 |
| LSHIFT | 7 |
| RSHIFT | 8 |
| ABS | 9 |
| CONSTANT | 10 |
| NEGATE | 11 |
| FLOOR | 12 |
| CEIL | 13 |
| ROUND | 14 |
Reference test (test_led_controller step 2, counter % 2, unsigned, output=4):
byte0 =(4-1 & 0x3) | ((1-1 & 0x3) << 2) | (0 << 4) = 0x03; op=MODULO=4; rhs=2 LE32; nch=0.
0x0A — Sample Delay¶
Buffers N samples before emitting.
Config bytes (2): [data_length - 1, bin_size]
0x0D — Threshold¶
Emits a value when the input crosses a boundary.
Config bytes (7): [byte0, boundary_b0, boundary_b1, boundary_b2, boundary_b3, hyst_b0, hyst_b1]
byte0 = (unit_size-1 & 0x3) | (is_signed << 2) | ((mode & 0x7) << 3)
Boundary is a signed Int32 in little-endian. Hysteresis is an unsigned UInt16 in little-endian.
| Mode | Value | Output |
|---|---|---|
| ABSOLUTE | 0 | raw value (only when crossing) |
| BINARY | 1 | Int32: +1 when above, –1 when below |
Reference test (test_freefall step 3, BINARY boundary=8192 unsigned):
byte0 =(2-1 & 0x3) | (0 << 2) | (1 << 3) = 0x09; boundary=8192=0x2000 LE32; hyst=0.
Module 0x0F — Macro¶
Register Opcodes¶
Protocol¶
Commands are written with response (unlike all others).
Begin macro recording:
Response:[0x0F, 0x02, macro_id]
Add command to macro: For commands <= 13 bytes (MW_CMD_MAX_LENGTH - 2):
For commands >= 14 bytes:
[0x0F, 0x09, cmd_byte0, cmd_byte1] <- ADD_PARTIAL (first 2 bytes)
[0x0F, 0x03, cmd_byte2..cmd_byteN-2] <- ADD_COMMAND (remaining)
End macro:
Execute macro:
Erase all macros:
Module 0x11 — Settings¶
Register Opcodes¶
DEVICE_NAME = 0x01
AD_INTERVAL = 0x02
TX_POWER = 0x03
START_ADVERTISING = 0x05
SCAN_RESPONSE = 0x07
PARTIAL_SCAN_RESPONSE = 0x08
CONNECTION_PARAMS = 0x09
DISCONNECT_EVENT = 0x0A
MAC = 0x0B
BATTERY_STATE = 0x0C
POWER_STATUS = 0x11
CHARGE_STATUS = 0x12
WHITELIST_FILTER_MODE = 0x13
WHITELIST_ADDRESSES = 0x14
THREE_VOLT_POWER = 0x1C
FORCE_1M_PHY = 0x1D
Revisions:
CONN_PARAMS_REVISION = 1
DISCONNECTED_EVENT_REVISION = 2
BATTERY_REVISION = 3
CHARGE_STATUS_REVISION = 5
WHITELIST_REVISION = 6
MMS_REVISION = 9
MMS_PHY_REVISION = 10
Commands¶
Set device name:
Constraints (enforced by firmware for BLE advertising): - Maximum 26 ASCII bytes (not UTF-8). Longer strings are silently truncated by the board. - Allowed characters:A-Z, a-z, 0-9, _, -, and space. Writing other code points will be rejected or mangled by the advertising layer.
Set advertising interval:
If revision >= 1: interval is divided by 0.625 (AD_INTERVAL_STEP) before encoding. If revision >= 6 (WHITELIST): appendad_type byte.
Set TX power:
Start advertising:
Set connection parameters:
[0x11, 0x09, min_ci_lo, min_ci_hi, max_ci_lo, max_ci_hi, latency_lo, latency_hi, timeout_lo, timeout_hi]
Read battery:
Response:[0x11, 0x8C, charge_percent, voltage_lo, voltage_hi]
val charge = response[2]
val voltage = ((response[3].toInt() and 0xFF) or ((response[4].toInt() and 0xFF) shl 8)).toFloat()
Read MAC address:
Response:[0x11, 0x8B, b0, b1, b2, b3, b4, b5]
Format: "%02X:%02X:%02X:%02X:%02X:%02X", b5, b4, b3, b2, b1, b0
Read power/charge status:
Response byte[2] contains the status value.3V regulator (MMS only, revision 9):
Force 1M PHY (MMS only, revision 10):
Module 0x05 — GPIO¶
Register Opcodes¶
SET_DO = 0x01
CLEAR_DO = 0x02
PULL_UP_DI = 0x03
PULL_DOWN_DI = 0x04
NO_PULL_DI = 0x05
READ_AI_ABS_REF = 0x06
READ_AI_ADC = 0x07
READ_DI = 0x08
PIN_CHANGE = 0x09
PIN_CHANGE_NOTIFY = 0x0A
PIN_CHANGE_NOTIFY_ENABLE= 0x0B
Commands¶
Set digital output high: [0x05, 0x01, pin]
Set digital output low: [0x05, 0x02, pin]
Enable pull-up on DI: [0x05, 0x03, pin]
Enable pull-down on DI: [0x05, 0x04, pin]
No pull on DI: [0x05, 0x05, pin]
Read analog (abs ref): [0x05, 0x86, pin] <- READ_REGISTER(0x06)
Read analog (ADC): [0x05, 0x87, pin] <- READ_REGISTER(0x07)
Read digital input: [0x05, 0x88, pin] <- READ_REGISTER(0x08)
Configure pin change: [0x05, 0x09, pin, change_type]
Enable pin change notify: [0x05, 0x0B, pin, 0x01]
Disable pin change notify:[0x05, 0x0B, pin, 0x00]
Module 0x07 — iBeacon¶
Register Opcodes¶
Commands¶
Enable iBeacon: [0x07, 0x01, 0x01]
Disable iBeacon: [0x07, 0x01, 0x00]
Set UUID (16 bytes BE): [0x07, 0x02, b0, b1, ..., b15]
Set major (UInt16 LE): [0x07, 0x03, major_lo, major_hi]
Set minor (UInt16 LE): [0x07, 0x04, minor_lo, minor_hi]
Set RX power (Int8): [0x07, 0x05, power_byte]
RX power = signal strength at 1m, broadcast in advert for ranging.
Typical: –55 dBm → 0xC9.
Set TX power (Int8): [0x07, 0x06, power_byte]
TX power = actual BLE transmission power.
Typical: –4 dBm → 0xFC, 0 dBm → 0x00.
Set period (UInt16 LE): [0x07, 0x07, period_lo, period_hi]
Period is in milliseconds. Typical: 700 ms → [0xBC, 0x02].
Reference test vectors¶
Enable: [0x07, 0x01, 0x01]
Disable: [0x07, 0x01, 0x00]
SetMajor(78): [0x07, 0x03, 0x4E, 0x00]
SetMinor(0x1D1D): [0x07, 0x04, 0x1D, 0x1D]
SetRXPower(-55): [0x07, 0x05, 0xC9]
SetTXPower(-12): [0x07, 0x06, 0xF4]
SetPeriod(0x3AB3):[0x07, 0x07, 0xB3, 0x3A]
Module 0x08 — Haptic¶
Register Opcodes¶
Command¶
| Field | Description |
|---|---|
duty_cycle_byte |
Motor: floor(dutyCycle% × 248 / 100), clamped to 0–248. Buzzer: always 0x7F. |
pulse_width_lo/hi |
Pulse duration in milliseconds, UInt16 little-endian. |
mode |
0x00 = ERM haptic motor, 0x01 = piezo buzzer. |
Reference test vectors¶
Motor 100%, 5000 ms: [0x08, 0x01, 0xF8, 0x88, 0x13, 0x00]
0xF8 = 248 (100% duty cycle), 0x1388 = 5000 ms, mode=0x00
Buzzer, 7500 ms: [0x08, 0x01, 0x7F, 0x4C, 0x1D, 0x01]
0x7F always for buzzer, 0x1D4C = 7500 ms, mode=0x01
Module 0x0D — Serial Passthrough (I2C / SPI)¶
Register Opcodes¶
I2C Write¶
| Field | Description |
|---|---|
device_addr |
7-bit I2C address of the peripheral. |
reg_addr |
Register (sub-address) to write to. |
data_len |
Number of payload bytes that follow. |
id |
Caller-assigned identifier (0–9); echoed in read responses. |
data... |
Payload bytes. |
I2C Read¶
Send:
0xC1 = 0x01 | 0x80 (read bit) | 0x40 (data_id bit) — the data_id bit tells the board to include id as byte[2] in its response.
Board responds with a plain notification (bit 7 NOT set):
Reference test vector¶
Read 10 bytes from device 0x1C, register 0x0D, id=1:
Send: [0x0D, 0xC1, 0x1C, 0x0D, 0x0A, 0x01]
Receive: [0x0D, 0x01, 0x01, data...]
SPI Write¶
SPI Read¶
Send:
0xC2 = 0x02 | 0x80 | 0x40 — same read+data_id bit pattern as I2C.
Board responds:
SPI clock enum values:
SPI mode (CPOL/CPHA):
0 = mode 0 (CPOL=0, CPHA=0)
1 = mode 1 (CPOL=0, CPHA=1)
2 = mode 2 (CPOL=1, CPHA=0)
3 = mode 3 (CPOL=1, CPHA=1)
msb_first: 1 = MSB transmitted first (typical), 0 = LSB first.
nrf_pins: 1 = use nRF internal SPI pins, 0 = use board expansion header pins.
Module 0xFE — Debug¶
Register Opcodes¶
RESET = 0x01
BOOTLOADER = 0x02
NOTIFICATION_SPOOF= 0x03
KEY_REGISTER = 0x04
RESET_GC = 0x05
DISCONNECT = 0x06
POWER_SAVE = 0x07
STACK_OVERFLOW = 0x09
SCHEDULE_QUEUE = 0x0A
Commands¶
Reset: [0xFE, 0x01]
Jump to bootloader: [0xFE, 0x02]
Spoof notification: [0xFE, 0x03, ...bytes...]
Set key register: [0xFE, 0x04, val_byte0, val_byte1, val_byte2, val_byte3]
Reset after GC: [0xFE, 0x05]
Disconnect: [0xFE, 0x06]
Enable power save: [0xFE, 0x07]
Set stack overflow assertion: [0xFE, 0x09, enable_byte]
Read stack overflow: [0xFE, 0x89] <- READ_REGISTER(0x09)
Read schedule queue: [0xFE, 0x8A] <- READ_REGISTER(0x0A)
Fake button event:
Data Scales Summary¶
| Data type | Scale constant | Notes |
|---|---|---|
| Accelerometer (all Bosch) | see FSR table | 16384, 8192, 4096, or 2048 LSB/g |
| Gyroscope (all Bosch) | see FSR table | 16.4, 32.8, 65.6, 131.2, 262.4 LSB/dps |
| Magnetometer BMM150 | 16.0 | LSB/uT |
| Barometer pressure | 256.0 | raw int / 256.0 = Pa |
| Barometer altitude | 256.0 | raw int / 256.0 = m |
| Temperature | 8.0 | raw int / 8.0 = °C |
| BME280 humidity | 1024.0 | raw / 1024.0 = % |
| Q16.16 fixed point | 65536.0 (0x10000) | raw / 65536 = value |
| Sensor fusion corrected acc | 1000.0 | raw float / 1000.0 = g |
| Sensor fusion gravity / linear acc | 9.80665 | raw float (m/s²) / g_scale = g |
| Battery voltage | direct uint16 | millivolts (raw bytes 1-2 of response) |
| Battery charge | direct uint8 | percent |
Response Dispatch¶
The SDK uses a two-key map (module_id, register_id) or three-key (module_id, register_id, data_id) to route incoming BLE notifications.
// Two-byte header (no ID): data starts at response[2]
fun responseHandlerDataNoId(response: ByteArray) {
val header = ResponseHeader(response[0], response[1])
forwardResponse(header, response.copyOfRange(2, response.size))
}
// Three-byte header (with ID): data starts at response[3]
fun responseHandlerDataWithId(response: ByteArray) {
val header = ResponseHeader(response[0], response[1], response[2])
forwardResponse(header, response.copyOfRange(3, response.size))
}
// Packed data (multiple samples per notification): response[2] onward in 6-byte chunks
fun responseHandlerPackedData(response: ByteArray) {
val header = ResponseHeader(response[0], response[1])
var i = 2
while (i < response.size) {
dispatchSample(header, response, i, 6)
i += 6
}
}
Tear Down Sequence¶
[0x09, 0x08] <- DataProcessor REMOVE_ALL
[0x0A, 0x05] <- Event REMOVE_ALL
[0x0B, 0x0A] <- Logging REMOVE_ALL
GATT UUIDs (Canonical)¶
| Characteristic | UUID |
|---|---|
| MetaWear Service | 326a9000-85cb-9195-d9dd-464cfbbae75a |
| Command (write) | 326a9001-85cb-9195-d9dd-464cfbbae75a |
| Notify (notify) | 326a9006-85cb-9195-d9dd-464cfbbae75a |
| Device Info Svc | 0000180a-0000-1000-8000-00805f9b34fb |
| Firmware Rev | 00002a26-0000-1000-8000-00805f9b34fb |
| Model Number | 00002a24-0000-1000-8000-00805f9b34fb |
| Hardware Rev | 00002a27-0000-1000-8000-00805f9b34fb |
| Manufacturer | 00002a29-0000-1000-8000-00805f9b34fb |
| Serial Number | 00002a25-0000-1000-8000-00805f9b34fb |