Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
700ab97
add clidabration app
MateoLostanlen Mar 31, 2026
c745330
fix camera param
MateoLostanlen Mar 31, 2026
966be37
add micro-pulse fallback for small angle PTZ moves
MateoLostanlen Apr 1, 2026
0eda417
move timing server-side to avoid VPN latency
MateoLostanlen Apr 1, 2026
93ef647
switch to auto detect
MateoLostanlen Apr 1, 2026
7aa648e
run all zooms
MateoLostanlen Apr 1, 2026
5453dd0
clean calibration
MateoLostanlen Apr 2, 2026
c1d75fb
recalibrate PTZ speed tables with server-side timing for both cameras
MateoLostanlen Apr 2, 2026
113f45e
fix ruff warnings in routes_control
MateoLostanlen Apr 2, 2026
c46c6c1
update calibration app: fetch speed tables from API, fix click-to-move
MateoLostanlen Apr 2, 2026
a21c10c
move FOV tables to fov_tables.json
MateoLostanlen Apr 2, 2026
e9bba63
align client
MateoLostanlen Apr 2, 2026
afd82eb
no zoom during click to move
MateoLostanlen Apr 2, 2026
a07e09f
add zoom-aware speed limiting and click_to_move to client
MateoLostanlen Apr 2, 2026
c2a9b83
use top speed for large angles beyond max_duration
MateoLostanlen Apr 15, 2026
bf4e4cc
simplify click_to_move to normalized coords and read zoom from camera
MateoLostanlen Apr 15, 2026
ee39e1a
Merge branch 'develop' into calibrate_reolink_cam
MateoLostanlen Apr 16, 2026
f3acb84
use pyro_camera_api_client in ptz calibration app
MateoLostanlen Apr 16, 2026
22be830
lock per-camera during click_to_move and zoom, return 409 if busy
MateoLostanlen Apr 16, 2026
5fcc21b
add livestream streamlit app with stop patrol, start stream, zoom and…
MateoLostanlen Apr 16, 2026
bd05ee1
lock move_camera in blocking modes (duration, degrees)
MateoLostanlen Apr 16, 2026
7061a19
add focused PTZ routes (goto_preset, start_move, stop_move, move_for_…
MateoLostanlen Apr 16, 2026
6a1340f
address PTZ review: adapter fallback, zoom-from-camera on legacy /mov…
MateoLostanlen Apr 16, 2026
d1d772f
make /stop preempt in-flight blocking PTZ handlers via cancellation e…
MateoLostanlen Apr 16, 2026
a9aa616
route click_to_move and zoom through _acquire_or_409 so stale stop ev…
MateoLostanlen Apr 16, 2026
187f959
docs(tools): document livestream app, FOV calibration, full speed tables
MateoLostanlen Apr 16, 2026
4d1eff1
move_by_degrees: auto-pick speed by default, reject invalid explicit …
MateoLostanlen Apr 16, 2026
9c1a00d
fix ruff/mypy and update move_by_degrees docstring to match hard-fail…
MateoLostanlen Apr 16, 2026
c8170ad
style: apply ruff formatting to livestream_app
MateoLostanlen Apr 16, 2026
e24f443
Merge branch 'develop' into calibrate_reolink_cam
MateoLostanlen May 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions pyro_camera_api/client/pyro_camera_api_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
from __future__ import annotations

import io
import logging
from typing import Any, Dict, List, Optional

import requests
from PIL import Image

logger = logging.getLogger(__name__)


class PyroCameraAPIClient:
def __init__(
Expand Down Expand Up @@ -174,21 +177,114 @@
speed: int = 10,
pose_id: Optional[int] = None,
degrees: Optional[float] = None,
duration: Optional[float] = None,
zoom: int = 0,
) -> Dict[str, Any]:
"""Legacy overloaded move endpoint. Prefer goto_preset / start_move /

Check notice on line 183 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L183

1 blank line required between summary line and description (found 0) (D205)

Check notice on line 183 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L183

First line should end with a period, question mark, or exclamation point (not '/') (D415)

Check notice on line 183 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L183

Multi-line docstring closing quotes should be on a separate line (D209)

Check notice on line 183 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L183

Multi-line docstring summary should start at the second line (D213)
stop_move / move_for_duration / move_by_degrees, which are the
focused replacements."""
if zoom > 0 and speed != 1:
logger.warning("zoom=%s > 0: speed will be forced to 1 server-side (requested %s)", zoom, speed)
params: Dict[str, Any] = {
"camera_ip": camera_ip,
"speed": speed,
"zoom": zoom,
}
if direction is not None:
params["direction"] = direction
if pose_id is not None:
params["pose_id"] = pose_id
if degrees is not None:
params["degrees"] = degrees
if duration is not None:
params["duration"] = duration

resp = self._request("POST", "/control/move", params=params)
return resp.json()

# ------------------------------------------------------------------
# Focused PTZ actions (preferred over move_camera)
# ------------------------------------------------------------------

def goto_preset(self, camera_ip: str, pose_id: int, speed: int = 50) -> Dict[str, Any]:
"""Move to a configured preset pose. Returns immediately."""
params = {"camera_ip": camera_ip, "pose_id": pose_id, "speed": speed}
resp = self._request("POST", "/control/goto_preset", params=params)
return resp.json()

def start_move(self, camera_ip: str, direction: str, speed: int = 10) -> Dict[str, Any]:
"""Start a continuous move; caller must call stop_move to halt."""
params = {"camera_ip": camera_ip, "direction": direction, "speed": speed}
resp = self._request("POST", "/control/start_move", params=params)
return resp.json()

def stop_move(self, camera_ip: str) -> Dict[str, Any]:
"""Halt any current movement."""
resp = self._request("POST", f"/control/stop_move/{camera_ip}")
return resp.json()

def move_for_duration(
self,
camera_ip: str,
direction: str,
duration: float,
speed: int = 10,
) -> Dict[str, Any]:
"""Move for a fixed wall-clock duration (seconds), then stop.

Check notice on line 233 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L233

1 blank line required between summary line and description (found 0) (D205)

Check notice on line 233 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L233

Multi-line docstring closing quotes should be on a separate line (D209)

Check notice on line 233 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L233

Multi-line docstring summary should start at the second line (D213)
Server holds a per-camera lock; raises on 409 if busy."""
params = {
"camera_ip": camera_ip,
"direction": direction,
"duration": duration,
"speed": speed,
}
# Allow the server-side sleep + a small margin.
req_timeout = max(self.timeout, duration + 5.0) if duration else self.timeout
resp = self._request("POST", "/control/move_for_duration", params=params, timeout=req_timeout)
return resp.json()

def move_by_degrees(
self,
camera_ip: str,
direction: str,
degrees: float,
speed: Optional[int] = None,
) -> Dict[str, Any]:
"""Move by an approximate angle using the server's calibrated speed

Check notice on line 253 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L253

1 blank line required between summary line and description (found 0) (D205)

Check notice on line 253 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L253

First line should end with a period, question mark, or exclamation point (not 'd') (D415)

Check notice on line 253 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L253

Multi-line docstring closing quotes should be on a separate line (D209)

Check notice on line 253 in pyro_camera_api/client/pyro_camera_api_client/client.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pyro_camera_api/client/pyro_camera_api_client/client.py#L253

Multi-line docstring summary should start at the second line (D213)
table. When ``speed`` is omitted the server auto-picks the best
calibrated level for the target angle (preferred). Server reads
current zoom and force-limits speed to 1 at zoom > 0. Raises on
409 if the camera is busy."""
params: Dict[str, Any] = {
"camera_ip": camera_ip,
"direction": direction,
"degrees": degrees,
}
if speed is not None:
params["speed"] = speed
resp = self._request("POST", "/control/move_by_degrees", params=params, timeout=30.0)
return resp.json()

def click_to_move(
self,
camera_ip: str,
click_x: float,
click_y: float,
) -> Dict[str, Any]:
"""click_x and click_y are normalized coordinates in [0, 1]."""
params: Dict[str, Any] = {
"camera_ip": camera_ip,
"click_x": click_x,
"click_y": click_y,
}
resp = self._request("POST", "/control/click_to_move", params=params, timeout=30.0)
return resp.json()

def get_speed_tables(self, camera_ip: str) -> Dict[str, Any]:
params = {"camera_ip": camera_ip}
resp = self._request("GET", "/control/speed_tables", params=params)
return resp.json()

def stop_camera(self, camera_ip: str) -> Dict[str, Any]:
resp = self._request("POST", f"/control/stop/{camera_ip}")
return resp.json()
Expand Down
182 changes: 182 additions & 0 deletions pyro_camera_api/pyro_camera_api/api/fov_tables.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
{
"h_fov": {
"reolink-823S2": [
54.2,
52.206,
50.405,
48.356,
46.167,
44.183,
42.63,
41.058,
39.117,
37.523,
35.393,
33.804,
32.341,
30.742,
29.446,
27.829,
26.394,
24.992,
23.604,
22.136,
20.948,
19.675,
18.652,
17.794,
16.352,
15.273,
14.278,
13.287,
12.577,
11.681,
10.832,
9.992,
9.298,
8.644,
8.022,
7.411,
6.84,
6.323,
5.793,
5.303,
4.787,
4.183
],
"reolink-823A16": [
54.2,
52.029,
50.146,
47.986,
46.384,
44.431,
42.376,
40.915,
38.623,
37.135,
35.303,
33.894,
32.273,
30.703,
29.167,
27.67,
26.181,
24.921,
23.489,
22.138,
20.887,
19.701,
18.467,
17.618,
16.244,
15.203,
14.174,
13.242,
12.332,
11.606,
10.771,
9.993,
9.283,
8.558,
7.914,
7.321,
6.777,
6.241,
5.744,
5.229,
4.704,
4.118
]
},
"v_fov": {
"reolink-823S2": [
41.7,
40.166,
38.78,
37.204,
35.52,
33.993,
32.799,
31.589,
30.096,
28.869,
27.23,
26.008,
24.882,
23.652,
22.655,
21.411,
20.307,
19.229,
18.16,
17.031,
16.117,
15.138,
14.351,
13.69,
12.581,
11.751,
10.985,
10.223,
9.676,
8.987,
8.334,
7.687,
7.154,
6.651,
6.172,
5.702,
5.263,
4.865,
4.457,
4.08,
3.683,
3.219
],
"reolink-823A16": [
41.7,
40.03,
38.581,
36.919,
35.686,
34.184,
32.603,
31.479,
29.716,
28.571,
27.161,
26.077,
24.83,
23.622,
22.44,
21.289,
20.143,
19.174,
18.072,
17.032,
16.07,
15.157,
14.208,
13.555,
12.498,
11.697,
10.905,
10.188,
9.488,
8.929,
8.287,
7.688,
7.142,
6.584,
6.089,
5.633,
5.214,
4.802,
4.42,
4.023,
3.619,
3.169
]
}
}
Loading
Loading