Skip to content

Commit 4e1c23d

Browse files
committed
refactor: better error reporting
1 parent 13f3362 commit 4e1c23d

File tree

4 files changed

+32
-44
lines changed

4 files changed

+32
-44
lines changed

src/arduino/app_peripherals/camera/base_camera.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ def start(self) -> None:
9595
self._is_started = True
9696
self._last_capture_time = time.monotonic()
9797
self.logger.info(f"Successfully started {self.name}")
98+
except CameraOpenError as e: # We consider this a fatal error so we don't retry
99+
self.logger.error(f"Fatal error while starting {self.name}: {e}")
100+
raise
98101
except Exception as e:
99102
if not self.auto_reconnect:
100103
raise
@@ -106,7 +109,8 @@ def start(self) -> None:
106109

107110
delay = min(self.auto_reconnect_delay * (2 ** (attempt - 1)), 60) # Exponential backoff
108111
self.logger.warning(
109-
f"Failed to start camera {self.name} (attempt {attempt}/{self.first_connection_max_retries}). Retrying in {delay:.1f}s..."
112+
f"Failed attempt {attempt}/{self.first_connection_max_retries} at starting camera {self.name}: {e}. "
113+
f"Retrying in {delay:.1f}s..."
110114
)
111115
time.sleep(delay)
112116

src/arduino/app_peripherals/camera/ip_camera.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,22 +82,17 @@ def _open_camera(self) -> None:
8282
try:
8383
self._cap = cv2.VideoCapture(url)
8484
if not self._cap.isOpened():
85-
raise CameraOpenError(f"Failed to open IP camera at {self.url}")
85+
raise RuntimeError(f"Failed to open IP camera at {self.url}")
8686

8787
self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer to minimize latency
8888

8989
# Test by reading one frame
9090
ret, frame = self._cap.read()
9191
if not ret and frame is None:
92-
raise CameraOpenError(f"Read test failed for IP camera at {self.url}")
92+
raise RuntimeError(f"Read test failed for IP camera at {self.url}")
9393

9494
self._set_status("connected", {"camera_url": self.url})
9595

96-
except CameraOpenError:
97-
if self._cap is not None:
98-
self._cap.release()
99-
self._cap = None
100-
raise
10196
except Exception as e:
10297
logger.error(f"Unexpected error opening IP camera at {self.url}: {e}")
10398
if self._cap is not None:
@@ -130,10 +125,10 @@ def _test_http_connectivity(self) -> None:
130125
response = requests.head(self.url, auth=auth, timeout=self.timeout, allow_redirects=True)
131126

132127
if response.status_code not in [200, 206]: # 206 for partial content
133-
raise CameraOpenError(f"HTTP camera returned status {response.status_code}: {self.url}")
128+
raise RuntimeError(f"HTTP camera returned status {response.status_code}: {self.url}")
134129

135130
except requests.RequestException as e:
136-
raise CameraOpenError(f"Cannot connect to HTTP camera {self.url}: {e}")
131+
raise RuntimeError(f"Cannot connect to HTTP camera {self.url}: {e}")
137132

138133
def _close_camera(self) -> None:
139134
"""Close the IP camera connection."""

src/arduino/app_peripherals/camera/v4l_camera.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,12 @@ def _open_camera(self) -> None:
152152
self._close_camera()
153153

154154
if not os.path.exists(self.v4l_path):
155-
raise CameraOpenError(f"No device found at {self.v4l_path}")
155+
raise RuntimeError(f"No device found at {self.v4l_path}")
156156

157157
try:
158158
self._cap = cv2.VideoCapture(self.v4l_path)
159159
if not self._cap.isOpened():
160-
raise CameraOpenError(f"Failed to open camera {self.name}")
160+
raise RuntimeError(f"Failed to open camera {self.name}")
161161

162162
self._cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # Reduce buffer to minimize latency
163163

