Introduction
dice-rs is a Rust library and toolkit for controlling
GoDice - physical
Bluetooth dice that communicate over Bluetooth Low Energy (BLE).
Motivation
GoDice are physical dice with embedded LEDs, an accelerometer, and a BLE
radio. They use the Nordic UART Service (NUS) profile for communication.
The official APIs from Particula are in JavaScript and Python. dice-rs
brings native Rust support with an idiomatic async API, type-safe domain
model, and a trait-based transport layer for testability.
Scope
The workspace provides five crates:
dice-rs- core library with domain types, BLE transport, and a high-level service APIdice-rs-cli- command-line tool for quick interactionsdice-rs-controller- GTK 4 desktop application with 3D dice renderingdice-rs-ws- WebSocket server for network-accessible dice eventsyatzy- Kniffel (Yatzy) game with GTK 4 UI, AI opponent, and GoDice integration (see Yatzy)
Where to Get Help
- GitHub Issues - bug reports and feature requests
- BLE Protocol - canonical protocol reference
- API Reference (docs.rs) - rustdoc for all crates
Getting Started
Add the Dependency
Add dice-rs to your Cargo.toml:
[dependencies]
dice-rs = "0.1"
tokio = { version = "1", features = ["full"] }
Tokio Runtime
dice-rs is async-only. You need a tokio runtime to drive the BLE operations:
use dice_rs::{DiceManager, DiceEvent};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
if devices.is_empty() {
println!("No GoDice devices found");
return Ok(());
}
for device in &devices {
println!("Found: {device}");
}
let dice = manager.connect(&devices[0]).await?;
let mut receiver = dice.subscribe();
while let Ok(event) = receiver.recv().await {
match event {
DiceEvent::Stable { face, .. } => {
println!("Rolled: {face}");
break;
}
DiceEvent::RollStart => println!("Rolling..."),
DiceEvent::Disconnected => break,
_ => {}
}
}
dice.disconnect().await?;
Ok(())
}
First Connection
- Create a
DiceManager- this initializes the BLE adapter. - Call
scan()to discover nearby GoDice devices (filtered byGoDice_prefix). - Call
connect()with aDiceDeviceto establish a BLE connection. - Call
subscribe()to get abroadcast::Receiver<DiceEvent>. - Listen for events in a loop.
The Dice handle is Clone, so you can share it across tasks. All clones
share the same underlying connection state.
Architecture
Workspace Structure
dice-rs/
├── dice-rs/ # Core library
│ ├── src/
│ │ ├── ble/ # BLE transport layer (commands, events, transport trait)
│ │ ├── model/ # Domain types (FaceValue, LedColor, DiceType, etc.)
│ │ ├── service/ # High-level API (DiceManager, Dice, DiceEvent)
│ │ ├── error.rs # Error types
│ │ └── lib.rs # Re-exports
├── dice-rs-cli/ # CLI tool
├── dice-rs-controller/ # GTK 4 desktop app
├── dice-rs-ws/ # WebSocket server
└── games/
└── yatzy/ # Kniffel (Yatzy) game
├── src/
│ ├── models/ # Domain types (GameState, Scorecard, Player, etc.)
│ ├── rules/ # Scoring rules, validation, cross-out advisor
│ ├── services/ # GameController, DiceService, EventBridge, LED, etc.
│ ├── strategy/ # ComputerAi, ExpectedValue, Probability
│ ├── ui/ # GTK 4 application, window, widgets
│ ├── i18n.rs # Fluent internationalization
│ ├── error.rs # Error types
│ └── lib.rs # Re-exports
Module Layout
ble - Protocol Layer
Handles raw BLE communication: encoding commands, decoding notification
events, and the BleTransport trait that abstracts the BLE backend.
command-Commandenum encoding host-to-dice byte sequencesevent-Eventenum decoding dice-to-host notificationstransport-BleTransportandBlePeripheraltraitsuuids- NUS service and characteristic UUIDs
model - Domain Types
Type-safe wrappers for GoDice data:
FaceValue- rolled face (1-based, rejects 0)Acceleration- raw XYZ accelerometer data with face interpretationLedColor- RGB color with named constants and hex parsingBatteryLevel- 0–100 percentChargingState- charging or not chargingDiceColor- physical shell color (Black, Red, Green, Blue, Yellow, Orange)DiceType- dice shell type (D6, D20, D10, D10X, D4, D8, D12)
service - High-Level API
DiceManager- manages BLE adapter and multiple dice connectionsDice- handle to a connected die with LED, battery, and event methodsDiceEvent- high-level events emitted via broadcast channelDiceScanner- device discovery with name-prefix filtering
Data Flow
flowchart TD
subgraph Dice["GoDice Hardware"]
accel["Accelerometer"]
ble_radio["BLE Radio"]
end
subgraph Transport["BLE Transport Layer"]
btleplug["btleplug
(BlueZ DBus)"]
trait["BleTransport Trait"]
end
subgraph Service["Service Layer"]
manager["DiceManager"]
dice["Dice Handle"]
channel["broadcast::Channel
DiceEvent"]
end
subgraph Consumer["Application"]
recv["Receiver"]
end
accel --> ble_radio
ble_radio -->|NUS notifications| btleplug
btleplug --> trait
trait -->|Event parse| dice
dice -->|DiceEvent| channel
channel --> recv
manager -->|connect/disconnect| trait
BLE Backend
The primary BLE backend is btleplug,
which uses BlueZ DBus on Linux. The BleTransport trait abstracts the backend,
enabling mock implementations for testing and future backends like
bluer.
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
| Property | Description | Value |
|---|---|---|
| Device name | Prefix | GoDice_ |
| Service UUID | NUS Service (16-bit offset 0x0001) | 6e400001-b5a3-f393-e0a9-e50e24dcca9e |
| Write Characteristic | NUS RX - host writes commands | 6e400002-b5a3-f393-e0a9-e50e24dcca9e |
| Notify Characteristic | NUS TX - dice sends notifications | 6e400003-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 - 3bytes (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.
| Opcode | Decimal | Command | Payload Bytes | Description |
|---|---|---|---|---|
| 0x03 | 3 | Get Battery Level | (none) | Response: Bat + level byte |
| 0x08 | 8 | Set LEDs | [R1, G1, B1, R2, G2, B2] (6 bytes, 0–255) | Sets both RGB LEDs; [0,0,0,0,0,0] turns off |
| 0x10 | 16 | Pulse 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 |
| 0x14 | 20 | Stop Pulse LEDs | (none) | Stops any active pulse LED animation |
| 0x17 | 23 | Get Dice Color | (none) | Response: Col + color byte |
| 0x19 | 25 | Init | [sensitivity, pulseCount, onTime, offTime, R, G, B, blinkMode, leds] (9 bytes) | Initializes dice with sensitivity and LED configuration |
| 0x31 | 49 | Set Tap Interrupt | [enable] (1 byte, 0=disable, 1=enable) | Enables/disables single tap event notifications. Disabled by default. |
| 0x32 | 50 | Set Double Tap Interrupt | [enable] (1 byte, 0=disable, 1=enable) | Enables/disables double tap event notifications. Disabled by default. |
| 0x65 | 101 | Detection 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) | 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 |
| 0x42 0x61 0x74 | Bat | BatteryLevel | [level] (1 byte, offset 3) | Battery level response (0–100 percent) |
| 0x43 0x6F 0x6C | Col | DiceColor | [color] (1 byte, offset 3) | Dice color response |
| 0x43 0x68 0x61 0x72 | Char | Charging | [charging] (1 byte, offset 4) | Charging status (0 = not charging, 1 = charging) |
| 0x54 0x61 0x70 | Tap | Tap | (none) | Single tap detected (no payload) |
| 0x44 0x54 0x61 0x70 | DTap | DoubleTap | (none) | Double tap detected (no payload) |
Dice Colors
| Value | Color |
|---|---|
| 0 | Black |
| 1 | Red |
| 2 | Green |
| 3 | Blue |
| 4 | Yellow |
| 5 | Orange |
Dice Types (Shells)
| Value | Type | Vector Table |
|---|---|---|
| 0 | D6 | d6Vectors |
| 1 | D20 | d20Vectors |
| 2 | D10 | d20Vectors → d10Transform |
| 3 | D10X | d20Vectors → d10XTransform |
| 4 | D4 | d24Vectors → d4Transform |
| 5 | D8 | d24Vectors → d8Transform |
| 6 | D12 | d24Vectors → 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:
- 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 withoutsqrtis functionally equivalent for finding the minimum, sincesqrtis 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.
D6 Vector Table
| Face | X | Y | Z |
|---|---|---|---|
| 1 | -64 | 0 | 0 |
| 2 | 0 | 0 | 64 |
| 3 | 0 | 64 | 0 |
| 4 | 0 | -64 | 0 |
| 5 | 0 | 0 | -64 |
| 6 | 64 | 0 | 0 |
D20 Vector Table
| Face | X | Y | Z |
|---|---|---|---|
| 1 | -64 | 0 | -22 |
| 2 | 42 | -42 | 40 |
| 3 | 0 | 22 | -64 |
| 4 | 0 | 22 | 64 |
| 5 | -42 | -42 | 42 |
| 6 | 22 | 64 | 0 |
| 7 | -42 | -42 | -42 |
| 8 | 64 | 0 | -22 |
| 9 | -22 | 64 | 0 |
| 10 | 42 | -42 | -42 |
| 11 | -42 | 42 | 42 |
| 12 | 22 | -64 | 0 |
| 13 | -64 | 0 | 22 |
| 14 | 42 | 42 | 42 |
| 15 | -22 | -64 | 0 |
| 16 | 42 | 42 | -42 |
| 17 | 0 | -22 | -64 |
| 18 | 0 | -22 | 64 |
| 19 | -42 | 42 | -42 |
| 20 | 64 | 0 | 22 |
D24 Vector Table
| Face | X | Y | Z |
|---|---|---|---|
| 1 | 20 | -60 | -20 |
| 2 | 20 | 0 | 60 |
| 3 | -40 | -40 | 40 |
| 4 | -60 | 0 | 20 |
| 5 | 40 | 20 | 40 |
| 6 | -20 | -60 | -20 |
| 7 | 20 | 60 | 20 |
| 8 | -40 | 20 | -40 |
| 9 | -40 | 40 | 40 |
| 10 | -20 | 0 | 60 |
| 11 | -20 | -60 | 20 |
| 12 | 60 | 0 | 20 |
| 13 | -60 | 0 | -20 |
| 14 | 20 | 60 | -20 |
| 15 | 20 | 0 | -60 |
| 16 | 40 | -20 | -40 |
| 17 | -20 | 60 | -20 |
| 18 | -40 | -40 | -40 |
| 19 | 40 | -20 | 40 |
| 20 | 20 | -60 | 20 |
| 21 | 60 | 0 | -20 |
| 22 | 40 | 20 | -40 |
| 23 | -20 | 0 | -60 |
| 24 | -20 | 60 | 20 |
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)
| Index | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 |
|---|---|---|---|---|---|---|---|---|
| Face | 3 | 1 | 4 | 1 | 4 | 4 | 1 | 4 |
| Index | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|
| Face | 2 | 3 | 1 | 1 | 1 | 4 | 2 | 3 |
| Index | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
|---|---|---|---|---|---|---|---|---|
| Face | 3 | 2 | 2 | 2 | 4 | 1 | 3 | 2 |
D8 Transform (D24 → D8)
| Index | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 |
|---|---|---|---|---|---|---|---|---|
| Face | 3 | 3 | 6 | 1 | 2 | 8 | 1 | 1 |
| Index | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|
| Face | 4 | 7 | 5 | 5 | 4 | 4 | 2 | 5 |
| Index | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
|---|---|---|---|---|---|---|---|---|
| Face | 7 | 7 | 8 | 2 | 8 | 3 | 6 | 6 |
D10 Transform (D20 → D10)
| Index | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Face | 8 | 2 | 6 | 1 | 4 | 3 | 9 | 0 | 7 | 5 |
| Index | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|---|
| Face | 5 | 7 | 0 | 9 | 3 | 4 | 1 | 6 | 2 | 8 |
D10X Transform (D20 → D10X)
| Index | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| Face | 80 | 20 | 60 | 10 | 40 | 30 | 90 | 0 | 70 | 50 |
| Index | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
|---|---|---|---|---|---|---|---|---|---|---|
| Face | 50 | 70 | 0 | 90 | 30 | 40 | 10 | 60 | 20 | 80 |
D12 Transform (D24 → D12)
| Index | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 |
|---|---|---|---|---|---|---|---|---|
| Face | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| Index | 09 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
|---|---|---|---|---|---|---|---|---|
| Face | 9 | 10 | 11 | 12 | 1 | 2 | 3 | 4 |
| Index | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
|---|---|---|---|---|---|---|---|---|
| Face | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Scanning & Connecting
DiceScanner
DiceScanner discovers GoDice devices by scanning for BLE peripherals with
the GoDice_ name prefix. It wraps the BleTransport trait and provides
configurable scan duration.
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
for device in &devices {
println!("Found: {device}");
}
Ok(())
}
DiceManager
DiceManager is the entry point for all BLE operations. It manages the BLE
adapter and provides methods for scanning, connecting, and disconnecting.
Multi-Dice Connections
You can connect to multiple dice concurrently. Each Dice handle is
independent and has its own event channel:
use dice_rs::DiceManager;
use dice_rs::DiceEvent;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let mut dice_list = Vec::new();
for device in &devices {
let dice = manager.connect(device).await?;
dice_list.push(dice);
}
// Each dice has its own event receiver
for dice in &dice_list {
let mut receiver = dice.subscribe();
// Spawn a task per dice to listen for events
let name = dice.name().to_string();
tokio::spawn(async move {
while let Ok(event) = receiver.recv().await {
println!("{name}: {event}");
}
});
}
Ok(())
}
Connection Retry
DiceManager::connect() retries up to 3 times with 1-second backoff. This
handles transient connection failures, such as a GoDice that is advertising
but not yet accepting connections while charging from 0% battery.
Reconnect
DiceManager::reconnect() attempts to reconnect a disconnected dice with
exponential backoff (500ms → 5s, up to 10 retries).
Find by Address
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let dice = manager.connect_by_address("AA:BB:CC").await?;
println!("Connected to {}", dice.name());
Ok(())
}
The address parameter accepts a partial MAC address - the first device whose address contains the given substring is selected.
Disconnect
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
// Disconnect all connected GoDice devices
let count = manager.disconnect_all().await?;
println!("Disconnected {count} dice");
Ok(())
}
Dice Events
Event Types
DiceEvent is the high-level event enum emitted by a connected GoDice.
Events are delivered via a tokio::sync::broadcast channel.
| Variant | Description |
|---|---|
RollStart | Dice has started rolling |
Stable { face, acceleration } | Dice is stable and flat after a roll |
TiltStable { face, acceleration } | Stable but tilted |
FakeStable { face, acceleration } | Stable after a fake roll |
MoveStable { face, acceleration } | Stable after small movement |
Charging { state } | Charging status changed |
Tap | Single tap detected (must be enabled) |
DoubleTap | Double tap detected (must be enabled) |
Disconnected | BLE link lost or dice disconnected |
Subscribing to Events
use dice_rs::{DiceManager, DiceEvent};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let mut receiver = dice.subscribe();
while let Ok(event) = receiver.recv().await {
match event {
DiceEvent::Stable { face, acceleration } => {
println!("Stable on face {face} (accel: {acceleration:?})");
}
DiceEvent::RollStart => println!("Rolling..."),
DiceEvent::TiltStable { face, .. } => println!("Tilt stable: {face}"),
DiceEvent::FakeStable { face, .. } => println!("Fake stable: {face}"),
DiceEvent::MoveStable { face, .. } => println!("Move stable: {face}"),
DiceEvent::Charging { state } => println!("Charging: {state}"),
DiceEvent::Tap => println!("Tap!"),
DiceEvent::DoubleTap => println!("Double tap!"),
DiceEvent::Disconnected => {
println!("Disconnected");
break;
}
}
}
Ok(())
}
Multiple Subscribers
The broadcast channel supports multiple subscribers. Each call to
subscribe() returns an independent receiver:
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let mut rx1 = dice.subscribe();
let mut rx2 = dice.subscribe();
// Both receivers get the same events
tokio::spawn(async move {
while let Ok(event) = rx1.recv().await {
println!("Subscriber 1: {event}");
}
});
while let Ok(event) = rx2.recv().await {
println!("Subscriber 2: {event}");
}
Ok(())
}
Accelerometer Data
Stable, TiltStable, FakeStable, and MoveStable events include an
Acceleration struct with raw XYZ accelerometer data (three i8 values).
The face value is computed by finding the closest reference vector in the
dice type’s vector table. See BLE Protocol for the
full vector tables.
Tap Events
Tap and double tap notifications are disabled by default. Enable them explicitly:
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
dice.enable_tap().await?;
dice.enable_double_tap().await?;
let mut receiver = dice.subscribe();
while let Ok(event) = receiver.recv().await {
println!("{event}");
}
Ok(())
}
LED Control
GoDice has two RGB LEDs. The dice-rs library provides methods for setting
colors and pulse animations.
LedColor
LedColor is an RGB color with channels 0–255:
#![allow(unused)]
fn main() {
use dice_rs::LedColor;
let red = LedColor::RED;
let custom = LedColor::new(128, 64, 255);
let from_hex = LedColor::from_hex(0xFF8800);
let off = LedColor::OFF;
}
Set LEDs
use dice_rs::{DiceManager, LedColor};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
// Set both LEDs to the same color
dice.set_led(LedColor::GREEN).await?;
// Set each LED independently
dice.set_leds(LedColor::RED, LedColor::BLUE).await?;
// Turn off
dice.turn_off_leds().await?;
Ok(())
}
Debouncing
set_leds() uses a debounce mechanism: rapid successive calls within a 30ms
window are coalesced into a single BLE write. Only the most recent colors
are written. This prevents BlueZ/DBus socket buffer overflow when an
application fires many color changes in quick succession.
For one-shot commands where coalescing is undesirable, use
set_leds_immediate():
use dice_rs::{DiceManager, LedColor};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
// Write immediately, bypassing debounce
dice.set_leds_immediate(LedColor::RED, LedColor::RED).await?;
Ok(())
}
Pulse LEDs
Pulse animations blink the LEDs with configurable timing:
use dice_rs::{DiceManager, LedColor};
use dice_rs::model::led::PulseBlinkMode;
use dice_rs::model::led::PulseLeds;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
// Pulse 3 times: 500ms on, 200ms off, green, solid color, both LEDs
dice.pulse_leds(3, 50, 20, LedColor::GREEN, PulseBlinkMode::Color, PulseLeds::Both).await?;
// Convenience: single pulse
dice.pulse_once(50, 20, LedColor::RED).await?;
Ok(())
}
on_time and off_time are in units of 10ms. Maximum value is 255 (2.55s).
PulseBlinkMode
| Mode | Description |
|---|---|
Color | Solid color blink |
Rainbow | Rainbow color cycle |
PulseLeds
| Value | Description |
|---|---|
Both | Both LEDs pulse |
Led1 | Only LED 1 pulses |
Led2 | Only LED 2 pulses |
Battery & Status
Battery Level
Query the battery level (0–100 percent):
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let battery = dice.get_battery_level().await?;
println!("Battery: {battery}");
Ok(())
}
The request uses a oneshot channel with a 5-second timeout. If the dice
does not respond, DiceError::ResponseTimeout is returned.
Dice Color
Query the physical shell color:
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let color = dice.get_color().await?;
println!("Color: {color}");
Ok(())
}
Charging State
The charging state is updated automatically from notifications. Query the last known state without sending a BLE command:
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let charging = dice.charging_state();
println!("Charging: {charging}");
Ok(())
}
Charging state changes are also delivered as DiceEvent::Charging events.
RSSI
Query the signal strength (if available):
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
if let Some(rssi) = dice.rssi().await? {
println!("RSSI: {rssi} dBm");
}
Ok(())
}
RSSI availability depends on the Bluetooth adapter and BlueZ version.
System Status
Get all status information in a single call:
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
let status = dice.system_status().await?;
println!("Battery: {}", status.battery_level);
println!("Color: {}", status.color);
println!("Connected: {}", status.connected);
if let Some(rssi) = status.rssi {
println!("RSSI: {rssi} dBm");
}
Ok(())
}
system_status() performs battery level and color queries concurrently for
efficiency.
Calibration
Software Calibration
Software calibration computes an AccelerationOffset from the next Stable
event. The offset is the difference between the measured acceleration and
the expected ideal gravity vector for the current dice type. The offset is
then subtracted from all subsequent accelerometer readings before face value
interpretation.
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
println!("Place the dice on a flat surface, then press Enter...");
// In a real app, wait for user input
let offset = dice.calibrate_software().await?;
println!("Calibration offset: {offset:?}");
Ok(())
}
After calibration, all subsequent Stable, TiltStable, FakeStable, and
MoveStable events use the corrected acceleration data for face value
determination.
Clear Calibration
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
dice.calibrate_software().await?;
// ... later ...
dice.clear_software_calibration()?;
Ok(())
}
Hardware Calibration
Hardware calibration sends a BLE command (opcode 0x13) to the dice. The
exact byte encoding is unconfirmed - this method is tentative and may not
work with all firmware versions.
use dice_rs::DiceManager;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = DiceManager::new().await?;
let devices = manager.scan().await?;
let dice = manager.connect(&devices[0]).await?;
dice.calibrate().await?;
println!("Hardware calibration complete");
Ok(())
}
If the dice reports a calibration failure, DiceError::CalibrationFailed is
returned. If the dice does not respond within 5 seconds,
DiceError::ResponseTimeout is returned.
CLI Tool
The dice-rs-cli crate provides a command-line tool (dice-rs) for
interacting with GoDice devices without writing code.
Installation
cargo install dice-rs-cli
Commands
Scan
Scan for nearby GoDice devices:
dice-rs scan 5
The argument is the scan duration in seconds.
Listen
Connect to a dice and print events:
dice-rs listen AA:BB:CC:DD:EE:FF d6
The first argument is the MAC address (or a partial prefix). The second is
the dice type (d6, d20, d10, d10x, d4, d8, d12).
Battery
Query battery level:
dice-rs battery AA:BB:CC:DD:EE:FF
LED Control
Set LED colors:
# Set both LEDs to red
dice-rs led AA:BB:CC:DD:EE:FF set red
# Set each LED independently
dice-rs led AA:BB:CC:DD:EE:FF set-dual red blue
# Pulse animation
dice-rs led AA:BB:CC:DD:EE:FF pulse green
# Turn off
dice-rs led AA:BB:CC:DD:EE:FF off
Tap Interrupts
Enable or disable tap notifications:
dice-rs tap AA:BB:CC:DD:EE:FF true
dice-rs double-tap AA:BB:CC:DD:EE:FF true
Calibration
dice-rs calibrate AA:BB:CC:DD:EE:FF
Status
Get comprehensive status:
dice-rs status AA:BB:CC:DD:EE:FF
Color
Query the dice shell color:
dice-rs color AA:BB:CC:DD:EE:FF
Charging
Query charging state:
dice-rs charging AA:BB:CC:DD:EE:FF
Disconnect
# Disconnect a specific dice
dice-rs disconnect AA:BB:CC:DD:EE:FF
# Disconnect all connected dice
dice-rs disconnect-all
Interactive Mode
Start a REPL for interactive exploration:
dice-rs interactive
Available commands in interactive mode: scan, connect <address>,
disconnect, battery, color, charging, led <color>, status,
calibrate, quit.
Controller
The dice-rs-controller crate is a GTK 4 desktop application for managing
GoDice devices with a graphical interface and 3D dice rendering.
Features
- Auto-scan on startup with manual rescan button
- Dice list showing all connected dice with face value, battery, and color
- Dice type selector - dropdown to choose between D6, D20, D10, D10X, D4, D8, D12
- 3D dice rendering using OpenGL (
glow+glam) with real-time orientation from accelerometer data and per-type geometry - LED controls - color pickers, set/pulse/off buttons
- Tap controls - enable/disable tap and double tap notifications
- Battery indicator with visual gauge
- Roll history showing recent face values
- Tap indicator flashing on tap events
- Reconnect support for dropped connections
Building
The controller requires GTK 4 development libraries:
# Ubuntu/Debian
sudo apt install libgtk-4-dev
# Build
cargo build -p dice-rs-controller
Running
cargo run -p dice-rs-controller
The application window appears with a scan button and an empty dice list. Connected dice appear as rows with interactive controls.
Architecture
flowchart TB
app["Application
(gtk4::Application)"]
window["MainWindow
(scan, dice list)"]
row["DiceRow
(per-dice UI)"]
controller["EventController
(async → GTK bridge)"]
dice3d["Dice3D
(OpenGL renderer)"]
models["Models
(DiceModelTrait)"]
led["LedControls
(color pickers)"]
tap["TapControls
(tap switches)"]
battery["BatteryIndicator"]
history["RollHistory"]
face["FaceDisplay"]
typesel["DropDown
(dice type)"]
app --> window
window --> row
row --> controller
row --> dice3d
dice3d --> models
row --> led
row --> tap
row --> battery
row --> history
row --> face
row --> typesel
typesel -->|set_dice_type| dice3d
EventController
The EventController bridges async dice events into the GTK main loop. It
runs a background tokio task that receives DiceEvents and sends UI updates
through an mpsc channel to the GTK main thread. This avoids blocking the
UI while waiting for BLE events.
Dice Type Selection
Each DiceRow includes a DropDown widget for selecting the dice type
(D6, D20, D10, D10X, D4, D8, D12). Changing the selection calls
dice.set_dice_type() on the dice-rs library handle (client-side setting,
no BLE command) and updates the 3D model geometry via Dice3D::set_dice_type().
The dice type controls how accelerometer data is interpreted into face values using vector tables and shell transforms. See Architecture for details on the interpretation pipeline.
3D Dice Models
The DiceModelTrait defines the interface for 3D geometry generation. Each
dice type has a dedicated model implementation:
| Type | Shape | Faces | Vertices |
|---|---|---|---|
| D6 | Cube | 6 quads | 24 |
| D4 | Tetrahedron | 4 triangles | 12 |
| D8 | Octahedron | 8 triangles | 24 |
| D10/D10X | Pentagonal trapezohedron | 10 kites | 60 |
| D12 | Dodecahedron | 12 pentagons | 72 |
| D20 | Icosahedron | 20 triangles | 60 |
The model_for_type() factory function selects the appropriate model based
on DiceType. The DiceRenderer uploads vertex data (positions, normals,
UVs, face IDs) to OpenGL and renders with diffuse lighting and edge
highlighting. D6 faces additionally render procedural pips via the fragment
shader; other die types show face color with edges only.
When the dice type changes at runtime, the Dice3D widget discards the
existing renderer and re-initializes with the new model geometry on the
next render frame.
WebSocket Server
The dice-rs-ws crate provides a WebSocket and REST server for exposing
GoDice events over a network API.
Running
cargo run -p dice-rs-ws
The server starts on 0.0.0.0:3000.
REST API
Scan
POST /api/scan
Returns a JSON array of discovered devices:
[
{
"address": "AA:BB:CC:DD:EE:FF",
"name": "GoDice_AAABBCC_O_v04",
"rssi": -45
}
]
Connect
POST /api/connect
Content-Type: application/json
{
"address": "AA:BB:CC:DD:EE:FF",
"dice_type": "d6"
}
Returns a session ID:
{
"session_id": "abc123"
}
dice_type is optional. Accepts: d6, d20, d10, d10x, d4, d8, d12.
Disconnect
POST /api/disconnect
Content-Type: application/json
{
"session_id": "abc123"
}
LED Control
POST /api/led
Content-Type: application/json
{
"session_id": "abc123",
"led1": "FF0000",
"led2": "00FF00"
}
Battery
GET /api/battery/:session_id
Status
GET /api/status/:session_id
Calibration
POST /api/calibrate
Content-Type: application/json
{
"session_id": "abc123"
}
WebSocket API
Connect to ws://localhost:3000/ws to receive a real-time stream of
DiceEvents as JSON:
{
"kind": "Stable",
"face": 6,
"acceleration": { "x": 64, "y": 0, "z": 0 }
}
{
"kind": "RollStart"
}
{
"kind": "Disconnected"
}
Deployment
The server uses axum with tokio. For production deployment, consider
running behind a reverse proxy (e.g. nginx) with TLS termination.
Yatzy (Kniffel)
The yatzy crate is a full Kniffel (Yatzy) game for GoDice, built with
GTK 4 and the dice-rs library. It demonstrates a complete game loop with
physical dice integration: scan, connect, roll, score, and celebrate.
Features
- Full Kniffel scorecard with all 13 categories in upper and lower sections
- Game state machine with turn phases: AwaitingRoll, Rolling, Holding, Scoring, TurnEnd
- Single-player mode with a computer AI opponent using expected-value calculations for optimal decisions
- Multi-player pass-and-play mode with turn transitions and hints disabled for fairness
- Strategy hints showing the best category and best hold decision via expected value analysis (single-player mode only)
- Cross-out advisor recommending the least-damaging category when no valid score is possible
- LED celebration effects for special rolls: Yatzy, Full House, Large Straight, Small Straight, Four-of-a-Kind
- LED color assignment per player for visual identification on the physical dice
- Dice slot mapping and roll detection for GoDice integration
- Reconnection manager for handling dropped dice connections during gameplay
- Persistent highscore list stored as JSON (top 20 entries)
- Persistent game settings (player names, colors, types, game mode)
- Internationalization with Fluent, German and English translations
Building
The game requires GTK 4 development libraries, same as the controller:
# Ubuntu/Debian
sudo apt install libgtk-4-dev
# Build
cargo build -p yatzy
Running
cargo run -p yatzy
The application starts with a setup screen for configuring players (names, colors, human/computer type) and selecting the game mode. After setup, scan for GoDice devices and connect. Each player rolls the physical dice, holds dice between rolls, and enters scores in the scorecard.
Game Flow
flowchart TD
setup["Setup Screen
(player config, game mode)"]
scan["Dice Scanning
& Connection"]
awaiting["AwaitingRoll
(player starts rolling)"]
rolling["Rolling
(dice in motion)"]
holding["Holding
(dice stable, decide holds)"]
scoring["Scoring
(must enter or cross out)"]
turnend["TurnEnd
(advance to next player)"]
gameover["GameOver
(final standings)"]
setup --> scan
scan --> awaiting
awaiting --> rolling
rolling --> holding
holding -->|roll again| rolling
holding -->|score early| scoring
holding -->|3 rolls used| scoring
scoring --> turnend
turnend -->|rounds remaining| awaiting
turnend -->|all rounds done| gameover
Turn Phases
| Phase | Description |
|---|---|
AwaitingRoll | Waiting for the player to start rolling |
Rolling | Dice are physically rolling (RollStart received, no Stable yet) |
Holding | Dice are stable; player can hold dice and roll again, or proceed to scoring |
Scoring | Player must select a category to enter or cross out |
TurnEnd | Score has been entered, transitioning to the next player |
Each player has up to 3 rolls per turn. After the 3rd roll, the player must enter a score or cross out a category.
Scorecard Categories
The 13 categories are divided into an upper and lower section:
Upper Section
| Category | Rule | Score |
|---|---|---|
| Ones | Sum of all 1s | Count × 1 |
| Twos | Sum of all 2s | Count × 2 |
| Threes | Sum of all 3s | Count × 3 |
| Fours | Sum of all 4s | Count × 4 |
| Fives | Sum of all 5s | Count × 5 |
| Sixes | Sum of all 6s | Count × 6 |
If the upper section total reaches 63 or more, a bonus of 35 points is awarded.
Lower Section
| Category | Rule | Score |
|---|---|---|
| Three-of-a-Kind | At least 3 dice with same value | Sum of all dice |
| Four-of-a-Kind | At least 4 dice with same value | Sum of all dice |
| Full House | 3 of one value + 2 of another | 25 |
| Small Straight | 4 consecutive values | 30 |
| Large Straight | 5 consecutive values | 40 |
| Yatzy | All 5 dice same value | 50 |
| Chance | Any combination | Sum of all dice |
Computer AI
The ComputerAi decision engine uses expected value (EV) calculations to
make optimal decisions:
- Roll or score: Compares the current best category score against the EV of re-rolling. If the current score meets or exceeds the EV, the AI enters the score; otherwise, it re-rolls.
- Hold decision: Enumerates all 32 possible hold masks (2^5) and selects the one with the highest EV across all empty categories.
- Category selection: When scoring, picks the empty category with the highest score.
- Cross-out: When no positive score is possible, the
CrossOutAdvisorrecommends the category with the lowest potential score to minimize lost points.
The EV calculation enumerates all possible re-roll outcomes (6^n where n is the number of re-rolled dice) and accumulates the expected score for each empty category.
Strategy Hints
In single-player mode, strategy hints are shown to help the player learn optimal play:
- Best category hint: Shows which category would yield the highest score with the current dice.
- Best hold hint: Shows which dice to hold for the highest EV on re-roll.
Hints are disabled in multi-player mode to ensure fair play between human players.
LED Effects
The game uses the physical dice LEDs for feedback:
| Event | Effect |
|---|---|
| Active player’s turn | All dice glow in the player’s color |
| Held dice | Held dice glow green, others off |
| Yatzy (5 of a kind) | 5 green pulses on all dice |
| Full House | 3 yellow pulses on all dice |
| Large Straight | 3 cyan pulses on all dice |
| Small Straight | 2 cyan pulses on all dice |
| Four-of-a-Kind | 2 orange pulses on all dice |
| Turn end / scoring | LEDs turned off |
Architecture
flowchart TB
subgraph UI["UI Layer (GTK 4)"]
app["Application"]
window["MainWindow
(setup, game, game-over screens)"]
widgets["Widgets
(scorecard, dice, hints, etc.)"]
end
subgraph Services["Service Layer"]
controller["GameController
(game flow, validation)"]
dice_service["DiceService
(BLE dice management)"]
event_bridge["EventBridge
(async → GTK bridge)"]
roll_detector["RollDetector
(roll/stable tracking)"]
led_service["LedService
(LED effect → BLE)"]
reconn["ReconnectionManager"]
end
subgraph Domain["Domain Layer"]
state["GameState
(players, rounds, phases)"]
scorecard["Scorecard
(13 categories)"]
scoring["Scoring Rules"]
validation["Validation"]
cross_out["CrossOutAdvisor"]
end
subgraph Strategy["Strategy Layer"]
ai["ComputerAi
(EV-based decisions)"]
ev["ExpectedValue
(hold/category analysis)"]
prob["Probability"]
end
subgraph DiceRS["dice-rs"]
manager["DiceManager"]
dice["Dice Handle"]
end
app --> window
window --> widgets
widgets --> controller
controller --> state
state --> scorecard
controller --> scoring
controller --> validation
controller --> cross_out
controller --> ai
ai --> ev
ev --> prob
controller --> led_service
led_service --> dice_service
dice_service --> manager
manager --> dice
event_bridge --> controller
event_bridge --> dice_service
roll_detector --> event_bridge
reconn --> dice_service
GameController
The GameController orchestrates the game flow. It wraps GameState and
validates all TurnActions against the current state before executing them.
Actions that are invalid for the current phase or game status return an
error. The controller emits ControllerEvents via a broadcast channel that
the UI layer subscribes to.
EventBridge
The EventBridge bridges async dice events from dice-rs into the GTK main
loop. It runs a background tokio task that receives DiceEvents, feeds them
to the RollDetector for roll/stable tracking, and sends UI updates through
a channel to the GTK main thread.
RollDetector
The RollDetector tracks the state of each physical die across roll events.
It maps RollStart and Stable events from individual dice to a unified
roll state, accounting for dice that may report at slightly different times.
Once all dice in a slot are stable, it produces a DiceSet for the game
controller.
Module Structure
games/yatzy/src/
├── models/ # Domain types (GameState, Scorecard, Player, etc.)
├── rules/ # Scoring rules, validation, cross-out advisor
├── services/ # GameController, DiceService, EventBridge, LED, etc.
├── strategy/ # ComputerAi, ExpectedValue, Probability
├── ui/ # GTK 4 application, window, widgets
├── i18n.rs # Fluent internationalization
├── error.rs # YatzyError types
└── lib.rs # Re-exports
Internationalization
The game uses the Fluent localization system
via i18n-embed. The fallback language is German (de), with English
translations provided as well. Translation files are located in
games/yatzy/i18n/{lang}/yatzy.ftl.
Platform Notes
Linux (BlueZ)
dice-rs uses btleplug which
communicates with BlueZ via DBus on Linux. This is the primary and
currently only supported platform.
Requirements
- BlueZ 5.x
bluetoothddaemon running- DBus session bus access
- A Bluetooth adapter with BLE support
Setup
# Check BlueZ is running
systemctl status bluetooth
# Start if needed
sudo systemctl start bluetooth
# Ensure the user has Bluetooth access
sudo usermod -aG bluetooth $USER
# Log out and back in for group changes to take effect
Permissions
The user running dice-rs needs DBus access to the Bluetooth adapter. In
most desktop environments this is granted automatically. On headless
servers or containers, you may need to:
- Start a DBus session:
export DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus - Ensure
bluetoothdis running with--experimentalflag if using extended BLE features.
Troubleshooting
“No Bluetooth adapter found”
Ensure bluetoothd is running and an adapter is present:
hciconfig -a
“Device not found during scan”
- Make sure the GoDice is awake (tap or roll to wake)
- Check that the dice is not already connected to another host
- Try disconnecting stale connections:
dice-rs disconnect-all - BlueZ does not deliver RSSI advertisement updates for connected devices. Disconnect before scanning.
“Connection failed after 3 retries”
- The dice may be charging from 0% battery and not yet accepting connections. Wait a few minutes and retry.
- Move closer to the Bluetooth adapter.
- Check for interference from other BLE devices.
“BlueZ/DBus socket buffer overflow”
This can happen when sending many LED commands in rapid succession. The
set_leds() method includes a 30ms debounce to prevent this. If using
set_leds_immediate(), throttle your calls.
macOS
dice-rs supports macOS via btleplug’s CoreBluetooth backend. The library
(dice-rs), CLI (dice-rs-cli), and WebSocket server (dice-rs-ws) are
fully supported. The GTK4 controller (dice-rs-controller) is Linux-only.
Requirements
- macOS 12 (Monterey) or later
- A Bluetooth adapter with BLE support (built-in on all modern Macs)
- Xcode Command Line Tools:
xcode-select --install
Permissions
macOS requires Bluetooth permission for the process. When running the CLI or WebSocket server for the first time, the OS will prompt for Bluetooth access. Grant the permission to your terminal or application.
Troubleshooting
“No Bluetooth adapter found”
Ensure Bluetooth is enabled in System Settings > Bluetooth.
“Device not found during scan”
- Make sure the GoDice is awake (tap or roll to wake)
- Check that the dice is not already connected to another host
- CoreBluetooth may cache device state — try toggling Bluetooth off/on
Windows
dice-rs supports Windows via btleplug’s WinRT backend. The library
(dice-rs), CLI (dice-rs-cli), and WebSocket server (dice-rs-ws) are
fully supported. The GTK4 controller (dice-rs-controller) is Linux-only.
Requirements
- Windows 10 (build 19041) or later
- A Bluetooth adapter with BLE support
- Visual Studio C++ Build Tools
Permissions
Windows requires Bluetooth access for the process. No explicit permission prompt is shown, but the Bluetooth radio must be enabled in Settings > Bluetooth & devices.
Troubleshooting
“No Bluetooth adapter found”
Ensure Bluetooth is enabled in Settings > Bluetooth & devices and that a compatible adapter is present.
“Device not found during scan”
- Make sure the GoDice is awake (tap or roll to wake)
- Check that the dice is not already connected to another host
- WinRT may not deliver RSSI updates for all devices — the library falls
back to cached properties when
read_rssi()fails
“Connection failed after 3 retries”
- The dice may be charging from 0% battery and not yet accepting connections. Wait a few minutes and retry.
- Move closer to the Bluetooth adapter.
- Check for interference from other BLE devices.
Controller (Linux-only)
The dice-rs-controller GTK4 desktop application is Linux-only. It depends
on GTK4, OpenGL (glow), and BlueZ-specific behaviors. There are no plans
to port it to Windows or macOS. For cross-platform UI access, use the
dice-rs-ws WebSocket server with a web-based or native client.