| Age | Commit message (Collapse) | Author | Files | Lines |
|
|
|
make build / cleanbuild / ota / verify / check wrap the recurring dev
loop, sourcing the ESP-IDF env per-recipe. `make ota` reconfigures first
(the embedded git hash stamps at configure time only), builds, pushes,
and verifies. tools/ota.sh encodes the OTA acceptance criterion — the
new image must show pending-verify on a fresh boot before flipping to
valid; "valid" with pending-verify never seen means the device silently
booted the old slot — and re-pushes once automatically on the known
first-push rollback quirk.
|
|
|
|
attachListener listened on BinarySensor+MotionSensor+ObjectDetector
unconditionally and relied on handleCameraEvent to filter by trigger —
correct, but a doorbell-only viewport still took a callback per motion
re-assert on a chatty camera, and the "subscribed to [...]" log implied
all three were wake sources. The listener set now follows the selected
triggers (zero triggers = tap-only: no listeners at all), and the
idempotency key becomes a "cameraId|triggers" signature
(attachedCameraId -> attachedListenerSig) so a trigger change forces a
re-attach through the existing rebind path. Event-time filtering stays
as defense in depth (ObjectDetector still needs the person-class check).
|
|
Creating a viewport with the name field blank fell back to "viewport"
with no way to change it afterward — display_name is deliberately the
canonical name (v.name drifts on reload), so the Scrypted device-name
pencil never propagated. Two fixes:
- createDevice: an empty name now inherits the discovered device's
advertised TXT name, so picking "10.0.13.83 — kitchen (…)" from the
host dropdown names the viewport "kitchen" without retyping.
- child settings gain a "Viewport name" field: updates display_name,
mirrors the name onto the Scrypted device record, and re-registers so
the firmware name + mDNS hostname follow.
|
|
|
|
Browse for _scrypted-viewport._tcp with a plain dgram socket on an
EPHEMERAL port: per RFC 6762 §6.7 a query from a non-5353 source port is
a legacy unicast query and responders reply unicast to that port —
verified in the ESP-IDF responder (mdns_send.c answers to the querier's
addr/port whenever src_port != 5353). No :5353 bind means no conflict
with Scrypted's own HomeKit mDNS or a host avahi-daemon, and it works
under Docker host-networking or native alike. One response packet
carries PTR+SRV+TXT+A (RFC 6763 §12.1), so parsing is per-packet with
no follow-up queries; the wire codec (name compression included) is
~150 lines, no dependencies.
Surfaced three ways:
- add-device form: host field is a combobox listing discovered
viewports as "ip — name (vX, WxH)"; manual entry still works and
only the address token is stored.
- child settings page: same choices on the existing host field.
- auto-heal: when a /config register fails, re-browse and match by MAC
(seeded from /state and discovery; survives renames) then by name;
on a hit at a new address, rewrite host and retry once. Runs only on
register failure, so it's rate-limited to the 5-min cycle and removes
the need for a DHCP reservation.
Browses are best-effort ([] on any failure) and cached 30s so settings
re-renders don't spam the LAN. diagnostic.ts gains the same browse as a
paste-and-run probe that validates dgram + multicast reachability from
inside the real Scrypted sandbox before trusting the feature.
|
|
Stable identity for the Scrypted plugin's discovery/auto-heal: the
viewport name is user-editable and the IP can renumber, but the MAC is
fixed. Discovery matches on it to relocate a device after a DHCP change.
|
|
|
|
- bootstrap: drain the shutdown-cleaner array FIRST, not after the child
re-discovery loop. The old order tore down the listeners the loop had
just attached, and since the instance maps still referenced them the
attachListener fast-path blocked the 5-min register-cycle self-heal
from ever re-attaching after a warm re-paste. The attach cleaner now
also invalidates listeners/attachedCameraId (one cleaner per attach),
so a drain by another instance can't strand dead registrations either.
- stream socket: single-flight reconnect scheduled from 'close' only.
A failed connect emits 'error' then 'close'; scheduling from both
doubled outstanding attempts every 500ms against a rebooting device.
openStreamSocket also destroys the previous socket first.
- onRequest wake: same streams/streamStarting guard as every other start
path (a concurrent second startStream overwrote the streams entry and
orphaned the first ffmpeg), respond 204 immediately (the device's POST
times out at 1s — awaiting startStream turned every tap-wake into a
firmware state_post_failure), and skip the redundant wake POST back.
- startStream: bail-out paths after the wake POST (camera missing, no
usable stream, ffmpeg-input conversion failure) now send a
compensating sleep instead of stranding the panel on Loading; the
last-ditch getVideoStream fallback no longer rejects out of the call.
- key streams/streamStarting/stopStream by nativeId — v.name drifts to
the nativeId on reload, stranding or duplicating streams keyed under
the drifted value; findByName matches display_name first for the same
reason.
- idle_timeout_ms=0: the Scrypted-side safety timeout still reclaims
ffmpeg/socket but no longer POSTs sleep over the always-on setting.
- pending bindingDebounce timers cleared across re-paste (they fired
against the dead instance and attached duplicate listeners);
releaseDevice drops lastRegisterSig/streamStarting/debounce entries.
- start() single-flights bootstrap; createDevice drops its redundant
second registerViewport; streamLogger rolls its window on quiet ticks
(post-lull rates were diluted) and lastLogUs -> lastLogMs; hoist the
JPEG EOI needle + resume-scan offset in the demux loop; postJSON drops
the dead 204 check; remove unused sandbox declares and the legacy
streams.interval field.
- tsconfig: moduleResolution Node -> Bundler (removed in TS 6).
|
|
- display: s_last_pwm cached the raw 0-100 percentage at init but
display_wake writes it straight to REG_PWM (0-255 duty) — first wake
ran visibly dim until a /config brightness change. Shared pct_to_duty
helper now converts in both paths.
- http_api: cap consecutive OTA recv timeouts (a stalled client spun
forever holding s_ota_in_progress, wedging OTA until reboot); cap
viewport name at 54 chars so viewport-<name> fits the 63-byte mDNS
label; log mdns_service_refresh failures; /state builds JSON from a
snapshot instead of holding the state lock across ~25 cJSON allocs;
respond_400 delegates to respond_status.
- stream_server: bytes_in_window now counts painted frames only (frames
discarded while asleep inflated the first post-wake window's MB/s,
avg-jpeg and chunk/wire averages; recv_bytes folded in); so_rcvbuf
carried into the stats snapshot under the mux; drop dead HEADER_BYTES;
merge read_body_instrumented into read_n.
- state_machine: transition mutex serializes concurrent wake/sleep
(display side-effects ran after the state lock dropped, so racing
callers could leave the backlight contradicting st->state); wake-path
placeholder paint now takes the decoder lock like the stream path
(concurrent esp_lcd_panel_draw_bitmap from two tasks isn't safe).
- local_screens: overlay paint takes the decoder lock too; check the
overlay timer create; panel dims from viewport_state.h.
- jpeg_decoder: try_lock before init returns busy instead of passing a
NULL semaphore to xSemaphoreTake.
- net_eth: clear cached IP string on link down (stale /state + info).
- dead code: display_present_bgr888, TOUCH_FT5426_ADDR, touch s_task,
JPEG_DECODER_MAX_OUTPUT_BYTES; doc drift in nvs_config.h /
jpeg_decoder.h / app_main flag legend.
|
|
Tear-free display path and thermal visibility:
- display: triple buffering + scan tracking via on_refresh_done —
decode target is never the scanning or pending fb, so the
flip-vs-scan tear (measured on ~6% of painted frames at full
stream rate) is impossible by construction, with zero added
latency. /state tear_guard_engaged counts averted frames.
- temp: on-die TSENS reported as /state temp_c, on the info
overlay, and on the Scrypted per-stream stats line.
- screens: INFO_MAX_LINES 16 -> 20 (temp line was silently capped).
- docs: README Display strategy rewritten for the triple-buffer
model; new TCP window + EMAC tuning section.
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.2.
|
|
The info overlay already had exactly 16 ADD lines; the temp line
added in f59cea3 was the 17th, and the ADD macro's bounds guard
drops overflow lines without any diagnostic — so temp never
rendered. Give the cap headroom and document the silent-drop
behavior at the definition.
|
|
|
|
- Scrypted per-stream stats line gains temp=<c>C from /state temp_c —
free thermal trending under streaming load.
- README Display strategy rewritten for the tear-free triple-buffer
model: why the deferred fb-index reload tears under double
buffering, why scan-tracked buffer roles beat vsync-waiting, and
the tear_guard_engaged counter.
- New 'TCP window + EMAC tuning' section documenting the measured
window raise, the EMAC-RX-pool-below-window RTO regression, and
the pool >= TCP_WND invariant; stale 'window bump is safe but
won't help' backlog text updated.
- Memory strategy updated (3 fbs, decoder writes into them directly,
stream body ring).
|
|
New chip_temp module wraps the ESP32-P4 TSENS driver (20-100C range
for best accuracy in the warm band a PoE + 200MHz-PSRAM device
lives in). /state gains temp_c (0.1C resolution, omitted when the
sensor is unavailable); the long-press info overlay gains a temp
line (lowercase c suffix — the local 8x8 font has no uppercase C).
Junction temperature, ~10-20C above ambient under load.
|
|
draw_bitmap on a direct fb pointer only updates the driver's
cur_fb_index; the DPI DMA reloads that index at the END of the
in-progress frame scan (~21ms period at ~47Hz). Under double
buffering, flipping and immediately decoding the next frame into
the other fb writes a buffer the DMA may still be scanning out —
a torn frame. This regime is common now that the TCP window fix
delivers frames back-to-back (decode starts ~6ms after flip).
Fix with zero added latency: num_fbs 2 -> 3 (+1.15MB PSRAM of 25MB
free), track the actually-scanning fb via on_refresh_done (fires in
the DMA-done ISR exactly when the DMA reloads cur_fb_index), and
pick the decode target as the fb that is neither pending display
nor scanning. Three buffers minus at most two excluded roles =
always a free one; no waiting on vsync anywhere.
Instrumented: /state tear_guard_engaged counts back-buffer picks
made while the previous fb was still mid-scan — each one is a
frame that would have torn under double buffering.
|
|
Stream throughput tuning, measured on hardware (kitchen panel):
wire 53 -> 74 Mbps, per-frame recv 30.5 -> 21.4 ms, painted fps
19.8 -> 23.6, g2g 73 -> 62 ms, sender backpressure 61% -> 47%.
- lwip: TCP_WND 5760 -> 23040 (16 x MSS), RECVMBOX 6 -> 32
- eth: EMAC RX DMA pool sized above the TCP window (1600B x 24);
below-window pool caused silent burst tail-drop + ~200-400ms
sender RTO stalls
- stream: TCP-window decomposition instrumentation (wire kbps,
hdr_gap, pend_age) in the window log and /state; connect log
stamps TCP_WND/MSS/RECVMBOX
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.1.
|
|
WND=23040 alone regressed: sender fps 20 -> 14, drop-oldest 3x,
recv bimodal (min 20ms but 200-450ms stalls; firmware wire_min
6.2Mbps / wire_max 75Mbps, recv_max 253ms). The stalls are RTO
recovery: the default EMAC RX pool (20 x 512B = 10KB) is smaller
than the 23KB the window now invites in flight, so a full-window
burst overruns the RX descriptors, the burst tail is dropped with
no dup-ACKs behind it, and the sender waits out a ~200ms min-RTO.
1600B buffers fit one MSS frame per buffer (1 descriptor/packet
instead of 3); 24 of them = 38.4KB >= window + slack. ~44KB more
internal RAM (491KB free).
|
|
Instrumented baseline at WND=5760 (kitchen, ~202KB frames, 20fps
sender) confirmed the receive window as the dominant throttle:
- wire 53.0/58.8 Mbps avg/max = the WND/RTT ceiling, vs ~94 Mbps
line rate on the 10/100 PHY
- recv_chunk_max pinned at exactly 5760 (drains window-quantized)
- recv_calls avg 60/frame; queued_at_body always 0
- sender backpressured 44-61% of frames, drop-oldest 17-54/window
- meanwhile pend_age avg 58us: decode idles, receive starves it
Expected: recv_avg ~30ms -> ~20ms, wire toward line rate, sender
bp%/drop-oldest down, g2g 73ms -> ~60ms. Tripwires (revert if hit):
pend_age or recv_dropped_oldest growing window-over-window, g2g
regressing. RECVMBOX scales with the window: 23040 = 16 segments
in flight, a 6-deep mbox would silently become the new cap.
|
|
Before touching CONFIG_LWIP_TCP_WND_DEFAULT, make the window question
decidable from the logs. New per-window metrics in the stream log,
/state, and stats struct:
- wire min/avg/max kbps: instantaneous throughput while each body
drained (jpeg_len/recv_us). Ceiling ~= TCP_WND/RTT, so it scales
with the window iff the window is the limiter.
- hdr_gap min/avg/max us: time blocked waiting for the next header
after finishing a body. Large = sender-paced; ~0 = receive path
is the bottleneck.
- pend_age min/avg/max us: publish->claim latency of painted frames.
Growing across windows = queue backlog building, the failure mode
that killed the previous WND=65535 attempt.
Together with recv/dec/paint the frame interval is now fully
decomposable: interval ~= hdr_gap + recv + pend_age + dec + paint.
Also stamp TCP_WND/TCP_MSS/RECVMBOX into the client-connect log line
so every capture is self-labeled with the config it ran under.
|
|
|
|
skip-oldest)
|
|
Unify firmware and plugin versions for the production release. Highlights
since v1.1.0/1.2.0:
- event wake fixed (self-healing camera-listener re-attach after the reload/
add storage race); doorbell/motion/person confirmed on hardware
- stop() fully tears down (stop+start == fresh load); stale childId pruning;
live-stream events ignored (no queue/relaunch)
- triggers default to person+doorbell; doorbell hidden for non-doorbell cams
- cold-start (wake -> live video) ~6s -> ~0.7s: prebuffered substream by id +
request >= GOP + burst-friendly ffmpeg flags; snapshot removed
- firmware: Loading screen on new stream connection (no stale-frame flash)
- logging trimmed for chatty cameras
- README/protocol docs rewritten for the TCP-socket streaming model
Set VIEWPORT_VERSION and scrypted/package.json to 1.3.0.
|
|
|
|
A chatty camera keeps a viewport streaming for long stretches, and the old
logging emitted ~5 cold-start stamps + a 10s window line per ~10s per stream.
Cut it down:
- keep a single cold-start stamp (first ffmpeg frame); drop spawned / first
stdout byte / first socket.write.
- gate the 10s window line on noteworthy windows only (socket-not-ready drops,
a buffer-cap flush this window, firmware shedding >=2fps, or painted <20fps);
healthy 24fps windows print nothing — the per-wake lines already show liveness.
- log "registered" only when the pushed config actually changes, not on every
5-minute reregister cycle. lastRegisterSig cleared on stop() for fresh-load
parity.
No behavior change beyond logging.
|
|
The Wake/Sleep, race-handling, and idempotency sections still described the
old model where Scrypted streamed via POST /frame and learned of sleep from a
/frame 409. Streaming now runs over the TCP data socket (:81); the firmware
/frame endpoint remains only for one-shot snapshots/debug. Reframe the recovery
model accordingly:
- race-free property: frames arriving while asleep are discarded by the decode
task (stream socket) / rejected 409 (/frame) — neither re-wakes; sleep is
learned from the device's state=sleep callback + Scrypted's safety timer
- drop every "recovered by the next /frame returning 409" clause in favor of
the safety timer + discard-while-asleep
- idempotency table: add the stream-socket row (paints if awake, discarded if
asleep, skip-oldest)
- Loading screen: shown on wake AND each new stream connection until first paint
- Philosophy: list the MJPEG stream server (:81) and /firmware
Left the POST /frame endpoint's own API/semantics intact — it still exists.
|
|
The Scrypted Integration section still framed the snapshot POST /frame path as
v1 and MJPEG streaming as a future POST /stream milestone, contradicting the
already-current "What's next" perf section. Bring it in line:
- v1 is the live ffmpeg MJPEG stream over the raw TCP data socket (:81) with
prebuffer fast-start (~0.7s wake-to-video); POST /stream was never the shape
- replace the /frame streaming code example with the TCP socket + framed-MJPEG
sketch; brightness default 100 (was 80)
- sleep is learned via the device's sleep callback, not a /frame 409
- Status table: streaming + end-to-end are done/verified, not "not started"
- Idle: timer resets on any painted frame, not specifically a /frame POST
Left intact: the POST /frame API + protocol/idempotency sections — that
firmware endpoint still exists for one-shot snapshots/debug and its 409-when-
asleep semantics are unchanged.
|
|
The v1 README still described the old snapshot-poll path. Update to match the
shipped script: live MJPEG over a TCP data socket (port 81, ~24fps) via ffmpeg,
not ~1fps snapshots. Fixes:
- intro + v1 limitations: MJPEG streaming is done, not a v2 "POST /stream" TODO
- Settings: replace the removed "Frame push interval" with the real Display
fields (JPEG quality, Max Scrypted-side buffer, Stream prebuffer); add the
Actions group; correct brightness default (100, was 80)
- Wake triggers default is person+doorbell (was "all three"), doorbell only for
doorbell-capable cameras
- drop the stale snapshot-source / snapshot-interval / POST /frame 409 mentions
- host field: no mDNS auto-resolve (was contradictory)
- smoke test: Loading -> live video, not "snapshots flowing"
|
|
Wake-to-live is ~0.7s only when the streamed substream keeps a rebroadcast
prebuffer. Add a "Fast wake — camera prebuffer (required)" section to
scrypted/README.md: enable Prebuffer on the STREAM: MEDIUM tab (default only
prebuffers High), keep duration >= the detected keyframe interval (~5s), leave
the viewport's Stream prebuffer (ms) at 6000, and how to verify via the log.
|
|
The panel used to hold its last framebuffer during the connect→first-frame gap,
flashing a stale old frame before video appeared — more visible now that the
Scrypted side no longer sends a bridging snapshot. The wake path already paints
the Loading screen, but state_machine_set(AWAKE) is a no-op when the device is
already awake (e.g. a stream restart), so the clear didn't happen there.
Tie the clear to the stream connection instead: recv-task paints
local_screens_show_loading() once per new conn_id, guarded by the decoder lock
so the RGB565 present can't race a trailing decode-task paint, and gated on
AWAKE so it never draws on a sleeping panel.
|
|
|
|
The prebuffered stream now paints in ~0.7s, so the takePicture→transform→POST
first-paint bridge was consistently slower than the video it was meant to
cover, while adding camera load and a stale-overpaint risk. Drop it: remove
pushSnapshot and its call, the shouldPaint gate, and the firstStreamFrameSeen
plumbing. On the slow paths (no/cold prebuffer) the panel holds its prior frame
until the stream's first frame lands. buildVf stays (used by the live path).
|
|
|
|
Now that the prebuffered stream paints in ~0.7s, the parallel snapshot usually
loses the race and, if slow (a Unifi takePicture cache-miss can be several
seconds), would land after live video and overpaint it with a staler still.
pushSnapshot takes a shouldPaint predicate; startStream passes
() => !firstStreamFrameSeen (flipped by the ffmpeg first-frame handler), so a
snapshot that finishes after the stream is already painting is dropped before
the POST. Preserves the snapshot as a gap-filler for the slow paths (live-edge
fallback / cold prebuffer) where the stream hasn't painted yet.
|
|
|
|
Remove the temporary instrumentation used to diagnose the event-wake and
cold-start work: the evtscan system-wide listener, the attach/trace/streamopt/
src/inputArgs logs, the StartStop entry logs, and ffmpeg -loglevel info/-nostats
(back to error). Keep the lightweight one-shot cold-start stamps (spawned →
first byte → first frame → first socket.write) and usingPrebuffer in the config
line — cheap regression signals — plus all the actual fixes and the prebuffered-
stream selection logic.
|
|
|
|
Event/lifecycle fixes:
- attachListener now self-heals: it's idempotent (tracks the bound camera
per viewport) and re-runs on registerViewport success, so the camera
subscription survives the reload/add storage-attach race that previously
left a bound viewport with no listener until a manual re-save.
- stop() always drains the global cleaner array (never gates on this.running)
and also cancels bindingDebounce timers + clears the stream idle timeout on
abort, so StartStop.stop()+start() == a fresh load with no orphaned ffmpeg,
sockets, listeners, or intervals.
- ignore all camera events while a stream is live or starting (guard moved to
the top of handleCameraEvent) — the wake window is anchored to the first
event; later events don't queue, relaunch, or extend it.
- prune stale childIds whose storage container is gone (deleted device).
Triggers:
- default to person + doorbell (motion opt-in; doorbell cameras are chatty);
doorbell only offered for doorbell-capable cameras. Reconcile stored
triggers + re-render settings live when the camera binding changes.
Cold-start (wake -> first live frame): ~6s -> ~0.7s.
- Root cause: Unifi GOP is ~5s and the rebroadcast RTSP serves live-edge, so
ffmpeg waited for the next keyframe.
- Fix: select the prebuffered substream by id (smallest that covers the panel,
not the 5MP High), request prebuffer >= GOP, and drop -avioflags direct /
-fflags nobuffer on the prebuffer path so ffmpeg gulps the buffered-keyframe
burst instead of trickling it. Live-edge fallback keeps the low-latency flags.
Steady-state g2g unchanged (~50-100ms).
Includes temporary diagnostics (evtscan, attach/trace/streamopt/src logs,
ffmpeg -loglevel info) to be stripped in the follow-up commit.
|
|
|
|
|
|
|
|
broken)
ScryptedDevice.listen() takes a single ScryptedInterface | string |
EventListenerOptions, not an array. We were passing
[BinarySensor, MotionSensor, ObjectDetector] which stringifies to a
comma-joined garbage filter, leaking unrelated events (Settings, ...)
through and apparently dropping BinarySensor entirely — bell rings
never surfaced. Also drops the child-device traversal: confirmed via
unifi-protect/src/main.ts that the bell BinarySensor lives on the
camera device itself for doorbells.
|
|
|
|
|
|
|
|
|
|
|
|
diagnostic
|
|
|