Fix: update message type to current implementation
This commit is contained in:
parent
b940790e4a
commit
96ecc321ec
3 changed files with 549 additions and 230 deletions
384
ARCHITECTURE.md
384
ARCHITECTURE.md
|
@ -141,18 +141,44 @@ WebSocketConnection() # Per-client connection wrapper
|
|||
#### `message_processor.py` - Message Processing Pipeline
|
||||
```python
|
||||
MessageProcessor()
|
||||
├── process_message() # Main message dispatcher
|
||||
├── _handle_subscribe() # Process subscription requests
|
||||
├── _handle_unsubscribe() # Process unsubscription
|
||||
├── _handle_state_request() # System state requests
|
||||
└── _handle_session_ops() # Session management operations
|
||||
├── parse_message() # Message parsing and validation
|
||||
├── _validate_set_subscription_list() # Validate declarative subscriptions
|
||||
├── _validate_set_session() # Session management validation
|
||||
├── _validate_patch_session() # Patch session validation
|
||||
├── _validate_progression_stage() # Progression stage validation
|
||||
└── _validate_patch_session_result() # Backend response validation
|
||||
|
||||
MessageType(Enum) # Supported message types
|
||||
├── SUBSCRIBE
|
||||
├── UNSUBSCRIBE
|
||||
├── REQUEST_STATE
|
||||
├── SET_SESSION_ID
|
||||
└── PATCH_SESSION
|
||||
MessageType(Enum) # Protocol-compliant message types per worker.md
|
||||
├── SET_SUBSCRIPTION_LIST # Primary declarative subscription command
|
||||
├── REQUEST_STATE # System state requests
|
||||
├── SET_SESSION_ID # Session association (supports null clearing)
|
||||
├── PATCH_SESSION # Session modification requests
|
||||
├── SET_PROGRESSION_STAGE # Real-time progression updates
|
||||
├── PATCH_SESSION_RESULT # Backend responses to patch requests
|
||||
├── IMAGE_DETECTION # Detection results (worker->backend)
|
||||
└── STATE_REPORT # Heartbeat messages (worker->backend)
|
||||
|
||||
# Protocol-Compliant Payload Classes
|
||||
SubscriptionObject() # Individual subscription per worker.md specification
|
||||
├── subscription_identifier # Format: "displayId;cameraId"
|
||||
├── rtsp_url # Required RTSP stream URL
|
||||
├── model_url # Required fresh model URL (1-hour TTL)
|
||||
├── model_id, model_name # Model identification
|
||||
├── snapshot_url # Optional HTTP snapshot URL
|
||||
├── snapshot_interval # Required if snapshot_url provided
|
||||
└── crop_x1/y1/x2/y2 # Optional crop coordinates
|
||||
|
||||
SetSubscriptionListPayload() # Declarative subscription command payload
|
||||
└── subscriptions: List[SubscriptionObject] # Complete desired state
|
||||
|
||||
SessionPayload() # Session management payload
|
||||
├── display_identifier # Target display ID
|
||||
├── session_id # Session ID (can be null for clearing)
|
||||
└── data # Optional session patch data
|
||||
|
||||
ProgressionPayload() # Progression stage payload
|
||||
├── display_identifier # Target display ID
|
||||
└── progression_stage # Stage: welcome|car_fueling|car_waitpayment|null
|
||||
```
|
||||
|
||||
### Stream Management (`detector_worker/streams/`)
|
||||
|
@ -452,105 +478,313 @@ sequenceDiagram
|
|||
return stream_manager.get_latest_frame(camera_id)
|
||||
```
|
||||
|
||||
## Protocol Compliance (worker.md Implementation)
|
||||
|
||||
### Key Protocol Features Implemented
|
||||
|
||||
The WebSocket communication layer has been **fully updated** to comply with the worker.md protocol specification, replacing deprecated patterns with modern declarative subscription management.
|
||||
|
||||
#### ✅ **Protocol-Compliant Message Types**
|
||||
|
||||
| Message Type | Direction | Purpose | Status |
|
||||
|--------------|-----------|---------|---------|
|
||||
| `setSubscriptionList` | Backend→Worker | **Primary subscription command** - declarative management | ✅ **Implemented** |
|
||||
| `setSessionId` | Backend→Worker | Associate session with display (supports null clearing) | ✅ **Implemented** |
|
||||
| `setProgressionStage` | Backend→Worker | Real-time progression updates for context-aware processing | ✅ **Implemented** |
|
||||
| `requestState` | Backend→Worker | Request immediate state report | ✅ **Implemented** |
|
||||
| `patchSessionResult` | Backend→Worker | Response to worker's patch session request | ✅ **Implemented** |
|
||||
| `stateReport` | Worker→Backend | **Heartbeat with performance metrics** (every 2 seconds) | ✅ **Implemented** |
|
||||
| `imageDetection` | Worker→Backend | Real-time detection results with session context | ✅ **Implemented** |
|
||||
| `patchSession` | Worker→Backend | Request modification to session data | ✅ **Implemented** |
|
||||
|
||||
#### ❌ **Deprecated Message Types Removed**
|
||||
|
||||
- `subscribe` - Replaced by declarative `setSubscriptionList`
|
||||
- `unsubscribe` - Handled by empty `setSubscriptionList` array
|
||||
|
||||
#### 🔧 **Key Protocol Implementations**
|
||||
|
||||
##### 1. **Declarative Subscription Management**
|
||||
```python
|
||||
# setSubscriptionList provides complete desired state
|
||||
{
|
||||
"type": "setSubscriptionList",
|
||||
"subscriptions": [
|
||||
{
|
||||
"subscriptionIdentifier": "display-001;cam-001",
|
||||
"rtspUrl": "rtsp://192.168.1.100/stream1",
|
||||
"modelUrl": "http://storage/models/vehicle-id.mpta?token=fresh-token",
|
||||
"modelId": 201,
|
||||
"modelName": "Vehicle Identification",
|
||||
"cropX1": 100, "cropY1": 200, "cropX2": 300, "cropY2": 400
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Worker Reconciliation Logic:**
|
||||
- **Add new subscriptions** not in current state
|
||||
- **Remove obsolete subscriptions** not in desired state
|
||||
- **Update existing subscriptions** with fresh model URLs (handles S3 expiration)
|
||||
- **Single stream optimization** - share RTSP streams across multiple subscriptions
|
||||
|
||||
##### 2. **Protocol-Compliant State Reports**
|
||||
```python
|
||||
# stateReport with flat structure per worker.md specification
|
||||
{
|
||||
"type": "stateReport",
|
||||
"cpuUsage": 75.5,
|
||||
"memoryUsage": 40.2,
|
||||
"gpuUsage": 60.0,
|
||||
"gpuMemoryUsage": 25.1,
|
||||
"cameraConnections": [
|
||||
{
|
||||
"subscriptionIdentifier": "display-001;cam-001",
|
||||
"modelId": 101,
|
||||
"modelName": "General Object Detection",
|
||||
"online": true,
|
||||
"cropX1": 100, "cropY1": 200, "cropX2": 300, "cropY2": 400
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
##### 3. **Session Context in Detection Results**
|
||||
```python
|
||||
# imageDetection with sessionId for proper session linking
|
||||
{
|
||||
"type": "imageDetection",
|
||||
"subscriptionIdentifier": "display-001;cam-001",
|
||||
"timestamp": "2025-09-12T10:00:00.000Z",
|
||||
"sessionId": 12345, # Critical for CMS session tracking
|
||||
"data": {
|
||||
"detection": {
|
||||
"carBrand": "Honda",
|
||||
"carModel": "CR-V",
|
||||
"licensePlateText": "ABC-123"
|
||||
},
|
||||
"modelId": 201,
|
||||
"modelName": "Vehicle Identification"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 🚀 **Enhanced Features**
|
||||
|
||||
1. **State Recovery**: Complete subscription restoration on worker reconnection
|
||||
2. **Fresh Model URLs**: Automatic S3 URL refresh via subscription updates
|
||||
3. **Multi-Display Support**: Proper display identifier parsing and session management
|
||||
4. **Load Balancing Ready**: Backend can distribute subscriptions across workers
|
||||
5. **Session Continuity**: Session IDs maintained across worker disconnections
|
||||
|
||||
## WebSocket Communication Flow
|
||||
|
||||
### Client Connection Lifecycle
|
||||
### Client Connection Lifecycle (Protocol-Compliant)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client as WebSocket Client
|
||||
participant Backend as CMS Backend
|
||||
participant WS as WebSocketHandler
|
||||
participant CM as ConnectionManager
|
||||
participant MP as MessageProcessor
|
||||
participant SM as StreamManager
|
||||
participant HB as Heartbeat Loop
|
||||
|
||||
Client->>WS: WebSocket Connection
|
||||
WS->>WS: handle_websocket()
|
||||
WS->>CM: add_connection()
|
||||
CM->>CM: create WebSocketConnection
|
||||
WS->>Client: Connection Accepted
|
||||
Backend->>WS: WebSocket Connection
|
||||
WS->>WS: handle_connection()
|
||||
WS->>HB: start_heartbeat_loop()
|
||||
WS->>Backend: Connection Accepted
|
||||
|
||||
loop Message Processing
|
||||
Client->>WS: JSON Message
|
||||
WS->>MP: process_message()
|
||||
loop Heartbeat (every 2 seconds)
|
||||
HB->>Backend: stateReport {cpuUsage, memoryUsage, gpuUsage, cameraConnections[]}
|
||||
end
|
||||
|
||||
loop Message Processing (Protocol Commands)
|
||||
Backend->>WS: JSON Message
|
||||
WS->>MP: parse_message()
|
||||
|
||||
alt Subscribe Message
|
||||
MP->>SM: create_stream()
|
||||
SM->>SM: initialize StreamReader
|
||||
MP->>Client: subscribeAck
|
||||
else Unsubscribe Message
|
||||
MP->>SM: remove_stream()
|
||||
SM->>SM: cleanup StreamReader
|
||||
MP->>Client: unsubscribeAck
|
||||
else State Request
|
||||
MP->>MP: collect_system_state()
|
||||
MP->>Client: stateReport
|
||||
alt setSubscriptionList (Declarative)
|
||||
MP->>MP: validate_subscription_list()
|
||||
WS->>WS: reconcile_subscriptions()
|
||||
WS->>SM: add/remove/update_streams()
|
||||
SM->>SM: handle_stream_lifecycle()
|
||||
WS->>HB: update_camera_connections()
|
||||
|
||||
else setSessionId
|
||||
MP->>MP: validate_set_session()
|
||||
WS->>WS: store_session_for_display()
|
||||
WS->>WS: apply_to_all_display_subscriptions()
|
||||
|
||||
else setProgressionStage
|
||||
MP->>MP: validate_progression_stage()
|
||||
WS->>WS: store_stage_for_display()
|
||||
WS->>WS: apply_context_aware_processing()
|
||||
|
||||
else requestState
|
||||
WS->>Backend: stateReport (immediate response)
|
||||
|
||||
else patchSessionResult
|
||||
MP->>MP: validate_patch_result()
|
||||
WS->>WS: log_patch_response()
|
||||
end
|
||||
end
|
||||
|
||||
Client->>WS: Disconnect
|
||||
WS->>CM: remove_connection()
|
||||
WS->>SM: cleanup_client_streams()
|
||||
Backend->>WS: Disconnect
|
||||
WS->>SM: cleanup_all_streams()
|
||||
WS->>HB: stop_heartbeat_loop()
|
||||
```
|
||||
|
||||
### Message Processing Detail
|
||||
|
||||
#### 1. Subscribe Message Flow (`message_processor.py:125-185`)
|
||||
#### 1. setSubscriptionList Flow (`websocket_handler.py:355-453`) - Protocol Compliant
|
||||
|
||||
```python
|
||||
async def _handle_subscribe(self, payload: Dict, client_id: str) -> Dict:
|
||||
"""Process subscription request"""
|
||||
async def _handle_set_subscription_list(self, data: Dict[str, Any]) -> None:
|
||||
"""Handle setSubscriptionList command - declarative subscription management"""
|
||||
|
||||
# 1. Extract subscription parameters
|
||||
subscription_id = payload["subscriptionIdentifier"]
|
||||
stream_url = payload.get("rtspUrl") or payload.get("snapshotUrl")
|
||||
model_url = payload["modelUrl"]
|
||||
subscriptions = data.get("subscriptions", [])
|
||||
|
||||
# 2. Create stream configuration
|
||||
stream_config = StreamConfig(
|
||||
stream_url=stream_url,
|
||||
stream_type="rtsp" if "rtsp" in stream_url else "http_snapshot",
|
||||
crop_region=[payload.get("cropX1"), payload.get("cropY1"),
|
||||
payload.get("cropX2"), payload.get("cropY2")]
|
||||
)
|
||||
# 1. Get current and desired subscription states
|
||||
current_subscriptions = set(subscription_to_camera.keys())
|
||||
desired_subscriptions = set()
|
||||
subscription_configs = {}
|
||||
|
||||
# 3. Load ML pipeline
|
||||
pipeline_config = await pipeline_loader.load_from_url(model_url)
|
||||
for sub_config in subscriptions:
|
||||
sub_id = sub_config.get("subscriptionIdentifier")
|
||||
if sub_id:
|
||||
desired_subscriptions.add(sub_id)
|
||||
subscription_configs[sub_id] = sub_config
|
||||
|
||||
# Extract display ID for session management
|
||||
parts = sub_id.split(";")
|
||||
if len(parts) >= 2:
|
||||
display_id = parts[0]
|
||||
self.display_identifiers.add(display_id)
|
||||
|
||||
# 4. Create stream (with sharing if same URL)
|
||||
stream_info = await stream_manager.create_stream(
|
||||
camera_id=subscription_id.split(';')[1],
|
||||
config=stream_config,
|
||||
subscription_id=subscription_id
|
||||
)
|
||||
# 2. Calculate reconciliation changes
|
||||
to_add = desired_subscriptions - current_subscriptions
|
||||
to_remove = current_subscriptions - desired_subscriptions
|
||||
to_update = desired_subscriptions & current_subscriptions
|
||||
|
||||
# 5. Register client subscription
|
||||
connection_manager.add_subscription(client_id, subscription_id)
|
||||
# 3. Remove obsolete subscriptions
|
||||
for sub_id in to_remove:
|
||||
camera_id = subscription_to_camera.get(sub_id)
|
||||
if camera_id:
|
||||
await self.stream_manager.stop_stream(camera_id)
|
||||
self.model_manager.unload_models(camera_id)
|
||||
subscription_to_camera.pop(sub_id, None)
|
||||
self.session_cache.clear_session(camera_id)
|
||||
|
||||
return {"type": "subscribeAck", "status": "success",
|
||||
"subscriptionId": subscription_id}
|
||||
# 4. Add new subscriptions
|
||||
for sub_id in to_add:
|
||||
await self._start_subscription(sub_id, subscription_configs[sub_id])
|
||||
|
||||
# 5. Update existing subscriptions (handle S3 URL refresh)
|
||||
for sub_id in to_update:
|
||||
current_config = subscription_to_camera.get(sub_id)
|
||||
new_config = subscription_configs[sub_id]
|
||||
|
||||
# Restart if model URL changed (handles S3 expiration)
|
||||
current_model_url = getattr(current_config, 'model_url', None)
|
||||
new_model_url = new_config.get("modelUrl")
|
||||
|
||||
if current_model_url != new_model_url:
|
||||
camera_id = subscription_to_camera.get(sub_id)
|
||||
if camera_id:
|
||||
await self.stream_manager.stop_stream(camera_id)
|
||||
self.model_manager.unload_models(camera_id)
|
||||
await self._start_subscription(sub_id, new_config)
|
||||
|
||||
async def _start_subscription(self, subscription_id: str, config: Dict[str, Any]) -> None:
|
||||
"""Start individual subscription with protocol-compliant configuration"""
|
||||
|
||||
# Extract camera ID from subscription identifier
|
||||
parts = subscription_id.split(";")
|
||||
camera_id = parts[1] if len(parts) >= 2 else subscription_id
|
||||
|
||||
# Store subscription mapping
|
||||
subscription_to_camera[subscription_id] = camera_id
|
||||
|
||||
# Start camera stream with full config
|
||||
await self.stream_manager.start_stream(camera_id, config)
|
||||
|
||||
# Load ML model from fresh URL
|
||||
model_id = config.get("modelId")
|
||||
model_url = config.get("modelUrl")
|
||||
if model_id and model_url:
|
||||
await self.model_manager.load_model(camera_id, model_id, model_url)
|
||||
```
|
||||
|
||||
#### 2. Detection Result Broadcasting (`websocket_handler.py:245-265`)
|
||||
#### 2. Detection Result Broadcasting (`websocket_handler.py:589-615`) - Protocol Compliant
|
||||
|
||||
```python
|
||||
async def broadcast_detection_result(self, subscription_id: str,
|
||||
detection_result: Dict):
|
||||
"""Broadcast detection to subscribed clients"""
|
||||
async def _send_detection_result(self, camera_id: str, stream_info: Dict[str, Any],
|
||||
detection_result: DetectionResult) -> None:
|
||||
"""Send detection result with protocol-compliant format"""
|
||||
|
||||
message = {
|
||||
# Get session ID for this display (protocol requirement)
|
||||
subscription_id = stream_info["subscriptionIdentifier"]
|
||||
display_id = subscription_id.split(";")[0] if ";" in subscription_id else subscription_id
|
||||
session_id = self.session_ids.get(display_id) # Can be None for null sessions
|
||||
|
||||
# Protocol-compliant imageDetection format per worker.md
|
||||
detection_data = {
|
||||
"type": "imageDetection",
|
||||
"payload": {
|
||||
"subscriptionId": subscription_id,
|
||||
"detections": detection_result["detections"],
|
||||
"timestamp": detection_result["timestamp"],
|
||||
"modelInfo": detection_result["model_info"]
|
||||
"subscriptionIdentifier": subscription_id, # Required at root level
|
||||
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"sessionId": session_id, # Required for session linking
|
||||
"data": {
|
||||
"detection": detection_result.to_dict(), # Flat detection object
|
||||
"modelId": stream_info["modelId"],
|
||||
"modelName": stream_info["modelName"]
|
||||
}
|
||||
}
|
||||
|
||||
await self.connection_manager.broadcast_to_subscription(
|
||||
subscription_id, message
|
||||
)
|
||||
# Send to backend via WebSocket
|
||||
await self.websocket.send_json(detection_data)
|
||||
```
|
||||
|
||||
#### 3. State Report Broadcasting (`websocket_handler.py:201-209`) - Protocol Compliant
|
||||
|
||||
```python
|
||||
async def _send_heartbeat(self) -> None:
|
||||
"""Send protocol-compliant stateReport every 2 seconds"""
|
||||
|
||||
while self.connected:
|
||||
# Get system metrics
|
||||
metrics = get_system_metrics()
|
||||
|
||||
# Build cameraConnections array as required by protocol
|
||||
camera_connections = []
|
||||
with self.stream_manager.streams_lock:
|
||||
for camera_id, stream_info in self.stream_manager.streams.items():
|
||||
is_online = self.stream_manager.is_stream_active(camera_id)
|
||||
|
||||
connection_info = {
|
||||
"subscriptionIdentifier": stream_info.get("subscriptionIdentifier", camera_id),
|
||||
"modelId": stream_info.get("modelId", 0),
|
||||
"modelName": stream_info.get("modelName", "Unknown Model"),
|
||||
"online": is_online
|
||||
}
|
||||
|
||||
# Add crop coordinates if available (optional per protocol)
|
||||
for coord in ["cropX1", "cropY1", "cropX2", "cropY2"]:
|
||||
if coord in stream_info:
|
||||
connection_info[coord] = stream_info[coord]
|
||||
|
||||
camera_connections.append(connection_info)
|
||||
|
||||
# Protocol-compliant stateReport format (worker.md lines 169-189)
|
||||
state_data = {
|
||||
"type": "stateReport", # Message type
|
||||
"cpuUsage": metrics.get("cpu_percent", 0), # CPU percentage
|
||||
"memoryUsage": metrics.get("memory_percent", 0), # Memory percentage
|
||||
"gpuUsage": metrics.get("gpu_percent", 0), # GPU percentage
|
||||
"gpuMemoryUsage": metrics.get("gpu_memory_percent", 0), # GPU memory
|
||||
"cameraConnections": camera_connections # Camera details array
|
||||
}
|
||||
|
||||
await self.websocket.send_json(state_data)
|
||||
await asyncio.sleep(2) # 2-second heartbeat interval per protocol
|
||||
```
|
||||
|
||||
## Detection Pipeline Flow
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue