The Status panel's Low / High thresholds readout was stuck at 0.38 /
0.78 regardless of the Sensitivity dropdown, so the setting looked
dead. Detection itself was correct -- classify() always used the real
sensitivity -- but get_status() computed the displayed thresholds with
a hardcoded thresholds("medium").
get_status() now takes an optional sensitivity argument and the
/obico/status route passes settings["sensitivity"] (it already loads
settings fresh). The readout updates as soon as the change is saved.
Obico polling could freeze the live camera stream within seconds of opening
the viewer. Cause: when the buffer-reuse path in obico_detection._capture_frame
saw an empty _last_frames[printer_id] entry (stream startup before the first
JPEG lands, or upstream mid-reconnect after a 30s read timeout), it fell
through to capture_camera_frame_bytes() and opened a second RTSP socket. On
firmwares that allow only one camera connection, that second socket forced
the printer to drop the live fan-out connection - the viewer's ffmpeg then
hit its own 30s timeout, looped through 30 reconnects at 0.2s, all racing
the next Obico poll, and the broadcaster pump exited.
Widen the gate from "do we have a buffered frame?" to "is any fan-out stream
registered for this printer?". New is_stream_active() helper checks
_active_streams / _active_chamber_streams independently of buffer state.
_capture_frame consults it first: if a viewer is attached, it returns the
buffered frame when available or None (skip this poll cycle) when not. Never
opens a competing socket while a viewer is connected.
Cost: at most one missed Obico detection cycle per viewer-attach (~10s lag).
Benefit: zero competing-socket events while any viewer is connected.
try_get_active_buffered_frame() refactored to delegate to is_stream_active()
so the two helpers stay in lockstep. The /camera/snapshot caller is unchanged
behaviorally (snapshot is a user-initiated single-shot; falling through to
fresh capture on an empty buffer is the desired behavior there).
The MJPEG fan-out broadcaster from #1089 only solved viewer-side
concurrency. Obico polling (every 5s) and the manual /camera/snapshot
endpoint kept opening their own fresh RTSP sockets, which X1/H2/P2
firmwares tolerated but X2D firmware 01.01.00.00 enforces strict
single-connection on — every poll kicked the live stream.
Add try_get_active_buffered_frame(printer_id): returns the broadcaster's
last buffered frame when a viewer is connected, None otherwise. Obico
and /camera/snapshot consult it before opening a fresh socket. When no
viewer is active they fall through to the existing fresh-capture path.
plate_detection and layer_timelapse intentionally not converted.
The 0.2.3b4 #1003 "fix" POSTed JPEG bytes as multipart form data,
but Obico's /p/ endpoint is declared methods=['GET'] upstream and
reads ?img=URL from the query string. Every POST was 405'd by
Flask's router before any handler ran, which is why the Obico
container logs were silent while Bambuddy kept reporting
"ML API call failed for printer N:" with a blank suffix —
raise_for_status() on the 405 produced an exception whose str()
rendered empty.
Restored the pre-#1003 nonce-URL approach (commit 3e434458):
capture locally with a 20s timeout we control, stash the JPEG
under a single-use 32-byte nonce, hand Obico a
GET /api/v1/obico/cached-frame/{nonce} URL that resolves in
<50ms so its hardcoded 5s read timeout never races RTSP.
Also guards against future silent exceptions: the error format
now falls back to type(exc).__name__ when str(exc) is empty.
Detection also early-returns with an explicit error if
external_url is unset instead of handing Obico a URL it can't
resolve.
The #1003 reverse-proxy scenario (Authelia/Authentik/CF Access
in front of Bambuddy) is addressed by documenting that the
/api/v1/obico/cached-frame/ path must be whitelisted from
external auth at the proxy layer — it is already public on
Bambuddy's side.
Backend: services/obico_detection.py, api/routes/obico.py,
main.py (PUBLIC_API_PATTERNS).
Frontend: FailureDetectionSettings banner + client.ts type +
all 7 locales restored.
Tests: 15 unit + 5 integration tests pass.
The ML API previously called back into Bambuddy to fetch snapshots,
which failed behind reverse proxies with external auth (Authelia, etc.).
Now the detection loop captures the JPEG locally and POSTs it directly
as multipart form data — no callback URL, no nonce cache, no
external_url dependency.
Obico's ML API has a hardcoded 5s read timeout on the URL it fetches, which
our /camera/snapshot regularly exceeds on cold calls (TLS proxy + ffmpeg +
RTSP keyframe wait). The detection loop now captures the JPEG locally with
a 20s timeout we control, stashes the bytes under a single-use 32-byte
nonce, and hands Obico a new /api/v1/obico/cached-frame/{nonce} URL that
returns the cached bytes instantly. The 5s ceiling is no longer a factor.
The nonce is the credential (URL-safe, 256 bits of entropy, single-use,
30s TTL) so the endpoint can be unauthenticated without widening the
camera access surface. Replaces the previous camera-stream-token snapshot
URL approach, which remained vulnerable to the upstream 5s timeout even
when auth was disabled.
Thanks to @fblix for the detailed reproducer with timeout numbers.
surfaced it as "Failed to get image", which Bambuddy reported back as a 400.
Fix: the snapshot endpoint already accepts a reusable camera-stream token (the same mechanism used by <img>-based camera consumers, since browsers can't send auth headers
on image loads). The detection service now appends that token to the URL it gives the ML API. The token is cached on the service, refreshed 5 min before its 60-min expiry,
and is simply ignored when Bambuddy auth is disabled — so no behavior change for users without auth.
Refs #172
Adds a Failure Detection tab under Settings that wires Bambuddy to a
self-hosted Obico ml_api container — no cloud, no account, no WebSocket.
While a print is running, the detection service periodically hands the
printer's camera snapshot URL to the ML API and smooths scores over
time (30-frame warmup + EWM, alpha=2/13, short/long rolling means) so
one noisy frame can't trigger an action. When the smoothed score
crosses HIGH, the configured action fires exactly once per print:
notify, pause, or pause-and-cut-power (via linked smart plugs).
- Backend: new obico_detection + obico_smoothing + obico_actions
services, /obico/status and /obico/test-connection routes
(SETTINGS_READ / SETTINGS_UPDATE), six obico_* AppSettings fields
with validators for sensitivity/action/enabled_printers.
- Frontend: FailureDetectionSettings component (enable, ML URL + test,
sensitivity, action, poll interval, per-printer monitor list, live
status + detection history), new sidebar tab with service-active
bullet, toast on save.
- Tests: 17 detection unit tests + 15 smoothing unit tests + 4
frontend component tests.
- Docs: README bullet, CHANGELOG entry, wiki page under Analytics,
website features.html entry.