- This document is the source of truth for the runtime HTTP API.
- Any added, changed, or removed HTTP endpoint behavior must be reflected here.
- Keep endpoints lean and focused on direct operational value.
- Accepted
/api/v1/setand/api/v1/defaultschanges are persisted to the registered config path before the response returns. Manual/api/v1/restartstill reloads exactly what is already on disk./api/v1/live/setis the deliberate exception: it applies live fields to the running config without touching disk (for high-cadence automated writers). - Keep JSON payloads simple and descriptive.
- Keep mutability semantics explicit:
live— applied immediately without pipeline restart.restart_required— triggers automatic pipeline reinit (teardown + rebuild).read_only— cannot be changed via API.
contract_version:0.31.0status:active
supported in /api/v1/capabilities is per backend and is not cosmetic — a
false field is rejected by GET /api/v1/set and /api/v1/live/set with
501 not_implemented rather than accepted and ignored.
| Field | Star6E | Maruko | CV610 |
|---|---|---|---|
video0.qpDelta |
true | true | false |
video0.minQp / maxQp |
true | true | true |
video0.intraRefreshQp |
false | false | true |
outgoing.sidecarPort |
true | true | true (from 0.74.0) |
image.mirror / image.flip |
true | true | true (from 0.75.0) |
image.rotate |
true | true | false |
fpv.roiEnabled / roiQp / roiSteps / roiCenter |
true | true | true (from 0.25.0) |
isp.gainMax / isp.shutterMaxUs |
true | true | true (from 0.78.0) |
isp.sensorBin |
true | true | true (from 0.81.0, PQTools .bin via libbin.so) |
fpv.roiQp accepted range |
±20 | ±20 | ±20 (±30 before 0.79.0) |
video0.maxQp interacts with fpv.roiQp |
yes | yes | yes (measured) |
isp.gainMin / isp.shutterMinUs / isp.awbMode / isp.awbCt |
true | true | false |
outgoing.server / outgoing.enabled mutability |
live | live | live from 0.26.0 (restart_required before) |
outgoing.server that cannot be brought up |
inert output, craft runs (from 0.82.0) | same | same |
video0.qpDelta is false on CV610 and video0.intraRefreshQp is false on
the SigmaStar backends because in each case the encoder accepts the value and
does not act on it. Clients should read supported rather than assume a field
present in the schema is live on the craft in front of them.
- Non-breaking changes: add optional fields, add new endpoints, extend enum values.
- Breaking changes: remove endpoints, rename fields, change required field semantics.
- For every breaking change: increment contract major version, add migration note, update
HISTORY.md. - For every non-breaking change: increment contract minor/patch version, update this file.
- HTTP/1.0, all methods use
GET(compatible with BusyBox wget) - Default port: 80 (configurable via
system.web_portin config) - Response content type:
application/json; charset=UTF-8 - Query parameters: field name is the key, value (if any) follows
=
{
"ok": true,
"data": {}
}{
"ok": false,
"error": {
"code": "string_code",
"message": "human readable message"
}
}| Code | HTTP Status | Meaning |
|---|---|---|
invalid_request |
400 | Missing or malformed parameters |
validation_failed |
400/409 | Value rejected by field or config validation |
not_found |
404 | Unknown field or route |
record_active |
409 | Action blocked while recording is in progress |
not_implemented |
501 | Apply callback not available for this field |
internal_error |
500 | Server-side failure |
Return app, backend, schema, and contract version information.
curl http://<device-ip>/api/v1/versionResponse 200:
{
"ok": true,
"data": {
"app_version": "0.84.0",
"contract_version": "0.31.0",
"config_schema_version": "1.0.0",
"backend": "star6e"
}
}Return the full active runtime config.
curl http://<device-ip>/api/v1/configResponse 200:
{
"ok": true,
"data": {
"config": {
"system": { "webPort": 80, "overclockLevel": 2, "verbose": false },
"sensor": { "index": -1, "mode": -1 },
"isp": { "sensorBin": "/etc/sensors/imx415_greg_fpvXVIII-gpt200.bin", "aeEngine": "sdk", "aeFps": 15, "gainMax": 0, "awbMode": "auto", "awbCt": 5500, "keepAspect": true },
"image": { "mirror": false, "flip": false, "rotate": 0 },
"video0": { "rcMode": "cbr", "fps": 90, "size": "auto", "bitrate": 8192, "gopSize": 1.0, "qpDelta": 0, "sceneThreshold": 0, "sceneHoldoff": 2, "sliceCount": 1, "resilience": "off", "intraRefreshQp": 0, "zoomX": 0.5, "zoomY": 0.5, "framing": "off" },
"outgoing": { "enabled": true, "server": "udp://192.168.2.20:5600", "streamMode": "rtp", "maxPayloadSize": 1400, "connectedUdp": false, "allowUnixEncoderStall": false },
"fpv": { "roiEnabled": false, "roiQp": -20, "roiSteps": 2, "roiCenter": 0.4, "noiseLevel": 0 },
"record": { "enabled": false, "mode": "off", "dir": "/tmp/sdcard", "format": "ts", "maxSeconds": 300, "maxMB": 500 },
"debug": { "showOsd": false }
},
"runtime": {
"active_precrop": { "x": 0, "y": 240, "w": 2560, "h": 1440 },
"vpe_taps": { "port0": ["main", "jpeg"], "port1": "detect" }
}
}
}The runtime block is read-only and reports pipeline state that is not
part of the editable config:
active_precrop— VIF crop rectangle currently programmed (includes any sensor overscan offsets or SCL crop origin). Present whenever a Star6E or Maruko pipeline has been started; absent before pipeline start or after pipeline stop.vpe_taps— VPE scaler-output ownership (Star6E only; absent on Maruko and before pipeline start).port0is the main SCL output, a 1:N-shareable buffer listing its consumers (main— the H.265 encoder, always present; plusjpegwhen the snapshot channel is up andrecordwhen a dual/record channel is bound — these bind alongside on the same buffer, not a second scaler).port1is the single second scaler output: a string naming its sole owner ("stab"or"detect"), ornullwhen free. The arbiter refuses a secondport1claim, sostabanddetectare mutually exclusive on the hardware;stab-fillridesport0only (noport1tap) but stays mutually exclusive withdetectby resource policy.
Return per-field mutability and backend support.
curl http://<device-ip>/api/v1/capabilitiesResponse 200:
{
"ok": true,
"data": {
"fields": {
"video0.bitrate": { "mutability": "live", "supported": true },
"video0.fps": { "mutability": "live", "supported": true },
"video0.gop_size": { "mutability": "live", "supported": true },
"video0.qp_delta": { "mutability": "live", "supported": true },
"video0.size": { "mutability": "restart_required", "supported": true },
"video0.scene_threshold": { "mutability": "restart_required", "supported": true },
"video0.scene_holdoff": { "mutability": "restart_required", "supported": true },
"video0.slice_count": { "mutability": "restart_required", "supported": true },
"video0.resilience": { "mutability": "restart_required", "supported": true },
"video0.intra_refresh_qp": { "mutability": "restart_required", "supported": false },
"video0.zoom_x": { "mutability": "live", "supported": true },
"video0.zoom_y": { "mutability": "live", "supported": true },
"video0.framing": { "mutability": "restart_required", "supported": true },
"system.verbose": { "mutability": "live", "supported": true },
"outgoing.enabled": { "mutability": "live", "supported": true },
"outgoing.server": { "mutability": "live", "supported": true },
"outgoing.stream_mode": { "mutability": "restart_required", "supported": true },
"outgoing.connected_udp": { "mutability": "restart_required", "supported": true },
"outgoing.allow_unix_encoder_stall": { "mutability": "restart_required", "supported": true },
"discovery.enabled": { "mutability": "restart_required", "supported": true },
"discovery.service_type": { "mutability": "restart_required", "supported": true },
"discovery.name": { "mutability": "restart_required", "supported": true },
"discovery.bare_alias": { "mutability": "restart_required", "supported": true },
"fpv.roi_qp": { "mutability": "live", "supported": true },
"video0.stab_crop_pct": {
"mutability": "restart_required", "supported": true,
"ui": {
"group": "Stabilization", "label": "Stab crop %", "control": "number",
"min": 60, "max": 100, "step": 1, "tooltip": "Kept-frame percentage ..."
}
},
"video0.pause_stab": {
"mutability": "live", "supported": true,
"ui": {
"group": "Stabilization", "label": "Pause stab", "control": "toggle",
"tooltip": "Live pause for framing=stab and stab-fill ..."
}
}
}
}
}(truncated — all fields listed in actual response)
video0.slice_count / JSON video0.sliceCount requests 1–32 H.265 slices
per picture; 1 disables splitting. Star6E, Maruko and CV610 advertise the
field. The request is mapped to backend row geometry, so delivered counts may
quantize or saturate; startup performs vendor Set/Get verification and fails
an explicit multi-slice request when the backend cannot apply it. Slice NALs
remain one whole access unit for RTP, recording and frame-SHM.
data.routes (added 0.18.4) reports which optional routes the running
backend actually services, so a client does not have to call an expensive
endpoint just to discover whether it exists:
{"ok":true,"data":{"routes":{"iq":true,"iq_import":false},"fields":{ ... }}}| key | meaning |
|---|---|
iq |
/api/v1/iq and /api/v1/iq/set are serviced (the backend registers both query_iq_info and apply_iq_param). |
iq_import |
/api/v1/iq/import is compiled in (Star6E/Maruko only). |
iq_export_bin |
/api/v1/iq/export_bin is serviced (the backend registers export_isp_bin; CV610 only). |
Absent routes means an older build: treat every route as possibly present
and fall back to calling it.
supported is backend-specific. Current Star6E and Maruko builds both expose
scene detection and digital zoom fields, and drive intra refresh from the
video0.resilience preset — but video0.intraRefreshQp is advertised on
CV610 only, because the SigmaStar encoder stores it and ignores it. See the
per-backend table above.
A field MAY carry an optional ui object (data-driven field schema): when
present the dashboard renders a control for it generically — no dashboard.html
edit or webui-blob rebuild is needed to surface a new module field. Keys:
group (collapsible section title), label, control
(toggle|number|select|text), min/max/step (for number),
options (array, for select), tooltip. Fields without ui use the
dashboard's static schema. The entire Stabilization section is data-driven:
the four persisted video0.stab_* knobs (stab_crop_pct, stab_kalman_q,
stab_kalman_r, stab_recenter_speed) plus the runtime-only video0.pause_stab
(the live stab pause — not in /api/v1/config) are all surfaced this way. So
is the Snapshot section (snapshot.enabled, snapshot.quality,
snapshot.width, snapshot.height) — the switch for both snapshot endpoints,
which was API-only before.
Majestic-compatible alias of /api/v1/config.
curl http://<device-ip>/api/v1/config.jsonRead a single config field. The field name is the query parameter key (no value needed).
# Read current bitrate
curl "http://<device-ip>/api/v1/get?video0.bitrate"
# Read current qpDelta
curl "http://<device-ip>/api/v1/get?video0.qp_delta"
# Read current resilience preset
curl "http://<device-ip>/api/v1/get?video0.resilience"
# Read a string field
curl "http://<device-ip>/api/v1/get?isp.sensor_bin"Response 200:
{"ok":true,"data":{"field":"video0.bitrate","value":8192}}{"ok":true,"data":{"field":"video0.resilience","value":"off"}}{"ok":true,"data":{"field":"video0.qp_delta","value":0}}{"ok":true,"data":{"field":"isp.sensor_bin","value":"/etc/sensors/imx415_greg_fpvXVIII-gpt200.bin"}}Error 400 — missing field name:
{"ok":false,"error":{"code":"invalid_request","message":"missing query parameter (field name)"}}Error 404 — unknown field:
{"ok":false,"error":{"code":"not_found","message":"unknown config field"}}Majestic-style camelCase aliases are also accepted for selected fields,
including fpv.roiQp, fpv.roiEnabled, fpv.roiSteps, fpv.roiCenter,
fpv.noiseLevel, isp.sensorBin, isp.awbMode, isp.awbCt,
isp.keepAspect, isp.shutterRule180,
video0.rcMode, video0.gopSize, video0.qpDelta,
video0.sceneThreshold, video0.sceneHoldoff,
video0.intraRefreshMode, video0.intraRefreshLines,
video0.intraRefreshQp, video0.zoomX, video0.zoomY, video0.framing,
outgoing.maxPayloadSize,
outgoing.audioPort, system.webPort, and system.overclockLevel.
Write a config field. The field name is the query key, the new value follows =.
Live fields (mutability: "live") are applied immediately without pipeline restart:
# Change bitrate to 4096 kbps
curl "http://<device-ip>/api/v1/set?video0.bitrate=4096"
# Change FPS
curl "http://<device-ip>/api/v1/set?video0.fps=60"
# Swap ISP tuning bin (empty = auto-detect /etc/sensors/<sensor>.bin)
curl "http://<device-ip>/api/v1/set?isp.sensorBin=/etc/sensors/imx415_fpv.bin"
# Change GOP interval (seconds between keyframes; 0 = all-intra)
curl "http://<device-ip>/api/v1/set?video0.gop_size=0.5"
# Bias relative I-frame QP (Majestic-compatible range: -12..12)
curl "http://<device-ip>/api/v1/set?video0.qp_delta=-4"
# Pan within the active digital zoom crop
curl "http://<device-ip>/api/v1/set?video0.zoomX=0.25&video0.zoomY=0.75"
# Apply multiple live fields atomically in one request
curl "http://<device-ip>/api/v1/set?video0.bitrate=4096&system.verbose=true"
# Coupled live timing changes can be sent together
curl "http://<device-ip>/api/v1/set?video0.fps=30&video0.gopSize=1.0"When video0.scene_threshold is non-zero, the inline scene detector tracks
frame size EMA and requests IDR after scene change spikes settle.
If a GET /api/v1/set request contains multiple key=value pairs joined by
&, every field must be live. Mixed live + restart requests are rejected.
Duplicate fields are also rejected after alias canonicalization, so
video0.qp_delta and video0.qpDelta cannot appear in the same batch.
Response 200:
{"ok":true,"data":{"field":"video0.bitrate","value":4096}}Response 200 for multi-set:
{"ok":true,"data":{"applied":[{"field":"video0.bitrate","value":4096},{"field":"system.verbose","value":true}]}}Restart-required fields (mutability: "restart_required") trigger an automatic
pipeline reinit (sensor→VIF→VPE→VENC teardown and rebuild):
# Change resolution (single call, triggers one pipeline reinit)
curl "http://<device-ip>/api/v1/set?video0.size=1280x720"
# Use sensor native resolution (default — no downscaling)
curl "http://<device-ip>/api/v1/set?video0.size=auto"
# Preset shortcuts also work
curl "http://<device-ip>/api/v1/set?video0.size=720p"
curl "http://<device-ip>/api/v1/set?video0.size=1080p"
# Enable scene-change IDR control
curl "http://<device-ip>/api/v1/set?video0.scene_threshold=150"
# Enable 2x digital zoom (encoded resolution becomes half width/height)
curl "http://<device-ip>/api/v1/set?video0.framing=zoom-2x"Response 200 (includes "reinit_pending": true):
{"ok":true,"data":{"field":"video0.size","value":"1280x720","reinit_pending":true}}Restart/reinit writes stay single-field by design. Even though the main loop debounces reinit requests, clients should send restart-required changes one at a time and let each accepted write schedule the pipeline rebuild.
Adaptive control usage notes:
- Keep
video0.scene_threshold=0for fixed-GOP workflows and drive keyframe interval throughvideo0.gop_size. - On the current Star6E IMX335 bench, a practical starting point is:
video0.sceneThreshold=150,video0.sceneHoldoff=2. - Tune threshold first, holdoff second. In practice, threshold changes are a safer first response than raising holdoff.
Example Star6E tuning sequence:
curl "http://<device-ip>/api/v1/set?video0.sceneThreshold=150"
curl "http://<device-ip>/api/v1/set?video0.sceneHoldoff=2"Validation errors — some values are rejected before being applied:
# Attempt to set the retired video0.codec field
curl "http://<device-ip>/api/v1/set?video0.codec=h264"Error 404:
{"ok":false,"error":{"code":"not_found","message":"unknown config field"}}Video codec is hardcoded H.265; the field was retired with the resilience-preset consolidation (see HISTORY 0.10.12).
Error 501 — apply callback not available:
{"ok":false,"error":{"code":"not_implemented","message":"apply callback not available"}}Error 400 — multi-set included a restart-required field:
{"ok":false,"error":{"code":"invalid_request","message":"multi-set only supports live fields; restart-required fields must be set one at a time"}}The same camelCase aliases listed above are accepted here for Majestic-oriented clients.
/api/v1/set's field surface, applied to the running config only — no
write to /etc/waybeam.json. Built for high-cadence automated writers
(waybeam-link adaptive bitrate/caps/fps actuation), where persist-on-set
would wear flash and boot into the last adaptive transient.
# Volatile bitrate change: applied live, gone after restart
curl "http://<device-ip>/api/v1/live/set?video0.bitrate=4096"
# Multi-set works identically (all fields must be live)
curl "http://<device-ip>/api/v1/live/set?video0.minQp=28&video0.maxQp=44"Semantics:
- Live fields only. Restart-required fields are rejected with
400("restart-class field requires persistence; use /api/v1/set") — a pipeline reinit reloads from disk, which would silently discard a volatile value. - Responses (success and error) are byte-identical in shape to
/api/v1/set. - A later persisting
/api/v1/setor/api/v1/defaultssnapshots the whole running config, earlier volatile changes included — one config struct, by design. Deployments that must keep a field volatile should route all writers of that field through/live/set(waybeam-link's single-bitrate-authority rule). - Detection: builds without this endpoint answer
404 no matching route, so clients can probe once and fall back to/api/v1/set.
- Type: signed integer
- Range:
-12..12 - Mutability:
live - Alias:
video0.qpDelta - Semantics: adjusts I-frame QP relative to P-frame. Note the sign: negative
values raise the I-frame's QP relative to P, making I-frames smaller;
positive values lower it, making them larger. Delivered rate does not move —
it is a redistribution knob, not a rate knob. See the measured sweep under
"Rate-control QP knobs" in
README.md.
video0.framing, video0.zoom_x, video0.zoom_y, video0.stab_crop_pct, video0.stab_kalman_q, video0.stab_kalman_r, video0.stab_recenter_speed
video0.framing: string preset — the single knob for what the VPE crop does.- Values:
off|stab(image stabilization, crop+shrink, Star6E only) |stab-fill(image stabilization, floating image on a black border, Star6E only; encode stays full-res,stabCropPctsets the shift/border budget) |zoom-1.25x|zoom-1.50x|zoom-1.75x|zoom-2x|zoom-3x|zoom-4x(digital zoom, both backends). - Mutability:
restart_required(changes encoded resolution / pipeline).
- Values:
video0.pauseStab: bool, live (stabandstab-fill) — glide the stabilized window (stab: HW crop) / floating image (stab-fill) back to centre via a software ramp, no rebind. Runtime-only: not persisted, always bootsfalse. No effect underframing=offor zoom.video0.stabCropPct: the stab crop / shift budget; clamped to [60, 100] for a stab preset (a smaller value is rejected by the API and floored on load).- The preset expands internally into the zoom crop fraction (zoom presets) or
the stabilization crop/recenter (
stab); the two are mutually exclusive. There is no settable continuouszoom_pct— use a zoom preset.
- The preset expands internally into the zoom crop fraction (zoom presets) or
the stabilization crop/recenter (
video0.stab_crop_pct,video0.stab_kalman_q,video0.stab_kalman_r,video0.stab_recenter_speed: mutabilityrestart_required. Aliasesvideo0.stabCropPct,video0.stabKalmanQ,video0.stabKalmanR,video0.stabRecenterSpeed. Tuning for the shared Kalman stabilization control law — read after preset expansion so an explicit value wins, while a plainframing=stab/stab-fillkeeps the preset defaults. Re-selecting the preset resets them, so set framing first. Inert underoff/zoom-*. Both presets use the same law, so identical values give identical behaviour.stab_crop_pct: uint.0= preset default (80); else60..100kept-frame % (stab) / shift+border budget (stab-fill). Clamped to [60, 100] for an active stab preset (smaller is rejected by the API and floored on load). Smaller % = larger dead border = more motion headroom, tighter/more-bordered frame.stab_kalman_q: double,0.001..1.0(default 0.03). Process noise / pan response — higher = the view follows slow pans sooner (weaker hold); lower = holds tighter and more locked. The estimate eases the offset back to centre on its own (no separate recenter).stab_kalman_r: double,0.1..50.0(default 2.0). Measurement noise / smoothness — higher = smoother but laggier; lower = snappier, more jitter passes through. Primary feel knob.stab_recenter_speed: uint,0..3600. Only thepauseStabglide-home rate (0= default ramp); inert during normal stabilization.
video0.zoom_x,video0.zoom_y: double,0.0..1.0, mutabilitylive. Aliasesvideo0.zoomX,video0.zoomY.- Semantics: digital zoom uses a 1:1 crop — the crop window and encoded output
resolution shrink together (1920×1080 →
zoom-2x960×528,zoom-3x640×352,zoom-4x480×256); no SCL upscale, no extra output bandwidth, so the deep 3×/4× crops are not bound by the SCL ~2× upscale ceiling.zoom_x/zoom_ymove the crop center live inside the active aspect-ratio-corrected source surface. Understabthe crop is always centered (zoom_x/zoom_yare ignored).
Return the configured target FPS from the active runtime config.
curl http://<device-ip>/api/v1/fps/configResponse 200:
{"ok":true,"data":{"fps":60}}Return the live/applied FPS reported by the active backend. If a backend does not expose a distinct live value, this falls back to the configured FPS.
curl http://<device-ip>/api/v1/fps/liveResponse 200:
{"ok":true,"data":{"fps":60}}The outgoing.enabled field controls whether encoded frames are sent over UDP.
# Enable output (starts sending, restores FPS, issues IDR)
curl "http://<device-ip>/api/v1/set?outgoing.enabled=true"
# Disable output (stops sending, reduces FPS to 5fps idle)
curl "http://<device-ip>/api/v1/set?outgoing.enabled=false"Behavior when disabled:
- FPS is reduced to 5fps (idle rate) to minimize sensor/ISP power draw.
- Encoder keeps running at the reduced rate; frames are encoded and discarded.
- The previous FPS is stored and restored when output is re-enabled.
- An IDR keyframe is issued on re-enable for immediate stream sync.
From 0.82.0, on all three backends, an outgoing.server the craft cannot
bring up is handled differently depending on when it is seen:
- At boot it is not fatal. The craft starts with the output inert: an
ERRORnaming the URI goes to the log,GET /api/v1/transport/statusreports"active": false, and the HTTP API is reachable. Setting a workingoutgoing.serverrecovers the output live, with no restart. Before 0.82.0 the daemon brought the whole pipeline up and then exited, leaving no video and no API — so a one-line config error could only be corrected over ssh. - On a live change it is refused:
/api/v1/set?outgoing.server=…returns an error, the running transport is untouched, and the previous value stays both in memory and on disk. The asymmetry is deliberate — a live retarget has a working output to lose, a boot does not.
Note that names are not resolved: a destination is udp://<IPv4>:<port> or
unix://<path>, so udp://somehost:5600 is refused at both points.
Default: false — output must be explicitly enabled. Configure outgoing.server
before enabling.
The outgoing.server field can be changed at runtime to redirect the stream.
# Redirect stream to a different GCS
curl "http://<device-ip>/api/v1/set?outgoing.server=udp://<receiver-ip>:5600"- Accepted URI schemes:
udp://HOST:PORT— standard UDP datagram outputunix://NAME— Linux abstract Unix datagram socket@NAMEshm://NAME— shared-memory RTP-packet ring bufferframe-shm://NAME— shared-memory whole-frame ring buffer (Annex-B frames, no RTP); for a same-host FEC consumer. Wire format:protocols/frame-shm.mdin the coordination repo.
- No pipeline restart required.
- An IDR keyframe is issued after the change for stream continuity.
- If
connectedUdpis enabled, the UDP socket is re-connected to the new destination. - Live redirects support
udp://andunix://. Live switch to/fromshm://orframe-shm://is not supported (restart required). connectedUdpapplies only toudp://.shm://andframe-shm://are video-only. They cannot share audio; use a nonzeroaudioPortfor separate UDP audio.- On Star6E,
audioPort=0piggybacks on the active video destination for bothudp://andunix://. - On Star6E, a nonzero
audioPortkeeps audio on a dedicated UDP port. Withunix://,shm://, orframe-shm://video output, that dedicated audio port is sent to127.0.0.1:<audioPort>. frame-shm://publishes whole Annex-B frames into an 8-slot SPSC ring (384 KB per slot on Star6E and Maruko, 512 KB on CV610 — 16 × 512 KB is the ring format's default, not what any venc backend creates); each slot is prefixed with an 8-byteVencFrameMeta(pts,codec,flags;flagsbit 0 = IDR). On a full ring the encoder drops the frame and keeps running (never blocks).outgoing.maxPayloadSizedoes not apply (no packetization).GET /api/v1/transport/statusreports"transport":"frame-shm"withframesSent/fillPct/transportDrops/oversizeDrops.
# Live: change max payload size on the fly (576..4000)
curl "http://<device-ip>/api/v1/set?outgoing.maxPayloadSize=4000"
# Restart-only
curl "http://<device-ip>/api/v1/set?outgoing.stream_mode=compact"
curl "http://<device-ip>/api/v1/set?outgoing.connected_udp=true"
curl "http://<device-ip>/api/v1/set?outgoing.allowUnixEncoderStall=true"outgoing.stream_mode:"rtp"(default) or"compact". Determines packetization format.outgoing.max_payload_size: Maximum RTP/compact packet payload in bytes. Default1400.MUT_LIVE— applies on the next encoded frame; in-flight packetization for the current frame keeps the old size. Range[576, 4000]. Values above ~1472 require end-to-end MTU support (e.g. Realtek's 3993-byte jumbo-frame links); on a standard 1500-MTU path the kernel will IP-fragment, defeating the point. Composes with other live fields in a single multi-set request — for example?video0.bitrate=8000&outgoing.maxPayloadSize=4000applies both atomically. Live updates are accepted across all transports (udp://,unix://,shm://): the SHM ring slot is sized at startup to fit the validated ceiling so any value in range applies live without restart, just like UDP/Unix.outgoing.connected_udp: Whentrue, callsconnect()on the UDP socket so the kernel returns ICMP port-unreachable errors viasendmsg(). Useful for detecting that a receiver is down. Defaultfalse(fire-and-forget).outgoing.allow_unix_encoder_stall: Restart-required boolean, defaultfalse. Whenfalse,unix://sends use the 2 ms socket timeout and cumulative approximately 4 ms RTP frame budget; sustained pressure drops the unsent remainder of the frame so the encoder stays live. Whentrue, those two bounds are disabled forunix://only: a full consumer queue blocks the encoder output thread until the consumer resumes, preserving the legacy behavior. UDP,shm://, andframe-shm://are unaffected. The 256-datagram queue recommendation remains active in both modes.
Setting video0.fps via the API applies hardware-level frame decimation within the
active sensor mode. The sensor continues running at its native maxFps; the MI_SYS bind
layer between VPE and VENC drops frames to match the requested rate.
# On a 90fps sensor mode: set output to 30fps (sensor stays at 90, VENC receives 30)
curl "http://<device-ip>/api/v1/set?video0.fps=30"
# Set output to 60fps
curl "http://<device-ip>/api/v1/set?video0.fps=60"
# Restore full sensor rate
curl "http://<device-ip>/api/v1/set?video0.fps=90"Clamping: If the requested FPS exceeds the current sensor mode's maxFps, the value
is silently clamped to the mode maximum. For example, requesting 120fps on a 90fps mode
sets the output to 90fps. To access a higher sensor mode, edit /etc/venc.json and
restart the process.
What happens under the hood:
- VPE→VENC bind is torn down and re-established with
src_fps:dst_fpsratio - VENC rate control
fpsNumis updated for correct bitrate allocation - No pipeline restart — latency is sub-second
Mode switching limitation: Changing sensor modes (e.g. 90fps→120fps) requires a full
process restart. The SigmaStar kernel driver does not reliably reinitialize the MIPI PHY
when switching modes in-process. Use /api/v1/restart (reloads /etc/venc.json) or
restart the venc process to change sensor modes.
Reload /etc/venc.json from disk and rebuild the pipeline. Equivalent to sending
SIGHUP. This endpoint does NOT write the in-memory config back to disk, so a manual
file swap (editor, scp, json_cli) followed by /api/v1/restart reloads exactly what
was placed on disk.
In v0.7.8 persistence moved into the /api/v1/set layer — every set (LIVE or RESTART)
now saves to disk before returning, so the WebUI "Save & Restart" flow (applyChanges
→ /api/v1/restart) ends with the on-disk copy already matching memory before the
reload runs.
curl http://<device-ip>/api/v1/restartResponse 200:
{"ok":true,"data":{"reinit":true}}Live fused attitude from the on-board IMU (complementary filter feeding the
RTP sidecar ATTITUDE trailer). Star6E only; requires attitude.enabled and
imu.enabled. Angles in degrees, camera frame after the attitude.axisFwd/
axisDown remap and boresight trims.
curl http://<device-ip>/api/v1/attitudeResponse 200:
{"ok":true,"data":{"valid":true,"settled":true,"rollDeg":0.5,"pitchDeg":-0.2,"yawDeg":1.9}}{"valid":false} until the estimator has a gravity reference. 501 on
backends without an attitude path (Maruko).
The "it's laying flat now" calibration. Hold the camera level and still;
venc averages the level-pose accelerometer samples — ODR-independent: it
completes early once it has the sample target, otherwise it takes whatever
the ≤2 s window gathered (as long as ≥32 samples arrived). It then solves
the boresight trims exactly (input-side rotation, not an output Euler
subtraction) and persists attitude.trimRollDeg/trimPitchDeg through the
standard restart-set path. Call /api/v1/restart afterwards to apply.
curl http://<device-ip>/api/v1/attitude/calibrate_levelResponse 200:
{"ok":true,"data":{"trimRollDeg":-2.60,"trimPitchDeg":20.43,"restartRequired":true}}409 calibration_failed when fewer than 32 IMU samples arrive in the ≤2 s
window (attitude.enabled/imu.enabled off) or the averaged gravity
magnitude is implausible (<0.5 g / >1.5 g — device moving). 501 on Maruko.
Overwrite the in-memory config with compiled-in defaults, persist to /etc/venc.json,
then trigger a full pipeline reinit. Drives the "Restore Defaults" button in the WebUI.
Added in v0.7.8. The saved field in the response reflects whether persistence
actually succeeded — false means the runtime is at defaults but the on-disk copy is
stale (e.g. disk full, readonly FS, permission error); check the venc log for the
[venc_config] ERROR: line.
curl http://<device-ip>/api/v1/defaultsResponse 200:
{"ok":true,"data":{"defaults":true,"reinit":true,"saved":true}}Return live AE diagnostics from the active backend.
curl http://<device-ip>/api/v1/aeResponse 200:
{
"ok": true,
"data": {
"sensor_plane": { "ret": 0, "pad": 0, "shutter_us": 1112, "sensor_gain_x1024": 10240, "comp_gain_x1024": 1024 },
"exposure_limit": { "ret": 0, "min_shutter_us": 150, "max_shutter_us": 10000, "min_sensor_gain": 1024, "max_sensor_gain": 30000, "min_isp_gain": 1024, "max_isp_gain": 1024 },
"exposure_info": { "ret": 0, "stable": true, "reach_boundary": false, "long_us": 9999, "long_sensor_gain_x1024": 1673, "long_isp_gain_x1024": 1024, "luma_y": 236, "avg_y": 247 },
"state": { "ret": 0, "raw": 0, "name": "normal" },
"expo_mode": { "ret": 0, "raw": 0, "name": "auto" },
"metrics": { "exposure_us": 9999, "sensor_gain_x1024": 1673, "isp_gain_x1024": 1024, "fps": 90 },
"runtime": { "sensor_fps": 90, "active_precrop": { "x": 0, "y": 240, "w": 2560, "h": 1440 } }
}
}runtime.active_precrop is included on both backends whenever the
pipeline has been started; it is omitted before the first start and
after a stop.
Error 501:
{"ok":false,"error":{"code":"not_implemented","message":"AE query not available"}}Return live AWB diagnostics from the active backend.
curl http://<device-ip>/api/v1/awbError 501:
{"ok":false,"error":{"code":"not_implemented","message":"AWB query not available"}}Query all ISP IQ parameter values. Star6E and Maruko share the SigmaStar response shape shown below. CV610 also serves this route, using the self-describing response documented under "CV610 IQ response shape".
curl http://<device-ip>/api/v1/iqResponse 200:
{
"ok": true,
"data": {
"lightness": {"ret": 0, "enabled": true, "op_type": "auto", "value": 50},
"contrast": {"ret": 0, "enabled": true, "op_type": "manual", "value": 70},
"color_to_gray": {"ret": 0, "value": false},
"demosaic": {"ret": 0, "enabled": true, "value": 45}
}
}Each parameter reports:
ret: MI_ISP return code (0 = success)enabled: bEnable flagop_type:"auto"or"manual"(omitted for bool-only and manual-only params)value: current primary value (backward-compat scalar)fields: (multi-field params only) object with all named sub-fields and arraysavailable:falseif the dlsym symbol was not found
Multi-field example (colortrans):
"colortrans": {
"ret": 0, "enabled": true, "value": 200,
"fields": {
"y_ofst": 200, "u_ofst": 0, "v_ofst": 0,
"matrix": [23, 45, 9, 1005, 987, 56, 56, 977, 1015]
}
}Error 501 when the active backend does not register an IQ query callback:
{"ok":false,"error":{"code":"not_implemented","message":"IQ query not available"}}Set a single IQ parameter. The parameter is switched to manual mode (for auto/manual params) and the value is written to the primary manual field.
Supports dot-notation for multi-field params, comma-separated arrays, and
enable/disable toggling via the .enabled virtual field:
# Simple scalar
curl "http://<device-ip>/api/v1/iq/set?contrast=70"
# Dot-notation for sub-field
curl "http://<device-ip>/api/v1/iq/set?colortrans.y_ofst=200"
# Array value (comma-separated)
curl "http://<device-ip>/api/v1/iq/set?colortrans.matrix=23,45,9,1005,987,56,56,977,1015"
# Enable/disable toggle (non-bool params only)
curl "http://<device-ip>/api/v1/iq/set?colortrans.enabled=0"
curl "http://<device-ip>/api/v1/iq/set?crosstalk.enabled=1"
# Bool toggle
curl "http://<device-ip>/api/v1/iq/set?color_to_gray=1"Response 200:
{"ok":true,"data":{"param":"colortrans.y_ofst","value":200}}
{"ok":true,"data":{"param":"colortrans.matrix","value":[23,45,9,1005,987,56,56,977,1015]}}CV610 only (501 not_implemented elsewhere — the SigmaStar backends
round-trip IQ as JSON through /api/v1/iq and /api/v1/iq/import).
Serializes the live ISP state into a PQTools .bin through the vendor
libbin.so, at the fixed path /tmp/isp_export.bin. The destination is fixed
deliberately: the endpoint is unauthenticated, and a caller-supplied path would
make it a write-anywhere primitive. Copy the file off with scp afterwards.
The written file has the same shape PQTools produces — an ISP parameter image
of exactly OT_PQ_GetISPDataTotalLen() bytes followed by an OTPQNRX 3DNR
section — so it can be fed back through isp.sensorBin.
The payload is integrity-checked by the vendor library: a .bin cannot be
edited or spliced in place (one flipped byte returns 0xcb000005). Produce
files with PQTools or with this endpoint, and treat them as opaque.
curl http://<device-ip>/api/v1/iq/export_bin
scp root@<device-ip>:/tmp/isp_export.bin .Response 200:
{"ok":true,"data":{"path":"/tmp/isp_export.bin","bytes":144774}}bytes is the length actually written, so a caller can confirm the write
without a second round trip; path is a constant and on its own is no evidence
the export happened. The value is OT_PQ_GetISPDataTotalLen() plus the 3DNR
section, which is 144774 on the shipping SDK but is queried, not fixed. If the
vendor library cannot supply 3DNR parameters the ISP half is exported alone and
bytes is correspondingly smaller.
The file is created with O_NOFOLLOW and rejected unless it is a plain,
single-linked regular file: the path is predictable, /tmp is world-writable
and the endpoint is unauthenticated, so a planted symlink must not be able to
redirect the daemon's write.
Import IQ parameters from a JSON body (output of GET /api/v1/iq).
Partial imports are supported — only parameters present in the JSON are applied.
The enabled field is respected during import — parameters with "enabled":false
will be disabled on the ISP.
# Full import from exported file
curl -X POST -H "Content-Type: application/json" \
-d @my_tuning.json http://<device-ip>/api/v1/iq/import
# Partial import — only specific params
echo '{"lightness":{"value":75},"demosaic":{"fields":{"dir_thrd":30}}}' | \
curl -X POST -H "Content-Type: application/json" -d @- http://<device-ip>/api/v1/iq/importResponse 200:
{"ok":true,"data":{"imported":true}}Capture one JPEG frame from the snapshot subsystem and return it as
image/jpeg. Gated by snapshot.enabled; returns 503
(snapshot_disabled) when the subsystem is off or the pipeline is not
running, 504 (snapshot_timeout) if no frame arrives, 500
(snapshot_failed) on backend error. Not a JSON endpoint on success.
This is also the QR-scanning source: tools/qr/qr_decode reads JPEG
directly (vendored stb_image, luma-only), so the boot-pairing flow is
curl … /snapshot.jpg | qr_decode. The MJPEG channel is created once at
pipeline start and pulse-encoded per capture (StartRecvPic → GetStream → StopRecvPic) — rapid back-to-back captures are safe. Frame geometry follows
snapshot.width/snapshot.height (0 = inherit the main stream); QR range
scales with pixels per module, so size the channel up for longer-distance
markers. Pairing, commands, boot scheduling, and action dispatch are
deliberately outside the waybeam binary and this endpoint.
Registered on all three backends since 0.20.0. On CV610 the JPEG channel
is a second bind target on the same VPSS output the H.265 channel consumes
(the SDK allows up to four destinations per source), pulse-encoded with
start_chn(recv_pic_num = 1) -> get_stream -> stop_chn. Because it shares
the main stream's VPSS channel it inherits that geometry: snapshot.width and
snapshot.height report supported:false on CV610 rather than being accepted
and ignored. snapshot.enabled, snapshot.quality and snapshot.channel are
honoured; quality maps to ss_mpi_venc_set_jpeg_param(qfactor).
Retired:
GET /api/v1/snapshot.pgm(grayscale P5 PGM, added 0.59.0) was removed in 0.60.0 and now answers404. It captured through a short-lived per-request VPE/SCL tap, and device stress-testing showed the tap's enable/disable cycle can race an in-flight MHAL buffer and wedge the whole VPE — up to a kernel panic or hard hang. The MJPEG channel above has no such cycle; consumers that want grayscale decode the JPEG's luma plane (qr_decodedoes this natively).
# QR scanning (boot pairing, bench)
curl -s http://<device-ip>/api/v1/snapshot.jpg | qr_decodeServes a self-contained HTML dashboard (gzip-compressed, ~14KB). The dashboard provides Settings, API Reference, and Image Quality tabs. All modern browsers decompress the gzip response automatically.
Available parameters (62 total, Star6E):
| Parameter | Type | Range | Description |
|---|---|---|---|
lightness |
u32 | 0-100 | Lightness level |
contrast |
u32 | 0-100 | Contrast level |
brightness |
u32 | 0-100 | Brightness level |
saturation |
u8 | 0-127 | Color saturation (32=1X) |
sharpness |
u8 | 0-255 | Overshoot gain |
hsv |
u8 | 0-64 | Hue LUT first entry |
nr3d |
u8 | 0-255 | 3D NR motion threshold |
nr3d_ex |
u32 | 0-1 | 3D NR extended AR enable |
nr_despike |
u8 | 0-15 | De-spike blend ratio |
nr_luma |
u8 | 0-255 | Luma NR strength |
nr_luma_adv |
u32 | 0-1 | Advanced luma NR debug enable |
nr_chroma |
u8 | 0-127 | Chroma NR match ratio |
nr_chroma_adv |
u8 | 0-255 | Advanced chroma NR strength |
false_color |
u8 | 0-255 | False color frequency threshold |
crosstalk |
u8 | 0-31 | Cross-talk correction strength |
demosaic |
u8 | 0-63 | Demosaic direction threshold |
obc |
u16 | 0-255 | Optical black correction R value |
dynamic_dp |
u8 | 0-1 | Hot pixel detection enable |
dp_cluster |
u32 | 0-1 | Cluster dead pixel edge mode |
r2y |
u16 | 0-1023 | R2Y matrix first coefficient |
colortrans |
u16 | 0-2047 | Color transform Y offset |
rgb_matrix |
u16 | 0-8191 | CCM first coefficient |
wdr |
u8 | 0-4 | WDR box number |
wdr_curve_adv |
u16 | 0-16384 | WDR curve slope |
pfc |
u8 | 0-255 | Phase focus correction strength |
pfc_ex |
u32 | 0-1 | Extended PFC debug enable |
hdr |
u8 | 0-1 | HDR NR enable |
hdr_ex |
u16 | 0-65535 | HDR sensor exposure ratio |
shp_ex |
u32 | 0-1 | Extended sharpness debug enable |
rgbir |
u8 | 0-7 | RGBIR position type |
iq_mode |
u32 | 0-1 | IQ mode (0=day, 1=night) |
lsc |
u16 | 0-65535 | Lens shading center X |
lsc_ctrl |
u8 | 0-255 | LSC R ratio by CCT |
alsc |
u8 | 0-255 | Adaptive LSC grid X |
alsc_ctrl |
u8 | 0-255 | ALSC R ratio by CCT |
obc_p1 |
u16 | 0-255 | OBC phase 1 R value |
stitch_lpf |
u16 | 0-256 | Stitch LPF first coefficient |
rgb_gamma |
bool | 0/1 | RGB gamma enable |
yuv_gamma |
bool | 0/1 | YUV gamma enable |
wdr_curve_full |
bool | 0/1 | WDR full curve enable |
dummy |
bool | 0/1 | Dummy tuning enable |
dummy_ex |
bool | 0/1 | Extended dummy enable |
defog |
bool | 0/1 | Defogging enable |
color_to_gray |
bool | 0/1 | Grayscale mode |
nr3d_p1 |
bool | 0/1 | 3D NR phase 1 enable |
fpn |
bool | 0/1 | Fixed pattern noise enable |
Hardware test results (SSC30KQ, imx335):
- 45/46 symbols resolved (
stitch_lpfnot present) - 40/45 params roundtrip correctly (set → query reads same value)
- 3 offset mismatches:
nr_despike,pfc,hdr(set succeeds but readback differs — struct padding) - 2 ISP-rejected:
nr3d_p1,fpn(set succeeds but ISP ignores on this sensor)
Return live observability for the audio capture/encode pipeline. Useful for
diagnosing silent audio failures (missing libmi_ai.so on Maruko, missing
libopus.so, capture thread not running, codec mismatch).
curl http://<device-ip>/api/v1/audio/statusResponse 200 (Star6E with audio enabled):
{
"ok": true,
"data": {
"enabled": true,
"backend": "star6e",
"lib_loaded": true,
"device_enabled": true,
"channel_enabled": true,
"running": true,
"codec": "opus",
"sample_rate": 48000,
"channels": 1,
"opus_loaded": true
}
}Response 200 (Maruko with audio enabled):
{
"ok": true,
"data": {
"enabled": true,
"backend": "maruko",
"lib_loaded": true,
"device_opened": true,
"group_enabled": true,
"running": true,
"codec": "opus",
"sample_rate": 48000,
"channels": 1,
"opus_loaded": true
}
}Response 200 (CV610 with audio enabled):
{
"ok": true,
"data": {
"enabled": true,
"backend": "cv610",
"running": true,
"codec": "opus",
"sample_rate": 48000,
"channels": 1,
"muted": false,
"frames": 1486,
"bytes": 60966,
"packets": 1486,
"drops": 0
}
}CV610 reports a different field set on purpose. It has no dlopened audio
library to report on (the MPI is linked), and its 48 kHz mono Opus
configuration is compiled in rather than read from config, so the useful
observability there is throughput: frames / bytes / packets / drops
are lifetime counters from the audio thread. A client must treat the field
set as backend-specific and key off backend.
Response 200 when audio.enabled=false:
{"ok":true,"data":{"enabled":false,"backend":"maruko"}}Field reference:
| Field | Meaning |
|---|---|
enabled |
audio.enabled=true and *_audio_init reached the run state |
lib_loaded |
The MI audio shared library (libmi_audio.so Star6E / libmi_ai.so Maruko) was found and dlopened |
device_enabled / device_opened |
The capture device handle is open |
channel_enabled / group_enabled |
The capture channel / group is enabled |
running |
Capture and encode threads are alive |
codec |
"g711a", "g711u", "opus", "pcm", or "unknown" |
sample_rate |
Configured audio sample rate (Hz) |
channels |
1 (mono) or 2 (stereo) |
opus_loaded |
When codec="opus", the Opus encoder was successfully initialized. false here while codec="opus" means audio falls back to raw PCM with a startup warning. |
Error 501 — backend has no audio observability hook (query_audio_status
not registered):
{"ok":false,"error":{"code":"not_implemented","message":"audio status not available on this backend"}}Return a compact Prometheus-style ISP metrics snapshot.
curl http://<device-ip>/metrics/ispResponse 200:
# HELP isp_again Analog Gain
# TYPE isp_again gauge
isp_again 1673
# HELP isp_dgain Digital Gain
# TYPE isp_dgain gauge
isp_dgain 1024
# HELP isp_fps Sensor fps
# TYPE isp_fps gauge
isp_fps 90
Start SD card recording. Optional ?dir=/path query parameter overrides the
default recording directory (from config record.dir, default /media).
# Start recording with default dir
wget -q -O- "http://<device-ip>/api/v1/record/start"
# Start with custom directory
wget -q -O- "http://<device-ip>/api/v1/record/start?dir=/media/clips"Response 200:
{"ok":true,"data":{"action":"start","dir":"/media"}}Recording format is determined by record.format config: "ts" (default, MPEG-TS
with audio) or "hevc" (raw HEVC NAL stream). File rotation is controlled by
record.maxSeconds and record.maxMB config fields.
Backend gating: all three backends run the runtime poll that honours
HTTP-driven start/stop — Star6E and Maruko in their encode loops, CV610 in its
drain loop. A backend that does not register the poll answers
/api/v1/record/start with
{"ok":false,"error":{"code":"not_implemented","message":"HTTP record control not available on this backend"}}and HTTP 501, rather than the older behaviour of returning {"ok":true}
while no recording started. No shipped backend takes that path today.
Stop SD card recording.
wget -q -O- "http://<device-ip>/api/v1/record/stop"Response 200:
{"ok":true,"data":{"action":"stop"}}Same backend gating applies — Maruko returns 501 not_implemented.
Query recording status.
wget -q -O- "http://<device-ip>/api/v1/record/status"Response 200:
{
"ok": true,
"data": {
"active": true,
"format": "ts",
"path": "/media/rec_01h23m45s_abcd.ts",
"frames": 1500,
"bytes": 12345678,
"elapsed_ms": 25000,
"segments": 1,
"stop_reason": "none"
}
}elapsed_ms is the elapsed duration of the active or most recently stopped
recording. stop_reason values: "none" (currently recording), "manual",
"disk_full", "size_limit" (from 0.30.0), "write_error".
From 0.31.0, segments is meaningful for format: "hevc" too — that
recorder rotates on maxSeconds/maxMB like the TS one. Earlier builds
ignored both thresholds on that path and reported segments as 0 (Star6E,
CV610) or a hardcoded 1 (Maruko) while a single unrotated file grew without
bound.
"size_limit" means a file-size ceiling was reached, not a fault: the
recorder stopped on a frame boundary and the file is intact and closed. It is
reported separately from "write_error" because the two call for different
responses — a ceiling is answered by lowering record.maxMB, an I/O error by
looking at the card.
From 0.30.0, when active is false because the recorder stopped by
itself, path, frames, bytes and segments describe the recording that
ended rather than being zero. They come from the recorder record.format
selects; since that field is restart_required, only one recorder can have
run in a given process, so there is no ambiguity about which recording is
being described. Earlier builds answered {"path":"","frames":0, "bytes":0,"segments":0} for every non-manual stop, which left a client with
no way to tell which file was cut short or how far it got. elapsed_ms is
still 0 while inactive.
List .ts and .hevc files in the configured record.dir along with
disk-usage totals.
wget -q -O- "http://<device-ip>/api/v1/recordings"Response 200:
{
"ok": true,
"data": {
"dir": "/mnt/mmcblk0p1",
"free_bytes": 1234567890,
"total_bytes": 15000000000,
"files": [
{ "name": "rec_01h00m00s_abcd.ts", "size": 12345678, "mtime": 1713600000 }
],
"truncated": false
}
}Error 503 not_available — directory not mounted.
Error 500 internal_error — cannot read directory or out of memory.
The listing is capped at 512 entries; truncated is true when the
cap was hit and older recordings were not included. free_bytes /
total_bytes are -1 when statvfs is unavailable.
Stream a single recording as an attachment download. file must be
a plain name from the listing — leading ., path separators and
control bytes are rejected.
wget "http://<device-ip>/api/v1/recordings/download?file=rec_01h00m00s_abcd.ts"Content-Type is video/mp2t for .ts files and
application/octet-stream for .hevc.
Error 400 invalid_request — missing or unsafe file parameter.
Error 404 not_found — file does not exist in record.dir.
Remove a recording from record.dir.
wget -q -O- "http://<device-ip>/api/v1/recordings/delete?file=rec_01h00m00s_abcd.ts"Response 200:
{"ok":true}Error 400 invalid_request — missing or unsafe file parameter.
Error 404 not_found — file already gone.
Error 409 record_active — file is currently being written; stop
recording first.
Error 500 delete_failed — filesystem error.
Report the intra-refresh (GDR) state the encoder actually applied, as opposed to what was requested. Served by Star6E, Maruko and CV610.
curl http://<device-ip>/api/v1/intra/status{"ok":true,"data":{
"mode":"gdr","active":true,"mi_supported":true,"apply_ok":true,
"target_ms":500,"total_rows":34,
"lines":{"requested":0,"effective":2,"clamped":false},
"qp":{"requested":0,"effective":0},
"gop":{"explicit_sec":0.000,"effective_sec":2.000,"auto":true}}}| field | meaning |
|---|---|
mode |
resolved refresh mode name (off, gdr, …). |
active |
the encoder is refreshing now. |
mi_supported |
the vendor library exports the refresh setter. |
apply_ok |
the setter was called and read back clean. active with apply_ok:false means requested-but-not-delivered. |
target_ms |
intended full-refresh cycle duration. |
total_rows |
picture height in refresh rows. |
lines.requested / .effective |
rows per P-frame asked for vs applied; clamped when geometry forced a change. |
qp.requested / .effective |
refresh QP offset asked for vs applied. |
gop.explicit_sec / .effective_sec |
configured GOP vs the one in force; auto when derived rather than configured. |
Always 200. A backend with no intra-refresh support reports mode:"off",
active:false, mi_supported:false.
Report the resolved video0.resilience preset and both mechanisms it drives.
Served by Star6E, Maruko and CV610.
curl http://<device-ip>/api/v1/resilience/status{"ok":true,"data":{
"preset":"rally",
"intra":{"mode":"gdr","active":true,"mi_supported":true,"apply_ok":true,
"effective_lines":2,"effective_qp":0},
"refPred":{"active":true,"mi_supported":true,"apply_ok":true,
"base":1,"enhance":3,"pred":true},
"gop":{"effective_sec":2.000,"auto":true}}}| field | meaning |
|---|---|
preset |
resolved video0.resilience value. |
intra.* |
the intra-refresh subset of /api/v1/intra/status. |
refPred.active |
base/enhance reference structure is in force. |
refPred.mi_supported |
the vendor library exports the reference-parameter setter. |
refPred.apply_ok |
the setter was called and read back clean. |
refPred.base / .enhance |
applied base layer and enhancement period. |
refPred.pred |
non-reference enhancement frames are being marked. |
gop.effective_sec / .auto |
GOP in force, and whether it was derived. |
Always 200. Read apply_ok on both mechanisms before trusting preset: a
preset names an intent, and only apply_ok says the encoder took it.
Request an IDR (keyframe) from the encoder.
curl http://<device-ip>/request/idrResponse 200:
{"ok":true,"data":{"idr":true}}If outgoing.sidecar_port is enabled at the same time, Star6E also appends
the scene-detector telemetry trailer to sidecar FRAME packets. That
is the intended external interface for per-frame size/type/complexity observations.
Query the secondary VENC channel status. Always returns 200; the active
field tells you whether dual or dual-stream mode is currently running.
wget -q -O- "http://<device-ip>/api/v1/dual/status"Response 200 — dual VENC active:
{"ok":true,"data":{"active":true,"channel":1,"bitrate":20000,"fps":120,"gop":240}}Response 200 — dual VENC not active (off, mirror, or any non-dual mode):
{"ok":true,"data":{"active":false}}Live-change secondary VENC channel parameters. Supported parameters:
| Parameter | Type | Description |
|---|---|---|
bitrate |
uint | Bitrate in kbps, applied immediately via MI_VENC_SetChnAttr. No IDR is requested; dual_apply_bitrate() only updates the channel attribute. Note that on Star6E MI_VENC_SetChnAttr emits an IDR of its own (measured 2026-08-23), so a rate change still produces a keyframe — it is simply not one venc asked for and it is not counted in /api/v1/idr/stats. For an explicit ch1 resync point use /api/v1/dual/idr. |
gop |
double | GOP interval in seconds (converted to frames using ch1 fps) |
# Change ch1 bitrate to 10 Mbps
wget -q -O- "http://<device-ip>/api/v1/dual/set?bitrate=10000"
# Change ch1 GOP to 1 second (120 frames at 120fps)
wget -q -O- "http://<device-ip>/api/v1/dual/set?gop=1.0"Response 200:
{"ok":true,"data":{"field":"bitrate","value":10000}}
{"ok":true,"data":{"field":"gop","value":1.00,"frames":120}}Error 400 — missing or invalid parameter:
{"ok":false,"error":{"code":"missing_param","message":"Usage: /api/v1/dual/set?bitrate=N or ?gop=N"}}Error 404 — dual VENC not active.
Error 501 — backend does not support live dual/set (Maruko). The Star6E
binding owns the low-level MI_VENC_*ChnAttr write path; a Maruko port has
not landed yet.
Request an IDR keyframe on the secondary VENC channel.
wget -q -O- "http://<device-ip>/api/v1/dual/idr"Response 200:
{"ok":true,"data":{"idr":true}}Error 404 — dual VENC not active.
Return per-channel IDR-rate-limit counters. The encoder enforces a minimum
spacing between honored IDRs to keep bitrate predictable when many sources
(scene detector, HTTP /request/idr and /api/v1/dual/idr, recorder
segment rotation) ask for keyframes simultaneously. This endpoint reports
how many requests were honored vs. coalesced (dropped) per channel.
curl http://<device-ip>/api/v1/idr/statsResponse 200:
{
"ok": true,
"data": {
"min_spacing_us": 100000,
"channels": [
{"idx": 0, "honored": 47, "dropped": 3},
{"idx": 1, "honored": 12, "dropped": 0}
]
}
}min_spacing_us is the compile-time minimum spacing in microseconds.
Channels with both counters at zero are omitted. Available on both
backends; always returns a valid response (even when no IDR has been
requested yet — channels is then an empty array).
Return live observability for the active video transport (UDP / Unix / SHM). Used by the WebUI status bar and by external link controllers that need to detect output backpressure.
curl http://<device-ip>/api/v1/transport/statusResponse 200 (SHM ring transport, common for outgoing.server=shm://...):
{
"ok": true,
"data": {
"active": true,
"transport": "shm",
"fillPct": 12,
"inPressure": false,
"transportDrops": 0,
"pressureDrops": 0,
"packetsSent": 184523,
"oversizeDrops": 0,
"slotCount": 1024,
"usedSlots": 122
}
}Response 200 (frame-shm ring):
{
"ok": true,
"data": {
"active": true,
"transport": "frame-shm",
"fillPct": 25,
"inPressure": false,
"transportDrops": 0,
"pressureDrops": 0,
"framesSent": 41207,
"oversizeDrops": 0,
"slotCount": 8,
"usedSlots": 2,
"ringLowWaterSlots": 1,
"otherDrops": 0,
"badAuDrops": 0
}
}Response 200 (UDP/Unix kernel-buffer fill_pct):
{
"ok": true,
"data": {
"active": true,
"transport": "udp",
"fillPct": 4,
"inPressure": false,
"pressureDrops": 0,
"transportDrops": 0,
"packetsSent": 184523,
"badAuDrops": 0
}
}Response 200 (output disabled or no socket open):
{"ok":true,"data":{"active":false,"transport":"none"}}Field reference:
| Field | Meaning |
|---|---|
transport |
"shm", "frame-shm", "udp", "unix", or "none" |
fillPct |
Current fill ratio 0..100. For SHM, ring fill. For UDP, kernel send-buffer fill. For Unix, fill against the peer's datagram queue — see the note below |
inPressure |
Point-in-time HTTP snapshot: true when the current fillPct >= 75, false otherwise. The RTP sidecar field uses 75/50 hysteresis |
transportDrops |
Lifetime drops: ring-full for SHM; unsent datagrams after EAGAIN/ENOBUFS or frame-budget exhaustion for UDP/Unix |
pressureDrops |
Frames dropped by the in-process backpressure path while a sidecar probe was subscribed |
packetsSent |
Lifetime sends accepted: ring writes on shm://, datagrams on udp:///unix://. Absent on frame-shm://, which reports whole frames as framesSent instead |
framesSent |
(frame-shm only) Lifetime whole frames written into the ring. The frame-shm counterpart of packetsSent: this transport carries one access unit per slot, not packets |
oversizeDrops |
(SHM only) Frames rejected for exceeding slot capacity |
slotCount / usedSlots |
(SHM only) Ring sizing; usedSlots is a snapshot |
otherDrops |
(frame-shm only, all three backends) Frames the producer discarded for a reason other than a full ring — an access unit it could not build at all (oversize, or a malformed SDK packet table). Kept apart from transportDrops on purpose: that one is congestion the consumer is causing and a rate controller should slow down for it, this one is not congestion and slowing down fixes nothing. Mirrored into the ring header at offset 96 so the consumer sees it too |
badAuDrops |
Access units discarded because the SDK's packet table was incomplete or invalid. Transport-independent — this happens on RTP as well — and a subset of otherDrops on frame-shm. Star6E and Maruko only: the CV610 stream path has no packet-table validation, so the field is absent there rather than reported as a permanent zero |
ringLowWaterSlots |
(frame-shm only, all three backends) Lowest ring occupancy reached in the last 200 ms window, in slots. <= 1 is the healthy band and >= 2 sustained is standing backlog — venc samples just after writing, so a consumer that is keeping up still leaves exactly one frame queued. Raw slots rather than a fraction of slotCount, because whether a fraction round-trips that 1 depends on the geometry — at the 8 slots venc creates it does (125 permille exactly), at 16 it does not (62.5 truncates to 62, back to 0: a healthy ring indistinguishable from a drained one) — and the header does not fix slotCount. A measurement, not an actuator — venc publishes it and changes nothing in response |
On unix://, fillPct is measured against the peer's datagram queue,
which is the limit that actually blocks a sender — not the local
SO_SNDBUF. The producer cannot read the peer's queue depth, so the
denominator is calibrated from the first send that saturates the queue and
is an estimate from net.unix.max_dgram_qlen before that. fillPct may
therefore read low until the transport has been pushed once.
With outgoing.allowUnixEncoderStall=false, transportDrops rising on
unix:// means the consumer is not keeping up: a frame's packets exhausted
the cumulative flush budget and the remainder was dropped rather than
stalling the encoder. With the compatibility option enabled, ordinary queue
pressure blocks instead and does not increment transportDrops; status reads
may show a full queue while the consumer is paused. The usual cause of
unexpected pressure is a shallow net.unix.max_dgram_qlen — see the
unix:// notes in the README.
A ringLowWaterSlots of 2 or more means the ring never got back down to
its one-frame idle occupancy inside the window; at slotCount it never
dropped below full and frames are being discarded after encode. venc does
not act on this — it is published for the rate controller, which on a
frame-shm:// deployment is co-located on the same SoC and reads the same
value from the ring header.
The healthy reading is 1, not 0. The measurement is taken immediately
after the frame is written, so even a consumer draining perfectly leaves the
frame just written in the ring. 0 means the consumer emptied the ring
between the write and the sample; 1 is the ordinary steady state. Treat
<= 1 as clean.
Low-water, not peak, and the distinction is what makes it usable. Measured on a Star6E at 100 fps into an 8-slot ring with a perfectly healthy consumer, the ring routinely spikes to 2-3 slots inside a 200 ms window and drains again — the consumer reads one frame per event-loop iteration, so short bursts are normal. A peak-based reading calls those bursts congestion 15-25% of the time with nothing wrong. Low-water asks whether the ring failed to drain at any point in the window, which is the question that discriminates a transient burst from standing backlog.
Why venc no longer clamps its own rate (0.19.0). Through 0.18.7 venc ran a
ring-fill bitrate clamp here. It was a second rate controller: on a
frame-shm:// craft waybeam-link already owns video0.bitrate and already
reads the egress ring, so two loops with different time constants acted on one
actuator — and every actuation costs an IDR, because MI_VENC_SetChnAttr
keyframes whenever the programmed rate changes. That was measured on
Star6E (2026-08-23); Maruko is assumed to behave the same way and is
labelled as an assumption in the code, not a measurement. Measured
2026-08-24 on a Star6E at 720p120: with the clamp engaged the receiver saw
6.5 IDR/s and 100-143 ms glass-to-glass; with it off, 0.2 IDR/s and 15-37 ms.
Ring-full drops remain the backpressure signal, readable from the ring header
(full_drops, low_water_slots), and a GDR stream heals a chain break
within one refresh cycle.
Error 501 — backend has no transport observability hook.
Return the table of sensor pads and resolution modes the underlying SDK
reports for the currently-loaded sensor driver. Used to populate the
WebUI sensor-mode dropdown and to validate sensor.mode writes.
curl http://<device-ip>/api/v1/modesResponse 200:
{
"ok": true,
"data": {
"selected_pad": 0,
"selected_mode": 1,
"pads": [
{
"pad": 0,
"modes": [
{"index": 0, "width": 1920, "height": 1080, "min_fps": 1, "max_fps": 60, "desc": "1080p60", "selected": false},
{"index": 1, "width": 1920, "height": 1080, "min_fps": 1, "max_fps": 90, "desc": "1080p90", "selected": true},
{"index": 2, "width": 1472, "height": 816, "min_fps": 1, "max_fps": 120, "desc": "1472x816@120", "selected": false}
]
}
]
}
}selected_pad / selected_mode reflect the currently-active pipeline
selection. The full pads[].modes[] list always shows every mode the
driver enumerates so callers can show an "available modes" UI.
CV610 note — every advertised mode is selectable, but only against a
sys_config that exposes the runtime sensor clock. The IMX662 runs 30/60/90
fps on a 37.125 MHz input clock and 100 fps on 27 MHz (27 MHz fed while the
sensor selects its 24 MHz INCK profile). The daemon sets the SoC-side clock
to match the requested mode during MIPI bring-up, by writing
/sys/module/open_sys_config/parameters/sns0_clk_hz.
Against an older open_sys_config without that parameter the daemon logs a
warning and continues, and the clock stays at whatever
CV610_SENSOR_PROFILE selected at insmod — correct for exactly one mode.
Every other mode then runs at the wrong rate, scaled by the clock ratio
(measured: video0.fps=60 on the 27 MHz profile delivers 43.6 fps =
60 x 27/37.125) while /api/v1/modes and /api/v1/fps/live still report
60. Neither endpoint measures anything, so a client must not treat a
selected mode as proof of the delivered rate — measure it from
framesSent in /api/v1/transport/status.
CV610 preserves the same envelope and selected_pad / selected_mode /
pads[].modes[] shape. Its initial IMX662 backend reports one synthetic pad
containing the four supported fixed-rate modes (1080p30/60 RAW12 and
1080p90/100 RAW10); each entry has equal min_fps and max_fps.
selected_pad / selected_mode are what bring-up actually selected, not
what the config asks for — the backend publishes them once the pipeline is
resolved, so a forced sensor.mode, a substituted rate and a failed bring-up
are all reported honestly. Both read -1 until bring-up completes, which in a
healthy daemon is never observable: the HTTP server starts after the backend
prepares.
The width / height of a CV610 entry is what the sensor captures, not
what is encoded. Every entry captures 1920x1080, and video0.size is the
encoded geometry VPSS scales that capture down to.
Aspect ratio is preserved by cropping, gated on isp.keepAspect (default
true), the same rule and the same shared code Star6E and Maruko use. A
video0.size whose aspect differs from the capture takes a centred crop of
the capture first, then scales: 1440x1080 out of 1920x1080 uses a
1440x1080 window at x=240 and scales 1:1, so 4:3 is framed rather than
squashed. A matching aspect crops nothing. Setting isp.keepAspect=false
restores the plain stretch-to-fit.
Selecting a CV610 mode uses the same two knobs as SigmaStar. An explicit
sensor.mode is an index into this list and wins outright; the mode's rate
becomes the pipeline rate and video0.fps is left as written. With
sensor.mode at -1 (or any negative value, meaning auto) video0.fps is a
target rather than a command: an exact match is used, otherwise the
slowest mode still faster than the target, and a target above every mode
clamps to the fastest. sensor.index accepts only -1 or 0 — one synthetic
pad — and anything else is 409, as is a sensor.mode past the end of the
list.
A substituted rate is announced on the daemon's log
(Requested 45 fps, using 60 fps (sensor mode 1: 1080p60 RAW12)) and is
visible over HTTP as /api/v1/fps/live disagreeing with
/api/v1/fps/config. Neither endpoint measures anything, so that pair states
the intended rate, not the delivered one. A client must not read a mode's width / height as the stream
resolution — video0.size in /api/v1/config is the encoded size, and
auto there means the mode's capture geometry.
Error 500 modes_failed — MI_SNR_QueryResCount failed (e.g. sensor
driver not loaded yet during a brief startup window). This SigmaStar query
failure does not apply to CV610's static IMX662 table.
An overlay-free NV12 luma tap on VPE port1 feeds the isolated
/usr/bin/qr_decode helper. port1 exists because MI_RGN composites per scaler output port and
every overlay producer targets port0 — debug_osd here, osd_render in
waybeam-hub — so the MJPEG snapshot channel (a port0 1:N consumer) carries
whatever HUD is running over anything a marker might occupy.
Gated on qr.tapEnabled. port1 is single-owner and shared with framing-stab
and NPU detect, so a scan is mutually exclusive with both: whoever holds it
wins, and QR is the lowest-priority claimant.
Open a scan window, or extend the one already running. ms defaults to
qr.windowMs, clamped to 1000–60000.
A supervisor thread owns the window: it opens port1, sends each fresh frame to the helper until the deadline, and closes the port on the way out — so a client that dies mid-scan cannot strand port1. A successful decode ends the window early; the point of a window is to find one code.
Extending never touches port state. That matters: re-opening port1 per request
is what wedged the retired /api/v1/snapshot.pgm.
curl "http://<device-ip>/api/v1/qr/scan?ms=10000"Response 200:
{"ok": true, "data": {"scanning": true, "window_ms": 10000,
"remaining_ms": 9999, "capture": "1080x1080"}}409 port1_busy— framing-stab or NPU detect holds port1. Reported immediately, never queued.503 tap_disabled—qr.tapEnabledis off, or the pipeline is not running.500 scan_failed— the SCL would not drive the configured geometry. The port is enabled and then verified to deliver a frame within 1 s; if it does not, port1 is released rather than held for a window that can never capture.
End the current window and hand port1 back. Blocks until the port is actually released, so port1 is free on return. Idempotent.
This requests a close rather than forcing one. The port is held for a minimum
of 750 ms after it comes up, because disabling one that only just opened —
while the SCL still has buffers in flight — panics the kernel (the same failure
that retired /api/v1/snapshot.pgm). A stop issued against a window older than
that returns immediately; against a brand-new one it waits out the remainder.
A further 500 ms floor applies between two opens. Together these cap the
port-cycle rate no matter how fast a client loops.
Note the path: not /api/v1/qr/scan/stop. The router matches on prefix and
accepts a / continuation, so a nested path would be swallowed by the
/qr/scan route.
Poll a window without disturbing it.
{
"ok": true,
"data": {
"armed": true,
"scanning": false,
"window_ms": 15000,
"remaining_ms": 0,
"capture": "1080x1080",
"frames": 3,
"grabs": 1,
"port1_owner": "",
"decode": {
"attempts": 1,
"decoded": true,
"payload": "P23456789ABCDEFG",
"stage": "qr_decode",
"decode_ms": 84,
"last_ms": 84
}
}
}frames/grabs— buffers drained, and buffers actually copied out, this window. Reset when a window opens, not when one is extended.port1_owner—"","qr","stab"or"detect".decode.stage—qr_decodewhen the isolated decoder helper succeeds. Detailed cascade-stage diagnostics are available fromqr_decode --stats.- The
decodeblock survives the window closing and is cleared only by the next/qr/scan, so a client polling at 1 Hz still sees the payload from a window that found its code and shut down between two polls.
payload is a Waybeam transport envelope: exactly 16 characters from the QR
alphanumeric alphabet (0-9 A-Z and $%*+-./:). Nothing in that set needs
JSON escaping; anything outside it is scrubbed to ? before serialization.
One frame of the tap as a binary P5 PGM (self-describing dimensions, stride removed). Debug instrumentation for validating capture — geometry, OSD-freedom, exposure — not part of the scanning flow.
503 tap_disabled— no window is open.504 tap_timeout— no frame arrived.409 scan_decoding— a cascade currently owns the latch. The latch is single-buffered; refusing beats blocking an httpd worker or returning a frame that is being overwritten.
In addition to the /api/v1/restart endpoint, the pipeline can be reinited by sending
SIGHUP to the venc process:
# From the device shell
killall -HUP venc
# Remotely via SSH
ssh root@<device-ip> "killall -HUP venc"Behavior:
- Tears down the full pipeline (VENC→VPE→VIF→sensor, unbinds, closes socket)
- Reloads
/etc/venc.jsonfrom disk - Rebuilds the pipeline with the new config
- The HTTP server survives reinit cycles (no port re-bind)
- Stress-tested: 10+ consecutive SIGHUPs without failure
-
Accepted config writes are persistent.
/api/v1/setpersists accepted live and restart-required field changes to the registered config path before returning./api/v1/defaultspersists compiled defaults./api/v1/restartreloads the on-disk config and does not synthesize new changes by itself. -
Video codec is hardcoded H.265. The
video0.codecfield was retired in 0.10.12. Setting it via/api/v1/setreturns404unknown config field. Legacy configs containing"codec": "h264"or"h265"load cleanly — the key is ignored and HEVC is used unconditionally. -
BusyBox compatibility. All endpoints use
GETmethod so they work with BusyBoxwget(which only supports GET):# On-device with BusyBox wget wget -q -O- "http://127.0.0.1/api/v1/get?video0.fps" wget -q -O- "http://127.0.0.1/api/v1/set?video0.bitrate=4096"
- Star6E is the reference behavior for API-touching features.
- Maruko may return
not_implementedfor specific apply paths until parity work is complete. - Shared
GETendpoints remain consistent across backends. Platform-specific routes are listed explicitly in the matrix below.
Endpoints that behave the same on all three backends are omitted. The table
compares the two SigmaStar implementations; CV610 differences are called out
in Notes. As of contract_version: 0.29.0:
| Feature / Endpoint | Star6E | Maruko | Notes |
|---|---|---|---|
/api/v1/record/{start,stop} |
yes | yes | Maruko has polled the start/stop flags since it registered venc_api_set_record_http_control_supported(true); the older 501 row was stale. Config-driven recording (record.enabled=true + record.mode="mirror"|"dual") works on both. CV610 from 0.20.0, record.mode=mirror only: the drain loop polls the same flags. dual/dual-stream need a second VENC channel and are refused with a warning rather than silently recording ch0. |
/api/v1/record/status |
live counters | live counters | Both backends register a status callback against the live Star6eTsRecorderState; Maruko reflects daemon-config-driven recording (mirror/dual). CV610 from 0.20.0 registers the same callback. |
/api/v1/qr/* |
yes | 404 | Star6E-only VPE port1 luma tap. QR capability fields remain in the shared schema but report supported:false on Maruko. |
/api/v1/recordings* |
yes | yes | File listing/download/delete works against record.dir regardless of which backend wrote the file. |
/api/v1/audio/status |
yes | yes | Both backends register query_audio_status. |
/api/v1/dual/status, /dual/idr |
yes | yes | /dual/status always 200 (active:false when off, active:true,channel,bitrate,fps,gop when on). /dual/idr returns 200 when active, 404 when not. Maruko HTTP registration landed in 0.10.4 — earlier Maruko builds returned 404 from these even when record.mode=dual was running. |
/api/v1/dual/set |
yes | 501 | Star6E-only: the underlying MI_VENC_*ChnAttr write path binds to i6_venc_chn, but Maruko's venc library expects i6c_venc_chn (different layout). Maruko returns 501 until the call path is ported. |
/api/v1/iq and /api/v1/iq/set |
full (≈45 params) | full (parity in maruko_iq.c) |
Star6E/Maruko share one IQ table schema. CV610 also serves these from 0.18.4, in a DIFFERENT shape — see "CV610 IQ response shape" below. /api/v1/iq/import stays Star6E/Maruko-only and 501s on CV610 (advertised as routes.iq_import:false). |
/api/v1/iq/export_bin |
501 | 501 | CV610-only from 0.81.0 (advertised as routes.iq_export_bin). Serializes the live ISP to a PQTools .bin through the vendor libbin.so, at the fixed path /tmp/isp_export.bin. The SigmaStar backends round-trip IQ as JSON instead, so they register no export_isp_bin callback. |
isp.sensorBin (.bin import) |
live, MI_ISP_*CmdLoadBinFile |
live, same | CV610 from 0.81.0, but a DIFFERENT format and library: HiSilicon PQ images via OT_PQ_BIN_ImportBinData. The two families are not interchangeable despite the shared field and extension. On CV610 an empty value is a no-op — there is no /etc/sensors/<sensor>.bin fallback. |
/api/v1/awb |
live | live | All three backends register query_awb_info. CV610 serves ot_isp_wb_info plus ot_isp_exp_info (the AE's own applied exp_time, a_gain, isp_d_gain, ave_lum) — a different payload from the SigmaStar one, and the instrument the isp.gainMax/isp.shutterMaxUs mapping was verified against. |
/api/v1/ae |
live + runtime.active_precrop |
live + runtime.active_precrop |
Both backends now include runtime.active_precrop in the AE response (Maruko parity landed in 0.8.4). |
RTP sidecar (outgoing.sidecarPort, UDP :5602) |
yes | yes | CV610 from 0.74.0; before that src/rtp_sidecar.c was not compiled into the CV610 binary at all, so the field was accepted, persisted and silently did nothing. CV610 emits the base FRAME plus TRANSPORT_INFO. It has no IMU and no detector, so ATTITUDE and DETECT trailers are never appended; ENC_INFO carries frame_size_bytes, frame_type, qp (the encoder's h265_info.start_qp) and frames_since_idr; complexity, scene_change and idr_inserted stay 0 because the shared scene detector is not compiled in on this backend. idr_inserted is deliberately not produced here (corrected in 0.81.0, which briefly set it): the field means "the controller requested an IDR after this frame", and on the SigmaStar backends the only writer is the scene detector, so there it is disjoint from frame_type == IDR. CV610 has no such controller, so setting it on every IDR would duplicate frame_type and FLAG_KEYFRAME — a ground consumer summing it as "IDR insertions" would read a once-per-GOP static scene as one insertion per second while an identically configured SigmaStar craft reported zero; gop_state is 0 on all three backends, as nothing in the tree writes it. qp is CV610-only: the SigmaStar encoder populates only refType in its H.265 stream-info struct — size, all eight CU counts, updAttrCnt and startQual read 0 on the device (dumped on i6 at 0.79.0; the struct offset is correct, since refType lands exactly where the header says). Reading startQual into the trailer was tried and shipped zeros, so Star6E and Maruko leave qp at 0 rather than advertise a value that is always 0. Recovering a per-frame QP there means parsing slice_qp_delta out of the slice header. Under frame-shm:// only seq_count is 0 on every backend (the packetizer never runs, so no sequence numbers are consumed); ssrc, rtp_timestamp and seq_first are seeded and non-zero, because the RTP session is gated on the stream mode rather than the transport. The trailer carries the ring state. |
/api/v1/transport/status |
yes | yes | Three distinct field sets by transport: frame-shm:// (ring fields plus ringLowWaterSlots/otherDrops), shm:// (packet-ring fields), and UDP/Unix (socket subset). badAuDrops appears on every transport. CV610 serves the same endpoint but emits no badAuDrops (no packet-table validation in its stream path); its frame-shm branch does carry ringLowWaterSlots and otherDrops. |
/api/v1/idr/stats |
yes | yes | Identical schema; values reflect each backend's IDR rate-limit. |
video0.codec=h264 |
404 unknown_field | 404 unknown_field | Field retired in 0.10.12; codec is hardcoded H.265 on both backends. |
video0.scene_threshold / scene_holdoff |
yes | yes | Restart-required fields; both backends run the shared scene detector. |
video0.framing / zoom_x / zoom_y |
yes | partial | framing requires reinit; zoom presets work on both backends, the stab preset is Star6E-only (no-op on Maruko); zoom_x/y are live pan controls (ignored under stab). |
detect.model_path / model_id / conf_thresh / nms_iou |
live | live | Both backends hot-swap the NPU detector on the pipeline thread without respawning video. Star6E uses VPE port 1; Maruko uses SCL port 3 and its drain-while-disable teardown. A model whose reported input geometry disagrees with the configured tap is refused and leaves detection off. |
detect.net_width / net_height |
restart | restart | Tap geometry is fixed when the VPE/SCL detector port is created. |
video0.min_qp / max_qp |
live | live | RC QP bounds, live and from config at startup on all three. Maruko gained them in 0.73.0; before that it reported unsupported. CV610 from 0.18.4 — it sets the P bounds and the I-frame ceiling, but the I-frame floor is not steerable there (video0.qp_delta is not offered on CV610; see Per-Backend Field Support). |
fpv.roi_enabled / roi_qp / roi_steps / roi_center |
live | live | Centre-priority horizontal delta-QP bands. CV610 from 0.76.0 (ss_mpi_venc_set_roi_attr); before that the fields were accepted by a config parse, reported supported:false and never reached the encoder. All three backends share one band geometry (pipeline_common_roi_band). roi_qp == 0 clears every region regardless of roi_enabled — a zero delta is not a region worth programming — and the disabled log line names which of the two caused it. roi_qp and video0.max_qp interact: the delta is subtracted from the frame QP, so CBR raises the base QP roughly 1:1 to pay for it and pins at the RC ceiling once `base_qp + |
isp.aeEngine ("sdk" only) |
applied | applied | Unified AE selector landed in 0.10.13. custom (userspace AE governor) is RETIRED — Maruko in 0.22.0, Star6E in 0.47.0 — and the value was removed in 0.47.0. sdk is the only accepted value; any other (e.g. a stale custom) warns and falls back to sdk. Both backends run the SDK firmware/bin AE for convergence plus a supervisory thread that enforces the isp.gain*/isp.shutter* limits. CV610 has no such thread and reports isp.aeEngine unsupported: its ISP owns AE outright, and the two ceilings it does honour are written straight into ot_isp_exposure_attr.auto_attr instead. |
-
0.29.0(additive — CV610 gains PQTools.binimport and export): AddsGET /api/v1/iq/export_bin, which writes the live ISP state to a PQTools.binat the fixed path/tmp/isp_export.binand answers{"path":...,"bytes":N}. CV610-only; the SigmaStar backends register noexport_isp_bincallback and return 501, advertised as the newroutes.iq_export_bincapability flag. The destination is fixed rather than caller-supplied because the endpoint is unauthenticated. Also flipsisp.sensorBinfrom unsupported to live on CV610 — the field, its mutability and its payload are unchanged, only the set of backends that honour it, socapabilities.fields[].supportedmoves fromfalsetotruethere. Device-verified on a Hi3516CV610 + IMX662 bench: a tune built for a different sensor imports cleanly (the format is locked to the chip register map and SDK ISP version, not to the sensor), and an exported file re-imported restores an identical/api/v1/iqread-back. A.binis integrity-checked by the vendor library and cannot be edited or spliced in place. -
0.28.0(narrowing —fpv.roi_qpaccepted range[-30, 30]->[-20, 20]): a client that sends±21..±30now gets 400 where it previously got 200.roi_qpis a relative delta and H.265 caps QP at 51, so past ±20 it stops being honoured at both ends — and the negative end is expensive rather than merely truncated: CBR pays for the ROI discount by raising the frame's base QP roughly 1:1 with|roiQp|, so oncebase + |roiQp|passes 51 the rate controller saturates and loses authority. Measured on a CV610 bench at 720p60:roiQp -30pinned every frame atqp 51/51/51and delivered 16976 kbps against a 2829 kbps target (6.0x), while-20sat at qp 45 and held 2802. Reproduced at three targets — the cliff is exactlybase_qp + |roiQp| > 51. The band width does not move it, only its severity (at-30,roiCenter0.6/0.4/0.2 all pinned at 51 and delivered 17661/12520/6977 kbps), so there is nothing to bound on that axis instead. PositiveroiQpis the benign end but truncates just as quietly: at+30the base sat at 21.5, so the region wanted 51.5 and got 51.- A config file is clamped, not rejected.
load_fpv()clamps to the new bound at parse time, so a craft already carrying-30still boots and lands on-20. Only the API path returns 400, so an operator typing-30is told rather than reading back a value they did not write. - Shipped default
fpv.roiQp-25->-20, so the default is inside the range it documents. - NOT reproduced on Star6E: 1080p60 at both a 19092 and a forced 1500 kbps
target held rate at
roiQp0/-15/-25/-30. Its sidecar reportsqp=0, so its rate controller could not be watched directly — treat SigmaStar as unreproduced rather than immune. The bound is applied on all three backends regardless, because the QP ceiling it derives from is an H.265 limit, not a vendor one.
- A config file is clamped, not rejected.
-
0.27.0(additive — the portable AE ceilings reach CV610):isp.gain_maxandisp.shutter_max_uschange fromsupported:falsetosupported:trueon CV610 only; both were alreadyMUT_LIVEin the shared table, so no mutability changes. They map onto the exposure group/api/v1/iqalready exposes, with no unit conversion —exp_time_rangeis documented "unit: us" anda_gain_range"Format:22.10 ... unit: times, 10bit precision", i.e. 1024 == 1x, the same scale the SigmaStar supervisory AE uses.isp.gain_maxmaps toauto.a_gain_max(the analog ceiling, which is what the name means on the other backends) rather thanauto.sys_gain_max(the product of analog, sensor-digital and ISP-digital gain).0still means "use the sensor plugin's default" and now restores it: the default is snapshotted before the first write, so clearing a ceiling is not a one-way door.- Both also apply at cold boot, not only on a live write — the ISP is seeded by the sensor plugin, which never sees the config file.
isp.gain_min,isp.shutter_min_us,isp.awb_modeandisp.awb_ctstaysupported:falseon CV610 and still return 501. The floors were simply not measured in this slice. For the AWB pair that is a two-call flow rather than a missing capability: where SigmaStar has one call (MI_ISP_AWB_SetCTMwbAttr(ct)), this SDK hasss_mpi_isp_cal_gain_by_temp(), which converts Kelvin to r/gr/gb/b gains against the currentot_isp_wb_attr; those gains then go back throughset_wb_attrwithop_typemanual. Calibration-dependent, so it needs the same device measurement every other CV610 field had to clear. Manual white balance on CV610 is reachable today, under its own name, through/api/v1/iq'swbgroup.
-
0.26.0(additive — live output retarget reaches CV610):outgoing.serverandoutgoing.enabledchange fromrestart_requiredtoliveon CV610 only. Both SigmaStar backends already reportedlive; CV610 read the URI once incv610_prepare()and rancv610_output_start()once, so the fields were honestly advertised restart-class. They are now backed bycv610_apply_server()/cv610_apply_output_enabled(). The callbacks landed BEFORE the mutability widened, not after: the live-apply gate keys on the callback being present, so the reverse order would have advertisedlivewhile every write was refused.Socket transports switch live; ring transports stay restart-class. A
udp://<->unix://change takes effect in place. A write namingshm://orframe-shm://(or made while a ring is running) is COMMITTED and answered200with"reinit_pending": true, and the craft respawns onto it — the ring is created once at start and cannot move in place. CV610 deliberately differs from Star6E and Maruko here, which refuse such a write: on this backendoutgoing.serverwas restart-required until 0.26.0, so refusing would have made the fleet's normal production transport unreachable through the API.Clients should therefore treat a
200withreinit_pendingonoutgoing.serveras "accepted, applying after respawn", exactly as they already do for restart-class fields.One deliberate divergence: Star6E drops the encoder to 5 fps while output is disabled. CV610 cannot —
video0.fpsis restart-only there — so a disable gates the send and the encoder keeps running at its configured rate. -
0.25.0(additive — centre-priority ROI reaches CV610):fpv.roi_enabled,fpv.roi_qp,fpv.roi_stepsandfpv.roi_centerflip fromsupported:falsetotrueon CV610, which had noapply_roi_qpat all — every craft carried anfpvblock nothing on the board read. Programmed throughss_mpi_venc_set_roi_attr();ot_venc_roi_attris field-for-fieldMI_VENC_RoiCfg_t, so the three backends now share one band geometry. Verified in the bitstream on CV610 itself, not by return code and not by inference from a sibling SoC — the SDK call differs per part, and issue #259 is the counter-example where a QP control returned success, logged as applied and read back clean while the picture never moved. CV610 720p100 CBR: in-band detail drops 7.4x betweenroiQp-30 and +30, the columns just outside the band move the opposite way, and a repeat of the -30 arm lands within 8.8%. Measured separately on Maruko for the shared band geometry. No wire or field-name change; a client that already renders the CV610 capability map sees four controls ungrey.Shipped defaults also change, which
/api/v1/defaultsactuates and a freshly provisioned craft reads back:fpv.roiEnabledtrue-> false andfpv.roiQp0-> -25. The old pair read as "ROI on" while the apply cleared every region (a zero delta is not a region worth programming). Existing crafts are unaffected — their own config values win. -
0.24.0(additive — sensor orientation reaches CV610):image.mirrorandimage.flipflip fromsupported:falsetotrueon CV610. Both were hardcodedTD_FALSEon the VI channel attribute, so an inverted lens mount had no software fix on that board. They are now applied at the sensor, through the plugin'spfn_mirror_flip, which is where both SigmaStar backends already apply orientation (MI_SNR_SetOrien) — soimage.*means one thing across the fleet and the encoder is never asked to do geometry. Mutability is unchanged (restart_requiredon all three; the sensor is programmed once at bring-up).image.rotatedoes not flip and stayssupported:falseon CV610: the decomposition it relies on —venc_config'sload_image()turningrotate: 180intomirror + flip— runs on a file parse and nowhere else, so a value arriving through/api/v1/setis never decomposed and never read by any backend. Advertising it was measured to turn a clean501into: accept90, persist90, raisereinit_pending, restart the encoder, then read back0. A config file carryingrotate: 180still works on all three backends. The SigmaStar backends report ittrueonly because they have no per-backend allowlist. -
0.23.0(additive — the RTP sidecar reaches CV610):outgoing.sidecarPortflips fromsupported:falsetotrueon CV610.src/rtp_sidecar.cwas never compiled into that binary, so the field was accepted, validated and persisted while no datagram was ever sent — the ground station's attitude and link-log consumers had no source on a CV610 craft, and the per-frame timing probe had nothing to read. No wire change: CV610 emits the same FRAME the SigmaStar backends do. ENC_INFO carriesframe_size_bytes,frame_type,qpandframes_since_idr(idr_insertedis not produced on this backend — see the Backend Support Matrix);complexityandscene_changestay 0 because the shared scene detector is not compiled in here;gop_stateis 0 on all three backends, as nothing writes it. ATTITUDE and DETECT are never appended (no IMU, no detector), so a consumer whose only input is the ATTITUDE trailer has no source here. Underframe-shm://seq_countis 0 on every backend and the other RTP identifiers are seeded non-zero; the session is gated on stream mode, not transport. -
0.21.0(breaking,video0.maxIBytes/video0.maxPBytesremoved): the per-frame size caps are gone from the config, the capabilities payload and both SigmaStar backends. They never imposed the ceiling they named. Device-measured on a SSC338Q (1280x720@60, H.265 CBR, GDR): acrossmaxPBytes33144 -> 25000 -> 16000 -> 10000 -> 6000 the delivered rate moved under 0.3%, and at the 6000 B step an AU census found all 863 access units over the cap (mean 40247 B).maxIBytes91788 -> 26000 -> 8000 -> 2000 left IDR size at 66-81 KB across 16 sampled IDRs — the weaker of the two arms, since a GDR stream's IDRs are requested on demand rather than being a natural population; corroborated by #111's 42-44 KB at caps 2000 / 26000 / 8. Scope: one SSC338Q, one SDK build, 720p60 H.265 CBR, GDR — Maruko was not measured, and removal there is taken on parity. Bounded claim.0.45.0recordedmaxPBytes=2000moving a Star6E from 5619 to 1868 kbps — real influence, 3x below this sweep's floor. But 1868 kbps at 60 fps is ~3892 B/frame against a 2000 B cap, ~1.95x over, and the Maruko datum in #111 (maxIBytes=2000-> IDR median 12195 -> 5866) is ~3x over. The caps influence below ~6000 B; they do not bind anywhere they have been measured, which is what disqualifies them as a ceiling. A config file still carrying the keys loads fine (unknown keys are ignored) and they disappear on the next config write, but aGET /api/v1/setorGET /api/v1/live/setnaming them now fails the whole request with404 unknown config field— and/api/v1/live/setis the path a volatile-first controller hits first. Strip them from saved batches and from any controller that pushes them. For I-frame size usevideo0.qpDelta— a 68x range on the same rig, and it does not keyframe. Fix #255 before depending on it: applied from venc's own startup path it logs success without reaching the encoder, so only a live write takes effect today. That is the same logs-success-without-taking-effect shape as the caps removed here. For P-frame size there is no direct replacement;video0.minQpis the surviving rate lever and is Star6E-only, so this removal leaves Maruko without a per-frame size control. -
0.20.1(/api/v1/record/status—droppedFrameswidened and scoped):droppedFramesnow counts every recording frame that did not reach the file, not only the ones the queue refused. An access unit the SDK-typed flatten rejects — an incompletepacketInfotable, or one over the queue's byte cap — never reaches the queue at all, and counting only the queue's refusals let those pass silently. The field's promise is that a damaged recording cannot look like a clean one, so it has to include them. Widening, not narrowing: no frame that used to be counted stops being.droppedFramesandwriterPeakDepthare per-RECORDING, not per-process. All three backends now create the recorder writer when a recording opens and destroy it when the recording closes, so the counters are scoped by construction rather than reset by an operation. Previously the SigmaStar writer outlived any one recording and both accumulated for the life of the daemon: a clean recording reported the previous one's drops, and one shed frame poisoned every recording that followed.- Note the denominators still differ slightly by backend: CV610 hands the transport's already-flattened buffer to the recorder and so has no flatten step of its own to fail.
-
0.20.0(additive — snapshot and recording reach CV610):/api/v1/record/statusgainsdroppedFramesandwriterPeakDepth. Recording writes now run on their own thread behind a bounded 4 MB queue, so a stalling disk sheds recording frames instead of stalling the live video path.droppedFramescounts what the queue refused — a silent drop would make a damaged recording look like a clean one. (Widened in0.20.1to every frame that did not reach the file.)writerPeakDepthis the high-water queue depth, which reads 0-1 when storage keeps up. Both are 0 on backends that still write synchronously.record.max_seconds/max_mbnow work on a GDR craft. Segment rotation can only cut on an IRAP; underresilience=racingthe stream emits none, so rotation was silently inert and the file grew unbounded. The recorder now requests one when rotation is due, rate-limited to one request per second. Device-measured on CV610:max_seconds=15over 50 s gave 1 segment before and 4 after, with/api/v1/idr/statsshowing exactly 4 honored requests (one start + three rotations).GET /api/v1/snapshot.jpgis now registered on CV610. It was the one route a backend omitted entirely; all three now serve it. The CV610 JPEG channel binds as a second destination on the main stream's VPSS output, so it inherits that geometry —snapshot.width/snapshot.heightreportsupported:falsethere.record.*reaches CV610 inmirrormode.record.enabled,dir,format(ts|hevc),mode,max_secondsandmax_mbare now advertised supported, and/api/v1/record/{start,stop,status}act on a real recorder.record.bitrate/fps/gop_size/serverstaysupported:false: they describe the second VENC channel thatdualanddual-streamwould need, which this backend does not create.- Record start on CV610 forces an un-coalescible IDR. The shipped
configuration is
resilience=racing(GDR, no periodic IDR), so a rate-limited request would produce a file with no IRAP access unit anywhere in it. - No field was removed, renamed or given a new meaning. A client written
against
0.19.0sees strictly more capability and no changed response shape.
-
0.19.0(breaking — venc stops actuating on its own egress, and the frame-SHM ring header goes to v2):- Removed
outgoing.shm_throttle(aliasoutgoing.shmThrottle) and the ring-fill bitrate clamp behind it. venc no longer writes the encoder rate in response to ring pressure at all. On aframe-shm://craft the rate controller (waybeam-link) is co-located, already reads the egress ring, and already ownsvideo0.bitrate— the clamp was a second controller on the same signal, and its only actuator keyframes. Measured 2026-08-24 on a Star6E at 720p120: clamp on, 6.5 IDR/s and 100-143 ms glass-to-glass; clamp off, 0.2 IDR/s and 15-37 ms. Config files tolerate the key; the API does not. Awaybeam.jsonstill carryingoutgoing.shmThrottleloads fine — the parser ignores unknown keys, and the key disappears on the next config write. APOST /api/v1/setnaming it is a different matter: the multi-field preflight rejects the whole batch404 unknown config fieldon the first unrecognised key, so a stored ground-side "apply my profile" batch that still carries it now applies none of its other fields. Strip it from any saved batch before upgrading. - Removed the ring-full recovery IDR entirely. Through 0.18.7 a
frame-shm://ring-full drop asked the encoder for an IDR on a plain-GOP stream. That request fired precisely when the ring was full, so the largest frame in the stream could not be delivered anyway — measured on a SSC338Q with the consumer stopped, 13 IDRs in 12 s, none of which reached anyone. Recovery is now the operator-selected GOP cadence, or an explicit request:/request/idr, or waybeam-link's §3.9RECOVERY_REQUEST. Every remaining venc-issued IDR is either explicitly asked for (the endpoints, the opt-in scene detector) or structural bootstrap (recorder start, live fps rebind, output enable, new destination) — a receiver that has never seen a parameter set has nothing to start from. GET /api/v1/transport/status:throttlePermilleandeffectiveBitrateKbpsare removed;ringLowWaterSlotsis added on theframe-shmbranch. Note the polarity is inverted — a LOW number is healthy here (<= 1), where1000was healthy for the clamp. Readvideo0.bitratefrom/api/v1/config; there is no longer a scaled "effective" rate, because nothing scales it.- Bootstrap IDRs bypass the 100 ms spacing gate. Output enable, a
destination change, a live fps rebind and recorder start hand the stream (or
a new file) to a receiver that has seen no parameter set, so they are
honored unconditionally rather than coalesced — still counted in
/api/v1/idr/stats, and they re-arm the window so an ordinary request behind one still coalesces. Every other source, including/api/v1/idrand the scene detector, is paced exactly as before. A failed bootstrap IDR is logged, not fatal: the apply it accompanies has already succeeded. - New
other_dropsat ring header offset 96 (u64, producer cumulative): frames the producer discarded for a reason other than a full ring. Onlyfull_dropswas ever published, so a consumer was structurally blind to an oversize or malformed access unit — the frame simply vanished. Deliberately separate fromfull_dropsbecause the two demand opposite responses from a rate controller.transport/statusgainsotherDropsandbadAuDrops. - Frame-SHM ring header is now version
2. Offset 88 changed meaning: it carriedthrottle_permille(1000= unclamped) and now carrieslow_water_slots(ring occupancy in slots,<= 1healthy).sizeofstays 192 and nothing before offset 88 moves, but the polarity inverts, so this is a hard version break by design — every consumer validatesversionand refuses to attach on a mismatch rather than silently misreading the field. A v2 producer will not serve a v1 consumer: waybeam-link, waybeam-hub and radeon-vrx must be rebuilt alongside. - Sidecar
TRANSPORT_INFOtrailer:throttle_permillereturns to_pad[2]. Trailer stays 16 bytes and later trailers keep their offsets.
- Removed
-
0.18.7was never a servable contract version. It was staged during review and folded into0.19.0above; no device ever reportscontract_version: 0.18.7, so do not match against it. Most of what it described — a ring-full recovery IDR kept for plain GOP, and a deadband on the ring-fill clamp — was superseded outright:0.19.0removed both the recovery IDR and the clamp. Three things it introduced do survive, and are in force as of0.19.0:- Rate-control writes no longer request an IDR.
video0.bitrate,video0.qpDeltaandvideo0.maxIBytes/maxPBytesno longer request one after applying, on Star6E and Maruko. A controller wanting a resync point calls/request/idr(ch0) or/api/v1/dual/idr(ch1) explicitly. CV610 never IDR'd on these paths. Device-measured on a SSC338Q, 2026-08-23, ten live writes spaced 300 ms, counted as IRAP access units in the encoder's own bitstream:qpDeltaandmaxIBytesfell from 11 to 1,bitratestayed at 11. The bitrate path is unchanged on the wire becauseMI_VENC_SetChnAttremits an IDR by itself andMI_VENC_RcParam_tcarries no bitrate field, leaving no rate-only actuator to switch to; what the removal fixes there is the shared 100 ms IDR gate, which a bitrate write used to consume — a genuine recovery request arriving inside that window was swallowed — and/api/v1/idr/stats, which counted an IDR per bitrate write that the write did not cause. - Maruko's
/request/idr, output-enable and server-change IDRs now go through the shared per-channel gate, so every Maruko IDR source is paced and counted in/api/v1/idr/stats. Previously none of these were. - Two corrections with no code change: the
/api/v1/dual/setbitraterow no longer claims venc requests an IDR (it never did), and the/api/v1/idr/statsexample reportsmin_spacing_us: 100000, matchingIDR_RATE_LIMIT_MIN_SPACING_US.
- Rate-control writes no longer request an IDR.
-
0.18.6(documentation + correctness; no shipped response changes):GET /api/v1/intra/statusandGET /api/v1/resilience/statusare now documented — both have been served for several releases and the CV610 branch added in 0.18.5 made them report live device state, but neither had a contract entry.data.routes.iqnow requires the backend to register bothquery_iq_infoandapply_iq_param, matching its documented meaning that/api/v1/iqand/api/v1/iq/setare serviced; every shipped backend registers both, so no response changes. -
0.18.5(additive — resilience and slices reach three-backend parity): CV610 now advertisesvideo0.resilienceandvideo0.slice_countand serves the existing intra/resilience status routes. Maruko now advertises and appliesvideo0.slice_count. Explicit multi-slice requests use pre-start vendor Set/Get verification on all three backends; no field or response key was removed. -
0.18.4(additive — CV610 gains the IQ surface and RC QP bounds):/api/v1/capabilitiesgainsdata.routes(iq,iq_import) so a client can discover optional routes without calling them./api/v1/iqand/api/v1/iq/setgo from 501 to live on CV610, in a second response shape documented under "CV610 IQ response shape" — group-keyed, self-describing via_schema, and rejecting out-of-range values rather than clamping.video0.min_qp/video0.max_qpbecome live on CV610. No field was removed and no existing response key changed, so 0.18.3 clients keep working. -
0.18.3(additive — CV610 sensor-mode selection reaches SigmaStar parity):sensor.indexandsensor.modeare now supported on CV610, bothrestart_required, the samemutabilitythe shared table gives them on Star6E and Maruko. They parsed before but were read by nothing, so/api/v1/setanswered409for a field the config file carried. A client that renders sensor controls from/api/v1/capabilitiesnow gets the same surface on all three backends.video0.fpsis a target on CV610, not a command. Withsensor.modeauto, an exact match is used, otherwise the slowest mode still faster than the target, and a target above every mode clamps to the fastest. Rates that are not in the table were409before and are now honoured with a substitution — see/api/v1/modes.video0.fps=0remains409./api/v1/modesreports the achieved selection.selected_pad/selected_modewere recomputed fromvideo0.fpsper request, so they described the configured mode even when another one was running. They are now published by the backend at bring-up, and read-1before it completes. Shape unchanged.isp.keepAspectis now supported on CV610 (restart_required), and a non-nativevideo0.sizeis centre-cropped before scaling instead of stretched. The field was in the config file and defaultedtrue, but CV610 advertised noisp.*field and read none, so it was a knob that did nothing —1440x1080validated and then squashed 16:9 into 4:3. CV610 now calls the samepipeline_common_compute_precrop()Star6E and Maruko use. No shape changed and no request that used to succeed now fails; the pixels are different.- CV610 now runs the shared field validation at config load, which it
had been skipping entirely:
venc_api_validate_loaded_config()dispatched to the CV610 backend validator instead of the sharedvalidate_field_cfg()sweep, so sixteen shared rules never ran on that backend. The HTTP/api/v1/setpath always applied them, so the same value was accepted from/etc/waybeam.jsonat boot and rejected with409over HTTP. A CV610 config carrying e.g.isp.awbMode:"bogus"used to load and now fails with the same message Star6E gives. Configs that were valid on Star6E are unaffected. video0.gopSizeis validated against the selected mode's rate. The encoder has always derived its GOP length from that rate; the check usedvideo0.fps, which the two knobs above can now separate from it. AgopSizethat was accepted and then exceeded the encoder's 65536-frame limit is now rejected at409.
-
0.18.2(additive — CV610video0.sizebecomes a real control):video0.sizeon CV610 now accepts any geometry the mode can be scaled down to, not just the capture size. VI has no scaler, so the backend previously bound VI straight to VENC and1920x1080was the only value that validated. A VPSS group now sits between them (VI chn → VPSS grp → VPSS chn → VENC) and does the scaling, sovideo0.sizeis the encoded geometry andvideo0.fpsalone selects the sensor mode — the same split Star6E has. Rejections are409: not a multiple of 8, below 128x128, or larger than the mode's capture size (VPSS does not invent detail, and upscaling would spend encoder bandwidth for no information)./api/v1/modeskeeps its shape, but a CV610 entry'swidth/heightis now explicitly the capture geometry rather than the stream resolution, andselectedfollowsvideo0.fpsalone. Previously a client could infer the two were the same, because they were.- A CV610 sensor clock that cannot be set is now fatal to bring-up, so
a mode either delivers its nominal rate or the daemon does not start. The
two failures are answered differently: a
sys_configwith nosns0_clk_hzparameter predates it, and that stays a warning with the clock left as loaded (one mode then runs correctly, the rest do not). A parameter that is present and rejects the write means the line timing is known to be running against the wrong MCLK, and the delivered rate would be wrong while/api/v1/modesand/api/v1/fps/liveboth report nominal — so bring-up aborts rather than serve a rate nothing reports. - Correction to
0.18.1, which stated thatvideo0.fpson CV610 is "honoured only within the sensor clock profile the kernel modules were loaded with". That shipped alongside the runtime sensor clock in the same release and was stale on arrival: the daemon sets the clock for the selected mode at bring-up. The boot-time profile matters only against asys_configtoo old to exposesns0_clk_hz, which is what the caveat under/api/v1/modesdescribes.
-
0.18.1(additive — CV610 control surface reaches what the backend reads):- CV610 now advertises
video0.fps,video0.size,outgoing.enabled,outgoing.server,outgoing.connected_udp,outgoing.allow_unix_encoder_stall,audio.enabledandaudio.muteas supported. All were already read by the backend; the capability set under-reported them, so/api/v1/setanswered501for a value the daemon would have honoured on the next start. mutabilityis now per backend. It may only be downgraded from the shared table, never widened. CV610 reportsvideo0.fps,outgoing.enabled,outgoing.serverandaudio.muteasrestart_requiredwhere Star6E and Maruko reportlive, because the CV610 slice reads them once at start./api/v1/live/setrejects them with400 invalid_requeston CV610 and still applies them on Star6E. A client must therefore readmutabilityfrom the target device rather than assuming it per field./api/v1/audio/statusis implemented on CV610, reporting the fixed 48 kHz mono Opus configuration plus liveframes/bytes/packets/dropscounters. It answered501 not_implementedbefore while audio was in fact streaming.video0.fpson CV610 is honoured only within the sensor clock profile the kernel modules were loaded with — see the CV610 caveat under/api/v1/modes. No contract shape changed; the constraint is documented because the mode list cannot express it.- Still unsupported on CV610, and deliberately so:
audio.sample_rate,audio.channels,audio.codec,audio.volume(hardcoded insrc/cv610_audio.c) andvideo0.rc_mode(hardcoded H.265 CBR). The shipped defaults coincide with the hardcoded values; that is not support.
- CV610 now advertises
-
0.18.0(additive — Unix encoder-stall compatibility):- Added restart-required
outgoing.allow_unix_encoder_stallwith camelCase aliasoutgoing.allowUnixEncoderStall. Defaultfalseretains bounded send/drop behavior;truerestores blockingunix://sends. - Added Star6E-only inline QR scan routes and the
qr.*configuration surface.qr.window_msapplies live to the next scan; Maruko advertises all QR fields unsupported and does not register the routes. - Added live Star6E
video0.min_qp/video0.max_qpRC bounds. Maruko advertises these fields unsupported. /api/v1/record/statusnow includeselapsed_mson both backends.
- Added restart-required
-
0.17.0(additive — socket transport telemetry):- The UDP/Unix response from
/api/v1/transport/statusnow reportstransportDropsandpacketsSent, matching the existing SHM field names. - Unix
fillPctis calibrated against the peer's observed full datagram queue rather than the sender'sSO_SNDBUF.
- The UDP/Unix response from
-
0.16.1(non-breaking):isp.keepAspectis now supported on Maruko — capabilities reportsupported:trueand/api/v1/setaccepts it (previously rejected withnot_implemented).falsepasses the full sensor frame through and the I6C SCL scales both axes non-uniformly (stretch-to-fill). Exception: a single-axis squeeze (one axis already matching the output) stalls the SCL, so that geometry is centre-cropped regardless, with a startup note.
-
0.16.0(breaking — snapshot.pgm retired):GET /api/v1/snapshot.pgmremoved; answers404. Its per-request VPE/SCL tap could wedge the SoC (device-verified:DisablePort … mhal not return buffer→EnsureInputPortFifoEmptystorm). QR scanning now consumesGET /api/v1/snapshot.jpg;qr_decodereads JPEG natively. Error codesbad_crop,bad_max_dim,snapshot_gray_busyandsnapshot_gray_unsupportedare gone with it.- The
snapshot.*config section is dashboard-visible (FieldUi group "Snapshot", rendered from capabilities).
-
0.15.0(additive — Maruko detector parity):- Maruko now implements the ABI-3 detector host on SCL port 3, including
live enable/disable and model reload,
detect.osd, and the unchanged RTP sidecar DETECT trailer. Detect fields no longer return 501 on Maruko. - Detection is refused while Maruko stabilization or zoom is active until the independent SCL-port crop can be mapped to encoded-frame coordinates.
- The default Maruko deployment uses an 800x448 I6C model. Small-flash
systems may store it as
/root/models/<name>.img.xz; the init script inflates the configured/tmp/<name>.imgbefore startup.
- Maruko now implements the ABI-3 detector host on SCL port 3, including
live enable/disable and model reload,
-
0.14.0(additive — detector live model swap):detect.model_pathchanged fromMUT_RESTARTtoMUT_LIVE, anddetect.model_id/detect.conf_thresh/detect.nms_iouare now settable (MUT_LIVE). Changing any of them re-creates only the NPU detector plugin + VPE port1 tap on the pipeline thread — the video0 encode/RTP path keeps running, so there is no pipeline respawn, keyframe reset, reconnect, or transport drop. Amodel_pathchange is not free, though: the NPU graph load runs on the pipeline thread (that is what makes the swap atomic against the per-frameDETECTsnapshot), so frame output stalls while it runs. Measured on Star6E .232 at 100 fps: ~100-450 ms on some runs, ~2.2-2.5 s on others, with nothing in between and the trigger not isolated (not memory pressure, not file I/O, not accumulation across reloads). Budget for ~2.5 s.model_id/conf_thresh/nms_ioudo not reload the graph: when the requestedmodel_pathmatches what is loaded, the label and thresholds are applied in place (thresholds via the plugin's optionalset_thresholds(), falling back to a full reload if the backend lacks it). Measured cost: ~50 ms for a threshold change, ~0 ms formodel_id, versus ~2300 ms before. So live threshold tuning is cheap; only swapping the.imgis expensive. The sidecarmodel_idflips to the new value in lockstep with the first new-modelDETECTtrailer. Star6E only; Maruko returns501(noapply_detect_reload).- The host now verifies the loaded model's real input geometry (reported
by the plugin via the new ABI-2
model_dims()) against the VPE port1 tap it created, and refuses a mismatch —net_width/net_heightare config, not evidence of what a.imgexpects. Becausemodel_pathis live but the dims are restart-scope, pointingmodel_pathat a different-geometry model used to be accepted silently and left an "active" detector that never detected (the backend rejects every frame andprocess()errors are not logged). A refused swap logs both geometries and the exactnetWidth/netHeightto set, leaves detection off, and releases the port1 claim (runtime.vpe_taps.port1readsnull); the stream is unaffected. Note/setstill returns200— the reload is serviced asynchronously on the pipeline thread, so the stored value is accepted even when the model is then rejected; checkruntime.vpe_tapsor the log for the outcome. detect.net_width/detect.net_heightare now settable asMUT_RESTART(a tap-geometry change needs the VPE port recreated). Both must be0(default) or a multiple of 32 (>=64).detect.confThresh/nmsIouaccept[0, 1)(0 = plugin default);netWidth/netHeightaccept0or a>=64multiple of 32.
-
0.57.0(additive — new config field + new response fields). All of it removed again in0.19.0; kept here so a reader of an older device can still decode what it sees.- Added
outgoing.shm_throttle(boolean, defaulttrue,MUT_LIVE, aliasoutgoing.shmThrottle). Enables theframe-shm://ring-fill bitrate clamp; inert on every other transport. Both backends. GET /api/v1/transport/statusgainsthrottlePermilleandeffectiveBitrateKbpson theframe-shmbranch only.- The clamp never writes
video0.bitrate, soGET /api/v1/configand everysetresponse are unaffected by it. Read the effective rate fromtransport/status, not from the config. - Sidecar
TRANSPORT_INFOtrailer:_pad[2]becamethrottle_permille(u16, network order). Trailer stays 16 bytes and later trailers keep their offsets;0means "not reported".
- Added
-
0.46.0(additive — new config fields):- Added
isp.gain_min(min sensor gain floor) andisp.shutter_min_us(min exposure floor, µs) to the config schema. Both default0= "use the ISP bin's calibrated floor" (no override), symmetric with the existingisp.gain_max/isp.shutter_max_usceilings. The supervisory cus3a thread writes them intominSensorGain/minShutterUsof the ISP exposure limit; each floor is clamped to not exceed its ceiling, andisp.shutter_rule_180(min==max pin) overrides a manualshutter_min.MUT_LIVE, both backends. - Added
isp.gainMin/isp.shutterMinUscamelCase aliases.
- Added
-
0.12.1(additive — new config field):- Added
isp.shutter_rule_180(boolean, defaultfalse) to config schema. Whentrue, pins exposure to exactly 1/(2×fps) — setsminShutterUs == maxShutterUsin the ISP exposure limit so the AE shutter is locked while gain still auto-adjusts. The supervisory cus3a thread continuously enforces the pin.MUT_RESTART. Both backends. - Added
isp.shutterRule180camelCase alias (isp.shutter_rule_180).
- Added
-
0.12.0(additive — new endpoints):GET /api/v1/attitude— live fused attitude snapshot (Star6E only;{"valid":false}untilattitude.enabled+imu.enabledare on).GET /api/v1/attitude/calibrate_level— level-pose boresight calibration: averages the level-pose accel (ODR-independent sample window), solves and persistsattitude.trimRollDeg/trimPitchDeg, returns them withrestartRequired:true. 409 when the IMU is off or gravity is implausible; 501 on Maruko.- New
attitude.*config section (0.39.1/0.40.0):enabled,axisFwd,axisDown,trimRollDeg,trimPitchDeg,mountDeg,invertRoll,invertPitch— all restart-required.
-
0.11.0(breaking — field removed):- Removed
video0.frameLost(and itsframe_lostcanonical / alias). The SDK VENC frame-lost strategy it drove is gone: on Star6E (i6e) it never fired as a bandwidth throttle — device tests showed a threshold set to ⅛ of the CBR target dropped zero frames — and thepskipvariant returnsE_MI_ERR_NOT_SUPPORT. It only ever acted as an I-frame overshoot guard that the CBR rate controller already covers. Bandwidth backpressure is driven byvideo0.bitrate(smooth) andvideo0.fps(temporal); a client that still sendsvideo0.frameLost/frameLostMode/frameLostThreshold/frameLostGapnow gets an unknown-field error instead of a silent accept.
- Removed
-
0.10.1(additive, no version bump):- Re-exposed
video0.stab_crop_pct+video0.stab_recenter_speed(aliasesstabCropPct/stabRecenterSpeed, bothrestart_required) as advanced overrides of thestabpreset. Read after preset expansion soframing=stabalone keeps 80/180; explicit values win.0/0= stick-to-patch (demo). Inert underoff/zoom-*. video0.framinggainedzoom-3x(1080p → 640×352) andzoom-4x(480×256) digital-zoom presets. Additive enum extension; existing values unchanged. Approach-C still shrinks crop+output 1:1, so the deep crops are not bound by the SCL ~2× upscale ceiling.video0.framingstabilization collapsed to a singlestabpreset (was the never-shippedlow/medium/high). Those names are now unknown values that fall back tooffon load;SETacceptsstab. There is no settablezoom_pct/zoomPct— the preset is the only knob.GET /api/v1/dual/statusalways returns200now. When dual VENC is not active the body is{"ok":true,"data":{"active":false}}instead of the previous404+not_activeerror envelope./dual/setand/dual/idrkeep the404+not_activesemantics — those are write endpoints that need a live ch1 to operate on.- Maruko:
/api/v1/dual/{status,idr}now actually reflect the live dual VENC state. Before this version Maruko started chn 1 whenrecord.mode = "dual"or"dual-stream"but never registered the handle with the HTTP API, so all three endpoints returned404even when dual was running. Star6E behaviour unchanged. /api/v1/dual/setreturns501on Maruko (was: silent 404). Star6E behaviour unchanged. See "Backend Support Matrix".
- Re-exposed
-
0.10.0:- Added digital zoom fields:
video0.zoom_pct(zoomPctalias, restart-required) plus live pan fieldsvideo0.zoom_x/video0.zoom_y(zoomX/zoomYaliases). - Added validation for zoom API writes:
zoom_pctmust be0.0or[0.25, 1.0];zoom_x/ymust be finite values in[0.0, 1.0]. - Updated WebUI-facing field metadata examples for intra refresh and zoom.
- Corrected the persistence note: accepted
/api/v1/setwrites have been persisted since v0.7.8.
- Added digital zoom fields:
-
0.8.4:GET /api/v1/record/statusnow reflects daemon-config-driven recording on Maruko (mirror/dual): previously the response was zero-fill (active:false, all counters 0) even when a TS file was being written. The Maruko runtime now registers a status callback against the sameStar6eTsRecorderStatethe recorder uses. No schema change.GET /api/v1/aeon Maruko now includesruntime.active_precrop, matching Star6E. The precrop was already being reported via/api/v1/config; only the AE response was missing it.- Internal (no API surface change): the
/api/v1/record/start|stop501 gate now keys off an explicitvenc_api_set_record_http_control_supported(true)opt-in instead of the status-callback presence. This decoupling is what allowed Maruko to add status visibility without accidentally re-enabling the HTTP-driven control endpoints (which it still doesn't consume).
-
0.8.3:- Added
GET /api/v1/audio/status— live observability for the audio capture/encode pipeline (lib loaded, capture running, codec, rate, channels, Opus encoder available). Available on both backends; returns501when the backend has no audio observability hook. GET /api/v1/record/startandGET /api/v1/record/stopnow return501 not_implementedon backends without a runtime record poll (currently only Maruko). Previously the requests appeared to succeed with{"ok":true}but did nothing. Star6E behaviour is unchanged.- Documented three pre-existing routes that had landed in code without
contract entries:
GET /api/v1/modes(sensor pad/mode introspection),GET /api/v1/transport/status(output transport observability), andGET /api/v1/idr/stats(per-channel IDR rate-limit counters). No behavioural change. - Added a Backend Support Matrix table covering Star6E vs Maruko
divergence post-Phase-5 (audio), Phase-6 (recording), Phase-7 (dual
VENC), and Phase-9 (
isp.aeMode). - In-binary
/api/v1/versionnow reportscontract_version=0.8.3(previously the constant was stuck at0.3.0while the doc moved forward to0.8.2).
- Added
-
0.8.2:outgoing.max_payload_sizeis nowMUT_LIVE(wasMUT_RESTART) and can be batched with other live fields in a single/api/v1/setcall, e.g.?video0.bitrate=8000&outgoing.maxPayloadSize=4000.- Validation range tightened to
[576, 4000](boot will refuse a config outside that range). - SHM ring slot is sized at startup to fit the validated ceiling
(4000 + 12 RTP header = 4012 bytes per slot, 8-byte aligned), so
shm://accepts the full live range with no restart-to-grow caveat, matchingudp://andunix://behavior. Costs ~1.3 MiB extra SHM per ring.
-
0.6.3:- Added
GET /api/v1/recordings— list files with size/mtime plusfree_bytes/total_bytesfor the configuredrecord.dir. - Added
GET /api/v1/recordings/download?file=<name>— stream a recording as an attachment download. - Added
GET /api/v1/recordings/delete?file=<name>— delete a file; refuses the currently-active recording with409 record_active. - New error code
record_active(409) for actions blocked while recording. - Browser UI for the above endpoints lives in the
Recordingstab on the dashboard at/; there is no separate HTML route.
- Added
-
0.6.2:- Added
isp.keepAspect(boolean, defaulttrue) to config schema. Whenfalse, VIF captures the full sensor area and VPE scales without aspect-ratio cropping (image is stretched if sensor and encode AR differ).MUT_RESTART— applied on SIGHUP / Save & Restart. Star6E only; Maruko reads but ignores the field until SCL crop port lands as a follow-up. - Added
isp.keepAspectcamelCase alias (isp.keep_aspect). GET /api/v1/configresponse gains aruntimeblock withactive_precrop({x,y,w,h}) — the VIF crop currently programmed (includes any sensor overscan offsets). Omitted when the pipeline has not started or after stop. Available on both backends.GET /api/v1/aeStar6E response includesruntime.active_precropwith the same rectangle.
- Added
-
0.5.0:- Added
GET /api/v1/iq— query all ISP IQ parameter values (46 params). - Added
GET /api/v1/iq/set?param=value— set individual IQ parameters live. - Always enabled on Star6E (no config toggle needed — zero runtime overhead).
- Params cover image quality, noise reduction, corrections, dynamic range, lens calibration, LUT enables, and ISP mode controls.
- Star6E: 45/46 symbols resolved, Maruko returns 501.
- Added
-
0.4.0:- Added
GET /api/v1/dual/status— query secondary VENC channel state. - Added
GET /api/v1/dual/set?bitrate=N— live ch1 bitrate change. - Added
GET /api/v1/dual/set?gop=N— live ch1 GOP change (in seconds). - Added
GET /api/v1/dual/idr— request IDR on secondary channel. - All dual endpoints return 404 when dual VENC is not active.
- Config
recordsection expanded:mode("off"/"mirror"/"dual"/"dual-stream"),bitrate,fps,gopSizefor ch1 config,serverfor dual-stream.
- Added
-
0.3.0:- Added
GET /api/v1/record/start— start SD card recording (optional?dir=). - Added
GET /api/v1/record/stop— stop SD card recording. - Added
GET /api/v1/record/status— query recording status (active, format, bytes, segments, stop_reason). - Config
recordsection expanded:format("hevc" or "ts"),maxSeconds,maxMB. - MPEG-TS muxer: HEVC video + PCM audio in power-loss safe container.
- File rotation at IDR boundaries by time (default 300s) or size (default 500MB).
- RTP streaming and recording operate concurrently.
- Added
-
0.2.3:- Added
GET /api/v1/aefor live AE diagnostics. - Added
GET /api/v1/awbfor live AWB diagnostics. - Added
GET /metrics/ispfor compact ISP metrics export. - Added Majestic-compatible
GET /api/v1/config.jsonalias. - Added
GET /api/v1/fps/configfor configured FPS queries. - Added
GET /api/v1/fps/livefor live/applied FPS queries. - Added support for selected Majestic-style camelCase field aliases on
GET /api/v1/getandGET /api/v1/set.
- Added
-
0.2.1:outgoing.max_payload_sizenow applies to RTP mode (was only used by compact mode). Default 850. Set to 0 to disable adaptive sizing.
-
0.2.0:- Added
outgoing.enabled(MUT_LIVE): enable/disable UDP output with FPS idle. - Added
outgoing.serverchanged from MUT_RESTART to MUT_LIVE: live destination redirect. - Added
outgoing.streamMode(MUT_RESTART): explicit stream mode selection. - Added
outgoing.connectedUdp(MUT_RESTART): connected UDP error reporting. - IDR keyframe issued on output enable, destination change, and bitrate change.
- Server URIs now accept
udp://,unix://, andshm://.
- Added
-
0.1.3:- Documented live FPS control behavior (hardware bind decimation, clamping, mode switching limitation).
video0.fpsset via API now uses MI_SYS_BindChnPort2 rebind instead of /proc write.- Removed
isp.exposureconfig field, capability, and Prometheus metric. Auto-cap to frame period (1/fps) is now the only exposure mode. - Changed
video0.sizedefault from"1920x1080"to"auto"(use sensor native resolution). Added"auto"preset to size parser. - Removed
"4MP"size preset (sensor-specific, not a standard resolution).
-
0.1.2:- Updated to reflect actual implemented API (was draft, now active).
- All endpoints use GET method (BusyBox wget compatibility).
- Documented query parameter format:
?field_namefor get,?field_name=valuefor set. - Added
/api/v1/restartendpoint (replaces plannedPOST /api/v1/actions/restart). - Added
/request/idrendpoint. - Removed unimplemented
PUT /api/v1/configandPATCH /api/v1/config(future work). - Added curl examples for all endpoints.
- Added SIGHUP reinit documentation.
- Added safety notes (in-memory only, codec restriction).
-
0.1.1:- Updated examples to use
video.capture_resolutionrestart semantics.
- Updated examples to use
-
0.1.0:- Initial draft contract and endpoint definitions.
/api/v1/iq on CV610 is a second, structurally different form from the
Star6E/Maruko one documented above. A client written against that shape reads
data["contrast"].value and gets undefined on CV610 — branch on
data._schema being present, not on backend name.
{"ok":true,"data":{
"_schema":[{"name":"saturation","fields":[
{"name":"manual.saturation","count":1,"min":0,"max":255,"domain":"manual"}]}],
"saturation":{"ret":0,"fields":{"op_type":0,"manual.saturation":128}},
"module_ctrl":{"ret":0,"bypass":{"drc":1,"dehaze":1}}}}- Keyed by ISP group, not by parameter. Each group has
ret(the MPI return, 0 on success) andfields, whose keys are dotted field names. - No
value,enabled,op_typeoravailableat group level, and no_diagblock. _schemadescribes the whole surface — name, elementcount,min/max, anddomain(direct|manual|auto). The WebUI renders from it, so the field table lives only in the backend.domainis load-bearing: writing amanual.*orauto.*field also selects thatop_type, because a value in the other half is ignored by the ISP and an ignored write is indistinguishable from a broken setter.module_ctrlis read-only and reports which ISP blocks the hardware is bypassing.- Values out of a field's declared range are rejected, not clamped, so the
value echoed by
/api/v1/iq/setis the value applied.