Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

BLE Protocol

This chapter is the canonical BLE documentation for the project. The full protocol was reverse-engineered from the official JavaScript API and Python API source code.

The content below is included from docs/BLE.md so that the source of truth remains in a single file and the book stays in sync automatically.

BLE Specifications

BLE specification of the GoDice dice.

The dice are powered by the Nordic nRF52805 SoC - a Bluetooth 5.2 System-on-Chip with a 64 MHz ARM Cortex-M4 processor, 192 KB Flash, and 24 KB RAM. The nRF52805 is optimized for small two-layer PCB designs in a 2.48 x 2.46 mm WLCSP package. The SoC’s 64 MHz Cortex-M4 processes the dice’s 3D sensor data to calculate roll results and detect movement, tilting, free fall, and taps.

Particula selected the nRF52805 for its reliability and low power consumption. GoDice uses supercapacitor technology for ultra-fast battery-free charging - thanks in part to the ultra-low power characteristics of the Nordic SoC (4.6 mA in TX at 0 dBm, 4.6 mA in RX, and 0.3 uA in System OFF).

The dice use the Nordic UART Service (NUS) profile internally. NUS is a custom GATT service that emulates a serial port over BLE, originally designed by Nordic for UART-to-BLE bridging. GoDice repurposes it as a raw byte transport: the application protocol (opcodes and events) is layered on top of the NUS RX/TX characteristics.

The nRF52805 runs a SoftDevice S112 or S113

  • a memory-optimized Peripheral-only Bluetooth LE protocol stack suited to the SoC’s 24 KB RAM. Both stacks support up to 4 concurrent Peripheral connections with a Broadcaster, Bluetooth 5.1 qualification, 2 Mbps high-throughput, and Channel Selection Algorithm #2.

The full protocol was reverse-engineered from the official JavaScript API, Python API, and C API source code.

Device Properties

PropertyDescriptionValue
Device namePrefixGoDice_
Service UUIDNUS Service (16-bit offset 0x0001)6e400001-b5a3-f393-e0a9-e50e24dcca9e
Write CharacteristicNUS RX - host writes commands6e400002-b5a3-f393-e0a9-e50e24dcca9e
Notify CharacteristicNUS TX - dice sends notifications6e400003-b5a3-f393-e0a9-e50e24dcca9e

NUS Transport Details

  • Write type: Both Write Request (with response) and Write Command (without response) are supported on the RX characteristic
  • Notifications: The dice sends all data via Handle Value Notifications on the TX characteristic; the host must enable notifications by writing to the CCCD (Client Characteristic Configuration Descriptor, value 0x0001)
  • Security: All permissions are open (SEC_OPEN) - no pairing or bonding required
  • Max payload: MTU_SIZE - 3 bytes (20 bytes with the default 23-byte ATT MTU)
  • No encryption: Communication is unencrypted; the dice accept connections from any central

Byte Commands (Host → Dice)

All commands are written as byte arrays to the Write Characteristic. The first byte is always the opcode.

OpcodeDecimalCommandPayload BytesDescription
0x033Get Battery Level(none)Response: Bat + level byte
0x088Set LEDs[R1, G1, B1, R2, G2, B2] (6 bytes, 0–255)Sets both RGB LEDs; [0,0,0,0,0,0] turns off
0x1016Pulse LEDs[pulseCount, onTime, offTime, R, G, B, blinkMode, leds]onTime/offTime in units of 10 ms; max 255. blinkMode and leds select which LEDs blink
0x1420Stop Pulse LEDs(none)Stops any active pulse LED animation
0x1723Get Dice Color(none)Response: Col + color byte
0x1925Init[sensitivity, pulseCount, onTime, offTime, R, G, B, blinkMode, leds] (9 bytes)Initializes dice with sensitivity and LED configuration
0x3149Set Tap Interrupt[enable] (1 byte, 0=disable, 1=enable)Enables/disables single tap event notifications. Disabled by default.
0x3250Set Double Tap Interrupt[enable] (1 byte, 0=disable, 1=enable)Enables/disables double tap event notifications. Disabled by default.
0x65101Detection Settings[samplesCount, movementCount, faceCount, minFlatDeg, maxFlatDeg, weakStable, movementDeg, rollThreshold] (8 bytes)Updates roll detection sensitivity parameters

Receiving Events (Dice → Host)

The dice sends byte packets on state changes as notifications on the Notify Characteristic. The first byte determines the event type. Some events use ASCII prefixes for identification.

