home

Hackerspace Display API

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.

based on WFDD project, courtesy of trimen and his reverse enginnering efforts


Quick Start

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!


Endpoints

GET /api/v1/capabilities

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

POST /api/v1/message

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.


Message Type: text

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"
}

Message Type: bitmap

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:

  • Any image format: PNG, JPEG, BMP, GIF
  • Automatically resized to display dimensions
  • Automatically converted to display color mode (mono/grayscale/RGB)
  • For best results: use PNG, pre-resize to display size

Response:

{
  "status": "ok",
  "message_id": "550e8400-e29b-41d4-a716-446655440001"
}

Message Type: animation

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:

  • All frames are decoded and validated before playback starts
  • Minimum frame duration depends on display hardware (flip-dot: ~80ms)
  • Animation stops when: last frame finishes, new message arrives, or stop is called
  • When loop: true, animation repeats until stopped

Response:

{
  "status": "ok",
  "message_id": "550e8400-e29b-41d4-a716-446655440002"
}

GET /api/v1/status

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

POST /api/v1/stop

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"
}

Error Handling

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

How Messages Work

Single message at a time:

  1. You send a message → old message stops immediately
  2. New message starts playing
  3. Message runs until: duration expires, timeout hits, or new message arrives
  4. Display clears automatically

Message timeout:

  • All messages have a max runtime (usually 5 minutes)
  • Prevents displays getting stuck forever
  • Configurable by server admin

Message queue:

New message arrives
    ↓
Stop current message
    ↓
Start new message
    ↓
Return message_id to you
    ↓
Message plays until finished/stopped
    ↓
Display clears

Fun Use Cases

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
})

Tips & Tricks

For images:

  • Pre-resize to display dimensions before sending
  • Use PNG for best quality
  • Test with small images first
  • Remember: flip-dot displays are monochrome!

For animations:

  • Flip-dot displays can't flip faster than ~12 FPS (80ms per frame)
  • Keep frame count reasonable (< 1000 frames)
  • Compress/skip frames if your video is too long
  • Use loop sparingly - messages have timeouts

For text:

  • Short messages work better static (scroll: false)
  • Long messages look better scrolling
  • Speed 1-3: easy to read
  • Speed 8-10: looks cool but hard to read

For cross-hackerspace fun:

  • Always fetch /capabilities first to see display specs
  • Different spaces have different display sizes
  • Some displays are color, most are monochrome
  • Be nice - don't spam other hackerspaces' displays!

Implementing Your Own Server

This API spec is generic - implement it however you want! Here's what you need:

Required endpoints:

  • GET /api/v1/capabilities - return your display specs
  • POST /api/v1/message - handle text/bitmap/animation
  • GET /api/v1/status - return current status
  • POST /api/v1/stop - stop current message

Message handling:

  • Only one message at a time
  • New messages replace old ones immediately
  • Set a maximum message timeout (5-10 minutes)
  • Clear display when message finishes

Image processing:

  • Accept base64-encoded images (PNG, JPEG, etc.)
  • Resize to your display dimensions
  • Convert to your display's color mode
  • Handle corrupt/invalid images gracefully

Good practices:

  • Thread-safe message queue
  • Proper error messages
  • Log requests for debugging
  • Consider adding rate limiting if public

Protocol Version

Current version: 1

This is version 1 of the protocol. Future versions might add:

  • More message types
  • More encoding formats
  • Authentication/API keys
  • Queued messages (not just single message)
  • Priority levels
  • Scheduled messages

But for now, keep it simple!


Examples with JavaScript

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"});

That's It!

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:

  • Takt Praha (Prague, CZ): https://display.taktpraha.cz - 84×16 mono flip-dot

If you implement this API at your hackerspace, send an email to info@taktpraha.cz and we will add you to the list. Cheers