← Project page

Wireless Paper API

Use either board as an HTTP-to-LoRa adapter on port 80, with text, drawing, and image controls for its screen. Wireless Paper uses 250 × 122 pixels; Wireless Tracker V1.1 uses 160 × 80. Read width, height, frame_bytes, and board_model from /api/status. Both builds use the same monochrome drawing API. The same display JSON works over the custom LoRa packet protocol. The setup hotspot runs for the first 60 minutes after startup. Connected clients keep it open until the last one leaves. A saved always-on option is available. Normal station Wi-Fi remains enabled.

Text and drawing

Send JSON to POST /api/display, with Content-Type: application/json.

{
  "text": "Hello!",
  "size": 3,
  "x": 8,
  "y": 8,
  "color": "white",
  "background": "black",
  "wrap": true,
  "clear": true
}
SettingValuesDefault
textText, up to 2,000 bytes. Built-in bitmap font; render a bitmap for Unicode or custom fonts.Omitted
sizeInteger 1–8. Scale of a 6 × 8 pixel font: size 2 uses a 12 × 16 pixel character cell.2 for top-level text; 1 for text operations unless a global size is supplied
x, yText origin in pixels from top left. x: −500 to 750; y: −500 to 622. Off-screen pixels are clipped.4, 4
color"black", "white", "#000000", "#ffffff", or numeric 1 = black / 0 = white. Names are case-insensitive. Sets the default ink for text and all operations."black"
backgroundSame color values. Fills the canvas when clear is true; ignored when clear is false."white"
wraptrue wraps text at the screen edge; false clips long lines. Newlines start at the left edge.true
cleartrue starts with the background color; false draws over the last completed picture.true
opsArray of drawing operations, executed in order after top-level text. Maximum 100 operations.No operations

The shared API renders black and white on both displays, including the Tracker TFT. Photos can use black-and-white dithering. Large text is clipped if it does not fit; automatic font fitting is not enabled.

Drawing operations

Every operation requires type, x, and y. All accept color, which overrides the global ink. Coordinates use the same ranges as top-level x/y.

TypeAdditional settings
texttext (required), size (1–8), wrap (true/false)
linex2, y2 (required end point); one pixel wide
rectw, h (required, 1–500), fill (true for solid, default false)
circler (required radius, 1–250), fill (true for solid, default false)
pixelNo extra settings
{
  "clear": true,
  "background": "white",
  "color": "black",
  "ops": [
    {"type":"rect","x":0,"y":0,"w":250,"h":32,"fill":true},
    {"type":"text","x":8,"y":8,"text":"STATUS","size":2,"color":"white"},
    {"type":"text","x":8,"y":48,"text":"All systems ready","size":1},
    {"type":"circle","x":225,"y":90,"r":12,"fill":false}
  ]
}

Images

Send exactly frame_bytes from GET /api/status (3,904 for Paper, 1,600 for Tracker) to POST /api/frame with Content-Type: application/octet-stream. Rows are top to bottom. Each row has ceil(width / 8) bytes, most significant bit first; 1 means black. Paper has six padding bits per row; Tracker has none. GET /api/frame returns the last completed frame. The browser controls convert pictures for you.

Progress and errors

Display writes return 202 {"accepted":true,"job":NUMBER}. Poll GET /api/status until completed_job matches the job and busy is false. A panel timeout sets display_error. The server returns 409 while the screen is busy and 400 for invalid input. JSON is limited to 8,192 bytes. Rejected input does not alter the screen.

LoRa adapter API

The board can send and receive raw LoRa packets as an HTTP-controlled adapter. It is not Meshtastic or LoRaWAN. Display commands still work using the WP packet format below.

GET /api/lora/status returns radio state. GET /api/lora/config returns the saved settings and state. POST /api/lora/config accepts any subset of these fields; settings are saved across restarts:

{"enabled":true,"frequency_mhz":868.1,"bandwidth_khz":125,"spreading_factor":7,"coding_rate":5,"sync_word":18,"power_dbm":10,"preamble":8,"crc":true,"tx_enabled":false,"duty_cycle_percent":1}

Match your hardware band. Set enabled:false to deactivate the radio without losing its frequency. Set enabled:true to activate reception. Frequency 0 also disables the radio. A frequency update without an enabled field activates a positive frequency for compatibility. Accepted ranges: frequency 430–510 or 863–928 MHz; bandwidth 125/250/500 kHz; spreading factor 5–12; coding-rate denominator 5–8; sync word 0–255; TX power −9 to 22 dBm; preamble 8–65535; duty limit 0.1–100%. Header mode is explicit. TX is disabled by default. Use an appropriate antenna and settings permitted for your location.