First Byte(s)ASCIIEventPayloadDescription
0x52RRollStart(none)Dice is currently rolling
0x53SStable[X, Y, Z] (3 signed bytes, offset 1)Dice is stable and flat; face derived from XYZ
0x46 0x53FSFakeStable[X, Y, Z] (3 signed bytes, offset 2)Stable after a “fake” roll; face derived from XYZ
0x54 0x53TSTiltStable[X, Y, Z] (3 signed bytes, offset 2)Stable but tilted (not flat); face derived from XYZ
0x4D 0x53MSMoveStable[X, Y, Z] (3 signed bytes, offset 2)Stable after small movement (face rotation); face derived from XYZ
0x42 0x61 0x74BatBatteryLevel[level] (1 byte, offset 3)Battery level response (0–100 percent)
0x43 0x6F 0x6CColDiceColor[color] (1 byte, offset 3)Dice color response
0x43 0x68 0x61 0x72CharCharging[charging] (1 byte, offset 4)Charging status (0 = not charging, 1 = charging)
0x54 0x61 0x70TapTap(none)Single tap detected (no payload)
0x44 0x54 0x61 0x70DTapDoubleTap(none)Double tap detected (no payload)

Dice Colors

ValueColor
0Black
1Red
2Green
3Blue
4Yellow
5Orange

Dice Types (Shells)

ValueTypeVector Table
0D6d6Vectors
1D20d20Vectors
2D10d20Vectors → d10Transform
3D10Xd20Vectors → d10XTransform
4D4d24Vectors → d4Transform
5D8d24Vectors → d8Transform
6D12d24Vectors → d12Transform

Note: D10X is also referred to as D100 (percentile) in the C API. The transform maps the D20 vector index to a D10 face value multiplied by 10 (i.e. d10_transform(roll) * 10), yielding values 0, 10, 20, …, 90.

setDieType is a client-side setting - no command is sent to the dice. Instead, it selects which vector table and transform to use when interpreting the XYZ accelerometer data to determine the face value.

Face Value Determination

The dice does not send the rolled number directly. Instead, it sends raw XYZ accelerometer data (3 signed 8-bit integers). The client determines the upper face by finding the closest vector in a pre-defined table:

  1. Extract [x, y, z] from the notification payload.
  2. Look up the vector table for the current DiceType.
  3. For each entry (face_value, reference_vector), compute the Euclidean distance: sqrt((x - rx)² + (y - ry)² + (z - rz)²). (The squared distance without sqrt is functionally equivalent for finding the minimum, since sqrt is monotonically increasing.)
  4. Return the face value with the smallest distance.
  5. If a shell transform applies (D10, D10X, D4, D8, D12), map the intermediate value through the transform table.

D6 Vector Table

FaceXYZ
1-6400
20064
30640
40-640
500-64
66400

D20 Vector Table

FaceXYZ
1-640-22
242-4240
3022-64
402264
5-42-4242
622640
7-42-42-42
8640-22
9-22640
1042-42-42
11-424242
1222-640
13-64022
14424242
15-22-640
164242-42
170-22-64
180-2264
19-4242-42
2064022

D24 Vector Table

FaceXYZ
120-60-20
220060
3-40-4040
4-60020
5402040
6-20-60-20
7206020
8-4020-40
9-404040
10-20060
11-20-6020
1260020
13-600-20
142060-20
15200-60
1640-20-40
17-2060-20
18-40-40-40
1940-2040
2020-6020
21600-20
224020-40
23-200-60
24-206020

Shell Transform Tables

Each transform maps the vector table index (1-based) to the final face value. D6 and D20 use identity (no transform). D10X multiplies the D10 transform by 10.

D4 Transform (D24 → D4)

Index0102030405060708
Face31414414
Index0910111213141516
Face23111423
Index1718192021222324
Face32224132

D8 Transform (D24 → D8)

Index0102030405060708
Face33612811
Index0910111213141516
Face47554425
Index1718192021222324
Face77828366

D10 Transform (D20 → D10)

Index01020304050607080910
Face8261439075
Index11121314151617181920
Face5709341628

D10X Transform (D20 → D10X)

Index01020304050607080910
Face8020601040309007050
Index11121314151617181920
Face5070090304010602080

D12 Transform (D24 → D12)

Index0102030405060708
Face12345678
Index0910111213141516
Face91011121234
Index1718192021222324
Face56789101112