Skip to content

Commit fbe2dcc

Browse files
committed
doc: update readme
1 parent 6381813 commit fbe2dcc

File tree

3 files changed

+93
-172
lines changed

3 files changed

+93
-172
lines changed

src/arduino/app_peripherals/camera/README.md

Lines changed: 63 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -5,200 +5,117 @@ The `Camera` peripheral provides a unified abstraction for capturing images from
55
## Features
66

77
- **Universal Interface**: Single API for V4L/USB, IP cameras, and WebSocket cameras
8-
- **Automatic Detection**: Automatically selects appropriate camera implementation based on source
8+
- **Automatic Detection**: Selects appropriate camera implementation based on source
99
- **Multiple Protocols**: Supports V4L, RTSP, HTTP/MJPEG, and WebSocket streams
10-
- **Flexible Configuration**: Resolution, FPS, compression, and protocol-specific settings
1110
- **Thread-Safe**: Safe concurrent access with proper locking
12-
- **Context Manager**: Automatic resource management with `with` statements
11+
- **Context Manager**: Automatic resource management
1312

1413
## Quick Start
1514

16-
### Basic Usage
17-
15+
Instantiate the default camera:
1816
```python
1917
from arduino.app_peripherals.camera import Camera
2018

21-
# USB/V4L camera (index 0)
22-
camera = Camera(0, resolution=(640, 480), fps=15)
23-
24-
with camera:
25-
frame = camera.capture() # Returns PIL Image
26-
if frame:
27-
frame.save("captured.png")
19+
# Default camera (V4L camera at index 0)
20+
camera = Camera()
2821
```
2922

30-
### Different Camera Types
23+
Camera needs to be started and stopped explicitly:
3124

3225
```python
33-
# V4L/USB cameras
34-
usb_camera = Camera(0) # Camera index
35-
usb_camera = Camera("1") # Index as string
36-
usb_camera = Camera("/dev/video0") # Device path
37-
38-
# IP cameras
39-
ip_camera = Camera("rtsp://192.168.1.100/stream")
40-
ip_camera = Camera("http://camera.local/mjpeg",
41-
username="admin", password="secret")
42-
43-
# WebSocket cameras
44-
- `"ws://localhost:8080"` - WebSocket server URL (extracts host and port)
45-
- `"localhost:9090"` - WebSocket server host:port format
46-
```
47-
48-
## API Reference
49-
50-
### Camera Class
26+
# Specify camera and configuration
27+
camera = Camera(0, resolution=(640, 480), fps=15)
28+
camera.start()
5129

52-
The main `Camera` class acts as a factory that creates the appropriate camera implementation:
30+
image = camera.capture()
5331

54-
```python
55-
camera = Camera(source, **options)
32+
camera.stop()
5633
```
5734

58-
**Parameters:**
59-
- `source`: Camera source identifier
60-
- `int`: V4L camera index (0, 1, 2...)
61-
- `str`: Camera index, device path, or URL
62-
- `resolution`: Tuple `(width, height)` or `None` for default
63-
- `fps`: Target frames per second (default: 10)
64-
- `transformer`: Pipeline of transformers that adjust the captured image
65-
66-
**Methods:**
67-
- `start()`: Initialize and start camera
68-
- `stop()`: Stop camera and release resources
69-
- `capture()`: Capture frame as Numpy array
70-
- `is_started()`: Check if camera is running
71-
72-
### Context Manager
73-
35+
Or you can leverage context support for doing that automatically:
7436
```python
7537
with Camera(source, **options) as camera:
7638
frame = camera.capture()
39+
if frame is not None:
40+
print(f"Captured frame with shape: {frame.shape}")
7741
# Camera automatically stopped when exiting
7842
```
7943

8044
## Camera Types
45+
The Camera class provides automatic camera type detection based on the format of its source argument. keyword arguments will be propagated to the underlying implementation.
8146

82-
### V4L/USB Cameras
47+
Note: constructor arguments (except source) must be provided in keyword format to forward them correctly to the specific camera implementations.
8348

84-
For local USB cameras and V4L-compatible devices:
49+
The underlying camera implementations can be instantiated explicitly (V4LCamera, IPCamera and WebSocketCamera), if needed.
8550

86-
```python
87-
camera = Camera(0, resolution=(1280, 720), fps=30)
88-
```
51+
### V4L Cameras
52+
For local USB cameras and V4L-compatible devices.
8953

9054
**Features:**
91-
- Device enumeration via `/dev/v4l/by-id/`
92-
- Resolution validation
93-
- Backend information
94-
95-
### IP Cameras
96-
97-
For network cameras supporting RTSP or HTTP streams:
55+
- Supports cameras compatible with the Video4Linux2 drivers
9856

