mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 11:47:46 +02:00
Reporter on a P2S running firmware 01.02.00.00 saw the camera connect
for a few seconds then time out, repeating. P1S on the same install
worked fine — different protocol (chamber-image port 6000 vs RTSP via
ffmpeg).
The P2S RTSP path was running ffmpeg with `-probesize 32
-analyzeduration 0`, tuned for X1/H2 fast startup. The P2S's slower
keyframe pacing means ffmpeg can't lock onto the stream within 32
bytes — its own stderr says "consider increasing probesize" before
giving up after ~2s. Bambuddy reconnects, cycle repeats.
Instead of bumping the globals (which would regress every other RTSP
model's startup latency), this lifts the per-model tuning into a new
`camera_profiles` registry. CameraProfile dataclass holds the
previously-global knobs (probesize, analyzeduration, rtsp_reconnect_max,
rtsp_reconnect_delay, plus an extra_ffmpeg_input_args hook for future
per-model flags). get_camera_profile(model) returns the model's profile
or DEFAULT_PROFILE.
Default profile preserves the historical X1/H2 fast-startup values
verbatim — X1, X1C, X1E, X2D, H2C, H2D, H2D Pro, H2S all see no
behaviour change. P2S is the only override:
P2S: probesize=1_000_000, analyzeduration=500_000
SSDP internal codes (N7→P2S) resolve via an alias map so the camera
path works during the early-connect window before the display name
is settled.
This is the first step of the camera-architecture overhaul agreed
after #1395. Adding the next quirky model is a config entry, not
another module-level constant.
112 lines
4.6 KiB
Python
112 lines
4.6 KiB
Python
"""Per-printer-model camera tuning knobs.
|
|
|
|
Bambuddy talks to multiple Bambu Lab printer models that all expose a
|
|
camera but in subtly different ways:
|
|
|
|
- **Chamber image** (port 6000, proprietary binary protocol) — A1, A1
|
|
Mini, P1P, P1S. Frame pacing and TLS quirks are firmware-driven and
|
|
don't go through ffmpeg.
|
|
- **RTSPS** (port 322) — X1 series, X2D, H2 series, P2S. Wrapped by a
|
|
local TLS proxy + ffmpeg to MJPEG.
|
|
|
|
The RTSPS path used to live with hard-coded module constants in
|
|
``camera.py``: a single ``-probesize 32 -analyzeduration 0`` tuned for
|
|
X1/H2 fast startup. That breaks the P2S on firmware 01.02.00.00, whose
|
|
RTSP keyframe pacing is slow enough that ffmpeg can't lock onto the
|
|
stream within 32 bytes and gives up with "not enough frames to estimate
|
|
rate" (#1395 follow-up — Tschipel's reproduction).
|
|
|
|
This module replaces those module constants with per-model
|
|
:class:`CameraProfile` entries. Defaults match the historical pre-fix
|
|
behaviour, so existing models (X1, H2, X2D, X1E) keep their fast-
|
|
startup tuning unchanged. Quirky models override the relevant fields
|
|
only — the P2S entry below is the first example.
|
|
|
|
Adding a new model's quirk is a config edit (an entry in ``_PROFILES``
|
|
plus the alias for its internal SSDP code if needed), not another
|
|
hard-coded global constant.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CameraProfile:
|
|
"""Tuning knobs for one printer model's camera path.
|
|
|
|
All defaults reflect the historical X1/H2 behaviour (fast startup,
|
|
minimal probing). Models with quirky firmware override individual
|
|
fields rather than re-defining the whole profile.
|
|
"""
|
|
|
|
# --- RTSPS / ffmpeg path -------------------------------------------------
|
|
# ffmpeg's `-probesize` (bytes). Smaller = lower startup latency but
|
|
# less margin to lock onto a stream whose first keyframe is delayed
|
|
# or whose container metadata is incomplete. P2S 01.02.00.00 needs a
|
|
# full MB to lock; X1/H2 lock within ~32 bytes.
|
|
probesize: int = 32
|
|
# ffmpeg's `-analyzeduration` (microseconds). 0 = skip format
|
|
# analysis entirely. Same trade-off as probesize.
|
|
analyzeduration: int = 0
|
|
# Max consecutive ffmpeg respawns when the printer drops the RTSP
|
|
# session mid-stream. Some firmwares cut the stream after a few
|
|
# seconds (originally noted on P2S), so we transparently respawn
|
|
# to keep the MJPEG client alive.
|
|
rtsp_reconnect_max: int = 30
|
|
# Seconds between ffmpeg respawn attempts.
|
|
rtsp_reconnect_delay: float = 0.2
|
|
|
|
# --- Extra ffmpeg input args ---------------------------------------------
|
|
# Hook for future per-model knobs (e.g. `-fflags` overrides) without
|
|
# changing the dataclass shape. Tuple, not list, so the dataclass
|
|
# stays hashable / frozen-friendly.
|
|
extra_ffmpeg_input_args: tuple[str, ...] = field(default_factory=tuple)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Profile registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Default profile = historical X1/H2 fast-startup behaviour. Used for
|
|
# every RTSP-capable model that doesn't have an entry in ``_PROFILES``.
|
|
DEFAULT_PROFILE = CameraProfile()
|
|
|
|
# Per-model overrides. Keys are uppercase display names (e.g. "P2S")
|
|
# AFTER alias normalisation, so internal SSDP codes ("N7") resolve via
|
|
# ``_MODEL_ALIASES`` below.
|
|
_PROFILES: dict[str, CameraProfile] = {
|
|
# P2S firmware 01.02.00.00 RTSP keyframe pacing is slow enough that
|
|
# ffmpeg's "32-byte probe + zero analyze" combo can't estimate the
|
|
# frame rate. ffmpeg's own stderr literally says "consider increasing
|
|
# probesize" (#1395 follow-up).
|
|
"P2S": CameraProfile(
|
|
probesize=1_000_000,
|
|
analyzeduration=500_000,
|
|
),
|
|
}
|
|
|
|
# SSDP internal codes that should resolve to a display-name profile.
|
|
# Display-name lookup is the canonical path; this just lets the camera
|
|
# code pass through whatever ``Printer.model`` carries without each
|
|
# call site needing to know the code→name map.
|
|
_MODEL_ALIASES: dict[str, str] = {
|
|
"N7": "P2S", # P2S internal SSDP code
|
|
}
|
|
|
|
|
|
def get_camera_profile(model: str | None) -> CameraProfile:
|
|
"""Return the :class:`CameraProfile` for *model*, or the default.
|
|
|
|
``model`` can be either a display name (e.g. ``"P2S"``) or an
|
|
internal SSDP code (e.g. ``"N7"``). Unknown models fall back to
|
|
:data:`DEFAULT_PROFILE` so the camera path is never blocked on a
|
|
missing entry.
|
|
"""
|
|
if not model:
|
|
return DEFAULT_PROFILE
|
|
key = model.upper().strip()
|
|
key = _MODEL_ALIASES.get(key, key)
|
|
return _PROFILES.get(key, DEFAULT_PROFILE)
|