22#
33# SPDX-License-Identifier: MPL-2.0
44
5- from arduino .app_utils import brick , Logger
6- from arduino .app_internal .core import load_brick_compose_file , resolve_address
7- from arduino .app_internal .core import EdgeImpulseRunnerFacade
85import time
6+ import json
7+ import inspect
98import threading
9+ import socket
1010from typing import Callable
11+
1112from websockets .sync .client import connect , ClientConnection
1213from websockets .exceptions import ConnectionClosedOK , ConnectionClosedError
13- import json
14- import inspect
14+
15+ from arduino .app_peripherals .camera import Camera
16+ from arduino .app_internal .core import load_brick_compose_file , resolve_address
17+ from arduino .app_internal .core import EdgeImpulseRunnerFacade
18+ from arduino .app_utils .image .adjustments import compress_to_jpeg
19+ from arduino .app_utils import brick , Logger
1520
1621logger = Logger ("VideoObjectDetection" )
1722
@@ -30,16 +35,19 @@ class VideoObjectDetection:
3035
3136 ALL_HANDLERS_KEY = "__ALL"
3237
33- def __init__ (self , confidence : float = 0.3 , debounce_sec : float = 0.0 ):
38+ def __init__ (self , camera : Camera = None , confidence : float = 0.3 , debounce_sec : float = 0.0 ):
3439 """Initialize the VideoObjectDetection class.
3540
3641 Args:
42+ camera (Camera): The camera instance to use for capturing video. If None, a default camera will be initialized.
3743 confidence (float): Confidence level for detection. Default is 0.3 (30%).
3844 debounce_sec (float): Minimum seconds between repeated detections of the same object. Default is 0 seconds.
3945
4046 Raises:
4147 RuntimeError: If the host address could not be resolved.
4248 """
49+ self ._camera = camera if camera else Camera ()
50+
4351 self ._confidence = confidence
4452 self ._debounce_sec = debounce_sec
4553 self ._last_detected : dict [str , float ] = {}
@@ -107,32 +115,25 @@ def on_detect_all(self, callback: Callable[[dict], None]):
107115
108116 def start (self ):
109117 """Start the video object detection process."""
118+ self ._camera .start ()
110119 self ._is_running .set ()
111120
112121 def stop (self ):
113- """Stop the video object detection process."""
122+ """Stop the video object detection process and release resources ."""
114123 self ._is_running .clear ()
124+ self ._camera .stop ()
125+
126+ @brick .execute
127+ def object_detection_loop (self ):
128+ """Object detection main loop.
115129
116- def execute (self ):
117- """Connect to the model runner and process messages until `stop` is called.
118-
119- Behavior:
120- - Establishes a WebSocket connection to the runner.
121- - Parses ``"hello"`` messages to capture model metadata and optionally
122- performs a threshold override to align the runner with the local setting.
123- - Parses ``"classification"`` messages, filters detections by confidence,
124- applies debounce, then invokes registered callbacks.
125- - Retries on transient WebSocket errors while running.
126-
127- Exceptions:
128- ConnectionClosedOK:
129- Propagated to exit cleanly when the server closes the connection.
130- ConnectionClosedError, TimeoutError, ConnectionRefusedError:
131- Logged and retried with a short backoff while running.
130+ Maintains WebSocket connection to the model runner and processes object detection messages.
131+ Retries on connection errors until stopped.
132132 """
133133 while self ._is_running .is_set ():
134134 try :
135135 with connect (self ._uri ) as ws :
136+ logger .info ("WebSocket connection established" )
136137 while self ._is_running .is_set ():
137138 try :
138139 message = ws .recv ()
@@ -142,21 +143,56 @@ def execute(self):
142143 except ConnectionClosedOK :
143144 raise
144145 except (TimeoutError , ConnectionRefusedError , ConnectionClosedError ):
145- logger .warning (f"Connection lost. Retrying..." )
146+ logger .warning (f"WebSocket connection lost. Retrying..." )
146147 raise
147148 except Exception as e :
148149 logger .exception (f"Failed to process detection: { e } " )
149150 except ConnectionClosedOK :
150- logger .debug (f"Disconnected cleanly, exiting WebSocket read loop." )
151+ logger .debug (f"WebSocket disconnected cleanly, exiting loop." )
151152 return
152153 except (TimeoutError , ConnectionRefusedError , ConnectionClosedError ):
153154 logger .debug (f"Waiting for model runner. Retrying..." )
154- import time
155-
156155 time .sleep (2 )
157156 continue
158157 except Exception as e :
159158 logger .exception (f"Failed to establish WebSocket connection to { self ._host } : { e } " )
159+ time .sleep (2 )
160+
161+ @brick .execute
162+ def camera_loop (self ):
163+ """Camera main loop.
164+
165+ Captures images from the camera and forwards them over the TCP connection.
166+ Retries on connection errors until stopped.
167+ """
168+ while self ._is_running .is_set ():
169+ try :
170+ with socket .socket (socket .AF_INET , socket .SOCK_STREAM ) as tcp_socket :
171+ tcp_socket .connect ((self ._host , "5050" ))
172+ logger .info (f"TCP connection established to { self ._host } :5050" )
173+
174+ while self ._is_running .is_set ():
175+ try :
176+ frame = self ._camera .capture ()
177+ if frame is None :
178+ time .sleep (0.01 ) # Brief sleep if no image available
179+ continue
180+
181+ jpeg_frame = compress_to_jpeg (frame )
182+ tcp_socket .sendall (jpeg_frame .tobytes ())
183+
184+ except (BrokenPipeError , ConnectionResetError , OSError ) as e :
185+ logger .warning (f"TCP connection lost: { e } . Retrying..." )
186+ break
187+ except Exception as e :
188+ logger .exception (f"Error capturing/sending image: { e } " )
189+
190+ except (ConnectionRefusedError , OSError ) as e :
191+ logger .debug (f"TCP connection failed: { e } . Retrying in 2 seconds..." )
192+ time .sleep (2 )
193+ except Exception as e :
194+ logger .exception (f"Unexpected error in TCP loop: { e } " )
195+ time .sleep (2 )
160196
161197 def _process_message (self , ws : ClientConnection , message : str ):
162198 jmsg = json .loads (message )
0 commit comments