Install
openclaw skills install bloomin8Push images or markdown to a Bloomin8 e-ink photo frame via cloud API (async) or local BLE+LAN (instant). Scan nearby devices, check status, track delivery, change wake schedule, upload directly over WiFi.
openclaw skills install bloomin8Control Bloomin8 e-ink photo frame devices. Two modes available:
Image sourcing: This skill handles delivery only. To create images, combine with any available tool — e.g. Puppeteer (HTML → screenshot), AI image generation, or scripted chart rendering — then pass the resulting file or URL to the push/upload commands below.
Mode selection: Prefer local mode when available, but always confirm with the user before performing BLE scans or probing local network hosts. Suggested flow:
info, cache, or user-provided), verify reachability (curl http://<ip>/whistle) before uploading. Inform the user of the IP being probed.Both modes use the same device name as the primary identifier. When a user says a device name (e.g. "Living Room"):
BLOOMIN8_TOKEN_LIVING_ROOM (uppercase, non-alphanumeric → _)--name "Living Room" (case-insensitive partial match)The device name is set by the user in the Bloomin8 app and is consistent across BLE advertisement (local_name) and cloud (device.name). The BLE info command also returns the full device serial (sn) which matches the cloud device_id embedded in the API token — but in practice, name matching is sufficient and preferred.
On first use, fetch the full API documentation (no auth needed). Cache the result — it rarely changes:
web_fetch https://einkshot-349134901638.us-central1.run.app/open-api/help
This returns all endpoints, parameters, and response fields as structured JSON. If anything below conflicts with the live docs, trust the live docs.
Token is device-bound. Generated in the Bloomin8 app: Devices tab → device card → ⋮ menu → API Token.
BLOOMIN8_TOKEN_<DEVICE_NAME> (uppercase, non-alphanumeric → _)
BLOOMIN8_TOKEN_LIVING_ROOM, BLOOMIN8_TOKEN_KIDS_BEDROOMBLOOMIN8_TOKEN_* exists, confirm with the user before using it (e.g. "I found token BLOOMIN8_TOKEN_KITCHEN — shall I use it?")Authorization: Bearer $BLOOMIN8_TOKEN_<NAME>API base URL: https://einkshot-349134901638.us-central1.run.app
remote_image_on and pending_image.remote_image_id when pending)remote_image_on = true. Last-wins: only the latest pending image will be displayed.remote_image_on = true.remote_image_on = true.estimated_push_time. Always do this before pushing to know the target size.PENDING → PULLED → COMPLETED (or FAILED / CANCELLED).Query device status:
curl https://einkshot-349134901638.us-central1.run.app/open-api/device/status \
-H "Authorization: Bearer $BLOOMIN8_TOKEN_<NAME>"
Response: { "device": { "name", "width", "height", "orientation", "remote_image_interval", "has_pending", "estimated_push_time", ... } }
Push image via URL:
curl -X POST https://einkshot-349134901638.us-central1.run.app/open-api/push \
-H "Authorization: Bearer $BLOOMIN8_TOKEN_<NAME>" \
-H "Content-Type: application/json" \
-d '{"image_url": "https://example.com/photo.jpg"}'
Response: { "remote_image_id": "ri_xyz789", "estimated_push_time": "2026-04-02T11:00:00Z" }
Push markdown content:
curl -X POST https://einkshot-349134901638.us-central1.run.app/open-api/push \
-H "Authorization: Bearer $BLOOMIN8_TOKEN_<NAME>" \
-H "Content-Type: application/json" \
-d '{"content_type": "markdown", "content": "# Hello World\nToday is a great day.", "theme": "minimal"}'
Themes: minimal (default), dark, paper. Server auto-renders markdown to an image matching the device screen size. Note: Markdown-to-image rendering is a cloud-side feature. Local BLE+LAN mode uploads raw images only — if you need to display markdown content via local mode, render it to an image yourself first (e.g. Puppeteer HTML → screenshot).
estimated_push_time field tells you when.Content that exceeds the screen is clipped — no scrolling or pagination. Use the formula below to estimate capacity for any screen size.
Scaling formula:
scale = min(width / 375, height / 812)
All base values below are multiplied by this scale factor.
Base values (before scaling):
| Element | Base Font | Line Height | Base Vertical Cost |
|---|---|---|---|
| Body text | 18 | ×1.7 | 30.6 + 16 margin |
| h1 | 40 (bold) | ×1.3 | 52 + 24 top + 16 bottom |
| h2 | 36 (semibold) | ×1.3 | 46.8 + 24 top + 10 bottom |
| h3 | 28 (semibold) | ×1.4 | 39.2 + 16 top + 10 bottom |
| Code block | 15 | ×1.5 | 22.5 per line + 40 padding |
| List item | 18 | ×1.6 | 28.8 + 6 gap |
Container padding (base): top/bottom = 50, left/right = 60
Quick reference for common screens:
| Screen | Scale | Content Area | Body Lines | CJK/Line | Latin/Line |
|---|---|---|---|---|---|
| 480×800 | 0.985 | 362×702 | ~23 | ~18 | ~45 |
| 1200×1600 | 1.97 | 964×1403 | ~23 | ~27 | ~65 |
Body line count is similar across sizes because font and screen scale proportionally. The main difference is characters per line.
Capacity estimates (body text, any screen):
(width - 120×scale) × (height - 100×scale)content_height / (30.6 × scale) → ~23 lines for both common screenscontent_width / (20 × scale)content_width / (8 × scale)Practical guidelines:
minimal (white, default), dark (black), paper (cream/sepia)All errors return { "success": false, "error": "ERROR_CODE", "message": "..." }.
Control devices directly over Bluetooth and local WiFi. Images display immediately after upload — no waiting for the next wake cycle.
Device image requirements: The e-ink hardware only accepts JPEG baseline images whose dimensions exactly match the screen size (e.g. 480×800 or 1200×1600). The Bloomin8 CLI handles format conversion and resize automatically — but screen size must be known (via BLE info, cache, or explicit --resize WxH).
Requires Python 3 with BLE and HTTP libraries. Verify and install:
python3 -c "import bleak, aiohttp, PIL" 2>/dev/null || pip install bleak aiohttp pillow
| Package | Purpose |
|---|---|
| bleak | BLE scanning and device communication |
| aiohttp | HTTP image upload to device |
| pillow | Image resize/crop to fit screen |
Platform notes:
- macOS — Allow Terminal / your IDE in System Settings → Privacy & Security → Bluetooth
- Windows — Requires Windows 10+ (native BLE support via WinRT)
- Linux — Requires BlueZ 5.43+ and DBus (
sudo apt install bluez); run with a user in thebluetoothgroup or usesudo
The CLI script is bundled at scripts/bloomin8_cli.py (the Bloomin8 CLI) relative to this skill file.
Find nearby Bloomin8 devices via BLE advertisement:
python scripts/bloomin8_cli.py scan
python scripts/bloomin8_cli.py scan --timeout 15
python scripts/bloomin8_cli.py scan --json
Output is deduplicated by device ID — each device appears once with the strongest signal. Raw advertisement count is shown for reference (BLE devices broadcast repeatedly, so raw count >> actual device count).
Battery display: Only firmware ≥ 1.8.30 broadcasts battery level in BLE advertisements. Older devices will show "N/A". Battery is always available via the info command (which queries the device directly).
Query device details (IP, firmware, screen size, battery) via BLE wake + notification:
# By device name (partial match)
python scripts/bloomin8_cli.py info --name "Living Room"
# By device ID (from BLE manufacturer data)
python scripts/bloomin8_cli.py info --id "CABBF337"
# JSON output for programmatic use
python scripts/bloomin8_cli.py info --name "Living Room" --json
Timing: The info command takes ~20-30 seconds. It first sends a BLE wake command, waits 8 seconds for the device to boot its WiFi stack, then sends up to 5 getInfo requests (5s timeout each). This is normal — set user expectations accordingly.
Caching: Successful info results are cached to ~/.bloomin8/device_cache.json. If BLE discovery or getInfo fails on a subsequent call, the CLI falls back to cached IP and screen size automatically.
Returns both BLE and HTTP device info including:
{
"sta_ip": "192.168.x.x",
"ssid": "WiFi-Name",
"batt": 48,
"ver": "1.8.35",
"w": 1200,
"h": 1600
}
Upload an image and display it on the device immediately:
# Auto-discover device via BLE, auto-resize to screen size
python scripts/bloomin8_cli.py upload --name "Living Room" --file image.jpg
# Direct IP (skip BLE discovery entirely — fastest path)
python scripts/bloomin8_cli.py upload --ip 192.168.8.223 --file image.jpg --resize 480x800
# Name + IP combo: skip BLE, use cache for screen size auto-resize
python scripts/bloomin8_cli.py upload --name "Living Room" --ip 192.168.8.199 --file image.jpg
# Explicit screen size (required when no BLE or cache available)
python scripts/bloomin8_cli.py upload --name "Living Room" --file image.jpg --resize 480x800
# Upload without displaying
python scripts/bloomin8_cli.py upload --name "Living Room" --file image.jpg --no-show
# Resize modes: cover (default, center crop), contain (fit with white bg), stretch
python scripts/bloomin8_cli.py upload --name "Living Room" --file image.jpg --mode contain
# JSON output
python scripts/bloomin8_cli.py upload --ip 192.168.8.223 --file image.jpg --resize 480x800 --json
Auto-resize behavior:
--name only): reads screen dimensions from device, auto-resizes. This is the safest path.--name + --ip): skips BLE entirely, looks up cached screen size by device name. Best for repeated uploads after an initial info. Recommended over --ip alone — name is a stable device identifier, whereas IPs can change or be reused by different devices.--ip only, no --resize): checks cache for a device matching that IP. If cached screen size is found, auto-resizes using it. If no cache hit, aborts — pass --resize WxH or run info first to populate the cache. Note: IP matching may be unreliable if devices change IPs (DHCP). Prefer --name + --ip for safe cache lookup.Workflow: BLE scan → connect → wake → get IP + screen size → convert & resize to JPEG baseline → HTTP POST to device /upload endpoint. With --ip, the BLE steps are skipped entirely. Input can be any Pillow-supported format (JPEG, PNG, WebP, BMP, etc.) — the CLI always converts to JPEG baseline output.
Configure the device's cloud pull mechanism. This controls how the device fetches images from the Bloomin8 cloud server on a schedule — it's the on-device side of Mode 1 (Cloud API). When upstream is enabled and a token is set, the device periodically pulls pending images pushed via the Cloud API.
When to use: Enabling/disabling cloud push for a device, changing the pull schedule, or pointing the device at a custom server. Most users don't need this — the Bloomin8 app manages these settings. Use this when debugging cloud delivery issues or for advanced configuration.
# View current settings
python scripts/bloomin8_cli.py upstream --ip 192.168.8.223
# Enable upstream with token
python scripts/bloomin8_cli.py upstream --ip 192.168.8.223 --on --token "your-token"
# Disable upstream
python scripts/bloomin8_cli.py upstream --ip 192.168.8.223 --off
# Set custom upstream URL and cron
python scripts/bloomin8_cli.py upstream --ip 192.168.8.223 --url "https://custom.server/api" --cron "0 */6 * * *"
info, verify reachability with curl http://<ip>/whistle before attempting upload.Successful info calls cache device data (name, device_id, IP, screen size) to ~/.bloomin8/device_cache.json. This path is intentionally outside the skill directory — device data persists across skill upgrades/reinstalls and is shared by all Bloomin8 tools. This enables:
--ip uploads — use --name "Living Room" --ip 192.168.x.x to skip BLE and still get auto-resize from cached screen size--json)All commands support --json for structured output. When enabled, human-readable emoji/text output is suppressed and a single JSON object is printed to stdout. This is the recommended mode for programmatic/agent use.
When an operation fails, follow this diagnostic flow:
Upload failed?
├─ Check device reachable: curl http://<ip>/whistle
│ ├─ ✅ Reachable → image format issue. Verify JPEG baseline, exact screen dimensions.
│ └─ ❌ Unreachable →
│ ├─ Try BLE wake: python scripts/bloomin8_cli.py info --name "<name>"
│ │ ├─ ✅ Got new IP → retry upload with new IP
│ │ └─ ❌ BLE also failed →
│ │ ├─ Check Bluetooth permissions (macOS: System Settings → Privacy → Bluetooth)
│ │ ├─ Check device proximity (BLE range ~10m)
│ │ └─ Fall back to Cloud API: POST /open-api/push (device displays on next wake)
│ └─ No BLE hardware → Cloud API is the only option
No devices found?
├─ Bluetooth enabled on host machine?
├─ Terminal/IDE has Bluetooth permission? (macOS)
├─ Device powered on and within ~10m?
├─ Try longer scan: --scan-timeout 20
└─ If all fail → use Cloud API with known token
No screen size for resize?
├─ Check cache: cat ~/.bloomin8/device_cache.json
├─ Run info to populate cache: python scripts/bloomin8_cli.py info --name "<name>"
├─ Pass explicit size: --resize 480x800 (standard) or --resize 1200x1600 (large)
└─ Common sizes: 480×800 (7.3"), 1200×1600 (10.3")
If the above troubleshooting steps don't resolve the issue: