A simple HTTP API for controlling (not just) flip-dot displays across hackerspaces. Send text, images, or animations and watch the satisfying click-click-click as mechanical pixels flip.
Made for hackerspaces to communicate with each other and share content across displays around the world.
Example: Takt Praha Hackerspace
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{"type": "text", "text": "Hello from Berlin!"}'
The Takt Praha display is 84×16 pixels, monochrome flip-dot. Your hackerspace might have different specs - check the capabilities endpoint first!
Find out what the display can do - size, color mode, features.
Example:
curl https://display.taktpraha.cz/api/v1/capabilities
Response:
{
"protocol_version": "1",
"display": {
"width": 84,
"height": 16,
"color": "mono",
"features": ["text", "bitmap", "animation"]
}
}
| Field | Type | Description |
|---|---|---|
protocol_version |
string | API version |
display.width |
integer | Display width in pixels |
display.height |
integer | Display height in pixels |
display.color |
string | Color mode: "mono", "grayscale", or "rgb" |
display.features |
array | Supported message types |
Send something to the display! Supports three message types: text, bitmap, animation.
Only one message runs at a time - new messages replace the current one.
Scrolling or static text messages.
Example - Scrolling text:
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"text": "Hack the planet!",
"scroll": true,
"speed": 1
}'
Example - Static text:
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{
"type": "text",
"text": "OPEN",
"scroll": false
}'
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | yes | - | Must be "text" |
text |
string | yes | - | Text to display |
scroll |
boolean | no | true |
Scroll horizontally across display |
speed |
integer | no | 1 |
Scroll speed: 1 (slow) to 10 (fast) |
Response:
{
"status": "ok",
"message_id": "550e8400-e29b-41d4-a716-446655440000"
}
Display a static image. Images are automatically resized and converted to match the display.
Example - Send an image:
# Encode image to base64
IMAGE_DATA=$(base64 -w 0 cool_logo.bmp)
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d "{
\"type\": \"bitmap\",
\"data\": \"$IMAGE_DATA\",
\"duration\": 10000
}"
Python example:
import requests, base64
with open("cool_logo.bmp", "rb") as f:
img_base64 = base64.b64encode(f.read()).decode()
requests.post("https://display.taktpraha.cz/api/v1/message", json={
"type": "bitmap",
"data": img_base64,
"duration": 10000
})
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | yes | - | Must be "bitmap" |
data |
string | yes | - | Base64-encoded image (PNG, JPG, BMP, GIF) |
duration |
integer | no | 5000 |
How long to show image (milliseconds) |
encoding |
string | no | "base64" |
Always "base64" for now |
Image processing:
Response:
{
"status": "ok",
"message_id": "550e8400-e29b-41d4-a716-446655440001"
}
Play multi-frame animations. Perfect for videos, GIFs, or custom animations.
Example - Send Bad Apple:
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{
"type": "animation",
"frames": [
{
"data": "iVBORw0KGgoAAAANSUhEUg...",
"duration": 100
},
{
"data": "iVBORw0KGgoAAAANSUhEUg...",
"duration": 100
}
],
"loop": true
}'
Python example:
import requests, base64
# Load frames from files
frames = []
for i in range(100):
with open(f"frame_{i:03d}.png", "rb") as f:
frames.append({
"data": base64.b64encode(f.read()).decode(),
"duration": 80
})
# Send to display
requests.post("https://display.taktpraha.cz/api/v1/message", json={
"type": "animation",
"frames": frames,
"loop": False
})
Fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
type |
string | yes | - | Must be "animation" |
frames |
array | yes | - | Array of frame objects (see below) |
loop |
boolean | no | false |
Loop animation continuously |
Frame object:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
data |
string | yes | - | Base64-encoded image |
duration |
integer | no | 200 |
Frame duration (milliseconds) |
encoding |
string | no | "base64" |
Always "base64" for now |
Notes:
loop: true, animation repeats until stoppedResponse:
{
"status": "ok",
"message_id": "550e8400-e29b-41d4-a716-446655440002"
}
Check if display is busy and what's currently showing.
Example:
curl https://display.taktpraha.cz/api/v1/status
Response:
{
"status": "ok",
"current_message_id": "550e8400-e29b-41d4-a716-446655440000",
"is_busy": true
}
| Field | Type | Description |
|---|---|---|
status |
string | Always "ok" |
current_message_id |
string | null | UUID of running message, or null if idle |
is_busy |
boolean | true if message is playing, false if idle |
Stop whatever's currently playing and clear the display.
Example:
curl -X POST https://display.taktpraha.cz/api/v1/stop
Response:
{
"status": "ok",
"message": "Message stopped"
}
Errors return HTTP status codes and JSON with error details.
Error response format:
{
"status": "error",
"error": "Human-readable error message"
}
Common errors:
| HTTP Status | Error Message | What it means |
|---|---|---|
400 |
"Missing 'type' field" |
Forgot to specify message type |
400 |
"Missing 'text' field" |
Text message needs text field |
400 |
"Missing 'data' field" |
Bitmap/frame needs data field |
400 |
"Speed must be 1-10" |
Speed out of range |
400 |
"Invalid base64 data" |
Malformed base64 string |
400 |
"Frames list is empty" |
Animation needs at least 1 frame |
500 |
"Failed to decode image" |
Corrupt or unsupported image format |
Single message at a time:
Message timeout:
Message queue:
New message arrives
↓
Stop current message
↓
Start new message
↓
Return message_id to you
↓
Message plays until finished/stopped
↓
Display clears
Cross-hackerspace greeting:
# London says hi to Prague
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{"type": "text", "text": "Greetings from London Hackspace!"}'
Display current projects:
curl -X POST https://display.taktpraha.cz/api/v1/message \
-H "Content-Type: application/json" \
-d '{"type": "text", "text": "Now working on: Laser cutter repair", "scroll": false}'
Animate Bad Apple (classic):
import cv2, requests, base64
cap = cv2.VideoCapture("bad_apple.mp4")
frames = []
while len(frames) < 500: # First 500 frames
ret, frame = cap.read()
if not ret: break
# Resize to display size
frame = cv2.resize(frame, (84, 16))
# Encode frame
_, buffer = cv2.imencode('.png', frame)
frames.append({
"data": base64.b64encode(buffer).decode(),
"duration": 80
})
cap.release()
# Send to Prague
requests.post("https://display.taktpraha.cz/api/v1/message", json={
"type": "animation",
"frames": frames,
"loop": False
})
For images:
For animations:
For text:
scroll: false)For cross-hackerspace fun:
/capabilities first to see display specsThis API spec is generic - implement it however you want! Here's what you need:
Required endpoints:
GET /api/v1/capabilities - return your display specsPOST /api/v1/message - handle text/bitmap/animationGET /api/v1/status - return current statusPOST /api/v1/stop - stop current messageMessage handling:
Image processing:
Good practices:
Current version: 1
This is version 1 of the protocol. Future versions might add:
But for now, keep it simple!
const BASE_URL = "https://display.taktpraha.cz/api/v1";
// Get capabilities
const caps = await fetch(`${BASE_URL}/capabilities`).then(r => r.json());
console.log(`Display: ${caps.display.width}x${caps.display.height}`);
// Send text
await fetch(`${BASE_URL}/message`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
type: "text",
text: "Hello from the web!",
scroll: true,
speed: 5
})
});
// Send image
async function sendImage(imageUrl) {
const response = await fetch(imageUrl);
const blob = await response.blob();
return new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = async () => {
const base64data = reader.result.split(',')[1];
const result = await fetch(`${BASE_URL}/message`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
type: "bitmap",
data: base64data,
duration: 5000
})
});
resolve(result.json());
};
reader.readAsDataURL(blob);
});
}
// Check status
const status = await fetch(`${BASE_URL}/status`).then(r => r.json());
if (status.is_busy) {
console.log(`Displaying message: ${status.current_message_id}`);
}
// Stop message
await fetch(`${BASE_URL}/stop`, {method: "POST"});
Go forth and make those flip-dots flip! Send messages between hackerspaces, display sensor data, play Bad Apple, or just say hi.
Remember: check /capabilities first, be respectful of other hackerspaces' displays, and have fun!
Example hackerspaces with this API:
https://display.taktpraha.cz - 84×16 mono flip-dotIf you implement this API at your hackerspace, send an email to info@taktpraha.cz and we will add you to the list. Cheers