Fix: update message type to current implementation

This commit is contained in:
ziesorx 2025-09-12 22:16:06 +07:00
parent b940790e4a
commit 96ecc321ec
3 changed files with 549 additions and 230 deletions

View file

@ -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