9957
```python
100-
camera = Camera("rtsp://admin:pass@192.168.1.100/stream",
101-
timeout=10, fps=5)
58+
camera = Camera(0) # Camera index
59+
camera = Camera("/dev/video0") # Device path
60+
camera = V4LCamera(0)
10261
```
10362

63+
### IP Cameras
64+
For network cameras supporting RTSP (Real-Time Streaming Protocol) and HLS (HTTP Live Streaming).
65+
10466
**Features:**
105-
- RTSP, HTTP, HTTPS protocols
67+
- Supports capturing RTSP, HLS streams
10668
- Authentication support
107-
- Connection testing
10869
- Automatic reconnection
10970

110-
### WebSocket Cameras
111-
112-
For hosting a WebSocket server that receives frames from clients (single client only):
113-
11471
```python
115-
camera = Camera("ws://0.0.0.0:9090", frame_format="json")
72+
camera = Camera("rtsp://admin:secret@192.168.1.100/stream")
73+
camera = Camera("http://camera.local/stream",
74+
username="admin", password="secret")
75+
camera = IPCamera("http://camera.local/stream",
76+
username="admin", password="secret")
11677
```
11778

79+
### WebSocket Cameras
80+
For hosting a WebSocket server that receives frames from a single client at a time.
81+
11882
**Features:**
119-
- Hosts WebSocket server (not client)
12083
- **Single client limitation**: Only one client can connect at a time
121-
- Additional clients are rejected with error message
122-
- Receives frames from connected client
84+
- Stream data from any client with WebSockets support
12385
- Base64, binary, and JSON frame formats
124-
- Frame buffering and queue management
125-
- Bidirectional communication with connected client
126-
127-
**Client Connection:**
128-
Only one client can connect at a time. Additional clients receive an error:
129-
```javascript
130-
// JavaScript client example
131-
const ws = new WebSocket('ws://localhost:8080');
132-
ws.onmessage = function(event) {
133-
const data = JSON.parse(event.data);
134-
if (data.error) {
135-
console.log('Connection rejected:', data.message);
136-
}
137-
};
138-
ws.send(base64EncodedImageData);
139-
```
140-
141-
## Advanced Usage
142-
143-
### Custom Configuration
86+
- Supports 8-bit images (e.g. JPEG, PNG 8-bit)
14487

14588
```python
146-
camera = Camera(
147-
source="rtsp://camera.local/stream",
148-
resolution=(1920, 1080),
149-
fps=15,
150-
compression=True, # PNG compression
151-
letterbox=True, # Square images
152-
username="admin", # IP camera auth
153-
password="secret",
154-
timeout=5, # Connection timeout
155-
max_queue_size=20 # WebSocket buffer
156-
)
89+
camera = Camera("ws://0.0.0.0:8080", timeout=5)
90+
camera = WebSocketCamera("0.0.0.0", 8080, timeout=5)
15791
```
15892

159-
### Error Handling
160-
93+
Client implementation example:
16194
```python
162-
from arduino.app_peripherals.camera.camera import CameraError
163-
164-
try:
165-
with Camera("invalid://source") as camera:
166-
frame = camera.capture()
167-
except CameraError as e:
168-
print(f"Camera error: {e}")
95+
import time
96+
import base64
97+
import cv2
98+
import websockets.sync.client as wsclient
99+
import websockets.exceptions as wsexc
100+
101+
102+
# Open camera
103+
camera = cv2.VideoCapture(0)
104+
with wsclient.connect("ws://<board-address>:8080") as websocket:
105+
while True:
106+
time.sleep(1.0 / 15.0) # 15 FPS
107+
ret, frame = camera.read()
108+
if ret:
109+
# Compress frame to JPEG
110+
_, buffer = cv2.imencode('.jpg', frame)
111+
# Convert to base64
112+
jpeg_b64 = base64.b64encode(buffer).decode('utf-8')
113+
try:
114+
websocket.send(jpeg_b64)
115+
except wsexc.ConnectionClosed:
116+
break
169117
```
170118

