22#
33# SPDX-License-Identifier: MPL-2.0
44
5- import base64
65import json
6+ import base64
77import os
88import threading
9- import queue
109import time
10+ import queue
1111import numpy as np
1212import cv2
1313import websockets
1414import asyncio
1515from collections .abc import Callable
16+ from concurrent .futures import CancelledError , TimeoutError
1617
1718from arduino .app_internal .core .peripherals import BPPCodec
1819from arduino .app_utils import Logger
1920
20- from .camera import BaseCamera
21+ from .base_camera import BaseCamera
2122from .errors import CameraOpenError
2223
2324logger = 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