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.
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.
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.
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
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)
ASCII
Event
Payload
Description
0x52
R
RollStart
(none)
Dice is currently rolling
0x53
S
Stable
[X, Y, Z] (3 signed bytes, offset 1)
Dice is stable and flat; face derived from XYZ
0x46 0x53
FS
FakeStable
[X, Y, Z] (3 signed bytes, offset 2)
Stable after a “fake” roll; face derived from XYZ
0x54 0x53
TS
TiltStable
[X, Y, Z] (3 signed bytes, offset 2)
Stable but tilted (not flat); face derived from XYZ
0x4D 0x53
MS
MoveStable
[X, Y, Z] (3 signed bytes, offset 2)
Stable after small movement (face rotation); face derived from XYZ
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.
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:
Extract [x, y, z] from the notification payload.
Look up the vector table for the current DiceType.
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.)
Return the face value with the smallest distance.
If a shell transform applies (D10, D10X, D4, D8, D12), map the
intermediate value through the transform table.
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.