This document describes the HTTP and discovery APIs used around PRG32:
- the board-local API exposed by PRG32 firmware;
- the external ScoreServer API used for classroom scoreboards;
- the external MetricsServer API used for performance studies;
- the companion CartridgeStore API used to publish, browse, and download cartridge bundles.
The APIs are intentionally small and readable. They are suitable for classroom
experiments with curl, Python, or simple JavaScript clients.
Use plain HTTP on classroom networks unless a reverse proxy adds HTTPS.
| Value | Meaning |
|---|---|
| Board access point URL | http://192.168.4.1 |
| CartridgeStore default URL | http://<host>:5080 |
| ScoreServer default URL | http://<host>:5000 |
| MetricsServer default URL | http://<host>:8080 |
| JSON content type | application/json |
| Cartridge content type | application/octet-stream |
| Bundle upload content type | multipart/form-data |
Most JSON endpoints return a 2xx status for success and a 4xx or 5xx
status with a short text or JSON body for errors. Firmware endpoints keep error
bodies compact because they run on constrained hardware.
The board API is served by the PRG32 firmware when Wi-Fi support is enabled. It is used by host tools, labs, and setup workflows.
Enable Wi-Fi in main/prg32_config.h or through project configuration:
#define PRG32_WIFI_ENABLE 1
#define PRG32_WIFI_SCORES_ENABLE 1
#define PRG32_WIFI_SSID "your-network"
#define PRG32_WIFI_PASSWORD "your-password"The board can also run as an access point. In that mode the usual URL is:
http://192.168.4.1
GET /api
GET /api/Returns a compact JSON index of the board-local device API. This endpoint is the first endpoint clients should call when discovering what the running firmware can serve.
Example:
curl http://192.168.4.1/apiResponse:
{
"ok": true,
"service": "PRG32",
"endpoints": [
{"method":"GET","path":"/api","available":true},
{"method":"GET","path":"/api/runtime","available":true},
{"method":"GET","path":"/api/games","available":true},
{"method":"POST","path":"/api/games","available":true},
{"method":"POST","path":"/api/games/select","available":true},
{"method":"GET","path":"/api/screenshot.bmp","available":true},
{"method":"GET","path":"/api/performance.json","available":true},
{"method":"GET","path":"/api/scores","available":false},
{"method":"POST","path":"/api/scores","available":false}
]
}Expected behavior:
/apiand/api/return the same shape;- endpoints compiled into the firmware are always listed consistently;
available:falsemeans the route exists in the API model but the current build/configuration does not serve it, for example score routes whenPRG32_WIFI_SCORES_ENABLEis disabled.
GET /api/runtimeReturns firmware metadata, cartridge ABI information, diagnostic state, the currently selected cartridge, and import addresses used by the host cartridge builder.
Example:
curl http://192.168.4.1/api/runtimeTypical response fields:
{
"name": "PRG32",
"firmware_version": "1.0.0",
"cart_magic": "PRG32CART",
"cart_abi_major": 1,
"cart_abi_minor": 1,
"cart_abi_hash": 3117075842,
"cart_abi_features": 511,
"cart_load_addr": 1107296256,
"cart_max_size": 65536,
"cart_ram_size": 32768,
"cart_loaded": true,
"qemu": false,
"cart": {
"name": "pong",
"loaded": true,
"stored": true,
"code_size": 12480,
"mem_size": 2048,
"audio_size": 0,
"flags": 0,
"audio": false,
"multiplayer": false,
"generation": 3
},
"diag": {
"frame_count": 1294,
"input_state": 0
}
}Expected behavior:
- runtime returns a compact single
application/jsonresponse with firmware, cartridge, display-backend, and diagnostic status; cart_abi_hashandcart_abi_featureslet host tools reject incompatible portable cartridges before upload;- runtime does not include the full cartridge import-address table, because that table is too large for a reliable board-local status response while Wi-Fi and display services are active;
cart_loadedisfalsewhen no cartridge is active.qemuistruefor QEMU RGB builds andfalsefor physical ESP32-C6 builds.
Main use cases:
- verify that the board is reachable;
- inspect the active cartridge;
- build a cartridge against the exact resident firmware ABI.
GET /api/gamesReturns one object for each cartridge slot.
Example:
curl http://192.168.4.1/api/gamesResponse:
[
{
"slot": "cart0",
"name": "pong",
"loaded": true,
"stored": true,
"code_size": 12480,
"mem_size": 2048,
"audio_size": 0,
"flags": 0,
"audio": false,
"multiplayer": false,
"generation": 3
},
{
"slot": "cart1",
"name": "",
"loaded": false,
"stored": false,
"code_size": 0,
"mem_size": 0,
"audio_size": 0,
"flags": 0,
"audio": false,
"multiplayer": false,
"generation": 0
}
]POST /api/games?slot=<slot>
Content-Type: application/octet-stream
<raw .prg32 image>Parameters:
| Parameter | Required | Meaning |
|---|---|---|
slot |
no | Cartridge slot name from cart0 through cart3; defaults to cart0 |
Example with the host tool:
python3 tools/prg32_game.py upload build/pong.prg32 \
--url http://192.168.4.1 \
--slot cart0Example with curl:
curl -X POST 'http://192.168.4.1/api/games?slot=cart0' \
-H 'Content-Type: application/octet-stream' \
--data-binary @build/pong.prg32Success response:
{
"ok": true,
"slot": "cart0",
"stored": true,
"loaded": true,
"name": "pong",
"code_size": 12480
}Expected behavior:
- upload is accepted only when
PRG32_GAME_UPLOAD_ENABLEis enabled; - the request body must fit in the 128 KiB cartridge package limit;
- invalid cartridge images return
400with the cartridge validation error; - disabled upload support returns
403.
Main use cases:
- upload a lab cartridge without reflashing the resident firmware;
- replace a slot during development;
- stage a cartridge downloaded from CartridgeStore.
POST /api/games/select?slot=<slot>Parameters:
| Parameter | Required | Meaning |
|---|---|---|
slot |
no | Cartridge slot name; defaults to cart0 |
Example:
curl -X POST 'http://192.168.4.1/api/games/select?slot=cart1'Success response:
{"ok":true,"slot":"cart1"}Expected behavior:
- the selected slot becomes the active cartridge;
- invalid or empty slots return
400with the cartridge error message.
GET /api/screenshot.bmpReturns a 320x240 BMP image of the full LCD surface, including the 320x200 game viewport and the physical top/bottom bands.
Example:
curl http://192.168.4.1/api/screenshot.bmp --output screenshot.bmpExpected behavior:
- the firmware snapshots the current framebuffer without forcing a display flush from the HTTP request;
- the response uses
image/bmp; - the response includes a fixed
Content-Length; - the bitmap is encoded as a conventional 24-bit BMP for broad client compatibility;
- the response is marked
Cache-Control: no-store; - screenshot transfer is larger than JSON endpoints, so clients should use a timeout of at least 30 seconds on weak Wi-Fi links.
Main use cases:
- collect screenshots for lab reports;
- inspect QEMU or hardware output from a script;
- compare visual regressions in simple tests.
GET /api/performance.jsonReturns the latest setup-mode performance test result. The endpoint streams JSON chunks so the firmware does not need to allocate a second full copy of the data.
Example:
curl http://192.168.4.1/api/performance.json \
--output prg32_performance.jsonExpected behavior:
- returns the most recent in-RAM setup performance test;
- rebooting the board or QEMU clears the stored result;
- use
docs/metrics_api.mdfor the full JSON field reference.
GET /api/scoresReturns the board-local persistent scoreboard. The firmware keeps the five best local records for each game even when no Cartridge Store URL has been configured.
Games can also access the same local records directly through
prg32_score_count and prg32_score_get, or show the built-in on-device
scoreboard with prg32_scoreboard_show.
Example:
curl http://192.168.4.1/api/scoresResponse:
[
{"game":"pong","player":"Ada","score":42}
]POST /api/scores
Content-Type: application/json
{"game":"breakout","player":"Grace","score":1200}Request fields:
| Field | Type | Meaning |
|---|---|---|
game |
string | Short game identifier |
player |
string | Player name or initials |
score |
number | Non-negative score value |
Example:
curl -X POST http://192.168.4.1/api/scores \
-H 'Content-Type: application/json' \
-d '{"game":"breakout","player":"Grace","score":1200}'Success response:
{"ok":true}Expected behavior:
- scores are stored in a dedicated local NVS partition by the board-local API;
- only the five best local records for each game are kept;
- each score record associates a short game identifier, player name, and numeric score;
- pending local records can be retried with
prg32_score_sync_remoteonce a Cartridge Store URL is configured; - board-local scores survive reboot and cartridge replacement;
- use the external ScoreServer for shared classroom leaderboards.
The standalone ScoreServer repository is the persistent classroom scoreboard:
https://github.com/riscv-prg32/ScoreServer
It uses the same score endpoints as the board-local API:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/scores |
List persisted scores |
POST |
/api/scores |
Add a score |
Run it on a classroom machine:
git clone https://github.com/riscv-prg32/ScoreServer.git
cd ScoreServer
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
python3 app.pySubmit from the host:
curl -X POST http://localhost:5000/api/scores \
-H 'Content-Type: application/json' \
-d '{"game":"pong","player":"Ada","score":42}'Submit from firmware C code:
prg32_score_submit_remote("http://192.168.1.20:5000",
"pong",
"Ada",
42);Main use cases:
- class tournaments;
- lab exercises about JSON and REST APIs;
- comparing board-local RAM storage with server-side persistence.
CartridgeStore is the companion catalog service for PRG32 cartridges:
https://github.com/riscv-prg32/CartridgeStore
Canonical discovery constants:
| Constant | Value |
|---|---|
| mDNS service type | _prg32store._tcp |
| mDNS default port | 5080 |
| Discovery ABI | prg32-store-discovery-1.0 |
CartridgeStore advertises _prg32store._tcp.local on port 5080 by default.
Physical ESP32-C6 firmware can discover this service on the local network.
QEMU builds cannot use mDNS through the virtual network and should use a
configured URL.
Host-side example:
python3 tools/prg32_game.py store-discoverGET /.well-known/prg32-store.jsonResponse:
{
"abi": "prg32-store-discovery-1.0",
"name": "PRG32 Cartridge Store",
"api": "http://192.168.1.42:5080/api",
"web": "http://192.168.1.42:5080/",
"version": "1.0.0"
}Expected behavior:
- clients must verify the
abivalue before treating a server as compatible; - firmware setup saves the chosen base URL in NVS;
- runtime URL priority is saved NVS value, then
CONFIG_PRG32_STORE_URL, then mDNS discovery.
The store API works with architecture-specific cartridge artifacts.
Canonical architecture strings:
| Target | Architecture |
|---|---|
| Physical ESP32-C6 board | esp32c6 |
| QEMU desktop build | qemu |
GET /api/gamesExample:
python3 tools/prg32_game.py store-list \
--store-url http://192.168.1.42:5080Typical response shape:
{
"games": [
{
"id": "org.uniparthenope.tetris-c",
"title": "tetris-c",
"version": "1.0.0",
"summary": "Tetris for PRG32",
"tags": ["game", "c"],
"architectures": ["esp32c6", "qemu"]
}
]
}Client note: PRG32 host tooling accepts a top-level array or an object with
games, items, or cartridges.
GET /api/games/<id>Returns one game detail record, including metadata, versions, assets, and available architectures. Use this before showing a detailed setup-page preview.
Example:
curl http://192.168.1.42:5080/api/games/org.uniparthenope.tetris-cGET /api/games/<id>/icon
GET /api/games/<id>/screenshot
GET /api/games/<id>/colophonExpected behavior:
iconreturns compact image bytes when an icon was published;screenshotreturns image bytes when a screenshot is available;colophonreturns compact JSON using theprg32-colophon-1.0ABI.
These endpoints are optional for minimal classroom stores. Clients should handle
404 by showing a simple text-only game record.
GET /api/games/<id>/download?architecture=<architecture>&version=<version>Parameters:
| Parameter | Required | Meaning |
|---|---|---|
architecture |
yes | esp32c6 or qemu |
version |
no | Requested semantic version; store default is usually latest |
Example:
python3 tools/prg32_game.py store-download org.uniparthenope.tetris-c \
--store-url http://192.168.1.42:5080 \
--architecture esp32c6 \
--out build-esp32c6/tetris-c.prg32Equivalent curl:
curl 'http://192.168.1.42:5080/api/games/org.uniparthenope.tetris-c/download?architecture=esp32c6&version=1.0.0' \
--output tetris-c.prg32Expected behavior:
- response body is the raw
.prg32cartridge image; - incompatible or missing architectures return
404or another documented store-side error; - downloaded physical cartridges should be uploaded to the board with
POST /api/games.
Publishing endpoints are used by tools/prg32_game.py. Stores may require a
Bearer token.
Default host config:
{
"store_url": "http://192.168.1.42:5080",
"store_token": "my-api-token"
}Save it as:
~/.prg32/config.json
Command-line --store-url and --token values take precedence.
POST /api/publish/bundle
Authorization: Bearer <token>
Content-Type: multipart/form-data
bundle=<zip file>The zip bundle contains:
manifest.json
<architecture cartridge>.prg32
<optional icon/splash/colophon assets>
Manifest example:
{
"abi": "prg32-metadata-1.0",
"id": "org.uniparthenope.tetris-c",
"title": "tetris-c",
"version": "1.0.0",
"summary": "Tetris for PRG32",
"tags": ["game", "c"],
"assets": {
"icon": "icon.png",
"splash": "splash.png"
},
"architectures": [
{"id": "esp32c6", "file": "tetris-c-esp32c6.prg32"}
]
}Tool example:
python3 tools/prg32_game.py publish \
examples/games/tetris/c/game.c \
--portable \
--entry-prefix tetris_c \
--name tetris-c \
--id org.uniparthenope.tetris-c \
--version 1.0.0 \
--summary "Tetris for PRG32" \
--architecture esp32c6 \
--store-url http://192.168.1.42:5080Expected behavior:
- missing or invalid tokens commonly return
401; - invalid bundles return
400; - successful responses are JSON and normally create a pending submission;
- the game appears in the public catalog after an editor verifies it.
POST /api/publish/bundle
Authorization: Bearer <token>
Content-Type: multipart/form-data
bundle=<zip file>Tool example:
python3 tools/prg32_game.py pack-bundle \
--manifest build-esp32c6/tetris-bundle/manifest.json \
--out tetris.zip
python3 tools/prg32_game.py publish-bundle tetris.zip \
--store-url http://192.168.1.42:5080Use this endpoint when the build artifacts already exist or when publishing a multi-architecture bundle.
POST /api/publish remains a compatibility alias for the same zip-bundle
upload shape. The Cartridge Store no longer accepts the old loose multipart
.prg32 upload fields.
MetricsServer receives streaming frame metrics from firmware and serves run reports for papers or lab analysis:
https://github.com/riscv-prg32/MetricsServer
Run it:
git clone https://github.com/riscv-prg32/MetricsServer.git
cd MetricsServer
python3 -m venv .venv
. .venv/bin/activate
python3 -m pip install -r requirements.txt
python3 app.py --host 0.0.0.0 --port 8080Endpoint summary:
| Method | Path | Purpose |
|---|---|---|
POST |
/api/runs |
Register or update run metadata |
POST |
/api/metrics/batch |
Store a batch of sampled frames |
POST |
/api/runs/<run_id>/finish |
Mark a run as finished |
GET |
/api/runs |
List recorded runs |
GET |
/api/runs/<run_id> |
Show one run with summary statistics |
GET |
/api/runs/<run_id>/samples.csv |
Download raw samples |
GET |
/api/runs/<run_id>/report.md |
Download a Markdown report |
POST /api/runs
Content-Type: application/jsonTypical payload:
{
"run_id": "prg32-board-20260608-101500",
"board_id": "prg32-board",
"target": "esp32c6",
"display_backend": "ili9341",
"firmware_version": "1.0.0",
"firmware_git_sha": "abc1234",
"game_name": "pong",
"cartridge_generation": 3,
"build_type": "release",
"sample_period_frames": 1
}Expected behavior:
- firmware calls this when a metrics run starts;
- repeated calls for the same
run_idupdate metadata.
POST /api/metrics/batch
Content-Type: application/jsonTypical payload:
{
"run_id": "prg32-board-20260608-101500",
"dropped_samples": 0,
"samples": [
{
"frame": 120,
"timestamp_ms": 4000,
"update_us": 900,
"draw_us": 2100,
"present_us": 8200,
"frame_us": 11200,
"heap_free": 173000,
"heap_min_free": 169000,
"input_mask": 0,
"fps_x100": 8928,
"upload_queue_depth": 4,
"deadline_missed": false
}
]
}Expected behavior:
prg32_metrics_record()only copies samples into a small queue;- network upload happens in a background task;
- if the queue fills, firmware reports dropped samples in a later batch.
POST /api/runs/<run_id>/finish
Content-Type: application/jsonMarks the run as complete. MetricsServer can then present final summary statistics and reports.
Examples:
curl http://192.168.1.20:8080/api/runs
curl http://192.168.1.20:8080/api/runs/prg32-board-20260608-101500
curl http://192.168.1.20:8080/api/runs/prg32-board-20260608-101500/samples.csv \
--output samples.csv
curl http://192.168.1.20:8080/api/runs/prg32-board-20260608-101500/report.md \
--output report.mdUse docs/metrics_api.md for the full setup-performance and streaming metrics
field reference.
python3 tools/prg32_game.py build examples/games/pong/c/game.c \
--portable \
--entry-prefix pong_c \
--out build-esp32c6/pong.prg32
python3 tools/prg32_game.py upload build-esp32c6/pong.prg32 \
--url http://192.168.4.1 \
--slot cart0python3 tools/prg32_game.py publish \
examples/games/tetris/c/game.c \
--portable \
--entry-prefix tetris_c \
--name tetris-c \
--id org.uniparthenope.tetris-c \
--version 1.0.0 \
--architecture esp32c6 \
--store-url http://192.168.1.42:5080
python3 tools/prg32_game.py store-download org.uniparthenope.tetris-c \
--store-url http://192.168.1.42:5080 \
--architecture esp32c6 \
--out build-esp32c6/tetris-c.prg32
python3 tools/prg32_game.py upload build-esp32c6/tetris-c.prg32 \
--url http://192.168.4.1 \
--slot cart0curl http://192.168.4.1/api/performance.json \
--output prg32_performance.json
python3 tools/prg32_metrics_paper.py prg32_performance.json \
--out paper_metrics/prg32_run01 \
--dpi 300| Symptom | Likely cause | Fix |
|---|---|---|
404 on a board endpoint |
Wi-Fi API not enabled or wrong base URL | Check firmware config and board IP |
403 on POST /api/games |
Upload support disabled | Enable PRG32_GAME_UPLOAD_ENABLE |
400 during upload |
Invalid image, oversized image, or bad slot | Rebuild the cartridge and check slot |
| Empty board score list after reflashing with an erased score partition | Local score partition was erased | Re-submit scores or sync from the classroom service |
| Store discovery finds nothing | mDNS blocked or QEMU build | Enter the store URL manually |
| Store download missing architecture | Only another target was published | Publish esp32c6 or qemu as needed |
401 on publish |
Missing or invalid Bearer token | Add --token or store_token |
| No metrics appear on server | Metrics disabled or server unreachable | Check metrics Kconfig and server URL |
docs/cartridges.md: cartridge upload workflow.docs/cartridge_store.md: CartridgeStore user workflow.docs/setup_mode_cartridge_store.md: firmware setup-mode integration notes.docs/score_api.md: focused score API guide.docs/metrics_api.md: performance and metrics field reference.docs/cartridge_metadata.md: metadata and colophon formats.