reai-board-sdk
An embeddable Rust crate that encapsulates USB / BLE connectivity, auto-reconnect, the HID protocol, mSBC decoding, and USB Audio capture for the ReAI Vibe Board — a voice-first mechanical keyboard built for AI coding workflows.
Product site | 中文文档 | API docs (docs.rs) | Changelog

The hardware
A CNC aluminium unibody board with a metal knob and a three-way mode lever. What the SDK can actually observe:
| Hardware | What you get from the SDK |
|---|---|
| Knob (rotate + press) | KeyPressEvent — KEY0 / KEY1 encoder phases, KEY2 press |
| 6 physical keys | KeyPressEvent KEY3–KEY8; KEY6 is the AI-voice key and also emits AiVoiceKeyEvent |
| Three-way mode lever | KEY9 / KEY10 / KEY11 → ModeChangeEvent (YOLO / PLAN / CHAT) |
| Microphone | PcmSink delivers 16 kHz mono f32 — USB Audio captured directly, BLE mSBC decoded first |
| USB-C / Bluetooth | Both transports, switched automatically |
The twelve key_index slots above cover every input the firmware reports:
three for the knob, six for the keys, three for the lever.
![]() |
![]() |
![]() |
| Metal knob | Dual-mic array | Tactile keys |
What it gives you
- One unified API for USB and BLE transports — same
BoardDevice, sameBoardEventstream, same audio callbacks. - Auto hotplug + reconnect — plug in USB, it takes over BLE; pull USB, BLE resumes. No glue code to write.
- 16 kHz mono f32 PCM delivered through a single
PcmSink::on_pcmtrait — USB Audio is captured directly, BLE mSBC frames are decoded in-Rust (no ffmpeg dependency). - Typed device commands — read/write key config, device info, bindings blob, silent-record flag, sleep timeout, work mode, factory physical-key test (firmware v1.58+), plus a vendor USB-HID DFU path for OTA upgrade and recovery.
- Three ways in for events —
events()returns anEventStream(recv().awaitfortokio::select!,blocking_recv()for plain threads),on_event()is callback style, andsubscribe()hands you the rawbroadcast::Receiverif you want to drive it yourself.
The protocol constants (USB VID/PID, BLE GATT service UUIDs, command opcodes, device-name prefix) are tuned for ReAI Vibe Board hardware. They are not generic USB/BLE abstractions.
Quick start
Add to your Cargo.toml:
[]
= "0.2"
= { = "1", = ["rt-multi-thread", "macros", "time", "sync"] }
Minimal usage:
use Arc;
use ;
use PcmSink;
;
async
Supported platforms
| OS | Status | Notes |
|---|---|---|
| macOS | Verified in CI | hidapi uses macos-shared-device |
| Linux | Builds in CI | Needs libdbus-1-dev libudev-dev libasound2-dev pkg-config; udev rules may be needed for raw HID |
| Windows | Expected to work, not yet verified | WinUSB / Zadig driver for raw HID access |
All three transports (USB HID, USB Audio, BLE GATT) are implemented for every platform above — the difference is only how much of it CI proves. Windows has no CI job yet; treat it as untested rather than unsupported.
ble uses btleplug 0.12 (native async). First start() may take ~40 s on
macOS while CoreBluetooth warms up its adapter — this is the OS, not the SDK.
OS permissions
The SDK never simulates or injects keyboard input — it reads from the device and sends it commands. That has two consequences:
- No Accessibility / Input Monitoring required in principle. The board's keys
flow over vendor HID
0xFFA0/ consumer0x000C, not standard keyboard Usage0x0007. macOS may still prompt — authorize if it does. - No microphone permission is required; USB Audio capture here is device→host only (no host mic).
macOS Bluetooth does prompt. Creating the CoreBluetooth adapter triggers
the system authorization dialog, so the SDK defers adapter creation until BLE
is actually needed — you will see the prompt on the first
scan_ble_devices() / BLE connect, not at start().
Note that device commands are not read-only: write_key_config(),
set_sleep_timeout(), shutdown_device() and start_dfu_upgrade() all change
device state. See Security notes.
Features
| Feature | What it pulls in | Default? |
|---|---|---|
usb |
hidapi 2.6 (USB HID) + cpal 0.15 (USB Audio capture) |
✅ |
ble |
btleplug 0.12 (BLE GATT) + futures-util |
✅ |
test-mode |
Factory test commands (e.g. shutdown_device(0x5E)) |
✅ |
BoardDeviceBlocking needs no feature flag — it ships with usb or ble.
To use only the protocol layer with no hardware deps:
= { = "0.2", = false, = ["test-mode"] }
Rust version
rust-version = "1.87" (uses usize::is_multiple_of in a couple of hot-path code sites and examples).
Architecture
BoardDevice (high-level API: open / start / subscribe)
│
┌─────────────┴─────────────┐
▼ ▼
HotplugManager (USB) UsbAudioCapture
(USB + BLE auto-connect / │ cpal UAC → PcmSink (f32)
auto-reconnect) │
│
┌────────┼────────────────┐
▼ ▼ ▼
HidMonitor KeyStateAggregator VendorGattClient
(Config / Consumer parser) (BLE GATT: scan / connect / notify)
│ │ │
└──────── broadcast::Sender<BoardEvent> ┘
│
Consumers subscribe()
Event decoupling: every internal module reports through one
broadcast::Sender<BoardEvent>. The high-frequency audio stream uses a
separate AudioFrameSink / PcmSink trait so semantic events are never
drowned out by 16 kHz PCM frames.
Four-layer split
| Layer | Module path | Purpose |
|---|---|---|
kernel |
protocol_hid / protocol_gatt / event / sink / msbc / key_aggregator / types / error |
Pure logic. No threads, no I/O. |
runtime |
device / hotplug / usb / ble / usb_capture |
tokio async orchestration (lifecycle / hotplug / USB / BLE I/O). |
facade |
device / events / blocking |
BoardDevice high-level entry point, the three event entry points, and the sync command bridge. |
tool |
parse / msbc_file |
I/O-aware helpers (parse device info from HID buffer, decode mSBC file). |
runtime and facade require at least one of usb / ble. With both off,
only kernel and tool are available — useful for embedding the protocol
layer without any hardware dependency.
Events (BoardEvent)
A single enum — one match covers everything:
Each variant is #[derive(Serialize)] with #[serde(tag = "type")] so it
serializes naturally to a JSON envelope if you need to forward events to a
WebSocket bridge or another process.
Audio sinks
- USB Audio is captured directly via cpal and forwarded to
PcmSink. - BLE mSBC frames are decoded to f32 by the built-in
MsbcDecoderSinkand forwarded to the samePcmSinkyou registered.
Built-in sinks: MsbcDecoderSink (mSBC → f32), CountingSink (frame / byte
statistics). set_pcm_sink() accepts Arc<dyn PcmSink>; call it before
start().await.
Device commands
All command methods are async on BoardDevice and sync on
BoardDeviceBlocking. They auto-pick USB HID or BLE GATT transport based on
the current connection.
Device info & work mode
device.read_device_info.await?; // CMD 0x13: mode / MAC / firmware / battery / chip_id
device.get_work_mode.await?; // CMD 0x12 + 0xC9 — reads the lever's current position
Key configuration
device.read_key_config.await?; // CMD 0x15
device.write_key_config.await?; // CMD 0x16
Bindings blob — a 4 KB application-defined config block persisted on the
keyboard, transferred in fragments with a CRC16 check. BlobRead distinguishes
never written (safe to initialize silently) from written but corrupt (raise
it to the user — never overwrite blindly), and reports Unsupported on older
firmware that does not answer these commands.
device.read_bindings_blob.await?; // CMD 0x69
device.write_bindings_blob.await?; // CMD 0x6A — payload ≤ 3830 bytes
Power & sleep
device.get_silent_record.await?; // CMD 0x61 (firmware v1.41+)
device.set_silent_record.await?; // CMD 0x62 — returns the effective value
device.get_sleep_timeout.await?; // CMD 0x63 (firmware v1.51+, idle / connected seconds)
device.set_sleep_timeout.await?; // CMD 0x64
device.notify_app_online.await?; // CMD 0x65 (firmware v1.53+)
device.get_app_online.await?; // CMD 0x66
device.get_open_url.await?; // CMD 0x67
device.set_open_url.await?; // CMD 0x68
device.shutdown_device.await?; // CMD 0x5E (test-mode only)
BLE connection management
device.scan_ble_devices.await?; // list nearby boards
device.connect_ble; // target a specific one
device.disconnect_ble.await?;
device.disconnect.await?; // CMD 0x60 — ask the device to drop the link
Firmware upgrade & recovery — the DFU path is USB-only.
device.start_dfu_upgrade.await?;
device.cancel_dfu_upgrade; // aborts within one ≤250 B transfer cycle
// If a board is left stranded in DFU mode (e.g. the host died mid-upgrade):
if device.is_stuck_in_dfu.await?
recover_from_dfu() only touches the staging partition, never the main
application partition — it cannot brick a device that is already stuck.
Factory physical-key test (test-mode, firmware v1.58+) — a 15-second
lease; renew every 5 s and release explicitly.
device.set_factory_key_test.await?; // CMD 0x6C; events arrive as 0x6D
Examples
Run from the crate root:
All examples need a board connected; they print events to stdout.
Security notes
This crate has no built-in authentication or transport-layer encryption. If you build a service that exposes the device commands over a network, you are responsible for:
- binding only to
127.0.0.1(or a Unix socket) on the host that owns the hardware; - putting a reverse proxy with TLS + token authentication in front of any remote surface;
- rate-limiting the DFU endpoint (
start_dfu_upgradewill reflash the device — it is not idempotent).
shutdown_device() and start_dfu_upgrade() are destructive. There is no
second-factor prompt — whoever can call them owns the device.
write_key_config() and write_bindings_blob() are not destructive but they
persist on the keyboard: a bad write survives reboots and unplugging, and
recovering means writing known-good data back. Read before you write, and treat
a BlobRead that comes back corrupt as a prompt for the user rather than a
licence to overwrite.
Contributing
Issues and PRs are welcome at github.com/ReAI-com/reai-board-sdk. For the product itself, see b.reai.com.
Local development loop:
License
This crate is MIT — see LICENSE. Copyright (c) 2026 ReAI Team.
One caveat worth reading before you ship:
The ble feature pulls in msbc-decoder, a separate crate in
this repository that decodes the mSBC audio arriving over BLE. That decoder is
a bit-exact translation of FFmpeg's libavcodec/sbcdec.c, so it inherits
FFmpeg's licence and is distributed under LGPL-2.1-or-later, not MIT.
It lives in its own crate precisely so the boundary is explicit:
| Your build | mSBC decoder compiled in? | Effective licence |
|---|---|---|
default-features = false (protocol only) |
no | MIT |
features = ["usb"] — USB HID + USB Audio |
no | MIT |
features = ["ble"] or default |
yes | MIT + LGPL-2.1-or-later |
If you only talk to the board over USB, no LGPL code reaches your binary. If
you need BLE audio and LGPL is a problem for your product, talk to your legal
team, or supply your own mSBC decoder through the AudioFrameSink trait —
set_audio_frame_sink() hands you the raw 57-byte frames before any decoding
happens.


