A lightweight driver for flipdot displays that fetches pre-rendered content from a remote server.
The new architecture separates the driver (runs on Raspberry Pi) from the content server (runs anywhere). This separation allows:
- Faster startup times on the Pi (no NumPy, minimal dependencies)
- Heavy lifting (rendering, fonts, animations) happens on a powerful server
- Easier development and testing
- Better scalability
┌─────────────────────────────────┐
│ CONTENT SERVER (anywhere) │
│ - Render frames │
│ - Generate animations │
│ - Return structured JSON │
└────────────┬────────────────────┘
│ HTTP/JSON
▼
┌─────────────────────────────────┐
│ DRIVER (Raspberry Pi) │
│ - Poll for content │
│ - Accept push notifications │
│ - Manage frame queue │
│ - Send to hardware │
└────────────┬────────────────────┘
│ Serial
▼
┌─────────────────────────────────┐
│ FLIPDOT HARDWARE │
└─────────────────────────────────┘
The driver has minimal dependencies:
pip install pydantic pyserialCopy the example configuration:
cp config.example.json config.jsonEdit config.json with your settings:
{
"poll_endpoint": "https://your-server.com/api/flipdot/content",
"auth": {
"type": "api_key",
"key": "your-secret-key"
},
"serial_device": "/dev/ttyUSB0",
"module_layout": [[1], [2]]
}python -m flipdot.driver.main --config config.jsonFor development (no hardware):
python -m flipdot.driver.main --config config.dev.jsonA single image to display:
{
"data_b64": "AQIDBAUGBwgJ...",
"width": 56,
"height": 14,
"duration_ms": 1000
}data_b64: Base64-encoded packed bits (little-endian)width,height: Dimensions in pixelsduration_ms: How long to display (null = indefinite)
A sequence of frames with playback instructions:
{
"content_id": "clock-12:00",
"frames": [
/* array of Frame objects */
],
"playback": {
"loop": true,
"loop_count": null,
"priority": 0,
"interruptible": true
}
}content_id: Unique identifierframes: Array of Frame objectsplayback.loop: Whether to loop framesplayback.loop_count: How many times to loop (null = infinite)playback.priority: Priority level (0=normal, 10=notification, 99=urgent)playback.interruptible: Can be interrupted by higher priority?
What the server returns:
{
"status": "updated",
"content": {
/* Content object */
},
"poll_interval_ms": 30000
}status: "updated", "no_change", or "clear"content: Content object (only if status="updated")poll_interval_ms: How long to wait before next poll
The driver expects a server endpoint that returns ContentResponse JSON.
GET /api/flipdot/content
Returns the current content to display.
Headers:
X-API-Key: your-secret-key(orAuthorization: Bearer token)
Response:
{
"status": "updated",
"content": {
"content_id": "clock-12:00",
"frames": [...],
"playback": {...}
},
"poll_interval_ms": 30000
}If enable_push: true in config, the driver runs a simple HTTP server:
POST http://pi-address:8080/
Push high-priority content immediately.
Headers:
X-API-Key: your-secret-keyContent-Type: application/json
Body:
{
"content_id": "notification",
"frames": [...],
"playback": {
"priority": 10,
"interruptible": false
}
}{
// Server settings
"poll_endpoint": "https://example.com/api/content",
"poll_interval_ms": 30000,
// Push server (optional)
"enable_push": false,
"push_port": 8080,
"push_host": "0.0.0.0",
// Authentication
"auth": {
"type": "api_key", // or "bearer"
"key": "secret-key",
"header_name": "X-API-Key"
},
// Hardware
"serial_device": "/dev/ttyUSB0",
"serial_baudrate": 57600,
"module_layout": [[1], [2]],
"module_width": 28,
"module_height": 7,
// Behavior
"error_fallback": "keep_last", // "keep_last", "blank", or "error_message"
"dev_mode": false,
"log_level": "INFO"
}The driver maintains a priority queue:
- Priority 0-9: Normal content (clock, weather, etc.)
- Priority 10-98: Notifications
- Priority 99: Urgent alerts
Higher priority content interrupts lower priority if marked as interruptible.
Example flow:
- Clock is displaying (priority 0)
- Notification arrives (priority 10)
- Clock pauses, notification plays
- Notification completes
- Clock resumes from where it left off
See examples/generate_content.py for examples of creating frames:
from flipdot.hardware import pack_bits_little_endian
import base64
# Create a 2x2 frame
bits = [1, 0, 1, 0] # Row-major order
packed = pack_bits_little_endian(bits)
b64 = base64.b64encode(packed).decode()
frame = {
"data_b64": b64,
"width": 2,
"height": 2,
"duration_ms": 1000
}Run tests:
pytest tests/test_driver.pyGenerate example content:
cd examples
python generate_content.pyUse dev_mode: true to test without hardware:
python -m flipdot.driver.main --config config.dev.jsonThe driver will print serial data to the console instead of sending to hardware.
Key changes:
- Driver is now minimal: Only handles display logic, no rendering
- NumPy removed: Faster startup on Pi
- Server provides frames: All rendering happens server-side
- New data format: Base64-encoded packed bits instead of live rendering
- Priority queue: Better support for notifications
What was removed from the Pi:
- FastAPI web server
- React frontend
- Display modes (Clock, ScrollText, Weather, etc.)
- NumPy dependency
What moved to the content server:
- All rendering logic
- Font handling
- Animation generation
- Display mode implementations
Check:
poll_endpointis correct- Authentication credentials match
- Server is running and accessible
Check:
- Device path is correct (
/dev/ttyUSB0,/dev/ttyACM0, etc.) - User has permission to access serial port
- Run:
sudo usermod -a -G dialout $USER
Check:
- Frame dimensions match display dimensions
data_b64is valid base64- Driver logs for errors (
log_level: "DEBUG")
This is Phase 1 and 2 of the refactor. Still TODO:
- Phase 3: Build the content server
- Migrate existing mode logic
- Create API endpoints
- Handle rendering server-side
- Phase 4: Advanced features
- Content caching
- Compression
- Transition effects