Repository navigation
Configuration Reference
timps reads a single flat key = value file (default /etc/timps.conf,
overridable with -c <path>) — see timps.conf.example /
scripts/camera.conf for worked examples. There is no INI-style [section]
syntax; the "section" is simply the dotted prefix of the key (image.,
audio., video0., ...).
This page documents every user-facing key recognized by src/config.c,
grouped by section, with type, default, valid range, and — importantly —
whether the key can be changed live via POST /control (see
HTTP /control API Reference) or is persist-only
(the value is stored/echoed but only takes effect the next time timpsd
restarts).
| Value | Meaning |
|---|---|
| Live | A /control POST applies the value to the running pipeline immediately (subject to per-SoC hardware support — see Platform & SDK Support) and persists it to the config file. |
| Live (next cycle) | Not applied via a direct HAL call, but the owning thread (recorder, timelapse, day/night) re-reads the live in-memory config on every loop iteration, so the change takes effect within a few hundred ms to a few seconds without a restart. |
| Restart-only | The key is only read once at startup (or at ISP/encoder init). A /control POST for one of these still updates the in-memory config and persists it to the file (when the section accepts POSTs at all — see below), but the running daemon keeps behaving as it did at boot; the new value takes effect on the next timpsd restart. |
| File-only | The key has no /control POST path at all — it can only be changed by editing the config file (or via an offline tool that calls config_write_keys()) and restarting. Some of these are still included in GET /control's read-only dump; others are not readable back at all (noted individually). |
This classification is derived directly from src/control.c's
control_apply_json() (which sections/keys it accepts) and
src/config.c's descriptor tables (which keys GET /control/change-detection
can read back) — not guessed from names or comments.
Two important caveats:
-
image.*live-apply is per-SoC: everyimage.*key is accepted live regardless of platform, but the HAL only issues the underlyingIMP_ISP_Tuning_*call when that platform's SDK actually exposes it (see the per-key notes below andcaps.imageinGET /control). On an unsupported platform the value is stored/echoed but has no visible effect. -
videoN.rtsp_pathis a documented exception:GET /control'scaps.restartarray lists the wholevideosection as restart-required, butconfig.hexplicitly carves it out (and since v1.9.20 the POST reply no longer lists it underdeferred_keys): RTSPDESCRIBEmatches the request path against the live config on every request, not the boot snapshot, so a/controledit tortsp_pathactually changes which URL serves that stream immediately, with no restart. Every othervideo.N.*key (codec/dimensions/fps/bitrate/...) genuinely needs a restart because the encoder/FrameSource pipeline is never reconfigured live.
Restart-only; the sensor is probed and configured once at ISP init
(config_sensor_finalize()). Not reachable via /control at all (no
"sensor" handling in control_apply_json beyond being listed for
completeness — actually it is accepted for persistence, see below).
Note: config_sensor_finalize() auto-detects unset fields from
/proc/jz/sensor/sensor0/* and a config value that disagrees with the
loaded kernel sensor driver is overridden (with a warning) rather than
trusted, because a mismatching name/i2c address would crash the ISP driver.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
sensor.model |
string | (unset → auto-detected, fallback gc2053) |
— | Restart-only | Sensor driver name; auto-filled from /proc/jz/sensor/sensor0/name if present. |
sensor.i2c_addr (alias i2c_address) |
int | (unset → auto-detected, fallback 0x37) |
0–0x7F | Restart-only | Sensor I2C address. |
sensor.fps |
int | (unset → driver max_fps, capped at 30 or the fastest enabled video<N>.fps if higher; else video0.fps, else 25) |
0–120 | Restart-only | Sensor capture frame rate, applied once at ISP init. The auto value is capped at 30, or at the fastest enabled video<N>.fps if that is higher; set it explicitly to go higher. The log line sensor fps: requested N, driver holds n/d shows what the driver accepted. Not the same as video<N>.fps. |
sensor.width |
int | (unset → auto/video0.width, fallback 1920) | 0–8192 | Restart-only | Sensor native width. |
sensor.height |
int | (unset → auto/video0.height, fallback 1080) | 0–8192 | Restart-only | Sensor native height. |
sensor.i2c_adapter (alias i2c_adapter_id) |
int | -1 (unset → 0) | -1–15 | File-only | T40/T41 only. I2C adapter the sensor is wired to. |
sensor.mclk |
int | -1 (unset → 0) | -1–2 | File-only | T40/T41 only. Sensor clock source, MCLK0/1/2. |
sensor.video_interface |
int | -1 (unset → 0) | -1–2 | File-only | T40/T41 only. 0 = MIPI CSI0, 1 = MIPI CSI1 (T40 only, ignored with a WARN on T41), 2 = DVP. |
sensor.default_boot |
int | -1 (unset → 0) | -1–15 | File-only | T40/T41 only. Sensor driver boot setting index. |
sensor.rst_gpio |
int | -1 (none) | -1–255 | File-only | T40/T41 only. Sensor reset GPIO number. |
sensor.pwdn_gpio |
int | -1 (none) | -1–255 | File-only | T40/T41 only. Sensor power-down GPIO number. |
sensor.* is accepted by POST /control ({"sensor":{...}}) for
persistence — it is listed in caps.restart alongside video, and a
changed key comes back in the POST reply's deferred_keys — but has
no live-apply path; the value is written to the config file and takes
effect at the next ISP init. The six board-wiring keys
(i2c_adapter … pwdn_gpio) are the exception: not POST-able, edit
/etc/timps.conf and restart. Use them when IMP_ISP_AddSensor fails because
the board wires the sensor off I2C adapter 0 / MCLK0 / CSI0 (e.g. Eufy S350:
i2c_adapter = 1, mclk = 1, default_boot = 4, rst_gpio = 91). An
invalid value is logged and dropped; other SoCs ignore them with a warning.
All keys live-apply, gated per-SoC (see caps.image in GET /control
and the ISP-capability column below; the underlying macros live in
src/isp_caps.h). A value the platform can't apply is still stored and
echoed back — it just has no visible effect until/unless a firmware update
adds support. image.running_mode is additionally special: re-posting the
same value still re-drives the ISP (skipping the flash write) because it
is treated as a hardware-sync command, not just stored state — see
Day/Night for why, including the fs_kick_chn0()
latch-kick this triggers.
| Key | Type | Default | Range | Live? | ISP capability gate | Description |
|---|---|---|---|---|---|---|
image.brightness |
int | 128 | 0–255 | Live | all SoCs | ISP brightness. |
image.contrast |
int | 128 | 0–255 | Live | all SoCs | ISP contrast. |
image.saturation |
int | 128 | 0–255 | Live | all SoCs | ISP saturation. |
image.sharpness |
int | 128 | 0–255 | Live | all SoCs | ISP sharpness. |
image.hue |
int | 128 | 0–255 | Live |
ISP_HAS_HUE: T23/T31/T40/T41/C100 |
Hue (SetBcshHue). |
image.hflip |
bool | 0 | 0/1 | Live | all SoCs (via SetHVFLIP on T40/T41) |
Horizontal flip. Global (all streams). Use with vflip for a 180° flip on SoCs without a rotation path. |
image.vflip |
bool | 0 | 0/1 | Live | all SoCs | Vertical flip. Global (all streams). |
image.running_mode |
int | 0 | 0 = day, 1 = night | Live | all SoCs | ISP day/night running mode. Normally driven by the Day/Night thread's switch script, not set directly by a user. |
image.anti_flicker |
int | 1 (2 up to 1.9.33) | 0 = off, 1 = 50 Hz, 2 = 60 Hz | Live | all SoCs | Anti-flicker (banding) compensation. Set it to your mains frequency, see below. |
image.ae_compensation |
int | 128 | 0–255 | Live |
ISP_HAS_AECOMP: all except T21 and T40; T41 only with OpenIMP (since 1.9.33) |
AE (auto-exposure) compensation/EV bias. |
image.max_again |
int | 160 | 0–255 | Live |
ISP_HAS_GAINS: all except T40/T41 |
Max analog gain ceiling. |
image.max_dgain |
int | 80 | 0–255 | Live | ISP_HAS_GAINS |
Max digital gain ceiling. |
image.sinter_strength |
int | 128 | 0–255 | Live |
ISP_HAS_NR: all except T40; T41 only with OpenIMP (since 1.9.33) |
Spatial noise reduction strength. |
image.temper_strength |
int | 128 | 0–255 | Live |
ISP_HAS_NR: all except T40; T41 only with OpenIMP (since v1.10.0) |
Temporal noise reduction strength; on T41 visible only at high gain. |
image.dpc_strength |
int | 128 | 0–255 | Live |
ISP_HAS_DPC: T23/T31/C100; OpenIMP also T10/T20/T21 and T41 (since v1.11.0) |
Defective pixel correction strength. |
image.defog_strength |
int | 128 | 0–255 | Live |
ISP_HAS_DEFOG: T23/T31/C100; OpenIMP also T20 and T41 (since v1.11.0) |
Defog strength. |
image.drc_strength |
int | 128 | 0–255 | Live |
ISP_HAS_DRC (WDR): T21/T23/T31/C100; OpenIMP also T20 and T41 (since v1.11.0) |
Dynamic range compression (WDR) strength. |
image.highlight_depress |
int | 0 | 0–10 | Live |
ISP_HAS_HILIGHT: all except T40/T41 |
Highlight suppression. |
image.colorfx |
int | 0 | 0–9 | Live | Colour effect (0 none, 1 B/W, 2 sepia on T20, 3 negative, 9 vivid). Only with an OpenIMP build (USE_OPENIMP); not in caps.image otherwise. |
|
image.scene |
int | 0 | 0–14 | Live | Scene preset (SDK IMPISPSceneMode). Only with an OpenIMP build (USE_OPENIMP). |
|
image.backlight_compensation |
int | 0 | 0–10 | Live |
ISP_HAS_BACKLIGHT: T23/T31/C100 |
Backlight compensation. |
image.core_wb_mode |
int | 0 | 0–9 (0–8 on T10/T20/T30) | Live |
ISP_HAS_WB: all except T40/T41 |
White balance mode: 0 auto, 1 manual, 2 daylight, 3 cloudy, 4 incandescent, 5 fluorescent, 6 twilight, 7 shade, 8 warm fluorescent, 9 custom. |
image.wb_rgain |
int | 0 | 0–255 (T10/T20/T30), 0–4095 (T21/T23/T31), 0–65535 elsewhere | Live | ISP_HAS_WB |
Manual WB red gain (used when core_wb_mode selects manual). The scale differs per SoC: 128 = 1.0 on T10/T20/T30, 256 = 1.0 on T21/T23/T31. The per-SoC clamp is new in v1.10.0 (up to 1.9.33: 0–65535 everywhere, T10/T20/T30 wrapped values above 255). |
image.wb_bgain |
int | 0 | same as wb_rgain
|
Live | ISP_HAS_WB |
Manual WB blue gain. Since v1.9.21, GET /control reports the gains in effect as read-only image.wb_live {"rgain","bgain"}, a start value for manual mode that matches auto. |
Set it to the mains frequency where the camera hangs:
-
1(50 Hz, the default since v1.10.0): Europe, most of Asia and Africa, Australia. -
2(60 Hz): most of the Americas, South Korea, Taiwan, the Philippines, Saudi Arabia, western Japan (eastern Japan is 50 Hz).
Lamps flicker at twice the mains frequency (100 or 120 Hz). Anti-flicker
quantises the AE's exposure time to whole half-periods (10 ms at 50 Hz,
8.33 ms at 60 Hz), so every exposure collects the same light whatever the
phase. The wrong value steps the exposure in the wrong unit and shows as
horizontal bands under LED or fluorescent light, rolling at the beat between
the light and the frame rate (5 Hz at 15 fps on 50 Hz mains). A T23 sc2336p
on 50 Hz mains measured 1.15 % RMS row ripple at 2 and 0.09 % at 1.
Up to 1.9.33 the default was 2 (60 Hz, the vendor SDK convention that
prudynt and raptor also follow), and thingino's shipped /etc/timps.conf
writes the key explicitly. An upgraded camera whose config was ever saved
therefore stays on 2. A fresh flash, a factory reset, a config that was
never saved (it still comes from the image) or one without the key gets 1.
In a 60 Hz region, set 2 after such an upgrade or reset:
curl -s -X POST http://127.0.0.1:8880/control -d '{"image":{"anti_flicker":2}}'It applies live and persists to /etc/timps.conf.
Anti-flicker puts a floor under the exposure. Use 0 (off) for a very bright
close-up scene whose exposure visibly pulses: there the AE needs an exposure
below the first node, and it oscillates between honouring the node
(overexposed) and breaking it.
At sensor.fps = 30 the GC2053 T31 driver publishes a 25 fps line model at
probe time, so the ISP scales the nodes by 1.2× and neither 1 nor 2 lands
on a true mains period; at sensor.fps = 25 the model is consistent and both
settings are correct.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
fcrop_enable |
bool | 0 | 0/1 |
Live where caps.fcrop.live; restart on T23 (OpenIMP) without the driver's fcrop_upscale_pct parameter |
ISP FrontCrop ("zoom"): only the window below reaches the streams; each stream scales it to its own size. |
fcrop_x |
int | 0 | 0–8192 | as above | Left edge, sensor-sized picture pixels (unrotated, as shown with image.hflip/vflip; OpenIMP T21/T23/T31 mirror x and y for the flips, T10/T20 only y). |
fcrop_y |
int | 0 | 0–8192 | as above | Top edge. |
fcrop_w |
int | 0 | 0–8192 | as above | Width; at least 64. |
fcrop_h |
int | 0 | 0–8192 | as above | Height; at least 64. |
fcrop_upscale_pct |
int | -1 | -1–caps.fcrop.upscale_max (100) |
Live |
Since v1.11.0, T23/T31 OpenIMP with the driver parameter only. Writes the open-tx-isp fcrop_upscale_pct so the scaler may upscale the window by up to this many percent per axis (100 = a window down to half the stream size). -1 = leave the driver's value. Never above 100: T31 at 150 hard-hangs. Elsewhere a POST answers unsupported and a file value is ignored with one WARN. |
x/y/w/h follow the FrameSource crop convention (left, top); the SDK struct
is ordered top, left and the HAL maps it. timps evens the values, moves the
window inside the sensor and refuses anything under 64×64; the SDK then
refuses a window no stream can show (-EINVAL: caps.fcrop.state =
rejected, one WARN; any other error: failed). Keep the stream aspect ratio
or the picture is stretched. Device-tested (by the OpenIMP maintainer) on T10,
T20 and T21 with OpenIMP — on T20 only the downscaled streams show the window.
T23 and T31 with OpenIMP are effective and live when the driver has the
open-tx-isp fcrop_upscale_pct parameter (checked at start; open-tx-isp
b15ef235 has it). Without it T31 is experimental and T23 experimental and
start-only: a POST persists and applies at the next /etc/init.d/S95timps restart; it needs open-tx-isp 3cf9bdac or newer (older drivers hung the
pipeline on a crop change). A window smaller than a stream that is not scaled
down is refused unless fcrop_upscale_pct allows it; lowering that key with
such a window active returns to the full sensor. Vendor T23 refuses the keys. See
Platform & SDK Support for
all SoCs. Privacy masks, OSD and ROI regions stay in stream-frame coordinates:
the scene moves under them.
Split between live-applicable knobs (real-time DSP/gain calls) and
persist-only attribute keys (codec/format/pipeline-init settings that
IMP_AI_SetPubAttr/encoder init need at bring-up time and cannot change
on a running capture channel). The Restart-only keys are listed in GET /control's
caps.restart as audio.<key>, and since v1.9.20 a changed one comes back
in the POST reply's deferred_keys.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
audio.volume |
int | 80 | 0–100 | Live | Mic capture volume (IMP_AI_SetVol). |
audio.gain |
int | 25 | 0–31 | Live | Mic capture gain (IMP_AI_SetGain). |
audio.alc_gain |
int | 0 | 0–7 |
Live on T21/T31/C100 (AUDIO_HAS_ALC_GAIN); accepted/persisted elsewhere with no effect |
Analog PGA gain level (IMP_AI_SetAlcGain). |
audio.mute |
bool | 0 | 0/1 | Live | Live mic mute: captured frames are dropped before the encoder/hub (works on every SoC — it's a publish-gate in timps, not an SDK call). |
audio.spk_volume |
int | 80 | 0–100 |
Live (only if USE_PLAY or USE_BACKCHANNEL compiled in) |
Speaker (IMP_AO) output volume — applies to whichever producer (backchannel/play queue) currently holds the AO device, and persists as the default for the next one. |
audio.spk_gain |
int | 25 | 0–100 (effective 0–31: IMP_AO_SetGain's range, −39 dB … +6 dB in 1.5 dB steps; higher values are clamped to 31) |
Live (same gate as spk_volume) |
Speaker output gain. |
audio.enabled |
bool | 1 | 0/1 | Restart-only | Master audio capture enable. |
audio.codec |
enum | aac |
aac|pcmu|pcma|opus|none (aliases g711u/ulaw→pcmu, g711a/alaw→pcma, off→none) |
Restart-only | Audio codec. AAC needs USE_FAAC; opus needs USE_STREAM_OPUS (RTSP-only, RFC 7587 — see Audio/Streaming Protocols) and is only an accepted token on such a build. Falls back to AAC at parse time for any unrecognized token. |
audio.codec2 |
enum |
pcmu on USE_WEBRTC builds, none otherwise |
pcmu|off/none
|
Restart-only | Optional second, independent G.711u encode of the same captured PCM, published on its own hub source. It exists for WebRTC/WHEP, which carries G.711 and nothing else: without it a camera on the default audio.codec = aac gets a video-only WHEP session. RTSP, the fMP4 preview and recordings keep the AAC primary untouched. Ignored (and not encoded) when audio.codec is already pcmu/pcma. Needs an 8 kHz or 16 kHz capture (16 kHz is filtered and halved to G.711's 8 kHz clock); any other audio.samplerate logs a warning and disables the second encode. |
audio.samplerate |
int | 16000 | 8000–96000 | Restart-only | Capture sample rate in Hz. G.711 is pinned to 8000 Hz regardless of this value (see Audio). |
audio.channels |
int | 1 | 1–2 | Restart-only | 1 = mono (native). 2 = "simulated stereo" — the mono mic duplicated to L=R, AAC only. |
audio.bitrate |
int | 32 | 8–320 (kbps) | Restart-only | AAC encode bitrate. |
audio.high_pass |
bool | 1 | 0/1 | Restart-only | High-pass filter. Not live: libimp runs HPF/AGC/NS on its own record thread and frees state unlocked — a live toggle would race the vendor thread (use-after-free risk), so this is restart-required by design, not merely unimplemented. |
audio.agc |
bool | 0 | 0/1 | Restart-only | Automatic gain control on/off (same race-avoidance rationale as high_pass). |
audio.agc_target_dbfs |
int | 10 | 0–31 | Restart-only | AGC target level (dBFS). |
audio.agc_compression_db |
int | 0 | 0–90 | Restart-only | AGC compression gain (dB). |
audio.ns |
int | 0 | 0–3 | Restart-only | Noise suppression level (0 = off). |
audio.force_stereo |
bool | 0 | 0/1 | Restart-only | Separate companion field to channels=2 (not an alias — both are settable independently). |
audio.spk_enabled |
bool | 1 | 0/1 |
Live (at the next AO open; USE_PLAY/USE_BACKCHANNEL builds) |
Master speaker-output enable. Read by speaker.c every time it is about to open the IMP_AO device, so 0 silences the next backchannel/play session without a restart; a session already holding the speaker keeps it until it ends. |
audio.backchannel |
bool | 0 | 0/1 | Restart-only | Enable the ONVIF two-way audio backchannel (USE_BACKCHANNEL build). |
audio.backchannel_codec |
enum |
pcmu (0) |
pcmu(0)|pcma(1)|aac(2) |
Restart-only | Advertised/accepted backchannel codec. |
audio.backchannel_rate |
int | 16000 | 8000–48000 | Restart-only | Speaker sample rate fed by the backchannel decoder. |
audio.talk_ws |
tri-state | 0 | 0|1|2 (true/on/yes → 1) |
Live (next /talk request; the backchannel it rides on is still restart-only) |
Serve the browser push-to-talk WebSocket at /talk (USE_BC_WS builds, and only when the backchannel itself came up at boot). httpd.c reads the value on every /talk upgrade request, so a /control POST changes the answer for the next connection immediately — including switching from 1 to 2, which drops the TLS requirement; treat it with the same care as a hand edit. 0 = not served; 1 = served over TLS only, a plaintext port answers 426; 2 = also served over plain ws://, a deliberate hand edit since the microphone audio and its token then cross the network in the clear. GET /control reports the resolved verdict as caps.backchannel.talk_ws (1 on a plaintext port resolves to 0). See Audio. |
audio.aec |
bool | 0 | 0/1 |
Not live — persists now, engages at the next AO open (only if USE_PLAY or USE_BACKCHANNEL compiled in) |
Opt-in Acoustic Echo Cancellation (IMP_AI_EnableAec) for the backchannel — subtracts the speaker output from the mic capture. Engages only once both AI capture and AO output are actually live, applied at the next AO open (same timing contract as spk_volume/spk_gain). Off by default since AEC quality/latency varies per SoC/mic/speaker pairing. |
See Audio for the backchannel/play-queue feature details.
Entirely file-only: there is no "jpeg" object in the /control POST
schema, so these keys can only be changed by editing the config file and
restarting. (Note this is distinct from the per-stream piggyback JPEG
encoder configured under video.N.jpeg*, below — see
Streaming Protocols for how the two JPEG sources
relate.)
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
jpeg.enabled |
bool | 0 | 0/1 | File-only | Enable the dedicated JPEG encoder channel (independent FrameSource). |
jpeg.width |
int | 640 | 64–4096 | File-only | JPEG channel width. |
jpeg.height |
int | 360 | 64–4096 | File-only | JPEG channel height. |
jpeg.quality |
int | 75 | 1–100 | File-only | JPEG quality. |
jpeg.fps |
int | 5 | 1–120 | File-only | Max snapshot/MJPEG publish rate for this channel. |
jpeg.imp_chn |
int | 2 | 0–7 | File-only | IMP encoder channel number (internal). (since v1.9.28) a value that collides with an enabled video stream's channel is moved to the lowest free channel at load, with an [ERR] line. |
jpeg.snapshot_path |
string | "" |
— | File-only | Optional path to periodically write the latest JPEG to disk ("" = disabled). |
File-only (no "rtsp" object accepted by /control). See
Streaming Protocols for the RTSP server's
runtime behavior.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
rtsp.enabled |
bool | 1 | 0/1 | File-only | Enable the RTSP server. |
rtsp.port |
int | 554 | 1–65535 | File-only | RTSP listener port. |
rtsp.mtu |
int | 1200 | 548–1472 | File-only | Max RTP packet size (header+payload) for UDP transport; 1200 leaves headroom for WireGuard/OpenVPN/PPPoE/IPv6 tunnel overhead — raise to ~1400 on LAN-only setups. |
rtsp.user (alias username) |
string | "" |
— | File-only | RTSP username. Empty = RTSP auth disabled. |
rtsp.pass (alias password) |
string | "" |
— | File-only | RTSP password. |
rtsp.tls (alias tls_enabled) |
bool | 0 | 0/1 | File-only | Also run an RTSPS (TLS) listener (USE_TLS builds). |
rtsp.tls_port |
int | 322 | 1–65535 | File-only | RTSPS port (RFC 7826 Annex C well-known port). |
File-only. USE_SRT builds only (see Streaming Protocols).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
srt.enabled |
bool | 0 | 0/1 | File-only | Enable the SRT output. |
srt.mode |
string | listener |
listener|caller
|
File-only |
listener waits for receivers to dial in; caller makes the camera dial srt.host:srt.port itself and reconnect forever with a doubling 1→30 s backoff (for cameras behind NAT). An unrecognized value warns and falls back to listener. |
srt.host |
string | "" |
— | File-only | Caller mode only: the receiver to dial. srt.mode=caller with this empty is a startup error and SRT stays off (it is not silently downgraded to a listener). Ignored in listener mode. |
srt.port |
int | 9000 | 1–65535 | File-only | Listener: local bind port. Caller: the remote port dialled on srt.host. |
srt.channel |
int | 0 | — | File-only | Video stream served over SRT. |
srt.latency_ms (alias latency) |
int | 120 | — | File-only | SRT latency (ms). |
srt.streamid |
string | "" |
— | File-only | Listener: required STREAMID for a connecting client ("" = not enforced). Caller: the STREAMID timps presents to the receiver. |
srt.passphrase |
string | "" |
— | File-only | AES passphrase for SRT encryption (10–79 chars if set; an invalid passphrase stops SRT — the listener never binds, the caller stops dialling — rather than running unencrypted). srt.streamid is plaintext, so without a passphrase a listener serves video to anyone regardless of rtsp.user/http.user; (since v1.9.28) that combination logs SRT port N has NO access control - set srt.passphrase (10-79 chars) at start. |
File-only. See HTTP /control API Reference and Streaming Protocols.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
http.enabled |
bool | 1 | 0/1 | File-only | Enable the HTTP server. |
http.port |
int | 8880 | 1–65535 | File-only | HTTP listener port. |
http.preview_chn |
int | 1 | — | File-only | Default video stream for the built-in preview page. |
http.adaptive_drop |
bool | 1 | 0/1 | File-only | Per-client adaptive frame dropping on /stream.mp4 when a slow client's own queue backs up (freeze-and-resume-at-keyframe instead of forwarding a corrupt headless GOP). |
http.user (alias username) |
string | "" |
— | File-only | HTTP Basic/Digest username. Falls back to rtsp.user if empty. |
http.pass (alias password) |
string | "" |
— | File-only | HTTP password. Falls back to rtsp.pass if empty. |
http.token |
string | "" |
— | File-only | Optional persistent remote secret token for /control//events/media endpoints (never written to the token file). |
http.token_file |
string | /run/timps.token |
— | File-only | Where the random per-boot token is published (mode 0640); "" disables publishing. |
http.https (alias tls) |
tri-state | 0 | 0/1/2 | File-only | TLS on the HTTP port (USE_TLS builds). 0 = plaintext only. 1 = both schemes on the same port — each connection is classified by its first byte (0x16 = TLS handshake record, any HTTP method starts with a letter), so http:// and https:// both work; this is wider than the pre-v1.9.11 meaning of 1, which was TLS-only. 2 = TLS only: a plaintext request gets 426 Upgrade Required and is closed (the old meaning of 1). Either on-value still fails closed — a cert/key that cannot be loaded means the listener is not bound at all, never a silent downgrade to plaintext. |
http.tls_cert (alias cert) |
string | /etc/ssl/certs/timps.crt |
— | File-only | Certificate, PEM or DER (shared with RTSPS). On thingino, S95timps symlinks this to the WebUI's /etc/ssl/certs/uhttpd.crt when that exists, so both ports present the same cert; otherwise it generates a self-signed pair here. |
http.tls_key (alias key) |
string | /etc/ssl/private/timps.key |
— | File-only | Private key matching http.tls_cert (shared with RTSPS). |
Security — empty credentials expose the media (by design). While both
http.userandrtsp.userare empty (the shipped default), the media endpoints — the RTSP video/audio stream,/snapshot.jpg,/stream.mjpegand/stream.mp4— are reachable by anyone on the network with no authentication./controland/eventsare the exception: they stay loopback-only (non-local requests get403) even with empty credentials, so config/state is never exposed off-device without a token or configured credentials. Setrtsp.user/rtsp.passand/orhttp.user/http.passto require auth for the media too. See HTTP /control API Reference § Empty credentials.
File-only, and the whole section only exists on USE_WEBRTC builds (which
imply USE_TLS + USE_CONTROL). GET /control reports the resolved state as
caps.webrtc. The top-level README.md § "WebRTC / WHEP" carries the media
contract and its deliberate limitations (LAN/VPN only, H.264 + G.711 only, no
NACK/FEC/congestion control, 4 concurrent sessions).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
webrtc.enabled |
tri-state | 2 | 0/1/2 (true/on/yes → 1) |
File-only | Serve POST /webrtc/whep. 0 = the endpoint answers 503. 1 = served, but a plaintext POST on a port that has TLS configured is refused with 426 Upgrade Required (same rule as audio.talk_ws=1) — the offer carries the fingerprint the peer is held to and the answer carries our ICE password, so a rewritable exchange hands over the whole session. 2 = served over either scheme. The default is 2 rather than 1 because most cameras have no http→https redirect, so 1 would silently 426 the feature; a port with no TLS configured at all behaves the same either way. |
webrtc.port |
int | 0 | 0–65535 | File-only | Bottom of the UDP media port range; 0 = one OS-picked ephemeral port per session. Every session binds its own socket, so a fixed value is a range start, not a single port. |
webrtc.port_max |
int | 0 | 0–65535 | File-only | Top of that range. 0 = webrtc.port + WEBRTC_MAX_SESSIONS - 1 (one port per session slot, so no extra configuration is needed). A range narrower than the 4 slots just answers 503 once every port in it is taken. |
webrtc.channel |
int | 0 | 0–(MS_MAX_VSTREAM-1) |
File-only | Which video stream to send when the WHEP POST carries no ?chn=N (a client picks the stream per session with it). It must be H.264 — there is no H.265 or transcoding over WebRTC. Its video<N>.profile decides which browsers can play the result: Chromium accepts any profile, Firefox only Baseline (0). |
Audio over WHEP needs G.711 on the wire: either audio.codec is already
pcmu/pcma, or audio.codec2 = pcmu (the USE_WEBRTC default) arms the
second encode. With neither, the answer rejects the audio m-section
(m=audio 0 …) and the session is video-only.
File-only startup settings (USE_CONTROL builds).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
events.enabled |
bool | 1 | 0/1 | File-only |
0 makes /events answer 404. |
events.stats_ms |
int | 2000 | — | File-only | Period of the stats SSE event; 0 disables it. |
events.max_clients |
int | 8 | — | File-only | Max concurrent /events connections; beyond this, /events answers 503. |
The master switch and the shared OSD infrastructure settings. Per-item
overlay fields are documented separately below (osd<S>.<N>.*).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
osd.enabled |
bool | 1 | 0/1 | Restart-only | Master OSD on/off switch, global across all streams. Settable via /control ({"osd":{"enabled":...}}) and persists, but the OSD groups are only ever built once at startup (imp_osd_setup), so the effect needs a restart. |
osd.monitor_stream |
int | 0 | — | Live | Which stream's measured fps/bitrate feeds the {fps} and {bitrate} placeholders — for every OSD layer, on every stream. Use the numbered {fpsN}/{bitrateN} forms instead when a layer should show its own stream's figures. Settable via /control ({"osd":{"monitor_stream":...}}); read directly off g_cfg on every OSD text refresh, so a POST applies on the next render, no restart needed. |
osd.font_path |
string | /usr/share/fonts/default.ttf |
— | Restart-only | Default TTF font for text items without a per-item font_path override. Settable via /control, same restart-required class as osd.enabled. |
osd.supersample |
int | 2 | 1–4 | Restart-only | TTF rasterizer anti-aliasing quality (samples per axis per pixel); cost scales ~quadratically, 2 is visually close to 4 at typical OSD sizes for roughly a quarter of the CPU cost. Settable via /control, same restart-required class as osd.enabled. |
osd.hinting |
bool | 1 | 0/1 | Restart-only (only if USE_OSD_HINTING compiled in) |
Opt-in lightweight geometric autohint for the TTF rasterizer. The rasterizer (msttf.c) does not execute the font's embedded TrueType hint bytecode (a real hint interpreter is real interpreter-writing work with a real correctness/security surface for an on-device, unsandboxed daemon); at small sizes (e.g. the substream OSD's default 12px) that shows up as visibly uneven stroke widths between glyphs. Enabling this snaps long, near-vertical/near-horizontal outline edges (typical letter stems/serifs) to the pixel grid before rasterizing, which measurably reduces that unevenness — it is a coarse heuristic, not real hinting, and does not preserve the font's authored hint intent. On by default in the runtime config; set it to 0 for byte-for-byte unhinted OSD bitmaps. Verified against the shipped UbuntuMono Regular (/usr/share/fonts/default.ttf); untested against other TTF files if a user swaps osd.font_path. Also gated at COMPILE time by USE_OSD_HINTING (BR2_PACKAGE_TIMPS_OSD_HINTING in the buildroot package, off by default — measured ~2.1KB smaller .text on T31/GCC 16.1.0/-Os when left off): on a build without it, setting this key is accepted but has no effect. Settable via /control, same restart-required class as osd.enabled. |
Any {name} token in an OSD text template that isn't one of the built-in
placeholders (hostname, ip, mac, fps, fpsN, bitrate, bitrateN,
uptime, net/tx,
cpu, mem, clients) is looked up in a fixed file instead - a plain
key = value text file, one entry per line. This lets an external script
drive OSD content with no timps code changes at all: temperature readings,
a doorbell state, whatever.
Since v1.9.28 the path is /tmp/timps_osd.vars, fixed in src/hal/osd_vars.c
(OSD_VARS_FILE) and not configurable - not even file-only via
timps.conf. It used to be (osd.vars_file), but any settable path could
point it at /etc/timps.conf itself and render its credentials into the
video through a {placeholder}.
- Pick a name and use it in the OSD text, e.g.
osd0.4.text = Temp: {room_temp}C. - From any script/cron job with filesystem access, write matching lines to
/tmp/timps_osd.vars:room_temp = 21.5 doorbell = idle - timps re-reads the file on every OSD refresh tick (~1x/s) - no signal or
restart needed. There's no locking, so writing in-place (e.g.
echo ... > file, which truncates then writes) has a small window where a concurrent read can land mid-write: either an empty file (blank placeholder for that tick) or a partially-written value line (a truncated-but-not-garbage value for that tick, e.g.21instead of21.5) - never a crash or memory issue, since reads are line-bounded, just a possible one-tick display glitch that self-corrects on the next read. Avoid it entirely with an atomic replace: write the new content to a temp file in the same directory, thenrename()/mvit onto the target path -rename()is atomic on Linux, so timps always sees either the complete old file or the complete new one, never a partial write.
Limits: each resolved value is capped at 127 bytes, and the whole expanded
text string (all placeholders combined, before strftime() substitution)
at 511 bytes - long enough for typical single-line status text, not a
replacement for a full custom rendering pipeline. Unmatched names (typo, or
/tmp/timps_osd.vars doesn't exist yet) resolve to an empty string rather
than an error, so a missing file just shows blank text for that placeholder.
Canonical form: osd<S>.<N>.<field> where S is the video stream index
(0/1) and N is the item slot (0–7, MS_MAX_OSD). A legacy shared form
osd<N>.<field> (no stream index) is still parsed for backward
compatibility and mirrors the value onto every stream's item N.
Default layout: item 0 = timestamp (top-left), item 1 = {hostname}
(top-center), item 2 = {uptime} (top-right), item 3 = thingino logo
(bottom-right); items 4–7 are unused placeholders (disabled).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
enabled |
bool | item 0–3: 1, items 4–7: 0
|
0/1 | Restart-only | Enabling an item that started disabled has no IMP region to attach to — only takes effect on restart. Disabling a running item also only persists (no live hide). |
type |
enum |
text (item 3: logo) |
text|logo
|
Restart-only | Overlay type. Settable via /control and persists, but imp_osd_apply()'s live re-render dispatch is fixed at region-creation time (rg->is_text), so switching an existing item between text and logo needs a restart to actually change what's drawn (same restart-required reasoning as enabled). |
text |
string(128) | per-item (see layout above) | — | Live (if the item had a region at startup) | Text template: literal text, {placeholder} tokens ({hostname} {ip} {mac} {fps} {fpsN} {bitrate} {bitrateN} {uptime} {net} {cpu} {mem} {clients}), and strftime() codes. Posting "" clears it (a deliberate exception to the usual "empty value refused" rule — see HTTP /control API Reference); null/missing stays refused, since some clients post null for fields they merely didn't intend to touch. |
logo (alias logo_path) |
string(128) |
/usr/share/images/thingino_100x30.bgra (item 3) |
— | File-only | Raw BGRA logo file path. |
logo_w (alias logo_width) |
int | 100 (item 3) | 0–4096 | File-only | Logo width in px. |
logo_h (alias logo_height) |
int | 30 (item 3) | 0–4096 | File-only | Logo height in px. |
x |
int | per-item | -8192–8192 (since v1.9.28) | Live | X position: 0 = centered, >0 = pixels from the left edge, <0 = pixels from the right edge. |
y |
int | per-item | -8192–8192 (since v1.9.28) | Live | Y position: same convention, top/bottom. |
font_size |
int | 32 (stream 0) / 12 (other streams) | 8–128 | Live | Absolute pixel font size (no per-stream auto-scaling). |
color (alias font_color) |
hex (0xAARRGGBB) | 0xFFFFFFFF |
— | Live | Text fill color. |
transparency |
int | 255 | 0–255 | Live | Group alpha. |
outline (alias stroke) |
int | 1 | 0–64 | Live | Text outline/stroke width in px (0 = off). On by default so overlays stay legible on light backgrounds. |
outline_color (alias stroke_color) |
hex (0xAARRGGBB) | 0xFF000000 |
— | Live | Outline/stroke color. |
font_path |
string(128) |
"" (inherits osd.font_path) |
— | File-only | Optional per-item TTF override. |
S = video stream index (0/1), N = region slot (0–3, MS_MAX_PRIVACY).
Implemented as IMP OSD cover regions; live-appliable as long as an OSD
group exists on that stream (i.e. OSD or a privacy region was enabled at
boot — GET /control's caps.privacy.available reflects this).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
enabled |
bool | 0 | 0/1 | Live* | Show/hide this cover region. |
x |
int | 0 | -8192–8192 (since v1.9.28) | Live* | Rectangle X origin, pixels, in the stream's frame. |
y |
int | 0 | -8192–8192 (since v1.9.28) | Live* | Rectangle Y origin. |
w (alias width) |
int | 0 | 0–8192 (since v1.9.28) | Live* | Rectangle width. |
h (alias height) |
int | 0 | 0–8192 (since v1.9.28) | Live* | Rectangle height. |
color (alias fill_color) |
hex (0xAARRGGBB) |
0xFF000000 (opaque black) |
— | Live* | Fill color. |
* Live only if an OSD group already exists for that stream at boot; otherwise the value persists but has no visible effect until restart.
S = video stream (0/1), N = region slot (0–3, MS_MAX_ROI; the SDK has 8,
timps uses 4). Per-region QP for H.264 via IMP_Encoder_SetChnROI. POST as
{"roi":{"0":{"1":{"enabled":1,"x":640,"y":320,"w":320,"h":240,"qp":-6}}}}.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
enabled |
bool | 0 | 0/1 | Live | Region on/off. |
x, y
|
int | 0 | 0–8192 | Live | Top-left corner, pixels of the encoded stream. |
w, h
|
int | 0 | 0–8192 | Live | Size; rounded outwards to 16×16 macroblocks. |
qp |
int | -5 | -51–51 | Live | Delta -26..25 or absolute 0..51, clamped per qp_mode. |
qp_mode |
int | 1 | 0/1 | Live |
1 delta to the frame QP, 0 absolute QP. |
The encode thread applies a change before its next frame; an idle stream gets
its regions when it starts. The SDK rectangle holds two inclusive corners, not
a width/height, and libimp clips nothing — timps converts and clamps. Only
unrotated H.264 streams take regions (caps.roi.streams); on an H.265 or
rotated stream a file value logs one WARN at start, while a POST is accepted
and persisted without any effect or log line. Device-tested (by the OpenIMP
maintainer, not timps) on T10, T20 and T21 with OpenIMP (T10: QP51 region
-25 % bitrate; T21: on by default, OPENIMP_T21_ROI=0 in the environment turns
it off and timps reports experimental). Vendor T10/T20/T21 are experimental
(T21: no effect seen). T23 refuses the keys (the encoder only stores the
region); T31 ROI exists in OpenIMP as SetChnRoiAttr but is not wired yet.
Since v1.11.0 T40/T41 with OpenIMP take regions as QP deltas when the libimp
exports OpenIMP_Cap_EncRoi (experimental; see
Platform & SDK Support).
Everything else refuses the keys. caps.roi.tier reports it.
See Motion Detection for the full grid model.
enabled/sensitivity/cols/rows/monitor_stream are POST-able and
Live; hold_ms/skip_frames are POST-able and Live too — they feed
the IVS grid/hold logic only at create/resync time, so ing_control() routes
them through the same deferred grid re-sync as the geometry keys, which runs
once at the end of the POST. cooldown_ms/on_motion stay config-file-only
by design (see below).
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
motion.enabled |
bool | 0 | 0/1 | Live | Enable/disable IVS motion detection (stops/recreates the whole IVS grid). |
motion.monitor_stream |
int (channel) | 1 | valid stream index | Live | Which video stream's frame the grid is computed over. Default is the sub stream (far less CPU than 1080p); stream 0 is used when video1 is not enabled. |
motion.sensitivity |
int | 128 | 0–255 | Live | UI-facing sensitivity; mapped to the SDK's native 0–4 range (v*4/255) applied uniformly to every cell. A change that maps to the same SDK level as before is treated as unchanged (no IVS rebuild). |
motion.cols |
int | 5 (or 2/1 on SDKs with a smaller ROI budget) | ≥1, cols*rows clamped to MOTION_CELL_LIMIT
|
Live | Grid columns. Setting one axis clamps against the current value of the other, never the reverse, so re-applying the same pair is idempotent. |
motion.rows |
int | 5 (or 2/1) | ≥1, same clamp | Live | Grid rows. |
motion.cooldown_ms |
int | 5000 | 250–INT_MAX (floor enforced) |
File-only | Minimum gap between on_motion hook executions. |
motion.hold_ms |
int | 800 | 0–60000 | Live | How long a cell reports "active" after its last hit, so an async /events//control reader reliably observes single-frame motion instead of racing IVS's own immediate clear. 0 = no hold. Settable via /control; applied through the grid re-sync batched at the end of the request. |
motion.skip_frames |
int | 5 | 1–100 | Live |
IMP_IVS_MoveParam.skipFrameCnt — analyze every Nth frame. Higher = cheaper/higher latency. Settable via /control; applied through the grid re-sync batched at the end of the request. |
motion.on_motion |
string(128) | "" |
— | File-only, not GET-readable either | Program launched by posix_spawn() on motion (no shell, no arguments, no PATH search — use an absolute path). "" = disabled. Receives MOTION_COLS/MOTION_ROWS/MOTION_CELLS/MOTION_TIME via the environment — see Motion Detection. |
motion.roi_x/roi_y/roi_w/roi_h
|
int | 0 | — | File-only, deprecated | Legacy single-ROI keys, still parsed for old configs but ignored — the grid replaced them. Setting a non-zero value logs a one-time warning. |
See Recording & Timelapse. All settable keys are re-read live by the recorder thread on every loop pass, so changes apply within its ~300 ms poll cycle without a restart.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
record.enabled |
bool | 0 | 0/1 | Live (next cycle) | Master enable; also gates on-boot start. |
record.channel |
int (channel) | 0 | valid stream index | Live (next cycle) | Video stream to record. |
record.mode |
enum |
1 (motion) |
continuous(0)|motion(1)|raw number |
Live (next cycle) | Continuous vs. motion-triggered recording. |
record.dir |
string(128) | /mnt/mmcblk0p1 |
— | Live (next cycle) | SD base directory. Since v1.9.28 refused if it (or its nearest existing parent) is on the root filesystem — typically the mount point with no card — instead of recording into flash; /tmp is allowed. |
record.name |
string(96) | %Y%m%d/%H/%Y%m%dT%H%M%S |
strftime template |
Live (next cycle) | Path template under <dir>/<hostname>/records/. |
record.segment_s (alias segment) |
int | 60 | 0–86400 | Live (next cycle) | Max segment length in seconds; rotation only happens at a video keyframe. 0 = single file, no rotation. |
record.pre_roll_s (alias pre_roll) |
int | 3 | 0–60 | Live (next cycle) | Motion mode: seconds of buffered video kept before the trigger (ring buffer). |
record.post_roll_s (alias post_roll) |
int | 10 | 1–300 | Live (next cycle) | Motion mode: keep recording this long after the last motion event. |
record.min_free_mb |
int | 200 | 0–1048576 | Live (next cycle) | Prune oldest segments until at least this much free space remains. Set it larger than the media can ever provide and the recorder refuses to record (saying so in the record status object's last_error) instead of emptying the archive chasing an unreachable target — see Recording & Timelapse. |
record.audio |
bool | 1 | 0/1 | Live (next cycle) | Mux AAC audio into the recording when available (G.711 cannot be muxed into fMP4). |
{"record":{"active":1|0}} (not a config key) is a transient manual
start/stop override, and {"record":{"clip":"<path>","seconds":N}}
triggers an independent on-demand fMP4 clip capture — neither is
persisted to the config file.
See Recording & Timelapse. Like record.*, the
running thread re-reads these live.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
timelapse.enabled |
bool | 0 | 0/1 | Live (next cycle) | Master enable; also gates on-boot start. |
timelapse.channel |
int (channel) | 0 | valid stream index | Live (next cycle) | Video stream whose JPEG is captured. |
timelapse.dir |
string(128) | /mnt/mmcblk0p1 |
— | Live (next cycle) | Base directory (SD, NFS, any writable path; since v1.9.28: not on the root filesystem, same rule as record.dir). |
timelapse.name |
string(96) | %Y%m%d/%H/%Y%m%dT%H%M%S |
strftime template |
Live (next cycle) | Path template under <dir>/<hostname>/timelapses/. |
timelapse.interval_s (alias interval) |
int | 60 | ≥1 | Live (next cycle) | Seconds between shots. |
timelapse.keep_days |
int | 7 | 0–3650 (since v1.9.28) | Live (next cycle) | Delete shots older than this; 0 = keep forever. Shots with a pre-2025 (pre-NTP) mtime are never age-pruned (since v1.9.28). |
See Day/Night for the full decision logic. The short version:
the metric is the exposure index (total_gain × integration_time / max_integration_time, higher = darker — equal to total_gain in a dark scene
and far below it in a bright one); day→night is a direct measurement on the
honest day pipeline, night→day only ever happens via a check of the day
pipeline, asked for by a look (IR illuminator off 8 s, silent); the
calendar (time_*, sun_*) is used by mode = schedule only.
switch_cmd, isp_path, trace_path and irprobe_cmd are not
GET-readable even though they are settable via the config file — each
names a path or program the daemon acts on as root.
Renamed 2026-08-17: total_gain_day_threshold → day_gain and
total_gain_night_threshold → night_gain. The old names still work as
aliases and the units and calibration are unchanged.
Removed 2026-08-17 (parsed, ignored, warned about once):
day_gain_pct, baseline_delay_s, boot_settle_max_s, boot_stable_pct,
night_reconfirm_s, probe_max_skip_s, threshold_low, threshold_high,
hysteresis.
Removed 2026-08-22 (config consolidation — parsed, ignored, warned about
once): the whole learn/state_path learning subsystem is gone outright.
Retired in v1.10.1 (the simple engine, dev_notes/dn-redesign.md;
parsed, ignored with one warning each, … is retired and IGNORED):
probe_min_gap_s, probe_confirm_s, boot_probe, diagnose_thresholds,
and the eight constants of the 2026-08-22 consolidation listed at the end of
this section, which are no longer echoed in GET /control.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
daynight.enabled |
bool | 1 | 0/1 | Live | Auto-detection on/off; 0 = manual (thread still samples but forces nothing). |
daynight.mode |
enum | auto |
auto|schedule
|
Live |
auto = the sensor automaton (the calendar is not used). schedule = the calendar decides outright, no sensor and no looks. The pre-2026-08-17 tokens still parse: sensor→auto, time/sun→schedule. |
daynight.time_night_start |
string(6) | "" |
"HH:MM" |
Live | Calendar: local time night begins. Set together with time_day_start; takes precedence over the sun calendar. (since v1.9.28) a window with only one edge set, a malformed value or two equal edges is ignored with a startup warning (and the sun calendar used if a location is set). |
daynight.time_day_start |
string(6) | "" |
"HH:MM" |
Live | Calendar: local time day begins. |
daynight.sun_latitude |
float | 0.0 | -90–90 | Live | Sun calendar: latitude. Used when no time window is set and either coordinate is non-zero. |
daynight.sun_longitude |
float | 0.0 | -180–180 | Live | Sun calendar: longitude. |
daynight.sun_sunrise_offset_min |
int | 0 | -1440–1440 | Live | Minutes added to computed sunrise. (since v1.9.28) an offset that pushes an edge across solar midnight is honoured; it used to be clipped to the current solar day. |
daynight.sun_sunset_offset_min |
int | 0 | -1440–1440 | Live | Minutes added to computed sunset. |
daynight.day_gain |
float | 768 | 1–1,000,000 | Live | Exposure index below which the day pipeline confirms day. 768 = 3× gain: "day is confirmed when the day pipeline can hold the scene at ≤3×". Alias: total_gain_day_threshold. |
daynight.night_gain |
float | 4096 | 1–1,000,000 | Live | Exposure index above which day ends. 4096 = 16×, "colour is hopeless". Alias: total_gain_night_threshold. |
daynight.jump_pct |
int | 20 | 5–70 | Live | since v1.10.1. At night: the illuminator-lit level this far below the last look for 20 s starts a look at once (new light). |
daynight.color_pct |
int | 3 | 0–50 | Live |
Since v1.11.0. At night: the colour of the illuminator-lit picture (global R/G or B/G of the ISP's AWB statistics, read every 5 s) this far from its value after the last look for 20 s starts a look at once — a weak lamp that hardly brightens the IR picture. At most every 5 min, the gap doubling up to 45 min until a timer look; 0 = off. Needs irprobe_cmd and a libimp with per-zone AWB statistics; status daynight.color_trigger (on/off/unavailable) and color_dev_pct. |
daynight.day_confirm_s |
int | 30 | 1–3600 | Live | How long the index must stay above night_gain (or the AE at its limit) before day→night; 6 s when very dark. |
daynight.heartbeat_s |
int | 14400 (4h) | 300–604800 | Live | Check interval at night for cameras without a switchable illuminator; also caps the 45-min look interval of the others. |
daynight.heartbeat_max_s |
int | 43200 (12h) | 300–604800 | Live | The longest night without a day-pipeline reading: then a check runs regardless of looks and lockouts. |
daynight.interval_ms |
int | 2000 | 100–60000 | Live | Sample interval. |
daynight.ir_idle_off |
int | 0 | 0/1 | Live |
Since v1.10.0. Idle-Dark: at night, once no picture consumer has been there for ir_idle_linger_s, switch only the IR LED off (IR-cut and ISP stay in night) and decide at the next consumer from the reading taken meanwhile. Needs the FrameSource keepalive (general.fs_keepalive, auto on OpenIMP T23/T40/T41), auto mode and a timps-irprobe with the idle/state verbs. Off by default: with 850 nm LEDs the glow going out shows that nobody watches. Status daynight.ir_idle; see Day/Night. |
daynight.ir_idle_linger_s |
int | 60 | 0–3600 | Live |
Since v1.10.0. How long after the last picture consumer the IR LED may go off. A poll slower than this costs one LED on/off pair and one held snapshot per poll; keep it longer than timelapse.interval_s (at 60/60 every shot can light the LED once). |
daynight.history_s |
int | 0 (off) | 0–172800 (48h) | Live | Seconds of decision history kept in RAM for the WebUI tuning graph, served by GET /control?dn_history=1; 0 = off, and at 0 nothing is allocated at all. One 16-byte sample per 10 s, allocated on first push and resized in place when this changes, so 4 h costs 22.5 KB and the 48 h ceiling 270 KB. Anything above the ceiling is clamped (the POST response echoes the clamped value, and the daemon logs the effective size). Opt-in and decoupled from the tuning page's lifetime: the page's "collect in background" switch is the only thing that POSTs it, and it persists like any other key, so an explicit opt-in survives a reboot. With it off the page collects live into the tab over SSE instead; with it on the daemon owns the series, which is what lets the graph show the hours no tab was open (the WebUI is plain HTTP and therefore has no Service Worker to collect with). |
daynight.irprobe_cmd |
string(64) | timps-irprobe |
— | File-only, not GET-readable | Board script run as <cmd> on|off|idle|state to drive the IR illuminator alone, without moving the IR-cut filter (on/off for a look, idle/state for Idle-Dark). Empty = no looks: the camera checks like one without a switchable illuminator (night level halved, heartbeat_s). Two failures in a row turn looks off for the session. Run via fork+execlp, never a shell. |
daynight.switch_cmd |
string(64) | daynight |
— | File-only, not GET-readable | Board script run as <cmd> day|night on a switch. |
daynight.isp_path |
string(128) | /proc/jz/isp/isp-m0 |
— | File-only, not GET-readable | ISP exposure proc file (gain and integration time). Since v1.11.0 never read on vendor T21 (that driver corrupts the kernel heap on the proc read); exposure from IMP only there. |
daynight.trace_path |
string(128) | "" |
— | File-only, not GET-readable | Opt-in decision-trace CSV for the replay harness. tmpfs only; 256 KB cap (since 1.11.0, before 1 MB), rotated once to <path>.1. |
These eight were per-camera daynight.* fields until the 2026-08-22
consolidation, then fixed DN_* constants echoed in GET /control. Since
v1.10.1 they are retired: a file value is ignored with one warning, and the
status no longer echoes them. The table describes the v1.10.0 engine.
| Key | Was type | Fixed value | What it did |
|---|---|---|---|
probe_jump_pct |
int | 50 | Probe when the index falls below this percentage of the night reference (the level at which night was last proven). 50 = one full stop of new brightening. |
probe_settle_s |
int | 8 | AE settling on the day pipeline before the probe verdict is taken. One verdict, binary: below day_gain it sticks, otherwise it reverts immediately (bypassing transition_s). |
ref_delay_s |
int | 30 | Wait after entering night before anchoring the night reference (IR LEDs settling). A floor — the anchor also waits for the reading to stop moving. |
ir_ratio_night |
float | 2.0 | Silent probe: an IR ratio r at or above this means the illuminator is carrying the scene — stay night, no click. r = D(illuminator off) / D(illuminator on), so it is dimensionless and fleet-wide, unlike any absolute level. |
ir_ratio_day |
float | 2.0 | …and at or below this the room supplies the light. Deliberately equal to ir_ratio_night, so every silent probe reaches a verdict; a reading between the two is inconclusive and escalates to the audible probe instead of deciding on thin evidence. |
ir_min_headroom |
int | 8 | Minimum AE reserve, in log2 units (32 = one stop), for a ratio to mean anything. An AE with nothing left cannot respond to the illuminator going off and returns r ≈ 1, which looks exactly like daylight — measured 1.14 on a pitch-dark scene. Below this the verdict is night: being pegged at the dark end is itself proof. |
boot_settle_s |
int | 5 | Minimum wait after thread start/re-enable before the first decision (also gated on the reading having settled). |
transition_s |
int | 5 | Minimum dwell between switches. A failed probe's revert bypasses it. |
File-only except debug_modules, which is POST-able as
{"general":{"debug_modules":"HAL_ING"}} and takes effect immediately.
Everything else in this section is read at startup.
Two keys are deliberately left out of the table below: general.trace and
general.trace_ms are a developer probe for send-pipeline tracing (see
src/trace.h), handled directly in set_kv() rather than as table fields —
not POST-able, not echoed, not part of /control?fields=1. They are
intentionally undocumented here; edit /etc/timps.conf by hand if you need
them.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
general.loglevel |
int |
LOG_INFO (2) |
SDK log-level enum | File-only | Startup log verbosity (-v on the command line also forces debug). |
general.debug_modules |
string(64) | "" |
comma list of log tags | Live | Modules raised to DEBUG regardless of loglevel (HAL_ING,DAYNIGHT). Names rather than a bitmask: a mask in a config file silently means something else once a module is added, and nothing warns. It matters because the ring buffer is 64 KB and recycles in minutes under load - raising the global level to reach one subsystem loses the lines it was raised for. Case-insensitive, up to 8 names; over-long or surplus names are rejected with a WARN, an unknown (but well-formed) name simply never matches. A listed module passes at every level, not only its DEBUG lines. See Logging. |
general.imp_polling_timeout |
int | 500 | — | File-only |
IMP_Encoder_PollingStream timeout, ms. |
general.osd_pool_size |
int | 1024 | — | File-only | Max OSD pool size (KB) reserved for small OSD regions. |
general.fs_keepalive |
int | -1 | -1..1 | File-only | FrameSource keepalive. 1: every video FrameSource is enabled once at start and held until the daemon stops (encoders stay on-demand), so nothing disables a FrameSource at runtime. 0: FrameSources run only while something consumes them (the classic on-demand model). -1 (auto): on for OpenIMP builds on T23, T40 and T41 (T23/T41: a FrameSource disable/enable while the sensor streams can hang the SoC; T40, since v1.11.0: the AE stops without frames and day/night read a frozen exposure); off everywhere else, because the idle cost of a permanently running FrameSource (CPU, heat, ~6.3 MB rmem for a 1080p main stream) is unmeasured there. Also auto, on/true/yes, off/false/no; an unknown word logs a warning and stays auto. Effective value: caps.framesource in GET /control. See Architecture. |
general.syslog |
bool | on | 0/1 | File-only | Also mirror log output to syslog (logread). A side-effecting key: applied immediately at config-load time via log_set_syslog(), but not stored as a field, so it cannot be echoed back by any getter. |
Only meaningful for make sim builds (src/hal/hal_sim.c); ignored by a
real-hardware (HAL_INGENIC) build. File-only.
| Key | Type | Default | Range | Live? | Description |
|---|---|---|---|---|---|
sim.video0 |
string(256) | "" |
file path | File-only | Annex-B H.264/H.265 file looped as the video0 source. |
sim.video1 |
string(256) | "" |
file path | File-only | Annex-B file looped as the video1 source. |
sim.audio |
string(256) | "" |
file path | File-only | ADTS AAC file looped as the audio source. |
sim.jpeg |
string(256) | "" |
file path | File-only | Still JPEG file republished as the JPEG source(s). |
N is the video stream index (0 = main, 1 = sub; MS_MAX_VSTREAM=2
streams total). Geometry/codec/identity keys (enabled/codec/width/
height/fps/gop/max_gop/profile/rotation/buffers) are
persist-only / restart-required (the encoder and FrameSource are never
reconfigured on the running pipeline); rtsp_path is honestly live — see
the caveat at the top of this page. The rate-control knobs below
(bitrate/rc_mode/qp/min_qp/max_qp/i_bias_lvl) are Mixed: a
per-platform subset reaches the running encoder live as of 2026-08-21 —
see Rate Control Parameters: Live vs.
restart for the exact
key × SoC table, or query the running build's own caps.video_live (never
hardcode the table into a client — see
HTTP Control API).
GET /control still groups this whole section under caps.restart
(entry "video") as the conservative default; a key that also appears in
caps.video_live, and rtsp_path, are the per-key exceptions.
| Key | Type | Default (video0 / video1) | Range | Live? | Description |
|---|---|---|---|---|---|
video<N>.enabled |
bool | 1 / 1 | 0/1 | Restart-only | Enable this stream. |
video<N>.codec |
enum |
h264 / h264
|
h264|h265 (aliases hevc) |
Restart-only | Video codec. H.265 only where the platform's encoder API supports it — see Platform & SDK Support. |
video<N>.width |
int | 1920 / 640 | 64–4096 | Restart-only | Encode width (pre-rotation). With neither width nor height set, a known sensor size that fits inside the default is used instead, so nothing is upscaled (a 1280x720 sensor streams 1280x720; since v1.10.0). |
video<N>.height |
int | 1080 / 360 | 64–4096 | Restart-only | Encode height (pre-rotation). Unset: see width. |
video<N>.fps |
int | 25 / 25 | 1–120 | Restart-only | Encode frame rate of this stream only. It does not set the sensor rate; that is sensor.fps. |
video<N>.bitrate |
int | 3000 / 512 | 16–50000 (kbps) | Mixed (live on every SoC as of 2026-08-21, see below) | Target bitrate. |
video<N>.rc_mode (alias mode) |
enum |
cbr / cbr
|
cbr|vbr|fixqp|smart|capped_vbr|capped_quality
|
Mixed (live on classic SoCs only, see below) | Rate-control mode. |
video<N>.gop |
int | 50 / 50 | 1–1000 | Restart-only | GOP length (I-frame interval). |
video<N>.max_gop |
int | 60 / 60 | 1–1000 | Restart-only |
RESERVED / no effect — parsed, clamped and persisted for compatibility but consumed by no HAL; the encoder's keyframe interval comes from video<N>.gop. Setting a non-zero value logs a one-shot warning. |
video<N>.profile |
int | 2 / 2 | 0 (baseline) – 2 (high) | Restart-only | H.264/H.265 encode profile. Matters for WebRTC: the WHEP answer's profile-level-id comes from this stream's live SPS, and Firefox offers (and accepts) H.264 as Baseline only — it refuses a Main/High answer at setRemoteDescription(), so the session never establishes. Chromium accepts any profile and decodes from the in-band SPS. Set 0 on the webrtc.channel stream if Firefox must work; the preview page detects the mismatch and disables its WebRTC option either way. See the top-level README.md § "WebRTC / WHEP". |
video<N>.qp |
int | 35 / 35 | 1–51 | Mixed, see below | Fixed QP, used only when video<N>.rc_mode=fixqp (passed as the encoder's initial/fixed QP: iInitialQP on the new API, attrH264FixQp.qp/attrH265FixQp.qp on the classic API). No effect under cbr/vbr/etc., which derive QP from rate control instead — use video<N>.min_qp/max_qp there. |
video<N>.min_qp |
int | 20 / 20 | 1–51 | Mixed (live on every SoC as of 2026-08-21, see below) | Minimum QP. |
video<N>.max_qp |
int | 45 / 45 | 1–51 | Mixed (live on every SoC as of 2026-08-21, see below) | Maximum QP. |
video<N>.quality_lvl |
int | 2 / 2 | 0–7 | Restart-only | Classic-SoC (T10..T30) VBR/Smart only: minimum quality floor. 0 = highest quality/largest stream, 7 = lowest. The vendor SDK derives minBitRate = bitrate * quality[lvl] from it (quality[]={0.8..0.1}), but that floor was not found to be binding in testing — see Rate Control Parameters. No equivalent field on the new encoder API (T31/C100/T40/T41); a non-default value there logs a one-shot warning and has no effect. |
video<N>.change_pos |
int | 80 / 80 | 50–100 | Restart-only | Classic-SoC VBR/Smart only: percentage of bitrate above which the controller starts raising QP. Measured to have no observable effect on a T23 (80/65/50 gave identical output) — treat as unverified rather than a reliable lever. No equivalent on the new API; same one-shot warning as quality_lvl. |
video<N>.i_bias_lvl |
int | 0 / 0 | -3–3 | Mixed (live on classic SoCs and T31/C100, see below) | I-frame QP bias (negative = more bits on keyframes). Classic SoCs: both CBR and VBR/Smart. New API: wired on T31/C100 via IMP_Encoder_SetChnQpIPDelta (readable back as encoder.<n>.rc.ip_delta); T40/T41 have no such call and warn once instead. |
video<N>.fluc_lvl |
int | 0 / 0 | 0–4 | Restart-only, H265 only | Classic-SoC H265 CBR/VBR/Smart: bitrate fluctuation allowed relative to the average. No H264 equivalent field, and the T23 SW-rotate path is H264-only so never sees this key. No equivalent on the new API. |
video<N>.rotation |
int | 0 / 0 |
0|90|270 (+180 on T40/T41 only); legacy 1/2 accepted as raw rotTo90 values |
Restart-only, USE_ROTATE builds only |
Image rotation — see Platform & SDK Support and docs/rotation.md. Unsupported values on a given SoC are coerced to 0 with a warning. T10/T20/T21 (OpenIMP builds, since v1.10.0): sub streams only, 16-aligned, ≤704x576, and only with an OpenIMP that has T1x rotation (6b7f5ae); otherwise the stream starts unrotated with a warning. GET /control reports why a stream runs unrotated in video.<N>.rotation_state (since v1.10.1) and how it is rotated in video.<N>.rotate_path (off/sw/hw/band, since v1.11.0). |
video<N>.buffers |
int | 2 / 2 | 1–8 | Restart-only | IMP video buffer count (nrVBs). Explicitly setting this makes the HAL trust it as-is instead of applying its own safety clamp. |
video<N>.rtsp_path |
string(64) |
/ch0 / /ch1
|
— | Live (documented exception, see above) | RTSP mount path for this stream. |
video<N>.jpeg (alias jpeg_enabled) |
bool | 1 / 1 | 0/1 | File-only | Enable a piggyback JPEG encoder sharing this stream's FrameSource (used by /snapshot.jpg//stream.mjpeg?chn=N and timelapse). |
video<N>.jpeg_quality |
int | 75 / 75 | 1–100 | File-only | Piggyback JPEG quality. |
video<N>.jpeg_fps |
int | 5 / 5 | 1–120 | File-only | Piggyback JPEG max publish rate. |
video<N>.jpeg_chn |
int | MS_MAX_VSTREAM+1+N |
0–8 | File-only | IMP encoder channel number for the piggyback JPEG encoder (internal). Must be unique; (since v1.9.28) a collision is moved to the lowest free channel at load (or that JPEG encoder disabled if none is free), with an [ERR] line. |
video<N>.imp_chn |
int | N |
0–7 | File-only, internal | IMP encoder channel number for the video stream itself. It doubles as the stream's hub slot, so it must equal N; (since v1.9.28) any other value is reset to N at load with an [ERR] line (before, it silently fed another stream's or the audio slot). |
For the live-apply mechanics (how a POST maps onto these keys, JSON
shapes, and the caps object a client can use to discover this
classification programmatically) see
HTTP /control API Reference.