You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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.
Context
Every PTZ endpoint in
routes_control.pygoes through_acquire_or_409(pyro_camera_api/api/routes_control.py:536): it takesMOVE_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:
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;pyro_camera_api/api/routes_cameras.py:122, where_capture_implcallscam.capture(patrol_id=patrol_id). For Reolink that runsmove_camera("ToPos", idx=patrol_id)followed bysleep(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:
_sync_azimuth_from_posewrites the preset azimuth under the running handler, which then overwrites it with its own dead reckoning, so/control/azimuthreports a confident wrong value instead of null;movingstays 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:
ToPos, release it before the snapshot. Narrows the window without starving manual control, but the camera is still settling unlocked.last_images[patrol_id]instead of repositioning. Changes the freshness contract of the endpoint.patrol_idon the capture route entirely and require callers to use/control/goto_presetfirst, 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
/control/azimuthexpose whether a patrol is currently driving the camera, somovingstops being misleading.Priority: low. No known caller captures with a
patrol_idduring a live session, and the engine skips inference while a stream is active.Co authored with claude.