POST /api/lora/activate accepts {"enabled":true} or {"enabled":false}, and optional tx_enabled. A frequency must be configured before activation. Activation and TX permission are saved across restarts. Deactivating the radio does not stop station Wi-Fi or change the hotspot policy. The older /api/radio path remains a configuration alias.

curl http://BOARD/api/lora/config -H 'Content-Type: application/json' \
  -d '{"frequency_mhz":868.1,"enabled":true,"tx_enabled":false}'

curl http://BOARD/api/lora/activate -H 'Content-Type: application/json' \
  -d '{"enabled":true,"tx_enabled":true}'

curl http://BOARD/api/lora/send -H 'Content-Type: application/json' \
  -d '{"text":"hello"}'

curl 'http://BOARD/api/lora/messages?after=0'

curl http://BOARD/api/lora/activate -H 'Content-Type: application/json' \
  -d '{"enabled":false}'

POST /api/lora/send accepts {"text":"hello"} or {"hex":"00ff1234"}, exactly one, with 1–255 payload bytes. Success returns {"sent":true,"id":1,"bytes":5,"receiver_ready":true}. This reports the local radio transmission, not remote receipt. Reception pauses while transmitting. An unavailable radio returns 409, disabled TX returns 403, invalid payload returns 400, and airtime pacing returns 429 with Retry-After. The pacing safeguard does not guarantee regulatory compliance.

Per-request radio settings

Add a radio object to a send request to temporarily override any supported modem settings. Omitted fields inherit the saved configuration. For a one-shot send from a deactivated board, include enabled:true, a positive frequency_mhz, and tx_enabled:true. These values are never saved. The configured receiver is restored after transmission, including its inactive state when applicable.

curl http://BOARD/api/lora/send -H 'Content-Type: application/json' \
  -d '{"text":"hello with another modem profile","radio":{"enabled":true,"frequency_mhz":868.1,"bandwidth_khz":250,"spreading_factor":8,"tx_enabled":true}}'

The response includes temporary_radio, the effective transmit radio settings, receive_radio after restoration, receiver_ready, and receiver_error. A temporary transmit profile does not create a listening window on those settings. Configure the saved receiver to match incoming replies. Invalid overrides return 400 before changing the modem. Pacing applies across profiles.

GET /api/lora/messages?after=0 returns the last 16 sent/received packets in RAM, each with id, uptime_ms, direction, byte count, hex payload, printable ASCII text where possible, the radio settings used for each packet, and RSSI/SNR for received packets. Use latest_id as the next after cursor. The truncated flag reports an expired cursor. Packet IDs and the log reset when the board restarts.

LoRa packets have a 16-byte little-endian header: ASCII WP (2 bytes), version 1 (1), kind 1=JSON or 2=bitmap (1), request ID (4), whole payload length (2), fragment offset (2), whole-payload CRC32 (4). Append up to 224 payload bytes. Offsets are multiples of 224; all but the final fragment contain 224 bytes. CRC matches Python zlib.crc32. Whole payloads are limited to 8,192 bytes; bitmaps must match the destination screen: 3,904 bytes for Paper or 1,600 for Tracker. Partial transfers expire after 120 seconds of inactivity. There are no acknowledgments. Use a second LoRa transmitter, matching settings, and suitable airtime pacing.

Setup hotspot

At every startup the screen shows the hotspot name, http://192.168.4.1/, and the selected policy. The default is a 60-minute setup window from boot. If clients are connected when that hour ends, the hotspot remains on until the last client disconnects. Restarting opens a new window.

POST /api/hotspot accepts {"mode":"always"} or {"mode":"first-hour"}. The selected policy survives restarts. GET /api/hotspot or /api/status reports hotspot_mode, hotspot_active, hotspot_clients, hotspot_seconds_remaining, and hotspot_held_open. The seconds remaining value describes the initial window, not a new hour started by saving a setting.

Only the hotspot can expire. Station Wi-Fi stays enabled. POST /api/power accepts only mode online; radio-only/off/sleep modes return 403. Legacy timed Wi-Fi display actions are rejected.

Wireless Paper API by Ramazan Yavuz. Provided as is, without warranty of any kind. You accept all risk from installation and use.