Skip to content

Commit 994bc6a

Browse files
committed
refactor: align websocket camera with other peripheral implementations
1 parent d8256a1 commit 994bc6a

File tree

2 files changed

+30
-27
lines changed

2 files changed

+30
-27
lines changed

src/arduino/app_bricks/camera_code_detection/detection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def __init__(
6868
self._detect_qr = detect_qr
6969
self._detect_barcode = detect_barcode
7070

71-
# These callbacks do not require locks as long as we're running on CPython
71+
# These callbacks don't require locking as long as we're running on CPython
7272
self._on_frame_cb = None
7373
self._on_error_cb = None
7474

src/arduino/app_peripherals/camera/websocket_camera.py

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,23 @@
22
#
33
# SPDX-License-Identifier: MPL-2.0
44

5-
import base64
65
import json
6+
import base64
77
import os
88
import threading
9-
import queue
109
import time
10+
import queue
1111
import numpy as np
1212
import cv2
1313
import websockets
1414
import asyncio
1515
from collections.abc import Callable
16+
from concurrent.futures import CancelledError, TimeoutError
1617

1718
from arduino.app_internal.core.peripherals import BPPCodec
1819
from arduino.app_utils import Logger
1920

20-
from .camera import BaseCamera
21+
from .base_camera import BaseCamera
2122
from .errors import CameraOpenError
2223

2324
logger = Logger("WebSocketCamera")
@@ -36,13 +37,12 @@ class WebSocketCamera(BaseCamera):
3637
- WebP
3738
- BMP
3839
- TIFF
40+
The video frames must then be serialized in the binary format supported by BPPCodec.
3941
4042
Secure communication with the WebSocket server is supported in three security modes:
4143
- Security disabled (empty secret)
4244
- Authenticated (secret + enable_encryption=False) - HMAC-SHA256
4345
- Authenticated + Encrypted (secret + enable_encryption=True) - ChaCha20-Poly1305
44-
45-
The frames can be serialized in one of the formats supported by BPPCodec.
4646
"""
4747

4848
def __init__(
@@ -60,15 +60,14 @@ def __init__(
6060
Initialize WebSocket camera server with security options.
6161
6262
Args:
63-
port: Port to bind the server to
64-
timeout: Connection timeout in seconds
65-
frame_format: Expected frame format ("binary", "json")
66-
secret: Secret key for authentication/encryption (empty = security disabled)
67-
enable_encryption: Enable encryption (only effective if secret is provided)
68-
resolution: Resolution as (width, height)
69-
fps: Frames per second to capture
70-
adjustments: Function to adjust frames
71-
auto_reconnect: Enable automatic reconnection on failure
63+
port (int): Port to bind the server to
64+
timeout (int): Connection timeout in seconds
65+
secret (str): Secret key for authentication/encryption (empty = security disabled)
66+
enable_encryption (bool): Enable encryption (only effective if secret is provided)
67+
resolution (tuple[int, int]): Resolution as (width, height)
68+
fps (int): Frames per second to capture
69+
adjustments (Callable[[np.ndarray], np.ndarray] | None): Function to adjust frames
70+
auto_reconnect (bool): Enable automatic reconnection on failure
7271
"""
7372
super().__init__(resolution, fps, adjustments, auto_reconnect)
7473

@@ -187,7 +186,7 @@ async def _ws_handler(self, conn: websockets.ServerConnection) -> None:
187186
try:
188187
rejection = json.dumps({"error": "Server busy", "message": "Only one client connection allowed at a time", "code": 1000})
189188
await self._send_to_client(rejection, client=conn)
190-
await conn.close(code=1000, reason="Server busy")
189+
await conn.close(code=1000, reason="Server busy, only one client allowed")
191190
except Exception as e:
192191
self.logger.warning(f"Failed to send rejection message to {client_addr}: {e}")
193192
return
@@ -199,14 +198,14 @@ async def _ws_handler(self, conn: websockets.ServerConnection) -> None:
199198
self.logger.debug(f"Client connected: {client_addr}")
200199

201200
try:
201+
# Send welcome message
202202
try:
203-
# Send welcome message
204203
welcome = json.dumps({
205204
"status": "connected",
206205
"message": "Connected to camera server",
206+
"security_mode": self.security_mode,
207207
"resolution": self.resolution,
208208
"fps": self.fps,
209-
"security_mode": self.security_mode,
210209
})
211210
await self._send_to_client(welcome)
212211
except Exception as e:
@@ -239,6 +238,7 @@ async def _ws_handler(self, conn: websockets.ServerConnection) -> None:
239238
if self._client == conn:
240239
self._client = None
241240
self._set_status("disconnected", {"client_address": client_addr})
241+
self.logger.debug(f"Client removed: {client_addr}")
242242

243243
def _parse_message(self, message: websockets.Data) -> np.ndarray | None:
244244
if isinstance(message, str):
@@ -250,7 +250,7 @@ def _parse_message(self, message: websockets.Data) -> np.ndarray | None:
250250

251251
decoded = self.codec.decode(message)
252252
if decoded is None:
253-
self.logger.warning("Failed to decode/authenticate message")
253+
self.logger.warning("Failed to decode message")
254254
return None
255255

256256
nparr = np.frombuffer(decoded, np.uint8)
@@ -259,13 +259,16 @@ def _parse_message(self, message: websockets.Data) -> np.ndarray | None:
259259

260260
def _close_camera(self):
261261
"""Stop the WebSocket server."""
262-
# Only attempt cleanup if the event loop is running
263262
if self._loop and not self._loop.is_closed() and self._loop.is_running():
264263
try:
265264
future = asyncio.run_coroutine_threadsafe(self._stop_and_disconnect_client(), self._loop)
266265
future.result(1.0)
266+
except CancelledError:
267+
self.logger.debug(f"Error stopping WebSocket server: CancelledError")
268+
except TimeoutError:
269+
self.logger.debug(f"Error stopping WebSocket server: TimeoutError")
267270
except Exception as e:
268-
self.logger.warning(f"Failed to stop WebSocket server cleanly: {e}")
271+
self.logger.warning(f"Error stopping WebSocket server: {e}")
269272

270273
# Wait for server thread to finish
271274
if self._server_thread and self._server_thread.is_alive():
@@ -300,18 +303,18 @@ async def _stop_and_disconnect_client(self):
300303
self._stop_event.set()
301304

302305
def _read_frame(self) -> np.ndarray | None:
303-
"""Read a frame from the queue."""
306+
"""Read a single frame from the queue."""
304307
try:
305308
return self._frame_queue.get(timeout=0.1)
306309
except queue.Empty:
307310
return None
308311

309-
async def _send_to_client(self, data: bytes | str, client: websockets.ServerConnection | None = None):
310-
"""Send secure message to connected client."""
311-
if isinstance(data, str):
312-
data = data.encode()
312+
async def _send_to_client(self, message: bytes | str, client: websockets.ServerConnection | None = None):
313+
"""Send a message to the connected client."""
314+
if isinstance(message, str):
315+
message = message.encode()
313316

314-
encoded = self.codec.encode(data)
317+
encoded = self.codec.encode(message)
315318

316319
# Keep a ref to current client to avoid locking
317320
client = client or self._client

0 commit comments

Comments
 (0)