@@ -186,15 +186,10 @@ def _open_camera(self) -> None:
186186
# Verify camera with a test read
187187
ret, frame = self._cap.read()
188188
if not ret and frame is None:
189-
raise CameraOpenError(f"Read test failed for camera {self.name}")
189+
raise RuntimeError(f"Read test failed for camera {self.name}")
190190

191191
self._set_status("connected", {"camera_name": self.name, "camera_path": self.v4l_path})
192192

193-
except CameraOpenError:
194-
if self._cap is not None:
195-
self._cap.release()
196-
self._cap = None
197-
raise
198193
except Exception as e:
199194
logger.error(f"Unexpected error opening camera {self.name}: {e}")
200195
if self._cap is not None:

src/arduino/app_peripherals/camera/websocket_camera.py

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import websockets
1414
import asyncio
1515
from collections.abc import Callable
16-
from concurrent.futures import CancelledError, TimeoutError
16+
from concurrent.futures import CancelledError, TimeoutError, Future
1717

1818
from arduino.app_internal.core.peripherals import BPPCodec
1919
from arduino.app_utils import Logger
@@ -138,37 +138,33 @@ def security_mode(self) -> str:
138138

139139
def _open_camera(self) -> None:
140140
"""Start the WebSocket server."""
141-
self._server_thread = threading.Thread(target=self._start_server_thread, daemon=True)
142-
self._server_thread.start()
143-
144-
# Wait for server to start
145-
start_time = time.time()
146-
while time.time() - start_time < self.timeout:
147-
if self._server is not None:
148-
self.logger.info(f"WebSocket camera server started on {self.url}, security: {self.security_mode}")
149-
return
150-
time.sleep(0.1)
141+
server_future = Future()
151142

152-
# Cleanup server thread if it failed to start in time
153-
if self._server_thread.is_alive():
154-
self._server_thread.join(timeout=1.0)
155-
156-
raise CameraOpenError(f"Failed to start WebSocket server on {self.url}")
143+
self._server_thread = threading.Thread(target=self._start_server_thread, args=(server_future,), daemon=True)
144+
self._server_thread.start()
157145

158-
def _start_server_thread(self) -> None:
146+
try:
147+
server_future.result(timeout=self.timeout)
148+
self.logger.info(f"WebSocket camera server available on {self.url}, security: {self.security_mode}")
149+
except (PermissionError, Exception) as e:
150+
if self._server_thread.is_alive():
151+
self._server_thread.join(timeout=1.0)
152+
if isinstance(e, PermissionError):
153+
raise CameraOpenError(f"Permission denied when attempting to bind WebSocket server on {self.url}")
154+
raise
155+
156+
def _start_server_thread(self, future: Future) -> None:
159157
"""Run WebSocket server in its own thread with event loop."""
160158
try:
161159
self._loop = asyncio.new_event_loop()
162160
asyncio.set_event_loop(self._loop)
163-
self._loop.run_until_complete(self._start_server())
164-
except Exception as e:
165-
self.logger.error(f"WebSocket server thread error: {e}")
161+
self._loop.run_until_complete(self._start_server(future))
166162
finally:
167163
if self._loop and not self._loop.is_closed():
168164
self._loop.close()
169165
self._loop = None
170166

171-
async def _start_server(self) -> None:
167+
async def _start_server(self, future: Future) -> None:
172168
"""Start the WebSocket server."""
173169
try:
174170
self._server = await asyncio.wait_for(
@@ -191,14 +187,12 @@ async def _start_server(self) -> None:
191187
server_socket = list(self._server.sockets)[0]
192188
self.port = server_socket.getsockname()[1]
193189

190+
future.set_result(True)
191+
194192
await self._server.wait_closed()
195193

196-
except TimeoutError as e:
197-
self.logger.error(f"Failed to start WebSocket server within {self.timeout}s: {e}")
198-
raise
199194
except Exception as e:
200-
self.logger.error(f"Failed to start WebSocket server: {e}")
201-
raise
195+
future.set_exception(e)
202196
finally:
203197
self._server = None
204198

0 commit comments

Comments
 (0)