171-
### Factory Pattern
172-
173-
```python
174-
from arduino.app_peripherals.camera.camera import CameraFactory
175-
176-
# Create camera directly via factory
177-
camera = CameraFactory.create_camera(
178-
source="ws://localhost:8080/stream",
179-
frame_format="json"
180-
)
181-
```
182-
183-
## Dependencies
184-
185-
### Core Dependencies
186-
- `opencv-python` (cv2) - Image processing and V4L/IP camera support
187-
- `Pillow` (PIL) - Image format handling
188-
- `requests` - HTTP camera connectivity testing
189-
190-
### Optional Dependencies
191-
- `websockets` - WebSocket server support (install with `pip install websockets`)
192-
193-
## Examples
194-
195-
See the `examples/` directory for comprehensive usage examples:
196-
- Basic camera operations
197-
- Different camera types
198-
- Advanced configuration
199-
- Error handling
200-
- Context managers
201-
202119
## Migration from Legacy Camera
203120

204-
The new Camera abstraction is backward compatible with the existing Camera implementation. Existing code using the old API will continue to work, but new code should use the improved abstraction for better flexibility and features.
121+
The new Camera abstraction is backward compatible with the existing Camera implementation. Existing code using the old API will continue to work, but will use the new Camera backend. New code should use the improved abstraction for better flexibility and features.

src/arduino/app_peripherals/camera/base_camera.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ def capture(self) -> Optional[np.ndarray]:
8989

9090
def _extract_frame(self) -> Optional[np.ndarray]:
9191
"""Extract a frame with FPS throttling and post-processing."""
92-
# FPS throttling
93-
if self._desired_interval > 0:
94-
current_time = time.monotonic()
95-
elapsed = current_time - self._last_capture_time
96-
if elapsed < self._desired_interval:
97-
time.sleep(self._desired_interval - elapsed)
98-
9992
with self._camera_lock:
93+
# FPS throttling
94+
if self._desired_interval > 0:
95+
current_time = time.monotonic()
96+
elapsed = current_time - self._last_capture_time
97+
if elapsed < self._desired_interval:
98+
time.sleep(self._desired_interval - elapsed)
99+
100100
if not self._is_started:
101101
return None
102102

src/arduino/app_peripherals/camera/websocket_camera.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def __init__(
7272
self._server_thread = None
7373
self._stop_event = asyncio.Event()
7474
self._client: Optional[websockets.ServerConnection] = None
75+
self._client_lock = asyncio.Lock()
7576

7677
def _open_camera(self) -> None:
7778
"""Start the WebSocket server."""
@@ -136,22 +137,24 @@ async def _ws_handler(self, conn: websockets.ServerConnection) -> None:
136137
"""Handle a connected WebSocket client. Only one client allowed at a time."""
137138
client_addr = f"{conn.remote_address[0]}:{conn.remote_address[1]}"
138139

139-
if self._client is not None:
140-
# Reject the new client
141-
logger.warning(f"Rejecting client {client_addr}: only one client allowed at a time")
142-
try:
143-
await conn.send(json.dumps({
144-
"error": "Server busy",
145-
"message": "Only one client connection allowed at a time",
146-
"code": 1000
147-
}))
148-
await conn.close(code=1000, reason="Server busy - only one client allowed")
149-
except Exception as e:
150-
logger.warning(f"Error sending rejection message to {client_addr}: {e}")
151-
return
140+
async with self._client_lock:
141+
if self._client is not None:
142+
# Reject the new client
143+
logger.warning(f"Rejecting client {client_addr}: only one client allowed at a time")
144+
try:
145+
await conn.send(json.dumps({
146+
"error": "Server busy",
147+
"message": "Only one client connection allowed at a time",
148+
"code": 1000
149+
}))
150+
await conn.close(code=1000, reason="Server busy - only one client allowed")
151+
except Exception as e:
152+
logger.warning(f"Error sending rejection message to {client_addr}: {e}")
153+
return
154+
155+
# Accept the client
156+
self._client = conn
152157

153-
# Accept the client
154-
self._client = conn
155158
logger.info(f"Client connected: {client_addr}")
156159

157160
try:
@@ -180,16 +183,17 @@ async def _ws_handler(self, conn: websockets.ServerConnection) -> None:
180183
# Drop oldest frame and try again
181184
self._frame_queue.get_nowait()
182185
except queue.Empty:
183-
break
186+
continue
184187

185188
except websockets.exceptions.ConnectionClosed:
186189
logger.info(f"Client disconnected: {client_addr}")
187190
except Exception as e:
188191
logger.warning(f"Error handling client {client_addr}: {e}")
189192
finally:
190-
if self._client == conn:
191-
self._client = None
192-
logger.info(f"Client removed: {client_addr}")
193+
async with self._client_lock:
194+
if self._client == conn:
195+
self._client = None
196+
logger.info(f"Client removed: {client_addr}")
193197

194198
async def _parse_message(self, message) -> Optional[np.ndarray]:
195199
"""Parse WebSocket message to extract frame."""

0 commit comments

Comments
 (0)