Open source macOS tools for controlling a Divoom MiniToo over Bluetooth Classic RFCOMM. The repository includes animated status screens, direct keyboard control, a persistent Swift transport, protocol tests, and a read only RuView sensing bridge.
It does not modify MiniToo or ESP32 firmware. It uses the stock Bluetooth service exposed by a normally paired MiniToo.
- Animated presence, alert, empty, and degraded screens.
- Live RuView occupancy, motion, heart rate, and sensor health displays.
- Keyboard controlled physical status terminals.
- Room occupancy and meeting room indicators.
- Custom renderers using the documented packet and animation pipeline.
Requirements are macOS, Node.js 20 or newer, Xcode Command Line Tools, zstd, and a paired MiniToo.
git clone https://github.com/ruvnet/minitoo-control.git
cd minitoo-control
npm ci
npm run build
system_profiler SPBluetoothDataType
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF npm start -- --offlineUse the physical Mac keyboard while the terminal is focused:
1 EMPTY
2 PRESENCE
3 or A ALERT
4 or H DEGRADED
Arrow keys Cycle screens
Space Acknowledge local alert
Q Quit
For a single screen without the long running bridge:
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF npm run example:state -- presence
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF npm run example:state -- alertThe MiniToo must be paired first. The default RFCOMM channel is 1.
RuView read-only WebSocket Mac keyboard
\ /
allowlisted state
|
local Node bridge
|
persistent stdin pipe
|
macOS IOBluetooth RFCOMM
|
MiniToo
The bridge recognizes only presence, occupancy, alert, degraded sensor-health, and derived heart-rate fields. Heart rate appears only during presence when it is between 35 and 220 BPM and both heartbeat confidence and signal quality initially reach 0.4. A 0.3 retention threshold, exponential smoothing, ten-sample loss window, and five-second detail refresh prevent threshold flicker and Bluetooth write churn. Presence, alert, zone, and control-source transitions remain immediate. It is an experimental RF estimate, not a medical measurement. The bridge ignores unrelated entities, caps inbound messages at 256 KiB, defaults the WebSocket origin to loopback, and never writes commands back to RuView. The Swift transport accepts only bounded, checksum-valid Divoom packet files. It exposes no TCP listener.
- macOS with Xcode Command Line Tools.
- Node.js 20 or newer.
zstdonPATH.- A paired MiniToo with its Bluetooth address from
system_profiler SPBluetoothDataType.
cd minitoo-control
npm ci
npm run build
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF npm startThe default event source is the local RuView sensing server at ws://127.0.0.1:3001/ws/sensing. Keyboard control remains available while the bridge reconnects if that stream is absent. Full sensing messages are projected immediately to display fields and are never logged or persisted. Display updates are deduplicated by mode, zone, low or medium or high confidence band, motion, people, 10 BPM heart-rate bucket, and local versus live source.
To test the complete bridge without RuView hardware, start the synthetic local stream in one terminal:
npm run example:streamThen connect the bridge from another terminal:
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF \
npm start -- --ws-url ws://127.0.0.1:8765Run without a WebSocket:
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF npm start -- --offlineConnect to an authenticated local RuView events endpoint:
RUVIEW_MINITOO_WS_TOKEN='<runtime token>' \
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF \
npm start -- --ws-url ws://127.0.0.1:8000/api/v1/stream/events?event_types=motion,presenceConnect to Homecore state changes:
RUVIEW_MINITOO_WS_TOKEN='<runtime token>' \
MINITOO_ADDRESS=AA:BB:CC:DD:EE:FF \
npm start -- --ws-url ws://127.0.0.1:3000/api/websocketTokens come from the environment and are never printed. Non-loopback event URLs fail closed unless --allow-remote is explicit. Use wss:// for any remote connection.
1 EMPTY
2 PRESENCE
3 or A ALERT
4 or H DEGRADED
Arrow keys Cycle screens
Space Acknowledge alert
Q Quit
Keyboard changes affect only the MiniToo display. They do not acknowledge or mutate RuView alerts.
The renderer produces eight 128 by 128 RGB frames at 125 ms each, compresses them with Zstandard window log 17, and wraps them in checksum-validated Divoom 0x8b packets. The Swift child keeps RFCOMM channel 1 open and requires both the device chunk request and final acknowledgement for every update.
The MiniToo protocol is undocumented by the manufacturer. This implementation follows independently published reverse-engineering evidence from divoom-minitoo-osx and the MiniToo SPP reference.
See docs/protocol.md for framing, compression, acknowledgements, known limits, and safe extension points.
The useful extension points are deliberately small:
- Edit
PALETTESand the state labels insrc/render.mjs. - Add a bounded state projection in
src/state.mjs. - Add the state to
DISPLAY_MODESand write a renderer test. - Keep packet length, frame count, checksum, and compressed size checks intact.
examples/send-state.mjs demonstrates direct rendering and transfer. examples/synthetic-ruview-server.mjs demonstrates a local event source. Neither example opens a listener beyond loopback.
The project writes display frames only. It does not flash firmware, change pairing, write to RuView, acknowledge production alerts, or persist raw sensing data. Remote WebSocket sources require explicit authorization with --allow-remote. Credentials in URLs are rejected.
Heart rate and breathing values derived from RF sensing are experimental indicators, not medical measurements.
npm test
npm run build
node bridge.mjs --dry-run --offline --no-keyboard[MEASURED] Unit tests cover event allowlisting, keyboard mapping, loopback enforcement, URL credential rejection, Bluetooth argument validation, pixel-frame bounds, packet checksums, and full Zstandard round-trip reconstruction.
[MEASURED transport, MiniToo firmware 2.4.0] Persistent-channel keyboard transitions completed in 141 to 230 ms across PRESENCE, ALERT, acknowledgement, and DEGRADED. A synthetic loopback RuView sensing event completed event projection, first-time rendering, Bluetooth transfer, device chunk request, and final acknowledgement in 221 ms. Cold startup took 924 ms. These measurements prove the transport path, not human-visible rendering latency.
Physical display rendering is a separate hardware gate. A successful RFCOMM write is insufficient without the expected request, final acknowledgement, and human-visible screen transition.
MIT. See LICENSE.
- Start the bridge with the MiniToo address.
- Press
2and verify the display changes toPRESENCE. - Press
3and verify the red alert animation appears. - Press Space and verify the prior state returns.
- Inject a presence event into the configured RuView stream.
- Verify the display changes within the
[CLAIMED target]of 500 ms and an unrelated entity produces no update.