Skip to content

/cameras/capture?patrol_id= moves the camera outside the per-camera lock #395

Description

@fe51

Context

Every PTZ endpoint in routes_control.py goes through _acquire_or_409 (pyro_camera_api/api/routes_control.py:536): it takes MOVE_LOCKS[camera_ip] or returns 409, and holds it for the whole duration of a blocking move, including the server-side sleep.

That is what guarantees a single mouvement at a time per camera, and since PR #390 it is also what protects the dead-reckoned azimuth from being written by two places at once.

Two code paths issue PTZ commands without ever touching that lock:

  1. the patrol loop, pyro_camera_api/camera/patrol.py:85, already documented as a known limitation in feat(camera): azimuth tracking and move-to-azimuth for PTZ cameras #390;
  2. the capture route, pyro_camera_api/api/routes_cameras.py:122, where _capture_impl calls cam.capture(patrol_id=patrol_id). For Reolink that runs move_camera("ToPos", idx=patrol_id) followed by sleep(1) and a snapshot (camera/adapters/reolink.py:98-100).

The second one is worth tracking separately because it is not covered by "stop the patrol before manual control". It is an endpoint that reads like a read-only capture, is reachable by any client at any time, and takes no lock and performs no active-stream check. Consequences while a manual PTZ move is in flight:

  • the camera physically leaves the operator's target mid-move;
  • _sync_azimuth_from_pose writes the preset azimuth under the running handler, which then overwrites it with its own dead reckoning, so /control/azimuth reports a confident wrong value instead of null;
  • moving stays false throughout, so no client can detect any of it.

Why this is not a one-liner

The obvious fix, wrapping the capture in _acquire_or_409, is worse than the problem. cam.capture(patrol_id=...) takes one to three seconds end to end, so holding the lock for its duration would make every manual PTZ command fail with a 409 during each engine capture, which happens continuously during patrol-driven inference.

Options worth weighing:

  • acquire the lock only around the ToPos, release it before the snapshot. Narrows the window without starving manual control, but the camera is still settling unlocked.
  • non-blocking acquire: if the camera is busy, skip the move and serve last_images[patrol_id] instead of repositioning. Changes the freshness contract of the endpoint.
  • refuse patrol_id on the capture route entirely and require callers to use /control/goto_preset first, which is locked. Cleanest semantics, but it is a breaking change for existing callers.

The same reasoning applies to the patrol loop itself, which would need to hold the lock per pose and release it between poses.

Related

Priority: low. No known caller captures with a patrol_id during a live session, and the engine skips inference while a stream is active.

Co authored with claude.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions