Tools for sending images, GIFs, and short videos to a Divoom MiniToo over Bluetooth Classic RFCOMM from macOS.
The core of this repo is a Swift daemon that keeps the Divoom app channel open, plus a small macOS menu-bar app and Python media conversion tooling.
omo-slim/contains local working media/assets and is not the main project API. The reusable project is the daemon, menu-bar app, CLI, and protocol notes.
- Opens the Divoom MiniToo app protocol over Bluetooth RFCOMM channel
1. - Avoids repeated macOS Bluetooth audio reconnect/disconnect by keeping one daemon connection open.
- Converts PNG/JPEG/GIF/MP4/video into the Divoom animation payload format.
- Sends jobs through a localhost daemon at
127.0.0.1:40583. - Provides a copyable macOS
.appthat starts the daemon from the menu bar.
The current defaults are for one tested device:
Bluetooth name: Divoom MiniToo-Audio
Bluetooth MAC: B1:21:81:B1:F0:84
RFCOMM channel: 1
Daemon port: 40583
If your device has a different Bluetooth address, update the address in the Swift tools or pass it manually to tools/divoom-daemon.
- macOS
- Xcode Command Line Tools / Swift compiler
- Homebrew
blueutilfor audio-profile disconnect/reconnect convenience:
brew install blueutilFor development CLI use:
- Python virtualenv with
Pillow,zstandard, andpyserial ffmpegfor GIF/video input
The packaged app bundles the repo .venv, so normal app usage does not need the active shell Python environment.
tools/build-divoom-app.shThis builds:
build/Divoom MiniToo.app
Install it:
cp -R "build/Divoom MiniToo.app" /Applications/
open "/Applications/Divoom MiniToo.app"On launch, the app:
- Disconnects the Divoom macOS audio profile once using
blueutil. - Starts the Swift RFCOMM daemon.
- Keeps the daemon available from the menu bar.
Logs and generated packet artifacts are written under:
~/Library/Application Support/DivoomMiniToo/
The menu-bar title indicates daemon state:
◇ Divoom = daemon stopped
◆ Divoom = daemon running
Useful actions:
- Send Image/GIF/Video… — choose a media file and send it.
- Disconnect Audio + Start Daemon — use when macOS audio owns the Bluetooth connection.
- Restart Daemon — stop, disconnect audio, and reopen RFCOMM.
- Open Menu Log / Open Daemon Log — inspect failures.
Start the daemon manually:
blueutil --disconnect B1:21:81:B1:F0:84 || true
tools/divoom-daemon B1:21:81:B1:F0:84 1 40583Send media through the daemon:
.venv/bin/python tools/divoom_send.py path/to/image.png
.venv/bin/python tools/divoom_send.py path/to/animation.gif
.venv/bin/python tools/divoom_send.py path/to/video.mp4Recommended compact video/GIF profile:
.venv/bin/python tools/divoom_send.py path/to/animation.gif \
--size 128 \
--fps 8 \
--speed 125 \
--max-frames 24 \
--posterize-bits 4Build packet files without sending:
.venv/bin/python tools/divoom_send.py path/to/media.mp4 --build-onlyAsk the daemon to parse but not send:
.venv/bin/python tools/divoom_send.py path/to/media.mp4 --daemon-dry-runFor most integrations, call divoom_send.py as a subprocess. It is the media-level API:
image/GIF/video -> Divoom packets -> daemon -> Bluetooth
Example:
"/Applications/Divoom MiniToo.app/Contents/Resources/.venv/bin/python" \
"/Applications/Divoom MiniToo.app/Contents/Resources/tools/divoom_send.py" \
"/absolute/path/to/file.gif" \
--size 128 --fps 8 --speed 125 --max-frames 24 --posterize-bits 4The daemon itself is packet-level. It listens on:
127.0.0.1:40583
It accepts JSON pointing to a prebuilt length-prefixed packet file:
{
"packets": "/absolute/path/to/file-packets-lenpref.bin",
"delay": 0.012,
"dryRun": false
}Python example:
import json
import socket
req = {
"packets": "/absolute/path/to/file-packets-lenpref.bin",
"delay": 0.012,
"dryRun": False,
}
with socket.create_connection(("127.0.0.1", 40583), timeout=10) as s:
s.sendall(json.dumps(req).encode() + b"\n")
s.shutdown(socket.SHUT_WR)
print(s.recv(65536).decode())Typical success response:
{"ok":true,"message":"sent","packets":457,"bytes":122949,"sawRequest":true,"sawAck":true}Full reverse-engineering notes are in PROTOCOL.md.
High-level transport:
01 <declared_len_le16> <cmd> <body...> <checksum_le16> 02
Animation/photo command:
0x8b = SPP_APP_NEW_GIF_CMD2020
Media payloads are RGB888 frames compressed with Zstandard and wrapped in Divoom 0x8b start/chunk packets.
Important finding:
zstd window_log=17
This matches Android captures and avoids black/glitched output seen with larger zstd windows.
PROTOCOL.md Reverse-engineering and validation notes
tools/DivoomDaemon.swift Swift RFCOMM daemon
tools/DivoomMenuBar.swift macOS menu-bar controller
tools/divoom_send.py Preferred media send CLI
tools/send_divoom_image.py Image/GIF/video conversion + packet builder
tools/divoom_clock.py Custom face selection helper
tools/build-divoom-app.sh Builds packaged macOS app
omo-slim/ Local test/media assets; not core project API
If daemon start fails with an RFCOMM error, disconnect the audio profile once:
blueutil --disconnect B1:21:81:B1:F0:84Then restart the daemon or reopen the app.
If a send reports sent but final ACK not observed, the device may still have updated successfully. This is usually an ACK-observation issue, not necessarily a failed transfer.
If video/GIF transfer is too slow, reduce frames before reducing pixels:
--size 128 --fps 6 --speed 167 --max-frames 18 --posterize-bits 4Keep --zstd-window-log 17 unless deliberately testing protocol behavior.