Web API#
The camera answers HTTP on port 5000. Any CineMate CLI command line can be sent over HTTP instead. Replies are plain text, so a microcontroller needs no JSON parser.
curl -d "rec" http://cinepi.local:5000/api/v1/cmd
Address#
| Hotspot SSID / password | CinePi / 11111111 (set in settings.jsonc under system.wifi_hotspot) |
| Camera address | cinepi.local, or 10.42.0.1 on the camera's own hotspot |
| API port | 5000 |
| Status broadcast port | 8888/udp |
| Base URL | http://cinepi.local:5000/api/v1/ |
Which address to use
Use cinepi.local from a laptop or phone. The installer enables avahi-daemon, so it
resolves on a joined network and on the hotspot, and it keeps working when the camera's
IP changes.
Hard-code 10.42.0.1 in microcontroller firmware. On the camera's own hotspot that
address never changes, so a name lookup buys nothing, and resolving a .local name needs
an mDNS resolver an ESP32 or Pico W build may not have. See
Building control units.
system.https.enabled moves the whole web server, API included, to https:// on the
same port 5000 with a self-signed certificate. Leave it off for microcontroller clients.
Send a command#
One endpoint: /api/v1/cmd. Send the command line exactly as you would type it in the CLI.
curl -d "set iso 800" http://cinepi.local:5000/api/v1/cmd
curl -d "set fps 48" http://cinepi.local:5000/api/v1/cmd
| Method | Use it from | Shape |
|---|---|---|
| POST | Microcontrollers, scripts | Body is the command line verbatim, no URL encoding and no JSON. Content-Type is not checked (the curl -d lines above send application/x-www-form-urlencoded and work); send text/plain when you can |
| GET | Browsers, quick tests | /api/v1/cmd?c=inc+iso — spaces may be + or %20 |
Replies#
| HTTP | Body | Meaning |
|---|---|---|
| 200 | ok |
Command matched and was dispatched |
| 200 | ok requested <x>, live value is <y> |
Dispatched, but the value read back had not stuck. Usually a pot or a lock writing the same parameter |
| 400 | err unknown command |
No command matched (an empty body counts as this) |
| 400 | err bad argument |
Matched, but the argument was the wrong type |
| 400 | err missing argument |
Command requires an argument |
| 401 | err unauthorized |
A token is configured and yours was wrong or absent |
| 403 | err blocked |
Destructive command, blocked by settings |
| 429 | err rate limited |
Too many commands per second |
| 503 | err busy |
Another command still held the dispatcher after 2 seconds |
Add ?json=1 for {"ok":true,"cmd":"set iso 800","message":""} instead.
200 means dispatched, not applied
set log restarts the camera when idle, and defers to the end of the take while
recording. set resolution restarts it only when the new mode changes the aspect ratio;
a same-aspect change is seamless. The reply arrives before any restart finishes. Read the
new state back with /api/v1/get/<key>.
Read a value#
GET /api/v1/get/is_recording → 1
Returns the raw value as text. This is the endpoint for a tally light. Unknown keys return 404 and err unknown key. Key names are the Redis keys.
Many values at once#
| Request | Result |
|---|---|
GET /api/v1/status |
JSON object of every key |
GET /api/v1/status?keys=is_recording,iso,fps |
JSON object of only those keys |
GET /api/v1/status?keys=iso,fps&fmt=text |
key=value lines instead of JSON |
Use ?keys= on memory-constrained devices; a full snapshot is all 88 keys, a couple of kilobytes of JSON. Unknown keys in ?keys= are skipped silently rather than erroring, so one firmware build works across CineMate versions.
/commands and /hello#
GET /api/v1/commands returns every command the camera accepts, as JSON [{"name":"set iso","arg":"int"}, ...], or one name per line with ?fmt=text. It is built live from the running command table.
GET /api/v1/hello returns cinemate 3.4.0 api=1 sensor=imx585 cams=1 rec=0. A configured token guards it, like every other /api/v1/ route.
Keep a device in sync#
| Method | Best for | Cost on the device | Cost on the camera |
|---|---|---|---|
| UDP broadcast | Tally lights, displays, many devices | ~10 lines, no connection state | One send serves every device |
| SSE | One to four devices needing every change | Open socket, read lines | One server thread per client |
Polling /get/<key> |
Anything; always works | Trivial | One short request per poll |
Use the UDP broadcast unless you have a reason not to. One plain-text line goes to port 8888, at both the wlan0 subnet broadcast address and 255.255.255.255, at 5 Hz plus immediately on change (coalesced to at most 10 Hz):
is_recording=1 iso=800 fps=24.0 shutter_angle_actual=180.0 recording_time_tod=01:02:03:04 space_left=412 drop_frame_count=0 is_mounted=1
The keys are the Redis keys, unabbreviated, truncated at 500 bytes. That line is the default set; pick your own in settings.jsonc.
Server-sent events#
GET /api/v1/events returns text/event-stream: one data: key=value per changed key, with a : ping heartbeat every 15 seconds. One thread is held per open connection, so SSE is capped (max_sse_clients, default 4). Beyond the cap the server returns 503 and err too many clients.
Settings#
The stock settings.jsonc already carries a full system.web_api block under system, with exactly the values below — edit it there. The same values are also hard-coded in src/module/web_api_settings.py, and they apply if you delete the block or fill it in only partly, so every field is optional.
| Field | Default | Effect |
|---|---|---|
enabled |
true |
false unregisters /api/v1 entirely, which also breaks the Web GUI's controls, since they post to it. The UDP broadcast has its own switch |
token |
"" |
When set, every /api/v1/ request must carry X-Cinemate-Token |
allow_destructive |
false |
When false, blocks reboot, shutdown, erase, format, matched on the first word of the command line |
max_commands_per_sec |
20 |
Per-client (per source IP) limit on /api/v1/cmd, over a sliding one-second window |
max_sse_clients |
4 |
Concurrent /events connections |
broadcast.enabled |
true |
The UDP status broadcast, switched independently of enabled |
broadcast.port |
8888 |
UDP destination port |
broadcast.hz |
5 |
Heartbeat rate when nothing is changing |
broadcast.keys |
see below | Which keys appear in the UDP line |
"system": {
"web_api": {
"enabled": true,
"token": "",
"allow_destructive": false,
"max_commands_per_sec": 20,
"max_sse_clients": 4,
"broadcast": {
"enabled": true,
"port": 8888,
"hz": 5,
"keys": ["is_recording", "iso", "fps", "shutter_angle_actual",
"recording_time_tod", "space_left", "drop_frame_count", "is_mounted"]
}
}
}
The hotspot password is public knowledge
CineMate ships with the hotspot password 11111111, so anyone within Wi-Fi range can
reach the API. allow_destructive therefore defaults to false: format and erase
cannot be triggered remotely on a stock unit. Change the hotspot password in
system.wifi_hotspot before enabling destructive commands, and set a token if the
camera will be used somewhere crowded.
Other control paths#
| Path | Transport | Same command lines? |
|---|---|---|
| CineMate CLI | cinemate in a terminal |
Yes |
| Serial | Tx/Rx pins or USB, 115200 baud | Yes |
| Web API | HTTP on port 5000 | Yes |
| Web GUI | Browser on port 5000, posts to /api/v1/cmd |
Yes |
| GPIO / rotary / I²C | settings.jsonc mappings |
No — see below |
The CLI, serial, the Web API and the Web GUI funnel into one dispatcher. GPIO buttons, rotary encoders and the I²C quad encoder are wired up differently: settings.jsonc names a controller method ({"method": "set_zoom"}), called directly rather than as a command line, so a command name is not a valid value there.
Next step: Building control units — wiring and firmware for a physical controller.