feat: Add MQTT publisher and tester scripts for USDA Vision Camera System
- Implemented mqtt_publisher_test.py for manual MQTT message publishing - Created mqtt_test.py to test MQTT message reception and display statistics - Developed test_api_changes.py to verify API changes for camera settings and filename handling - Added test_camera_recovery_api.py for testing camera recovery API endpoints - Introduced test_max_fps.py to demonstrate maximum FPS capture functionality - Implemented test_mqtt_events_api.py to test MQTT events API endpoint - Created test_mqtt_logging.py for enhanced MQTT logging and API endpoint testing - Added sdk_config.py for SDK initialization and configuration with error suppression
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -85,3 +85,5 @@ Camera/log/*
|
||||
|
||||
# Python cache
|
||||
*/__pycache__/*
|
||||
old tests/Camera/log/*
|
||||
old tests/Camera/Data/*
|
||||
|
||||
175
API_CHANGES_SUMMARY.md
Normal file
175
API_CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# API Changes Summary: Camera Settings and Filename Handling
|
||||
|
||||
## Overview
|
||||
Enhanced the `POST /cameras/{camera_name}/start-recording` API endpoint to accept optional camera settings (shutter speed/exposure, gain, and fps) and ensure all filenames have datetime prefixes.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. API Models (`usda_vision_system/api/models.py`)
|
||||
- **Enhanced `StartRecordingRequest`** to include optional parameters:
|
||||
- `exposure_ms: Optional[float]` - Exposure time in milliseconds
|
||||
- `gain: Optional[float]` - Camera gain value
|
||||
- `fps: Optional[float]` - Target frames per second
|
||||
|
||||
### 2. Camera Recorder (`usda_vision_system/camera/recorder.py`)
|
||||
- **Added `update_camera_settings()` method** to dynamically update camera settings:
|
||||
- Updates exposure time using `mvsdk.CameraSetExposureTime()`
|
||||
- Updates gain using `mvsdk.CameraSetAnalogGain()`
|
||||
- Updates target FPS in camera configuration
|
||||
- Logs all setting changes
|
||||
- Returns boolean indicating success/failure
|
||||
|
||||
### 3. Camera Manager (`usda_vision_system/camera/manager.py`)
|
||||
- **Enhanced `manual_start_recording()` method** to accept new parameters:
|
||||
- Added optional `exposure_ms`, `gain`, and `fps` parameters
|
||||
- Calls `update_camera_settings()` if any settings are provided
|
||||
- **Automatic datetime prefix**: Always prepends timestamp to filename
|
||||
- If custom filename provided: `{timestamp}_{custom_filename}`
|
||||
- If no filename provided: `{camera_name}_manual_{timestamp}.avi`
|
||||
|
||||
### 4. API Server (`usda_vision_system/api/server.py`)
|
||||
- **Updated start-recording endpoint** to:
|
||||
- Pass new camera settings to camera manager
|
||||
- Handle filename response with datetime prefix
|
||||
- Maintain backward compatibility with existing requests
|
||||
|
||||
### 5. API Tests (`api-tests.http`)
|
||||
- **Added comprehensive test examples**:
|
||||
- Basic recording (existing functionality)
|
||||
- Recording with camera settings
|
||||
- Recording with settings only (no filename)
|
||||
- Different parameter combinations
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Recording (unchanged)
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"filename": "test.avi"
|
||||
}
|
||||
```
|
||||
**Result**: File saved as `20241223_143022_test.avi`
|
||||
|
||||
### Recording with Camera Settings
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"filename": "high_quality.avi",
|
||||
"exposure_ms": 2.0,
|
||||
"gain": 4.0,
|
||||
"fps": 5.0
|
||||
}
|
||||
```
|
||||
**Result**:
|
||||
- Camera settings updated before recording
|
||||
- File saved as `20241223_143022_high_quality.avi`
|
||||
|
||||
### Maximum FPS Recording
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"filename": "max_speed.avi",
|
||||
"exposure_ms": 0.1,
|
||||
"gain": 1.0,
|
||||
"fps": 0
|
||||
}
|
||||
```
|
||||
**Result**:
|
||||
- Camera captures at maximum possible speed (no delay between frames)
|
||||
- Video file saved with 30 FPS metadata for proper playback
|
||||
- Actual capture rate depends on camera hardware and exposure settings
|
||||
|
||||
### Settings Only (no filename)
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"exposure_ms": 1.5,
|
||||
"gain": 3.0,
|
||||
"fps": 7.0
|
||||
}
|
||||
```
|
||||
**Result**:
|
||||
- Camera settings updated
|
||||
- File saved as `camera1_manual_20241223_143022.avi`
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. **Backward Compatibility**
|
||||
- All existing API calls continue to work unchanged
|
||||
- New parameters are optional
|
||||
- Default behavior preserved when no settings provided
|
||||
|
||||
### 2. **Automatic Datetime Prefix**
|
||||
- **ALL filenames now have datetime prefix** regardless of what's sent
|
||||
- Format: `YYYYMMDD_HHMMSS_` (Atlanta timezone)
|
||||
- Ensures unique filenames and chronological ordering
|
||||
|
||||
### 3. **Dynamic Camera Settings**
|
||||
- Settings can be changed per recording without restarting system
|
||||
- Based on proven implementation from `old tests/camera_video_recorder.py`
|
||||
- Proper error handling and logging
|
||||
|
||||
### 4. **Maximum FPS Capture**
|
||||
- **`fps: 0`** = Capture at maximum possible speed (no delay between frames)
|
||||
- **`fps > 0`** = Capture at specified frame rate with controlled timing
|
||||
- **`fps` omitted** = Uses camera config default (usually 3.0 fps)
|
||||
- Video files saved with 30 FPS metadata when fps=0 for proper playback
|
||||
|
||||
### 5. **Parameter Validation**
|
||||
- Uses Pydantic models for automatic validation
|
||||
- Optional parameters with proper type checking
|
||||
- Descriptive field documentation
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test script to verify functionality:
|
||||
```bash
|
||||
# Start the system first
|
||||
python main.py
|
||||
|
||||
# In another terminal, run tests
|
||||
python test_api_changes.py
|
||||
```
|
||||
|
||||
The test script verifies:
|
||||
- Basic recording functionality
|
||||
- Camera settings application
|
||||
- Filename datetime prefix handling
|
||||
- API response accuracy
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Camera Settings Mapping
|
||||
- **Exposure**: Converted from milliseconds to microseconds for SDK
|
||||
- **Gain**: Converted to camera units (multiplied by 100)
|
||||
- **FPS**: Stored in camera config, used by recording loop
|
||||
|
||||
### Error Handling
|
||||
- Settings update failures are logged but don't prevent recording
|
||||
- Invalid camera names return appropriate HTTP errors
|
||||
- Camera initialization failures are handled gracefully
|
||||
|
||||
### Filename Generation
|
||||
- Uses `format_filename_timestamp()` from timezone utilities
|
||||
- Ensures Atlanta timezone consistency
|
||||
- Handles both custom and auto-generated filenames
|
||||
|
||||
## Similar to Old Implementation
|
||||
The camera settings functionality mirrors the proven approach in `old tests/camera_video_recorder.py`:
|
||||
- Same parameter names and ranges
|
||||
- Same SDK function calls
|
||||
- Same conversion factors
|
||||
- Proven to work with the camera hardware
|
||||
158
CAMERA_RECOVERY_GUIDE.md
Normal file
158
CAMERA_RECOVERY_GUIDE.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Camera Recovery and Diagnostics Guide
|
||||
|
||||
This guide explains the new camera recovery functionality implemented in the USDA Vision Camera System API.
|
||||
|
||||
## Overview
|
||||
|
||||
The system now includes comprehensive camera recovery capabilities to handle connection issues, initialization failures, and other camera-related problems. These features use the underlying mvsdk (python demo) library functions to perform various recovery operations.
|
||||
|
||||
## Available Recovery Operations
|
||||
|
||||
### 1. Connection Test (`/cameras/{camera_name}/test-connection`)
|
||||
- **Purpose**: Test if the camera connection is working
|
||||
- **SDK Function**: `CameraConnectTest()`
|
||||
- **Use Case**: Diagnose connection issues
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraTestResponse`
|
||||
|
||||
### 2. Reconnect (`/cameras/{camera_name}/reconnect`)
|
||||
- **Purpose**: Soft reconnection to the camera
|
||||
- **SDK Function**: `CameraReConnect()`
|
||||
- **Use Case**: Most common fix for connection issues
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraRecoveryResponse`
|
||||
|
||||
### 3. Restart Grab (`/cameras/{camera_name}/restart-grab`)
|
||||
- **Purpose**: Restart the camera grab process
|
||||
- **SDK Function**: `CameraRestartGrab()`
|
||||
- **Use Case**: Fix issues with image capture
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraRecoveryResponse`
|
||||
|
||||
### 4. Reset Timestamp (`/cameras/{camera_name}/reset-timestamp`)
|
||||
- **Purpose**: Reset camera timestamp
|
||||
- **SDK Function**: `CameraRstTimeStamp()`
|
||||
- **Use Case**: Fix timing-related issues
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraRecoveryResponse`
|
||||
|
||||
### 5. Full Reset (`/cameras/{camera_name}/full-reset`)
|
||||
- **Purpose**: Complete camera reset (uninitialize and reinitialize)
|
||||
- **SDK Functions**: `CameraUnInit()` + `CameraInit()`
|
||||
- **Use Case**: Hard reset for persistent issues
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraRecoveryResponse`
|
||||
|
||||
### 6. Reinitialize (`/cameras/{camera_name}/reinitialize`)
|
||||
- **Purpose**: Reinitialize cameras that failed initial setup
|
||||
- **SDK Functions**: Complete recorder recreation
|
||||
- **Use Case**: Cameras that never initialized properly
|
||||
- **HTTP Method**: POST
|
||||
- **Response**: `CameraRecoveryResponse`
|
||||
|
||||
## Recommended Troubleshooting Workflow
|
||||
|
||||
When a camera has issues, follow this order:
|
||||
|
||||
1. **Test Connection** - Diagnose the problem
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/test-connection
|
||||
```
|
||||
|
||||
2. **Try Reconnect** - Most common fix
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/reconnect
|
||||
```
|
||||
|
||||
3. **Restart Grab** - If reconnect doesn't work
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/restart-grab
|
||||
```
|
||||
|
||||
4. **Full Reset** - For persistent issues
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/full-reset
|
||||
```
|
||||
|
||||
5. **Reinitialize** - For cameras that never worked
|
||||
```http
|
||||
POST http://localhost:8000/cameras/camera1/reinitialize
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
All recovery operations return structured responses:
|
||||
|
||||
### CameraTestResponse
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Camera camera1 connection test passed",
|
||||
"camera_name": "camera1",
|
||||
"timestamp": "2024-01-01T12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
### CameraRecoveryResponse
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Camera camera1 reconnected successfully",
|
||||
"camera_name": "camera1",
|
||||
"operation": "reconnect",
|
||||
"timestamp": "2024-01-01T12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### CameraRecorder Methods
|
||||
- `test_connection()`: Tests camera connection
|
||||
- `reconnect()`: Performs soft reconnection
|
||||
- `restart_grab()`: Restarts grab process
|
||||
- `reset_timestamp()`: Resets timestamp
|
||||
- `full_reset()`: Complete reset with cleanup and reinitialization
|
||||
|
||||
### CameraManager Methods
|
||||
- `test_camera_connection(camera_name)`: Test specific camera
|
||||
- `reconnect_camera(camera_name)`: Reconnect specific camera
|
||||
- `restart_camera_grab(camera_name)`: Restart grab for specific camera
|
||||
- `reset_camera_timestamp(camera_name)`: Reset timestamp for specific camera
|
||||
- `full_reset_camera(camera_name)`: Full reset for specific camera
|
||||
- `reinitialize_failed_camera(camera_name)`: Reinitialize failed camera
|
||||
|
||||
### State Management
|
||||
All recovery operations automatically update the camera status in the state manager:
|
||||
- Success: Status set to "connected"
|
||||
- Failure: Status set to appropriate error state with error message
|
||||
|
||||
## Error Handling
|
||||
|
||||
The system includes comprehensive error handling:
|
||||
- SDK exceptions are caught and logged
|
||||
- State manager is updated with error information
|
||||
- Proper HTTP status codes are returned
|
||||
- Detailed error messages are provided
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided test files:
|
||||
- `api-tests.http`: Manual API testing with VS Code REST Client
|
||||
- `test_camera_recovery_api.py`: Automated testing script
|
||||
|
||||
## Safety Features
|
||||
|
||||
- Recording is automatically stopped before recovery operations
|
||||
- Camera resources are properly cleaned up
|
||||
- Thread-safe operations with proper locking
|
||||
- Graceful error handling prevents system crashes
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Camera Lost Connection**: Use reconnect
|
||||
2. **Camera Won't Capture**: Use restart-grab
|
||||
3. **Camera Initialization Failed**: Use reinitialize
|
||||
4. **Persistent Issues**: Use full-reset
|
||||
5. **Timing Problems**: Use reset-timestamp
|
||||
|
||||
This recovery system provides robust tools to handle most camera-related issues without requiring system restart or manual intervention.
|
||||
187
MQTT_LOGGING_GUIDE.md
Normal file
187
MQTT_LOGGING_GUIDE.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# MQTT Console Logging & API Guide
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Your USDA Vision Camera System now has **enhanced MQTT console logging** and **comprehensive API endpoints** for monitoring machine status via MQTT.
|
||||
|
||||
## ✨ What's New
|
||||
|
||||
### 1. **Enhanced Console Logging**
|
||||
- **Colorful emoji-based console output** for all MQTT events
|
||||
- **Real-time visibility** of MQTT connections, subscriptions, and messages
|
||||
- **Clear status indicators** for debugging and monitoring
|
||||
|
||||
### 2. **New MQTT Status API Endpoint**
|
||||
- **GET /mqtt/status** - Detailed MQTT client statistics
|
||||
- **Message counts, error tracking, uptime monitoring**
|
||||
- **Real-time connection status and broker information**
|
||||
|
||||
### 3. **Existing Machine Status APIs** (already available)
|
||||
- **GET /machines** - All machine states from MQTT
|
||||
- **GET /system/status** - Overall system status including MQTT
|
||||
|
||||
## 🖥️ Console Logging Examples
|
||||
|
||||
When you run the system, you'll see:
|
||||
|
||||
```bash
|
||||
🔗 MQTT CONNECTED: 192.168.1.110:1883
|
||||
📋 MQTT SUBSCRIBED: vibratory_conveyor → vision/vibratory_conveyor/state
|
||||
📋 MQTT SUBSCRIBED: blower_separator → vision/blower_separator/state
|
||||
📡 MQTT MESSAGE: vibratory_conveyor → on
|
||||
📡 MQTT MESSAGE: blower_separator → off
|
||||
⚠️ MQTT DISCONNECTED: Unexpected disconnection (code: 1)
|
||||
🔗 MQTT CONNECTED: 192.168.1.110:1883
|
||||
```
|
||||
|
||||
## 🌐 API Endpoints
|
||||
|
||||
### MQTT Status
|
||||
```http
|
||||
GET http://localhost:8000/mqtt/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"connected": true,
|
||||
"broker_host": "192.168.1.110",
|
||||
"broker_port": 1883,
|
||||
"subscribed_topics": [
|
||||
"vision/vibratory_conveyor/state",
|
||||
"vision/blower_separator/state"
|
||||
],
|
||||
"last_message_time": "2025-07-28T12:00:00",
|
||||
"message_count": 42,
|
||||
"error_count": 0,
|
||||
"uptime_seconds": 3600.5
|
||||
}
|
||||
```
|
||||
|
||||
### Machine Status
|
||||
```http
|
||||
GET http://localhost:8000/machines
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"vibratory_conveyor": {
|
||||
"name": "vibratory_conveyor",
|
||||
"state": "on",
|
||||
"last_updated": "2025-07-28T12:00:00",
|
||||
"last_message": "on",
|
||||
"mqtt_topic": "vision/vibratory_conveyor/state"
|
||||
},
|
||||
"blower_separator": {
|
||||
"name": "blower_separator",
|
||||
"state": "off",
|
||||
"last_updated": "2025-07-28T12:00:00",
|
||||
"last_message": "off",
|
||||
"mqtt_topic": "vision/blower_separator/state"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Status
|
||||
```http
|
||||
GET http://localhost:8000/system/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"system_started": true,
|
||||
"mqtt_connected": true,
|
||||
"last_mqtt_message": "2025-07-28T12:00:00",
|
||||
"machines": { ... },
|
||||
"cameras": { ... },
|
||||
"active_recordings": 0,
|
||||
"total_recordings": 5,
|
||||
"uptime_seconds": 3600.5
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 How to Use
|
||||
|
||||
### 1. **Start the Full System**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
You'll see enhanced console logging for all MQTT events.
|
||||
|
||||
### 2. **Test MQTT Demo (MQTT only)**
|
||||
```bash
|
||||
python demo_mqtt_console.py
|
||||
```
|
||||
Shows just the MQTT client with enhanced logging.
|
||||
|
||||
### 3. **Test API Endpoints**
|
||||
```bash
|
||||
python test_mqtt_logging.py
|
||||
```
|
||||
Tests all the API endpoints and shows expected responses.
|
||||
|
||||
### 4. **Query APIs Directly**
|
||||
```bash
|
||||
# Check MQTT status
|
||||
curl http://localhost:8000/mqtt/status
|
||||
|
||||
# Check machine states
|
||||
curl http://localhost:8000/machines
|
||||
|
||||
# Check overall system status
|
||||
curl http://localhost:8000/system/status
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
The MQTT settings are in `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mqtt": {
|
||||
"broker_host": "192.168.1.110",
|
||||
"broker_port": 1883,
|
||||
"username": null,
|
||||
"password": null,
|
||||
"topics": {
|
||||
"vibratory_conveyor": "vision/vibratory_conveyor/state",
|
||||
"blower_separator": "vision/blower_separator/state"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Console Output Features
|
||||
|
||||
- **🔗 Connection Events**: Green for successful connections
|
||||
- **📋 Subscriptions**: Blue for topic subscriptions
|
||||
- **📡 Messages**: Real-time message display with machine name and payload
|
||||
- **⚠️ Warnings**: Yellow for unexpected disconnections
|
||||
- **❌ Errors**: Red for connection failures and errors
|
||||
- **❓ Unknown Topics**: Purple for unrecognized MQTT topics
|
||||
|
||||
## 📊 Monitoring & Debugging
|
||||
|
||||
### Real-time Monitoring
|
||||
- **Console**: Watch live MQTT events as they happen
|
||||
- **API**: Query `/mqtt/status` for statistics and health
|
||||
- **Logs**: Check `usda_vision_system.log` for detailed logs
|
||||
|
||||
### Troubleshooting
|
||||
1. **No MQTT messages?** Check broker connectivity and topic configuration
|
||||
2. **Connection issues?** Verify broker host/port in config.json
|
||||
3. **API not responding?** Ensure the system is running with `python main.py`
|
||||
|
||||
## 🎯 Use Cases
|
||||
|
||||
1. **Development**: See MQTT messages in real-time while developing
|
||||
2. **Debugging**: Identify connection issues and message patterns
|
||||
3. **Monitoring**: Use APIs to build dashboards or monitoring tools
|
||||
4. **Integration**: Query machine states from external applications
|
||||
5. **Maintenance**: Track MQTT statistics and error rates
|
||||
|
||||
---
|
||||
|
||||
**🎉 Your MQTT monitoring is now fully enhanced with both console logging and comprehensive APIs!**
|
||||
429
api-endpoints.http
Normal file
429
api-endpoints.http
Normal file
@@ -0,0 +1,429 @@
|
||||
###############################################################################
|
||||
# USDA Vision Camera System - Complete API Endpoints Documentation
|
||||
# Base URL: http://localhost:8000
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
# SYSTEM ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Root endpoint - API information
|
||||
GET http://localhost:8000/
|
||||
# Response: SuccessResponse
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "USDA Vision Camera System API",
|
||||
# "data": null,
|
||||
# "timestamp": "2025-07-28T12:00:00"
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Health check
|
||||
GET http://localhost:8000/health
|
||||
# Response: Simple health status
|
||||
# {
|
||||
# "status": "healthy",
|
||||
# "timestamp": "2025-07-28T12:00:00"
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Get system status
|
||||
GET http://localhost:8000/system/status
|
||||
# Response: SystemStatusResponse
|
||||
# {
|
||||
# "system_started": true,
|
||||
# "mqtt_connected": true,
|
||||
# "last_mqtt_message": "2025-07-28T12:00:00",
|
||||
# "machines": {
|
||||
# "vibratory_conveyor": {
|
||||
# "name": "vibratory_conveyor",
|
||||
# "state": "off",
|
||||
# "last_updated": "2025-07-28T12:00:00"
|
||||
# }
|
||||
# },
|
||||
# "cameras": {
|
||||
# "camera1": {
|
||||
# "name": "camera1",
|
||||
# "status": "connected",
|
||||
# "is_recording": false
|
||||
# }
|
||||
# },
|
||||
# "active_recordings": 0,
|
||||
# "total_recordings": 5,
|
||||
# "uptime_seconds": 3600.5
|
||||
# }
|
||||
|
||||
###############################################################################
|
||||
# MACHINE ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Get all machines status
|
||||
GET http://localhost:8000/machines
|
||||
# Response: Dict[str, MachineStatusResponse]
|
||||
# {
|
||||
# "vibratory_conveyor": {
|
||||
# "name": "vibratory_conveyor",
|
||||
# "state": "off",
|
||||
# "last_updated": "2025-07-28T12:00:00",
|
||||
# "last_message": "off",
|
||||
# "mqtt_topic": "vision/vibratory_conveyor/state"
|
||||
# },
|
||||
# "blower_separator": {
|
||||
# "name": "blower_separator",
|
||||
# "state": "on",
|
||||
# "last_updated": "2025-07-28T12:00:00",
|
||||
# "last_message": "on",
|
||||
# "mqtt_topic": "vision/blower_separator/state"
|
||||
# }
|
||||
# }
|
||||
|
||||
###############################################################################
|
||||
# MQTT ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Get MQTT status and statistics
|
||||
GET http://localhost:8000/mqtt/status
|
||||
# Response: MQTTStatusResponse
|
||||
# {
|
||||
# "connected": true,
|
||||
# "broker_host": "192.168.1.110",
|
||||
# "broker_port": 1883,
|
||||
# "subscribed_topics": [
|
||||
# "vision/vibratory_conveyor/state",
|
||||
# "vision/blower_separator/state"
|
||||
# ],
|
||||
# "last_message_time": "2025-07-28T12:00:00",
|
||||
# "message_count": 42,
|
||||
# "error_count": 0,
|
||||
# "uptime_seconds": 3600.5
|
||||
# }
|
||||
|
||||
### Get recent MQTT events history
|
||||
GET http://localhost:8000/mqtt/events
|
||||
# Optional query parameter: limit (default: 5, max: 50)
|
||||
# Response: MQTTEventsHistoryResponse
|
||||
# {
|
||||
# "events": [
|
||||
# {
|
||||
# "machine_name": "vibratory_conveyor",
|
||||
# "topic": "vision/vibratory_conveyor/state",
|
||||
# "payload": "on",
|
||||
# "normalized_state": "on",
|
||||
# "timestamp": "2025-07-28T15:30:45.123456",
|
||||
# "message_number": 15
|
||||
# },
|
||||
# {
|
||||
# "machine_name": "blower_separator",
|
||||
# "topic": "vision/blower_separator/state",
|
||||
# "payload": "off",
|
||||
# "normalized_state": "off",
|
||||
# "timestamp": "2025-07-28T15:29:12.654321",
|
||||
# "message_number": 14
|
||||
# }
|
||||
# ],
|
||||
# "total_events": 15,
|
||||
# "last_updated": "2025-07-28T15:30:45.123456"
|
||||
# }
|
||||
|
||||
### Get recent MQTT events with custom limit
|
||||
GET http://localhost:8000/mqtt/events?limit=10
|
||||
|
||||
###############################################################################
|
||||
# CAMERA ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Get all cameras status
|
||||
GET http://localhost:8000/cameras
|
||||
# Response: Dict[str, CameraStatusResponse]
|
||||
# {
|
||||
# "camera1": {
|
||||
# "name": "camera1",
|
||||
# "status": "connected",
|
||||
# "is_recording": false,
|
||||
# "last_checked": "2025-07-28T12:00:00",
|
||||
# "last_error": null,
|
||||
# "device_info": {
|
||||
# "friendly_name": "MindVision Camera",
|
||||
# "serial_number": "ABC123"
|
||||
# },
|
||||
# "current_recording_file": null,
|
||||
# "recording_start_time": null
|
||||
# }
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Get specific camera status
|
||||
GET http://localhost:8000/cameras/camera1/status
|
||||
### Get specific camera status
|
||||
GET http://localhost:8000/cameras/camera2/status
|
||||
# Response: CameraStatusResponse (same as above for single camera)
|
||||
|
||||
###############################################################################
|
||||
# RECORDING CONTROL ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Start recording (with all optional parameters)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "test_recording.avi",
|
||||
"exposure_ms": 1.5,
|
||||
"gain": 3.0,
|
||||
"fps": 10.0
|
||||
}
|
||||
# Request Parameters (all optional):
|
||||
# - filename: string - Custom filename (datetime prefix auto-added)
|
||||
# - exposure_ms: float - Exposure time in milliseconds
|
||||
# - gain: float - Camera gain value
|
||||
# - fps: float - Target frames per second (0 = maximum speed, omit = use config default)
|
||||
#
|
||||
# Response: StartRecordingResponse
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "Recording started for camera1",
|
||||
# "filename": "20250728_120000_test_recording.avi"
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Start recording (minimal - only filename)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "simple_test.avi"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording (only camera settings)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"exposure_ms": 2.0,
|
||||
"gain": 4.0,
|
||||
"fps": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording (empty body - all defaults)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
###
|
||||
|
||||
### Stop recording
|
||||
POST http://localhost:8000/cameras/camera1/stop-recording
|
||||
POST http://localhost:8000/cameras/camera2/stop-recording
|
||||
# No request body required
|
||||
# Response: StopRecordingResponse
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "Recording stopped for camera1",
|
||||
# "duration_seconds": 45.2
|
||||
# }
|
||||
|
||||
###############################################################################
|
||||
# CAMERA RECOVERY & DIAGNOSTICS ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Test camera connection
|
||||
POST http://localhost:8000/cameras/camera1/test-connection
|
||||
POST http://localhost:8000/cameras/camera2/test-connection
|
||||
# No request body required
|
||||
# Response: CameraTestResponse
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "Camera camera1 connection test passed",
|
||||
# "camera_name": "camera1",
|
||||
# "timestamp": "2025-07-28T12:00:00"
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Reconnect camera (soft recovery)
|
||||
POST http://localhost:8000/cameras/camera1/reconnect
|
||||
POST http://localhost:8000/cameras/camera2/reconnect
|
||||
# No request body required
|
||||
# Response: CameraRecoveryResponse
|
||||
# {
|
||||
# "success": true,
|
||||
# "message": "Camera camera1 reconnected successfully",
|
||||
# "camera_name": "camera1",
|
||||
# "operation": "reconnect",
|
||||
# "timestamp": "2025-07-28T12:00:00"
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Restart camera grab process
|
||||
POST http://localhost:8000/cameras/camera1/restart-grab
|
||||
POST http://localhost:8000/cameras/camera2/restart-grab
|
||||
# Response: CameraRecoveryResponse (same structure as reconnect)
|
||||
|
||||
###
|
||||
|
||||
### Reset camera timestamp
|
||||
POST http://localhost:8000/cameras/camera1/reset-timestamp
|
||||
POST http://localhost:8000/cameras/camera2/reset-timestamp
|
||||
# Response: CameraRecoveryResponse (same structure as reconnect)
|
||||
|
||||
###
|
||||
|
||||
### Full camera reset (hard recovery)
|
||||
POST http://localhost:8000/cameras/camera1/full-reset
|
||||
### Full camera reset (hard recovery)
|
||||
POST http://localhost:8000/cameras/camera2/full-reset
|
||||
# Response: CameraRecoveryResponse (same structure as reconnect)
|
||||
|
||||
###
|
||||
|
||||
### Reinitialize failed camera
|
||||
POST http://localhost:8000/cameras/camera1/reinitialize
|
||||
POST http://localhost:8000/cameras/camera2/reinitialize
|
||||
# Response: CameraRecoveryResponse (same structure as reconnect)
|
||||
|
||||
###############################################################################
|
||||
# RECORDING SESSIONS ENDPOINT
|
||||
###############################################################################
|
||||
|
||||
### Get all recording sessions
|
||||
GET http://localhost:8000/recordings
|
||||
# Response: Dict[str, RecordingInfoResponse]
|
||||
# {
|
||||
# "rec_001": {
|
||||
# "camera_name": "camera1",
|
||||
# "filename": "20250728_120000_test.avi",
|
||||
# "start_time": "2025-07-28T12:00:00",
|
||||
# "state": "completed",
|
||||
# "end_time": "2025-07-28T12:05:00",
|
||||
# "file_size_bytes": 1048576,
|
||||
# "frame_count": 1500,
|
||||
# "duration_seconds": 300.0,
|
||||
# "error_message": null
|
||||
# }
|
||||
# }
|
||||
|
||||
###############################################################################
|
||||
# STORAGE ENDPOINTS
|
||||
###############################################################################
|
||||
|
||||
### Get storage statistics
|
||||
GET http://localhost:8000/storage/stats
|
||||
# Response: StorageStatsResponse
|
||||
# {
|
||||
# "base_path": "/storage",
|
||||
# "total_files": 25,
|
||||
# "total_size_bytes": 52428800,
|
||||
# "cameras": {
|
||||
# "camera1": {
|
||||
# "file_count": 15,
|
||||
# "total_size_bytes": 31457280
|
||||
# }
|
||||
# },
|
||||
# "disk_usage": {
|
||||
# "total": 1000000000,
|
||||
# "used": 500000000,
|
||||
# "free": 500000000
|
||||
# }
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Get recording files list (with filters)
|
||||
POST http://localhost:8000/storage/files
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"start_date": "2025-07-25T00:00:00",
|
||||
"end_date": "2025-07-28T23:59:59",
|
||||
"limit": 50
|
||||
}
|
||||
# Request Parameters (all optional):
|
||||
# - camera_name: string - Filter by specific camera
|
||||
# - start_date: string (ISO format) - Filter files from this date
|
||||
# - end_date: string (ISO format) - Filter files until this date
|
||||
# - limit: integer (max 1000, default 100) - Maximum number of files to return
|
||||
#
|
||||
# Response: FileListResponse
|
||||
# {
|
||||
# "files": [
|
||||
# {
|
||||
# "filename": "20250728_120000_test.avi",
|
||||
# "camera_name": "camera1",
|
||||
# "file_size_bytes": 1048576,
|
||||
# "created_date": "2025-07-28T12:00:00",
|
||||
# "duration_seconds": 300.0
|
||||
# }
|
||||
# ],
|
||||
# "total_count": 1
|
||||
# }
|
||||
|
||||
###
|
||||
|
||||
### Get all files (no camera filter)
|
||||
POST http://localhost:8000/storage/files
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"limit": 100
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Cleanup old storage files
|
||||
POST http://localhost:8000/storage/cleanup
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"max_age_days": 7
|
||||
}
|
||||
# Request Parameters:
|
||||
# - max_age_days: integer (optional) - Remove files older than this many days
|
||||
# If not provided, uses config default (30 days)
|
||||
#
|
||||
# Response: CleanupResponse
|
||||
# {
|
||||
# "files_removed": 5,
|
||||
# "bytes_freed": 10485760,
|
||||
# "errors": []
|
||||
# }
|
||||
|
||||
###############################################################################
|
||||
# ERROR RESPONSES
|
||||
###############################################################################
|
||||
# All endpoints may return ErrorResponse on failure:
|
||||
# {
|
||||
# "error": "Error description",
|
||||
# "details": "Additional error details",
|
||||
# "timestamp": "2025-07-28T12:00:00"
|
||||
# }
|
||||
# Common HTTP status codes:
|
||||
# - 200: Success
|
||||
# - 400: Bad Request (invalid parameters)
|
||||
# - 404: Not Found (camera/resource not found)
|
||||
# - 500: Internal Server Error
|
||||
# - 503: Service Unavailable (camera manager not available)
|
||||
|
||||
###############################################################################
|
||||
# NOTES
|
||||
###############################################################################
|
||||
# 1. All timestamps are in ISO 8601 format
|
||||
# 2. File sizes are in bytes
|
||||
# 3. Camera names: "camera1", "camera2"
|
||||
# 4. Machine names: "vibratory_conveyor", "blower_separator"
|
||||
# 5. FPS behavior:
|
||||
# - fps > 0: Capture at specified frame rate
|
||||
# - fps = 0: Capture at MAXIMUM possible speed (no delay)
|
||||
# - fps omitted: Uses camera config default
|
||||
# 6. Filenames automatically get datetime prefix: YYYYMMDD_HHMMSS_filename.avi
|
||||
# 7. Recovery endpoints should be used in order: test-connection → reconnect → restart-grab → full-reset → reinitialize
|
||||
242
api-tests.http
242
api-tests.http
@@ -8,33 +8,151 @@ GET http://localhost:8000/cameras/camera1/status
|
||||
|
||||
###
|
||||
|
||||
### Get camera2 status
|
||||
### Get camera2 status
|
||||
GET http://localhost:8000/cameras/camera2/status
|
||||
|
||||
###
|
||||
### RECORDING TESTS
|
||||
### Note: All filenames will automatically have datetime prefix added
|
||||
### Format: YYYYMMDD_HHMMSS_filename.avi (or auto-generated if no filename)
|
||||
###
|
||||
### FPS Behavior:
|
||||
### - fps > 0: Capture at specified frame rate
|
||||
### - fps = 0: Capture at MAXIMUM possible speed (no delay between frames)
|
||||
### - fps omitted: Uses camera config default (usually 3.0 fps)
|
||||
### - Video files saved with 30 FPS metadata when fps=0 for proper playback
|
||||
###
|
||||
|
||||
### Start recording camera1
|
||||
### Start recording camera1 (basic)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera1",
|
||||
"filename": "manual_test_cam1.avi"
|
||||
"filename": "manual22_test_cam1.avi"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2
|
||||
### Start recording camera1 (with camera settings)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "test_with_settings.avi",
|
||||
"exposure_ms": 2.0,
|
||||
"gain": 4.0,
|
||||
"fps": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2 (basic)
|
||||
POST http://localhost:8000/cameras/camera2/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"camera_name": "camera2",
|
||||
"filename": "manual_test_cam2.avi"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2 (with different settings)
|
||||
POST http://localhost:8000/cameras/camera2/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "high_fps_test.avi",
|
||||
"exposure_ms": 0.5,
|
||||
"gain": 2.5,
|
||||
"fps": 10.0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera1 (no filename, only settings)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"exposure_ms": 1.5,
|
||||
"gain": 3.0,
|
||||
"fps": 7.0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera1 (only filename, no settings)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "just_filename_test.avi"
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2 (only exposure setting)
|
||||
POST http://localhost:8000/cameras/camera2/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"exposure_ms": 3.0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera1 (only gain setting)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"gain": 5.5
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2 (only fps setting)
|
||||
POST http://localhost:8000/cameras/camera2/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"fps": 15.0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera1 (maximum fps - no delay)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "max_fps_test.avi",
|
||||
"fps": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera2 (maximum fps with settings)
|
||||
POST http://localhost:8000/cameras/camera2/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"filename": "max_fps_low_exposure.avi",
|
||||
"exposure_ms": 0.1,
|
||||
"gain": 1.0,
|
||||
"fps": 0
|
||||
}
|
||||
|
||||
###
|
||||
|
||||
### Start recording camera1 (empty body - all defaults)
|
||||
POST http://localhost:8000/cameras/camera1/start-recording
|
||||
Content-Type: application/json
|
||||
|
||||
{}
|
||||
|
||||
###
|
||||
|
||||
### Stop camera1 recording
|
||||
POST http://localhost:8000/cameras/camera1/stop-recording
|
||||
|
||||
@@ -43,6 +161,8 @@ POST http://localhost:8000/cameras/camera1/stop-recording
|
||||
### Stop camera2 recording
|
||||
POST http://localhost:8000/cameras/camera2/stop-recording
|
||||
|
||||
###
|
||||
### SYSTEM STATUS AND STORAGE TESTS
|
||||
###
|
||||
|
||||
### Get all cameras status
|
||||
@@ -77,4 +197,112 @@ Content-Type: application/json
|
||||
###
|
||||
|
||||
### Health check
|
||||
GET http://localhost:8000/health
|
||||
GET http://localhost:8000/health
|
||||
|
||||
###
|
||||
### CAMERA RECOVERY AND DIAGNOSTICS TESTS
|
||||
###
|
||||
### These endpoints help recover cameras that have failed to initialize or lost connection.
|
||||
###
|
||||
### Recovery Methods (in order of severity):
|
||||
### 1. test-connection: Test if camera connection is working
|
||||
### 2. reconnect: Soft reconnection using CameraReConnect()
|
||||
### 3. restart-grab: Restart grab process using CameraRestartGrab()
|
||||
### 4. reset-timestamp: Reset camera timestamp using CameraRstTimeStamp()
|
||||
### 5. full-reset: Hard reset - uninitialize and reinitialize camera
|
||||
### 6. reinitialize: Complete reinitialization for cameras that never initialized
|
||||
###
|
||||
### Recommended troubleshooting order:
|
||||
### 1. Start with test-connection to diagnose the issue
|
||||
### 2. Try reconnect first (most common fix)
|
||||
### 3. If reconnect fails, try restart-grab
|
||||
### 4. If still failing, try full-reset
|
||||
### 5. Use reinitialize only for cameras that failed initial setup
|
||||
###
|
||||
|
||||
### Test camera1 connection
|
||||
POST http://localhost:8000/cameras/camera1/test-connection
|
||||
|
||||
###
|
||||
|
||||
### Test camera2 connection
|
||||
POST http://localhost:8000/cameras/camera2/test-connection
|
||||
|
||||
###
|
||||
|
||||
### Reconnect camera1 (soft recovery)
|
||||
POST http://localhost:8000/cameras/camera1/reconnect
|
||||
|
||||
###
|
||||
|
||||
### Reconnect camera2 (soft recovery)
|
||||
POST http://localhost:8000/cameras/camera2/reconnect
|
||||
|
||||
###
|
||||
|
||||
### Restart camera1 grab process
|
||||
POST http://localhost:8000/cameras/camera1/restart-grab
|
||||
|
||||
###
|
||||
|
||||
### Restart camera2 grab process
|
||||
POST http://localhost:8000/cameras/camera2/restart-grab
|
||||
|
||||
###
|
||||
|
||||
### Reset camera1 timestamp
|
||||
POST http://localhost:8000/cameras/camera1/reset-timestamp
|
||||
|
||||
###
|
||||
|
||||
### Reset camera2 timestamp
|
||||
POST http://localhost:8000/cameras/camera2/reset-timestamp
|
||||
|
||||
###
|
||||
|
||||
### Full reset camera1 (hard recovery - uninitialize and reinitialize)
|
||||
POST http://localhost:8000/cameras/camera1/full-reset
|
||||
|
||||
###
|
||||
|
||||
### Full reset camera2 (hard recovery - uninitialize and reinitialize)
|
||||
POST http://localhost:8000/cameras/camera2/full-reset
|
||||
|
||||
###
|
||||
|
||||
### Reinitialize camera1 (for cameras that failed to initialize)
|
||||
POST http://localhost:8000/cameras/camera1/reinitialize
|
||||
|
||||
###
|
||||
|
||||
### Reinitialize camera2 (for cameras that failed to initialize)
|
||||
POST http://localhost:8000/cameras/camera2/reinitialize
|
||||
|
||||
###
|
||||
### RECOVERY WORKFLOW EXAMPLES
|
||||
###
|
||||
|
||||
### Example 1: Basic troubleshooting workflow for camera1
|
||||
### Step 1: Test connection
|
||||
POST http://localhost:8000/cameras/camera1/test-connection
|
||||
|
||||
### Step 2: If test fails, try reconnect
|
||||
# POST http://localhost:8000/cameras/camera1/reconnect
|
||||
|
||||
### Step 3: If reconnect fails, try restart grab
|
||||
# POST http://localhost:8000/cameras/camera1/restart-grab
|
||||
|
||||
### Step 4: If still failing, try full reset
|
||||
# POST http://localhost:8000/cameras/camera1/full-reset
|
||||
|
||||
### Step 5: If camera never initialized, try reinitialize
|
||||
# POST http://localhost:8000/cameras/camera1/reinitialize
|
||||
|
||||
###
|
||||
|
||||
### Example 2: Quick recovery sequence for camera2
|
||||
### Try reconnect first (most common fix)
|
||||
POST http://localhost:8000/cameras/camera2/reconnect
|
||||
|
||||
### If that doesn't work, try full reset
|
||||
# POST http://localhost:8000/cameras/camera2/full-reset
|
||||
32
config.json
32
config.json
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"system": {
|
||||
"camera_check_interval_seconds": 2,
|
||||
"log_level": "INFO",
|
||||
"log_level": "DEBUG",
|
||||
"log_file": "usda_vision_system.log",
|
||||
"api_host": "0.0.0.0",
|
||||
"api_port": 8000,
|
||||
@@ -32,7 +32,20 @@
|
||||
"exposure_ms": 1.0,
|
||||
"gain": 3.5,
|
||||
"target_fps": 0,
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"sharpness": 120,
|
||||
"contrast": 110,
|
||||
"saturation": 100,
|
||||
"gamma": 100,
|
||||
"noise_filter_enabled": true,
|
||||
"denoise_3d_enabled": false,
|
||||
"auto_white_balance": true,
|
||||
"color_temperature_preset": 0,
|
||||
"anti_flicker_enabled": true,
|
||||
"light_frequency": 1,
|
||||
"bit_depth": 8,
|
||||
"hdr_enabled": false,
|
||||
"hdr_gain_mode": 0
|
||||
},
|
||||
{
|
||||
"name": "camera2",
|
||||
@@ -41,7 +54,20 @@
|
||||
"exposure_ms": 1.0,
|
||||
"gain": 3.5,
|
||||
"target_fps": 0,
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"sharpness": 120,
|
||||
"contrast": 110,
|
||||
"saturation": 100,
|
||||
"gamma": 100,
|
||||
"noise_filter_enabled": true,
|
||||
"denoise_3d_enabled": false,
|
||||
"auto_white_balance": true,
|
||||
"color_temperature_preset": 0,
|
||||
"anti_flicker_enabled": true,
|
||||
"light_frequency": 1,
|
||||
"bit_depth": 8,
|
||||
"hdr_enabled": false,
|
||||
"hdr_gain_mode": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
117
demo_mqtt_console.py
Normal file
117
demo_mqtt_console.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script to show MQTT console logging in action.
|
||||
|
||||
This script demonstrates the enhanced MQTT logging by starting just the MQTT client
|
||||
and showing the console output.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import signal
|
||||
import logging
|
||||
|
||||
# Add the current directory to Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from usda_vision_system.core.config import Config
|
||||
from usda_vision_system.core.state_manager import StateManager
|
||||
from usda_vision_system.core.events import EventSystem
|
||||
from usda_vision_system.core.logging_config import setup_logging
|
||||
from usda_vision_system.mqtt.client import MQTTClient
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Handle Ctrl+C gracefully"""
|
||||
print("\n🛑 Stopping MQTT demo...")
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
"""Main demo function"""
|
||||
print("🚀 MQTT Console Logging Demo")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("This demo shows enhanced MQTT console logging.")
|
||||
print("You'll see colorful console output for MQTT events:")
|
||||
print(" 🔗 Connection status")
|
||||
print(" 📋 Topic subscriptions")
|
||||
print(" 📡 Incoming messages")
|
||||
print(" ⚠️ Disconnections and errors")
|
||||
print()
|
||||
print("Press Ctrl+C to stop the demo.")
|
||||
print("=" * 50)
|
||||
|
||||
# Setup signal handler
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
try:
|
||||
# Setup logging with INFO level for console visibility
|
||||
setup_logging(log_level="INFO", log_file="mqtt_demo.log")
|
||||
|
||||
# Load configuration
|
||||
config = Config()
|
||||
|
||||
# Initialize components
|
||||
state_manager = StateManager()
|
||||
event_system = EventSystem()
|
||||
|
||||
# Create MQTT client
|
||||
mqtt_client = MQTTClient(config, state_manager, event_system)
|
||||
|
||||
print(f"\n🔧 Configuration:")
|
||||
print(f" Broker: {config.mqtt.broker_host}:{config.mqtt.broker_port}")
|
||||
print(f" Topics: {list(config.mqtt.topics.values())}")
|
||||
print()
|
||||
|
||||
# Start MQTT client
|
||||
print("🚀 Starting MQTT client...")
|
||||
if mqtt_client.start():
|
||||
print("✅ MQTT client started successfully!")
|
||||
print("\n👀 Watching for MQTT messages... (Press Ctrl+C to stop)")
|
||||
print("-" * 50)
|
||||
|
||||
# Keep running and show periodic status
|
||||
start_time = time.time()
|
||||
last_status_time = start_time
|
||||
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
# Show status every 30 seconds
|
||||
current_time = time.time()
|
||||
if current_time - last_status_time >= 30:
|
||||
status = mqtt_client.get_status()
|
||||
uptime = current_time - start_time
|
||||
print(f"\n📊 Status Update (uptime: {uptime:.0f}s):")
|
||||
print(f" Connected: {status['connected']}")
|
||||
print(f" Messages: {status['message_count']}")
|
||||
print(f" Errors: {status['error_count']}")
|
||||
if status['last_message_time']:
|
||||
print(f" Last Message: {status['last_message_time']}")
|
||||
print("-" * 50)
|
||||
last_status_time = current_time
|
||||
|
||||
else:
|
||||
print("❌ Failed to start MQTT client")
|
||||
print(" Check your MQTT broker configuration in config.json")
|
||||
print(" Make sure the broker is running and accessible")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Demo stopped by user")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
finally:
|
||||
# Cleanup
|
||||
try:
|
||||
if 'mqtt_client' in locals():
|
||||
mqtt_client.stop()
|
||||
print("🔌 MQTT client stopped")
|
||||
except:
|
||||
pass
|
||||
|
||||
print("\n👋 Demo completed!")
|
||||
print("\n💡 To run the full system with this enhanced logging:")
|
||||
print(" python main.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
234
mqtt_publisher_test.py
Normal file
234
mqtt_publisher_test.py
Normal file
@@ -0,0 +1,234 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MQTT Publisher Test Script for USDA Vision Camera System
|
||||
|
||||
This script allows you to manually publish test messages to the MQTT topics
|
||||
to simulate machine state changes for testing purposes.
|
||||
|
||||
Usage:
|
||||
python mqtt_publisher_test.py
|
||||
|
||||
The script provides an interactive menu to:
|
||||
1. Send 'on' state to vibratory conveyor
|
||||
2. Send 'off' state to vibratory conveyor
|
||||
3. Send 'on' state to blower separator
|
||||
4. Send 'off' state to blower separator
|
||||
5. Send custom message
|
||||
"""
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import time
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
# MQTT Configuration (matching your system config)
|
||||
MQTT_BROKER_HOST = "192.168.1.110"
|
||||
MQTT_BROKER_PORT = 1883
|
||||
MQTT_USERNAME = None # Set if your broker requires authentication
|
||||
MQTT_PASSWORD = None # Set if your broker requires authentication
|
||||
|
||||
# Topics (from your config.json)
|
||||
MQTT_TOPICS = {
|
||||
"vibratory_conveyor": "vision/vibratory_conveyor/state",
|
||||
"blower_separator": "vision/blower_separator/state"
|
||||
}
|
||||
|
||||
class MQTTPublisher:
|
||||
def __init__(self):
|
||||
self.client = None
|
||||
self.connected = False
|
||||
|
||||
def setup_client(self):
|
||||
"""Setup MQTT client"""
|
||||
try:
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_publish = self.on_publish
|
||||
|
||||
if MQTT_USERNAME and MQTT_PASSWORD:
|
||||
self.client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error setting up MQTT client: {e}")
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
"""Connect to MQTT broker"""
|
||||
try:
|
||||
print(f"🔗 Connecting to MQTT broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}...")
|
||||
self.client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)
|
||||
self.client.loop_start() # Start background loop
|
||||
|
||||
# Wait for connection
|
||||
timeout = 10
|
||||
start_time = time.time()
|
||||
while not self.connected and (time.time() - start_time) < timeout:
|
||||
time.sleep(0.1)
|
||||
|
||||
return self.connected
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect to MQTT broker: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Disconnect from MQTT broker"""
|
||||
if self.client:
|
||||
self.client.loop_stop()
|
||||
self.client.disconnect()
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
"""Callback when client connects"""
|
||||
if rc == 0:
|
||||
self.connected = True
|
||||
print(f"✅ Connected to MQTT broker successfully!")
|
||||
else:
|
||||
self.connected = False
|
||||
print(f"❌ Connection failed with return code {rc}")
|
||||
|
||||
def on_disconnect(self, client, userdata, rc):
|
||||
"""Callback when client disconnects"""
|
||||
self.connected = False
|
||||
print(f"🔌 Disconnected from MQTT broker")
|
||||
|
||||
def on_publish(self, client, userdata, mid):
|
||||
"""Callback when message is published"""
|
||||
print(f"📤 Message published successfully (mid: {mid})")
|
||||
|
||||
def publish_message(self, topic, payload):
|
||||
"""Publish a message to a topic"""
|
||||
if not self.connected:
|
||||
print("❌ Not connected to MQTT broker")
|
||||
return False
|
||||
|
||||
try:
|
||||
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
print(f"📡 [{timestamp}] Publishing message:")
|
||||
print(f" 📍 Topic: {topic}")
|
||||
print(f" 📄 Payload: '{payload}'")
|
||||
|
||||
result = self.client.publish(topic, payload)
|
||||
|
||||
if result.rc == mqtt.MQTT_ERR_SUCCESS:
|
||||
print(f"✅ Message queued for publishing")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ Failed to publish message (error: {result.rc})")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error publishing message: {e}")
|
||||
return False
|
||||
|
||||
def show_menu(self):
|
||||
"""Show interactive menu"""
|
||||
print("\n" + "=" * 50)
|
||||
print("🎛️ MQTT PUBLISHER TEST MENU")
|
||||
print("=" * 50)
|
||||
print("1. Send 'on' to vibratory conveyor")
|
||||
print("2. Send 'off' to vibratory conveyor")
|
||||
print("3. Send 'on' to blower separator")
|
||||
print("4. Send 'off' to blower separator")
|
||||
print("5. Send custom message")
|
||||
print("6. Show current topics")
|
||||
print("0. Exit")
|
||||
print("-" * 50)
|
||||
|
||||
def handle_menu_choice(self, choice):
|
||||
"""Handle menu selection"""
|
||||
if choice == "1":
|
||||
self.publish_message(MQTT_TOPICS["vibratory_conveyor"], "on")
|
||||
elif choice == "2":
|
||||
self.publish_message(MQTT_TOPICS["vibratory_conveyor"], "off")
|
||||
elif choice == "3":
|
||||
self.publish_message(MQTT_TOPICS["blower_separator"], "on")
|
||||
elif choice == "4":
|
||||
self.publish_message(MQTT_TOPICS["blower_separator"], "off")
|
||||
elif choice == "5":
|
||||
self.custom_message()
|
||||
elif choice == "6":
|
||||
self.show_topics()
|
||||
elif choice == "0":
|
||||
return False
|
||||
else:
|
||||
print("❌ Invalid choice. Please try again.")
|
||||
|
||||
return True
|
||||
|
||||
def custom_message(self):
|
||||
"""Send custom message"""
|
||||
print("\n📝 Custom Message")
|
||||
print("Available topics:")
|
||||
for i, (name, topic) in enumerate(MQTT_TOPICS.items(), 1):
|
||||
print(f" {i}. {name}: {topic}")
|
||||
|
||||
try:
|
||||
topic_choice = input("Select topic (1-2): ").strip()
|
||||
if topic_choice == "1":
|
||||
topic = MQTT_TOPICS["vibratory_conveyor"]
|
||||
elif topic_choice == "2":
|
||||
topic = MQTT_TOPICS["blower_separator"]
|
||||
else:
|
||||
print("❌ Invalid topic choice")
|
||||
return
|
||||
|
||||
payload = input("Enter message payload: ").strip()
|
||||
if payload:
|
||||
self.publish_message(topic, payload)
|
||||
else:
|
||||
print("❌ Empty payload, message not sent")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n❌ Cancelled")
|
||||
|
||||
def show_topics(self):
|
||||
"""Show configured topics"""
|
||||
print("\n📋 Configured Topics:")
|
||||
for name, topic in MQTT_TOPICS.items():
|
||||
print(f" 🏭 {name}: {topic}")
|
||||
|
||||
def run(self):
|
||||
"""Main interactive loop"""
|
||||
print("📤 MQTT Publisher Test")
|
||||
print("=" * 50)
|
||||
print(f"🎯 Broker: {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
|
||||
|
||||
if not self.setup_client():
|
||||
return False
|
||||
|
||||
if not self.connect():
|
||||
print("❌ Failed to connect to MQTT broker")
|
||||
return False
|
||||
|
||||
try:
|
||||
while True:
|
||||
self.show_menu()
|
||||
choice = input("Enter your choice: ").strip()
|
||||
|
||||
if not self.handle_menu_choice(choice):
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n🛑 Interrupted by user")
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
finally:
|
||||
self.disconnect()
|
||||
print("👋 Goodbye!")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
publisher = MQTTPublisher()
|
||||
|
||||
try:
|
||||
publisher.run()
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
242
mqtt_test.py
Normal file
242
mqtt_test.py
Normal file
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
MQTT Test Script for USDA Vision Camera System
|
||||
|
||||
This script tests MQTT message reception by connecting to the broker
|
||||
and listening for messages on the configured topics.
|
||||
|
||||
Usage:
|
||||
python mqtt_test.py
|
||||
|
||||
The script will:
|
||||
1. Connect to the MQTT broker
|
||||
2. Subscribe to all configured topics
|
||||
3. Display received messages with timestamps
|
||||
4. Show connection status and statistics
|
||||
"""
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import time
|
||||
import json
|
||||
import signal
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
# MQTT Configuration (matching your system config)
|
||||
MQTT_BROKER_HOST = "192.168.1.110"
|
||||
MQTT_BROKER_PORT = 1883
|
||||
MQTT_USERNAME = None # Set if your broker requires authentication
|
||||
MQTT_PASSWORD = None # Set if your broker requires authentication
|
||||
|
||||
# Topics to monitor (from your config.json)
|
||||
MQTT_TOPICS = {
|
||||
"vibratory_conveyor": "vision/vibratory_conveyor/state",
|
||||
"blower_separator": "vision/blower_separator/state"
|
||||
}
|
||||
|
||||
class MQTTTester:
|
||||
def __init__(self):
|
||||
self.client: Optional[mqtt.Client] = None
|
||||
self.connected = False
|
||||
self.message_count = 0
|
||||
self.start_time = None
|
||||
self.last_message_time = None
|
||||
self.received_messages = []
|
||||
|
||||
def setup_client(self):
|
||||
"""Setup MQTT client with callbacks"""
|
||||
try:
|
||||
# Create MQTT client
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
|
||||
|
||||
# Set callbacks
|
||||
self.client.on_connect = self.on_connect
|
||||
self.client.on_disconnect = self.on_disconnect
|
||||
self.client.on_message = self.on_message
|
||||
self.client.on_subscribe = self.on_subscribe
|
||||
|
||||
# Set authentication if provided
|
||||
if MQTT_USERNAME and MQTT_PASSWORD:
|
||||
self.client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
|
||||
print(f"🔐 Using authentication: {MQTT_USERNAME}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error setting up MQTT client: {e}")
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
"""Connect to MQTT broker"""
|
||||
try:
|
||||
print(f"🔗 Connecting to MQTT broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}...")
|
||||
self.client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to connect to MQTT broker: {e}")
|
||||
return False
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
"""Callback when client connects to broker"""
|
||||
if rc == 0:
|
||||
self.connected = True
|
||||
self.start_time = datetime.now()
|
||||
print(f"✅ Successfully connected to MQTT broker!")
|
||||
print(f"📅 Connection time: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print()
|
||||
|
||||
# Subscribe to all topics
|
||||
print("📋 Subscribing to topics:")
|
||||
for machine_name, topic in MQTT_TOPICS.items():
|
||||
result, mid = client.subscribe(topic)
|
||||
if result == mqtt.MQTT_ERR_SUCCESS:
|
||||
print(f" ✅ {machine_name}: {topic}")
|
||||
else:
|
||||
print(f" ❌ {machine_name}: {topic} (error: {result})")
|
||||
|
||||
print()
|
||||
print("🎧 Listening for MQTT messages...")
|
||||
print(" (Manually turn machines on/off to trigger messages)")
|
||||
print(" (Press Ctrl+C to stop)")
|
||||
print("-" * 60)
|
||||
|
||||
else:
|
||||
self.connected = False
|
||||
print(f"❌ Connection failed with return code {rc}")
|
||||
print(" Return codes:")
|
||||
print(" 0: Connection successful")
|
||||
print(" 1: Connection refused - incorrect protocol version")
|
||||
print(" 2: Connection refused - invalid client identifier")
|
||||
print(" 3: Connection refused - server unavailable")
|
||||
print(" 4: Connection refused - bad username or password")
|
||||
print(" 5: Connection refused - not authorised")
|
||||
|
||||
def on_disconnect(self, client, userdata, rc):
|
||||
"""Callback when client disconnects from broker"""
|
||||
self.connected = False
|
||||
if rc != 0:
|
||||
print(f"🔌 Unexpected disconnection from MQTT broker (code: {rc})")
|
||||
else:
|
||||
print(f"🔌 Disconnected from MQTT broker")
|
||||
|
||||
def on_subscribe(self, client, userdata, mid, granted_qos):
|
||||
"""Callback when subscription is confirmed"""
|
||||
print(f"📋 Subscription confirmed (mid: {mid}, QoS: {granted_qos})")
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
"""Callback when a message is received"""
|
||||
try:
|
||||
# Decode message
|
||||
topic = msg.topic
|
||||
payload = msg.payload.decode("utf-8").strip()
|
||||
timestamp = datetime.now()
|
||||
|
||||
# Update statistics
|
||||
self.message_count += 1
|
||||
self.last_message_time = timestamp
|
||||
|
||||
# Find machine name
|
||||
machine_name = "unknown"
|
||||
for name, configured_topic in MQTT_TOPICS.items():
|
||||
if topic == configured_topic:
|
||||
machine_name = name
|
||||
break
|
||||
|
||||
# Store message
|
||||
message_data = {
|
||||
"timestamp": timestamp,
|
||||
"topic": topic,
|
||||
"machine": machine_name,
|
||||
"payload": payload,
|
||||
"message_number": self.message_count
|
||||
}
|
||||
self.received_messages.append(message_data)
|
||||
|
||||
# Display message
|
||||
time_str = timestamp.strftime('%H:%M:%S.%f')[:-3] # Include milliseconds
|
||||
print(f"📡 [{time_str}] Message #{self.message_count}")
|
||||
print(f" 🏭 Machine: {machine_name}")
|
||||
print(f" 📍 Topic: {topic}")
|
||||
print(f" 📄 Payload: '{payload}'")
|
||||
print(f" 📊 Total messages: {self.message_count}")
|
||||
print("-" * 60)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error processing message: {e}")
|
||||
|
||||
def show_statistics(self):
|
||||
"""Show connection and message statistics"""
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 MQTT TEST STATISTICS")
|
||||
print("=" * 60)
|
||||
|
||||
if self.start_time:
|
||||
runtime = datetime.now() - self.start_time
|
||||
print(f"⏱️ Runtime: {runtime}")
|
||||
|
||||
print(f"🔗 Connected: {'Yes' if self.connected else 'No'}")
|
||||
print(f"📡 Messages received: {self.message_count}")
|
||||
|
||||
if self.last_message_time:
|
||||
print(f"🕐 Last message: {self.last_message_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
if self.received_messages:
|
||||
print(f"\n📋 Message Summary:")
|
||||
for msg in self.received_messages[-5:]: # Show last 5 messages
|
||||
time_str = msg["timestamp"].strftime('%H:%M:%S')
|
||||
print(f" [{time_str}] {msg['machine']}: {msg['payload']}")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
def run(self):
|
||||
"""Main test loop"""
|
||||
print("🧪 MQTT Message Reception Test")
|
||||
print("=" * 60)
|
||||
print(f"🎯 Broker: {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
|
||||
print(f"📋 Topics: {list(MQTT_TOPICS.values())}")
|
||||
print()
|
||||
|
||||
# Setup signal handler for graceful shutdown
|
||||
def signal_handler(sig, frame):
|
||||
print(f"\n\n🛑 Received interrupt signal, shutting down...")
|
||||
self.show_statistics()
|
||||
if self.client and self.connected:
|
||||
self.client.disconnect()
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# Setup and connect
|
||||
if not self.setup_client():
|
||||
return False
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
# Start the client loop
|
||||
try:
|
||||
self.client.loop_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"❌ Error in main loop: {e}")
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
tester = MQTTTester()
|
||||
|
||||
try:
|
||||
success = tester.run()
|
||||
if not success:
|
||||
print("❌ Test failed")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -18,7 +18,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": 26,
|
||||
"id": "imports",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -42,7 +42,7 @@
|
||||
"from datetime import datetime\n",
|
||||
"\n",
|
||||
"# Add the python demo directory to path to import mvsdk\n",
|
||||
"sys.path.append('./python demo')\n",
|
||||
"sys.path.append('../python demo')\n",
|
||||
"import mvsdk\n",
|
||||
"\n",
|
||||
"print(\"Libraries imported successfully!\")\n",
|
||||
@@ -51,7 +51,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"execution_count": 27,
|
||||
"id": "error-codes",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"execution_count": 28,
|
||||
"id": "status-functions",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -215,7 +215,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"execution_count": 29,
|
||||
"id": "test-capture-availability",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -375,7 +375,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"execution_count": 30,
|
||||
"id": "comprehensive-check",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -408,7 +408,7 @@
|
||||
"FINAL RESULTS:\n",
|
||||
"Camera Available: False\n",
|
||||
"Capture Ready: False\n",
|
||||
"Status: (6, 'AVAILABLE')\n",
|
||||
"Status: (42, 'AVAILABLE')\n",
|
||||
"==================================================\n"
|
||||
]
|
||||
}
|
||||
@@ -455,7 +455,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"execution_count": 31,
|
||||
"id": "status-check-function",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
|
||||
@@ -18,9 +18,19 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ All imports successful!\n",
|
||||
"OpenCV version: 4.11.0\n",
|
||||
"NumPy version: 2.3.2\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import cv2\n",
|
||||
"import numpy as np\n",
|
||||
@@ -50,9 +60,17 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"✅ Utility functions loaded!\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def display_image(image, title=\"Image\", figsize=(10, 8)):\n",
|
||||
" \"\"\"Display image inline in Jupyter notebook\"\"\"\n",
|
||||
@@ -130,9 +148,22 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"Storage directory exists: True\n",
|
||||
"Storage directory writable: True\n",
|
||||
"📁 Directory ready: /storage/test_images\n",
|
||||
"📁 Directory ready: /storage/test_videos\n",
|
||||
"📁 Directory ready: /storage/camera1\n",
|
||||
"📁 Directory ready: /storage/camera2\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Check storage directory\n",
|
||||
"storage_path = Path(\"/storage\")\n",
|
||||
@@ -155,9 +186,92 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 4,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔍 Scanning for available cameras...\n",
|
||||
"❌ No cameras found\n",
|
||||
"\n",
|
||||
"📊 Summary: Found 0 camera(s): []\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[ WARN:0@9.977] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.977] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.977] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video1): can't open camera by index\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.977] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.977] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video2): can't open camera by index\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.977] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.977] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video3): can't open camera by index\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.977] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.977] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.977] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video4): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video5): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video6): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video7): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video8): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video9): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.978] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@9.978] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video10): can't open camera by index\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.978] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@9.979] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@9.979] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@9.979] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Scan for cameras\n",
|
||||
"cameras = list_available_cameras()\n",
|
||||
@@ -173,9 +287,41 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 5,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"🔧 Testing camera 0...\n",
|
||||
" Trying Default backend...\n",
|
||||
" ❌ Default backend failed to open\n",
|
||||
" Trying GStreamer backend...\n",
|
||||
" ❌ GStreamer backend failed to open\n",
|
||||
" Trying V4L2 backend...\n",
|
||||
" ❌ V4L2 backend failed to open\n",
|
||||
" Trying FFmpeg backend...\n",
|
||||
" ❌ FFmpeg backend failed to open\n",
|
||||
"❌ Camera 0 not accessible with any backend\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"[ WARN:0@27.995] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index\n",
|
||||
"[ WARN:0@27.995] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@27.995] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ WARN:0@27.995] global obsensor_stream_channel_v4l2.cpp:82 xioctl ioctl: fd=-1, req=-2140645888\n",
|
||||
"[ WARN:0@27.995] global obsensor_stream_channel_v4l2.cpp:138 queryUvcDeviceInfoList ioctl error return: 9\n",
|
||||
"[ERROR:0@27.995] global obsensor_uvc_stream_channel.cpp:158 getStreamChannelGroup Camera index out of range\n",
|
||||
"[ WARN:0@27.996] global cap_v4l.cpp:913 open VIDEOIO(V4L2:/dev/video0): can't open camera by index\n",
|
||||
"[ WARN:0@27.996] global cap.cpp:478 open VIDEOIO(V4L2): backend is generally available but can't be used to capture by index\n",
|
||||
"[ WARN:0@27.996] global cap.cpp:478 open VIDEOIO(FFMPEG): backend is generally available but can't be used to capture by index\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# Test a specific camera (change camera_id as needed)\n",
|
||||
"camera_id = 0 # Change this to test different cameras\n",
|
||||
@@ -327,9 +473,9 @@
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "usda-vision-cameras",
|
||||
"display_name": "USDA-vision-cameras",
|
||||
"language": "python",
|
||||
"name": "usda-vision-cameras"
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
@@ -341,7 +487,7 @@
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.0"
|
||||
"version": "3.11.2"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
|
||||
@@ -17,4 +17,5 @@ dependencies = [
|
||||
"websockets>=12.0",
|
||||
"requests>=2.31.0",
|
||||
"pytz>=2023.3",
|
||||
"ipykernel>=6.30.0",
|
||||
]
|
||||
|
||||
173
test_api_changes.py
Normal file
173
test_api_changes.py
Normal file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the API changes for camera settings and filename handling.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
# API base URL
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
def test_api_endpoint(endpoint, method="GET", data=None):
|
||||
"""Test an API endpoint and return the response"""
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
|
||||
try:
|
||||
if method == "GET":
|
||||
response = requests.get(url)
|
||||
elif method == "POST":
|
||||
response = requests.post(url, json=data, headers={"Content-Type": "application/json"})
|
||||
|
||||
print(f"\n{method} {endpoint}")
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print(f"Response: {json.dumps(result, indent=2)}")
|
||||
return result
|
||||
else:
|
||||
print(f"Error: {response.text}")
|
||||
return None
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"Error: Could not connect to {url}")
|
||||
print("Make sure the API server is running with: python main.py")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return None
|
||||
|
||||
def test_camera_recording_with_settings():
|
||||
"""Test camera recording with new settings parameters"""
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing Camera Recording API with New Settings")
|
||||
print("=" * 60)
|
||||
|
||||
# Test 1: Basic recording without settings
|
||||
print("\n1. Testing basic recording (no settings)")
|
||||
basic_request = {
|
||||
"camera_name": "camera1",
|
||||
"filename": "test_basic.avi"
|
||||
}
|
||||
|
||||
result = test_api_endpoint("/cameras/camera1/start-recording", "POST", basic_request)
|
||||
if result and result.get("success"):
|
||||
print("✅ Basic recording started successfully")
|
||||
print(f" Filename: {result.get('filename')}")
|
||||
|
||||
# Stop recording
|
||||
time.sleep(2)
|
||||
test_api_endpoint("/cameras/camera1/stop-recording", "POST")
|
||||
else:
|
||||
print("❌ Basic recording failed")
|
||||
|
||||
# Test 2: Recording with camera settings
|
||||
print("\n2. Testing recording with camera settings")
|
||||
settings_request = {
|
||||
"camera_name": "camera1",
|
||||
"filename": "test_with_settings.avi",
|
||||
"exposure_ms": 2.0,
|
||||
"gain": 4.0,
|
||||
"fps": 5.0
|
||||
}
|
||||
|
||||
result = test_api_endpoint("/cameras/camera1/start-recording", "POST", settings_request)
|
||||
if result and result.get("success"):
|
||||
print("✅ Recording with settings started successfully")
|
||||
print(f" Filename: {result.get('filename')}")
|
||||
|
||||
# Stop recording
|
||||
time.sleep(2)
|
||||
test_api_endpoint("/cameras/camera1/stop-recording", "POST")
|
||||
else:
|
||||
print("❌ Recording with settings failed")
|
||||
|
||||
# Test 3: Recording with only settings (no filename)
|
||||
print("\n3. Testing recording with settings only (no filename)")
|
||||
settings_only_request = {
|
||||
"camera_name": "camera1",
|
||||
"exposure_ms": 1.5,
|
||||
"gain": 3.0,
|
||||
"fps": 7.0
|
||||
}
|
||||
|
||||
result = test_api_endpoint("/cameras/camera1/start-recording", "POST", settings_only_request)
|
||||
if result and result.get("success"):
|
||||
print("✅ Recording with settings only started successfully")
|
||||
print(f" Filename: {result.get('filename')}")
|
||||
|
||||
# Stop recording
|
||||
time.sleep(2)
|
||||
test_api_endpoint("/cameras/camera1/stop-recording", "POST")
|
||||
else:
|
||||
print("❌ Recording with settings only failed")
|
||||
|
||||
# Test 4: Test filename datetime prefix
|
||||
print("\n4. Testing filename datetime prefix")
|
||||
timestamp_before = datetime.now().strftime("%Y%m%d_%H%M")
|
||||
|
||||
filename_test_request = {
|
||||
"camera_name": "camera1",
|
||||
"filename": "my_custom_name.avi"
|
||||
}
|
||||
|
||||
result = test_api_endpoint("/cameras/camera1/start-recording", "POST", filename_test_request)
|
||||
if result and result.get("success"):
|
||||
returned_filename = result.get('filename', '')
|
||||
print(f" Original filename: my_custom_name.avi")
|
||||
print(f" Returned filename: {returned_filename}")
|
||||
|
||||
# Check if datetime prefix was added
|
||||
if timestamp_before in returned_filename and "my_custom_name.avi" in returned_filename:
|
||||
print("✅ Datetime prefix correctly added to filename")
|
||||
else:
|
||||
print("❌ Datetime prefix not properly added")
|
||||
|
||||
# Stop recording
|
||||
time.sleep(2)
|
||||
test_api_endpoint("/cameras/camera1/stop-recording", "POST")
|
||||
else:
|
||||
print("❌ Filename test failed")
|
||||
|
||||
def test_system_status():
|
||||
"""Test basic system status to ensure API is working"""
|
||||
print("\n" + "=" * 60)
|
||||
print("Testing System Status")
|
||||
print("=" * 60)
|
||||
|
||||
# Test system status
|
||||
result = test_api_endpoint("/system/status")
|
||||
if result:
|
||||
print("✅ System status API working")
|
||||
print(f" System started: {result.get('system_started')}")
|
||||
print(f" MQTT connected: {result.get('mqtt_connected')}")
|
||||
else:
|
||||
print("❌ System status API failed")
|
||||
|
||||
# Test camera status
|
||||
result = test_api_endpoint("/cameras")
|
||||
if result:
|
||||
print("✅ Camera status API working")
|
||||
for camera_name, camera_info in result.items():
|
||||
print(f" {camera_name}: {camera_info.get('status')}")
|
||||
else:
|
||||
print("❌ Camera status API failed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("USDA Vision Camera System - API Changes Test")
|
||||
print("This script tests the new camera settings parameters and filename handling")
|
||||
print("\nMake sure the system is running with: python main.py")
|
||||
|
||||
# Test system status first
|
||||
test_system_status()
|
||||
|
||||
# Test camera recording with new features
|
||||
test_camera_recording_with_settings()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test completed!")
|
||||
print("=" * 60)
|
||||
92
test_camera_recovery_api.py
Normal file
92
test_camera_recovery_api.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for camera recovery API endpoints.
|
||||
|
||||
This script tests the new camera recovery functionality without requiring actual cameras.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from typing import Dict, Any
|
||||
|
||||
# API base URL
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
def test_endpoint(method: str, endpoint: str, data: Dict[Any, Any] = None) -> Dict[Any, Any]:
|
||||
"""Test an API endpoint and return the response"""
|
||||
url = f"{BASE_URL}{endpoint}"
|
||||
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
response = requests.get(url, timeout=10)
|
||||
elif method.upper() == "POST":
|
||||
response = requests.post(url, json=data or {}, timeout=10)
|
||||
else:
|
||||
raise ValueError(f"Unsupported method: {method}")
|
||||
|
||||
print(f"\n{method} {endpoint}")
|
||||
print(f"Status: {response.status_code}")
|
||||
|
||||
if response.headers.get('content-type', '').startswith('application/json'):
|
||||
result = response.json()
|
||||
print(f"Response: {json.dumps(result, indent=2)}")
|
||||
return result
|
||||
else:
|
||||
print(f"Response: {response.text}")
|
||||
return {"text": response.text}
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"❌ Connection failed - API server not running at {BASE_URL}")
|
||||
return {"error": "connection_failed"}
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"❌ Request timeout")
|
||||
return {"error": "timeout"}
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
def main():
|
||||
"""Test camera recovery API endpoints"""
|
||||
print("🔧 Testing Camera Recovery API Endpoints")
|
||||
print("=" * 50)
|
||||
|
||||
# Test basic endpoints first
|
||||
print("\n📋 BASIC API TESTS")
|
||||
test_endpoint("GET", "/health")
|
||||
test_endpoint("GET", "/cameras")
|
||||
|
||||
# Test camera recovery endpoints
|
||||
print("\n🔧 CAMERA RECOVERY TESTS")
|
||||
|
||||
camera_names = ["camera1", "camera2"]
|
||||
|
||||
for camera_name in camera_names:
|
||||
print(f"\n--- Testing {camera_name} ---")
|
||||
|
||||
# Test connection
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/test-connection")
|
||||
|
||||
# Test reconnect
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/reconnect")
|
||||
|
||||
# Test restart grab
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/restart-grab")
|
||||
|
||||
# Test reset timestamp
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/reset-timestamp")
|
||||
|
||||
# Test full reset
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/full-reset")
|
||||
|
||||
# Test reinitialize
|
||||
test_endpoint("POST", f"/cameras/{camera_name}/reinitialize")
|
||||
|
||||
time.sleep(0.5) # Small delay between tests
|
||||
|
||||
print("\n✅ Camera recovery API tests completed!")
|
||||
print("\nNote: Some operations may fail if cameras are not connected,")
|
||||
print("but the API endpoints should respond with proper error messages.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
131
test_max_fps.py
Normal file
131
test_max_fps.py
Normal file
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to demonstrate maximum FPS capture functionality.
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
BASE_URL = "http://localhost:8000"
|
||||
|
||||
def test_fps_modes():
|
||||
"""Test different FPS modes to demonstrate the functionality"""
|
||||
|
||||
print("=" * 60)
|
||||
print("Testing Maximum FPS Capture Functionality")
|
||||
print("=" * 60)
|
||||
|
||||
# Test configurations
|
||||
test_configs = [
|
||||
{
|
||||
"name": "Normal FPS (3.0)",
|
||||
"data": {
|
||||
"filename": "normal_fps_test.avi",
|
||||
"exposure_ms": 1.0,
|
||||
"gain": 3.0,
|
||||
"fps": 3.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "High FPS (10.0)",
|
||||
"data": {
|
||||
"filename": "high_fps_test.avi",
|
||||
"exposure_ms": 0.5,
|
||||
"gain": 2.0,
|
||||
"fps": 10.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Maximum FPS (fps=0)",
|
||||
"data": {
|
||||
"filename": "max_fps_test.avi",
|
||||
"exposure_ms": 0.1, # Very short exposure for max speed
|
||||
"gain": 1.0, # Low gain to avoid overexposure
|
||||
"fps": 0 # Maximum speed - no delay
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Default FPS (omitted)",
|
||||
"data": {
|
||||
"filename": "default_fps_test.avi",
|
||||
"exposure_ms": 1.0,
|
||||
"gain": 3.0
|
||||
# fps omitted - uses camera config default
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
for i, config in enumerate(test_configs, 1):
|
||||
print(f"\n{i}. Testing {config['name']}")
|
||||
print("-" * 40)
|
||||
|
||||
# Start recording
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/cameras/camera1/start-recording",
|
||||
json=config['data'],
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if result.get('success'):
|
||||
print(f"✅ Recording started successfully")
|
||||
print(f" Filename: {result.get('filename')}")
|
||||
print(f" Settings: {json.dumps(config['data'], indent=6)}")
|
||||
|
||||
# Record for a short time
|
||||
print(f" Recording for 3 seconds...")
|
||||
time.sleep(3)
|
||||
|
||||
# Stop recording
|
||||
stop_response = requests.post(f"{BASE_URL}/cameras/camera1/stop-recording")
|
||||
if stop_response.status_code == 200:
|
||||
stop_result = stop_response.json()
|
||||
if stop_result.get('success'):
|
||||
print(f"✅ Recording stopped successfully")
|
||||
if 'duration_seconds' in stop_result:
|
||||
print(f" Duration: {stop_result['duration_seconds']:.1f}s")
|
||||
else:
|
||||
print(f"❌ Failed to stop recording: {stop_result.get('message')}")
|
||||
else:
|
||||
print(f"❌ Stop request failed: {stop_response.status_code}")
|
||||
|
||||
else:
|
||||
print(f"❌ Recording failed: {result.get('message')}")
|
||||
else:
|
||||
print(f"❌ Request failed: {response.status_code} - {response.text}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(f"❌ Could not connect to {BASE_URL}")
|
||||
print("Make sure the API server is running with: python main.py")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
# Wait between tests
|
||||
if i < len(test_configs):
|
||||
print(" Waiting 2 seconds before next test...")
|
||||
time.sleep(2)
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("FPS Test Summary:")
|
||||
print("=" * 60)
|
||||
print("• fps > 0: Controlled frame rate with sleep delay")
|
||||
print("• fps = 0: MAXIMUM speed capture (no delay between frames)")
|
||||
print("• fps omitted: Uses camera config default")
|
||||
print("• Video files with fps=0 are saved with 30 FPS metadata")
|
||||
print("• Actual capture rate with fps=0 depends on:")
|
||||
print(" - Camera hardware capabilities")
|
||||
print(" - Exposure time (shorter = faster)")
|
||||
print(" - Processing overhead")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("USDA Vision Camera System - Maximum FPS Test")
|
||||
print("This script demonstrates fps=0 for maximum capture speed")
|
||||
print("\nMake sure the system is running with: python main.py")
|
||||
|
||||
test_fps_modes()
|
||||
168
test_mqtt_events_api.py
Normal file
168
test_mqtt_events_api.py
Normal file
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for MQTT events API endpoint
|
||||
|
||||
This script tests the new MQTT events history functionality by:
|
||||
1. Starting the system components
|
||||
2. Simulating MQTT messages
|
||||
3. Testing the API endpoint to retrieve events
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Test configuration
|
||||
API_BASE_URL = "http://localhost:8000"
|
||||
MQTT_EVENTS_ENDPOINT = f"{API_BASE_URL}/mqtt/events"
|
||||
|
||||
def test_api_endpoint():
|
||||
"""Test the MQTT events API endpoint"""
|
||||
print("🧪 Testing MQTT Events API Endpoint")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
# Test basic endpoint
|
||||
print("📡 Testing GET /mqtt/events (default limit=5)")
|
||||
response = requests.get(MQTT_EVENTS_ENDPOINT)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ API Response successful")
|
||||
print(f"📊 Total events: {data.get('total_events', 0)}")
|
||||
print(f"📋 Events returned: {len(data.get('events', []))}")
|
||||
|
||||
if data.get('events'):
|
||||
print(f"🕐 Last updated: {data.get('last_updated')}")
|
||||
print("\n📝 Recent events:")
|
||||
for i, event in enumerate(data['events'], 1):
|
||||
timestamp = datetime.fromisoformat(event['timestamp']).strftime('%H:%M:%S')
|
||||
print(f" {i}. [{timestamp}] {event['machine_name']}: {event['payload']} -> {event['normalized_state']}")
|
||||
else:
|
||||
print("📭 No events found")
|
||||
|
||||
else:
|
||||
print(f"❌ API Error: {response.status_code}")
|
||||
print(f" Response: {response.text}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ Connection Error: API server not running")
|
||||
print(" Start the system first: python -m usda_vision_system.main")
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
print()
|
||||
|
||||
# Test with custom limit
|
||||
try:
|
||||
print("📡 Testing GET /mqtt/events?limit=10")
|
||||
response = requests.get(f"{MQTT_EVENTS_ENDPOINT}?limit=10")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ API Response successful")
|
||||
print(f"📋 Events returned: {len(data.get('events', []))}")
|
||||
else:
|
||||
print(f"❌ API Error: {response.status_code}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
|
||||
def test_system_status():
|
||||
"""Test system status to verify API is running"""
|
||||
print("🔍 Checking System Status")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/system/status")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"✅ System Status: {'Running' if data.get('system_started') else 'Not Started'}")
|
||||
print(f"🔗 MQTT Connected: {'Yes' if data.get('mqtt_connected') else 'No'}")
|
||||
print(f"📡 Last MQTT Message: {data.get('last_mqtt_message', 'None')}")
|
||||
print(f"⏱️ Uptime: {data.get('uptime_seconds', 0):.1f} seconds")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ System Status Error: {response.status_code}")
|
||||
return False
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
print("❌ Connection Error: API server not running")
|
||||
print(" Start the system first: python -m usda_vision_system.main")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def test_mqtt_status():
|
||||
"""Test MQTT status"""
|
||||
print("📡 Checking MQTT Status")
|
||||
print("=" * 50)
|
||||
|
||||
try:
|
||||
response = requests.get(f"{API_BASE_URL}/mqtt/status")
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f"🔗 MQTT Connected: {'Yes' if data.get('connected') else 'No'}")
|
||||
print(f"🏠 Broker: {data.get('broker_host')}:{data.get('broker_port')}")
|
||||
print(f"📋 Subscribed Topics: {len(data.get('subscribed_topics', []))}")
|
||||
print(f"📊 Message Count: {data.get('message_count', 0)}")
|
||||
print(f"❌ Error Count: {data.get('error_count', 0)}")
|
||||
|
||||
if data.get('subscribed_topics'):
|
||||
print("📍 Topics:")
|
||||
for topic in data['subscribed_topics']:
|
||||
print(f" - {topic}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ MQTT Status Error: {response.status_code}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Main test function"""
|
||||
print("🧪 MQTT Events API Test")
|
||||
print("=" * 60)
|
||||
print(f"🎯 API Base URL: {API_BASE_URL}")
|
||||
print(f"📡 Events Endpoint: {MQTT_EVENTS_ENDPOINT}")
|
||||
print()
|
||||
|
||||
# Test system status first
|
||||
if not test_system_status():
|
||||
print("\n❌ System not running. Please start the system first:")
|
||||
print(" python -m usda_vision_system.main")
|
||||
return
|
||||
|
||||
print()
|
||||
|
||||
# Test MQTT status
|
||||
if not test_mqtt_status():
|
||||
print("\n❌ MQTT not available")
|
||||
return
|
||||
|
||||
print()
|
||||
|
||||
# Test the events API
|
||||
test_api_endpoint()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎯 Test Instructions:")
|
||||
print("1. Make sure the system is running")
|
||||
print("2. Turn machines on/off to generate MQTT events")
|
||||
print("3. Run this test again to see the events")
|
||||
print("4. Check the admin dashboard to see events displayed")
|
||||
print()
|
||||
print("📋 API Usage:")
|
||||
print(f" GET {MQTT_EVENTS_ENDPOINT}")
|
||||
print(f" GET {MQTT_EVENTS_ENDPOINT}?limit=10")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
117
test_mqtt_logging.py
Normal file
117
test_mqtt_logging.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to demonstrate enhanced MQTT logging and API endpoints.
|
||||
|
||||
This script shows:
|
||||
1. Enhanced console logging for MQTT events
|
||||
2. New MQTT status API endpoint
|
||||
3. Machine status API endpoint
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Add the current directory to Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
def test_api_endpoints():
|
||||
"""Test the API endpoints for MQTT and machine status"""
|
||||
base_url = "http://localhost:8000"
|
||||
|
||||
print("🧪 Testing API Endpoints...")
|
||||
print("=" * 50)
|
||||
|
||||
# Test system status
|
||||
try:
|
||||
print("\n📊 System Status:")
|
||||
response = requests.get(f"{base_url}/system/status", timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" System Started: {data.get('system_started')}")
|
||||
print(f" MQTT Connected: {data.get('mqtt_connected')}")
|
||||
print(f" Last MQTT Message: {data.get('last_mqtt_message')}")
|
||||
print(f" Active Recordings: {data.get('active_recordings')}")
|
||||
print(f" Total Recordings: {data.get('total_recordings')}")
|
||||
else:
|
||||
print(f" ❌ Error: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Connection Error: {e}")
|
||||
|
||||
# Test MQTT status
|
||||
try:
|
||||
print("\n📡 MQTT Status:")
|
||||
response = requests.get(f"{base_url}/mqtt/status", timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
print(f" Connected: {data.get('connected')}")
|
||||
print(f" Broker: {data.get('broker_host')}:{data.get('broker_port')}")
|
||||
print(f" Message Count: {data.get('message_count')}")
|
||||
print(f" Error Count: {data.get('error_count')}")
|
||||
print(f" Last Message: {data.get('last_message_time')}")
|
||||
print(f" Uptime: {data.get('uptime_seconds'):.1f}s" if data.get('uptime_seconds') else " Uptime: N/A")
|
||||
print(f" Subscribed Topics:")
|
||||
for topic in data.get('subscribed_topics', []):
|
||||
print(f" - {topic}")
|
||||
else:
|
||||
print(f" ❌ Error: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Connection Error: {e}")
|
||||
|
||||
# Test machine status
|
||||
try:
|
||||
print("\n🏭 Machine Status:")
|
||||
response = requests.get(f"{base_url}/machines", timeout=5)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data:
|
||||
for machine_name, machine_info in data.items():
|
||||
print(f" {machine_name}:")
|
||||
print(f" State: {machine_info.get('state')}")
|
||||
print(f" Last Updated: {machine_info.get('last_updated')}")
|
||||
print(f" Last Message: {machine_info.get('last_message')}")
|
||||
print(f" MQTT Topic: {machine_info.get('mqtt_topic')}")
|
||||
else:
|
||||
print(" No machines found")
|
||||
else:
|
||||
print(f" ❌ Error: {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f" ❌ Connection Error: {e}")
|
||||
|
||||
def main():
|
||||
"""Main test function"""
|
||||
print("🔍 MQTT Logging and API Test")
|
||||
print("=" * 50)
|
||||
print()
|
||||
print("This script tests the enhanced MQTT logging and new API endpoints.")
|
||||
print("Make sure the USDA Vision System is running before testing.")
|
||||
print()
|
||||
|
||||
# Wait a moment
|
||||
time.sleep(1)
|
||||
|
||||
# Test API endpoints
|
||||
test_api_endpoints()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("✅ Test completed!")
|
||||
print()
|
||||
print("📝 What to expect when running the system:")
|
||||
print(" 🔗 MQTT CONNECTED: [broker_host:port]")
|
||||
print(" 📋 MQTT SUBSCRIBED: [machine] → [topic]")
|
||||
print(" 📡 MQTT MESSAGE: [machine] → [payload]")
|
||||
print(" ⚠️ MQTT DISCONNECTED: [reason]")
|
||||
print()
|
||||
print("🌐 API Endpoints available:")
|
||||
print(" GET /system/status - Overall system status")
|
||||
print(" GET /mqtt/status - MQTT client status and statistics")
|
||||
print(" GET /machines - All machine states from MQTT")
|
||||
print(" GET /cameras - Camera statuses")
|
||||
print()
|
||||
print("💡 To see live MQTT logs, run: python main.py")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -15,6 +15,7 @@ from datetime import datetime
|
||||
# Add the current directory to Python path
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def test_imports():
|
||||
"""Test that all modules can be imported"""
|
||||
print("Testing imports...")
|
||||
@@ -27,46 +28,49 @@ def test_imports():
|
||||
from usda_vision_system.storage.manager import StorageManager
|
||||
from usda_vision_system.api.server import APIServer
|
||||
from usda_vision_system.main import USDAVisionSystem
|
||||
|
||||
print("✅ All imports successful")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Import failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_configuration():
|
||||
"""Test configuration loading"""
|
||||
print("\nTesting configuration...")
|
||||
try:
|
||||
from usda_vision_system.core.config import Config
|
||||
|
||||
|
||||
# Test default config
|
||||
config = Config()
|
||||
print(f"✅ Default config loaded")
|
||||
print(f" MQTT broker: {config.mqtt.broker_host}:{config.mqtt.broker_port}")
|
||||
print(f" Storage path: {config.storage.base_path}")
|
||||
print(f" Cameras configured: {len(config.cameras)}")
|
||||
|
||||
|
||||
# Test config file if it exists
|
||||
if os.path.exists("config.json"):
|
||||
config_file = Config("config.json")
|
||||
print(f"✅ Config file loaded")
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Configuration test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_camera_discovery():
|
||||
"""Test camera discovery"""
|
||||
print("\nTesting camera discovery...")
|
||||
try:
|
||||
sys.path.append('./python demo')
|
||||
sys.path.append("./python demo")
|
||||
import mvsdk
|
||||
|
||||
|
||||
devices = mvsdk.CameraEnumerateDevice()
|
||||
print(f"✅ Camera discovery successful")
|
||||
print(f" Found {len(devices)} camera(s)")
|
||||
|
||||
|
||||
for i, device in enumerate(devices):
|
||||
try:
|
||||
name = device.GetFriendlyName()
|
||||
@@ -74,13 +78,14 @@ def test_camera_discovery():
|
||||
print(f" Camera {i}: {name} ({port_type})")
|
||||
except Exception as e:
|
||||
print(f" Camera {i}: Error getting info - {e}")
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Camera discovery failed: {e}")
|
||||
print(" Make sure GigE cameras are connected and python demo library is available")
|
||||
return False
|
||||
|
||||
|
||||
def test_storage_setup():
|
||||
"""Test storage directory setup"""
|
||||
print("\nTesting storage setup...")
|
||||
@@ -88,22 +93,23 @@ def test_storage_setup():
|
||||
from usda_vision_system.core.config import Config
|
||||
from usda_vision_system.storage.manager import StorageManager
|
||||
from usda_vision_system.core.state_manager import StateManager
|
||||
|
||||
|
||||
config = Config()
|
||||
state_manager = StateManager()
|
||||
storage_manager = StorageManager(config, state_manager)
|
||||
|
||||
|
||||
# Test storage statistics
|
||||
stats = storage_manager.get_storage_statistics()
|
||||
print(f"✅ Storage manager initialized")
|
||||
print(f" Base path: {stats.get('base_path', 'Unknown')}")
|
||||
print(f" Total files: {stats.get('total_files', 0)}")
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Storage setup failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_mqtt_config():
|
||||
"""Test MQTT configuration (without connecting)"""
|
||||
print("\nTesting MQTT configuration...")
|
||||
@@ -112,45 +118,47 @@ def test_mqtt_config():
|
||||
from usda_vision_system.mqtt.client import MQTTClient
|
||||
from usda_vision_system.core.state_manager import StateManager
|
||||
from usda_vision_system.core.events import EventSystem
|
||||
|
||||
|
||||
config = Config()
|
||||
state_manager = StateManager()
|
||||
event_system = EventSystem()
|
||||
|
||||
|
||||
mqtt_client = MQTTClient(config, state_manager, event_system)
|
||||
status = mqtt_client.get_status()
|
||||
|
||||
|
||||
print(f"✅ MQTT client initialized")
|
||||
print(f" Broker: {status['broker_host']}:{status['broker_port']}")
|
||||
print(f" Topics: {len(status['subscribed_topics'])}")
|
||||
for topic in status['subscribed_topics']:
|
||||
for topic in status["subscribed_topics"]:
|
||||
print(f" - {topic}")
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ MQTT configuration test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_system_initialization():
|
||||
"""Test full system initialization (without starting)"""
|
||||
print("\nTesting system initialization...")
|
||||
try:
|
||||
from usda_vision_system.main import USDAVisionSystem
|
||||
|
||||
|
||||
# Create system instance
|
||||
system = USDAVisionSystem()
|
||||
|
||||
|
||||
# Check system status
|
||||
status = system.get_system_status()
|
||||
print(f"✅ System initialized successfully")
|
||||
print(f" Running: {status['running']}")
|
||||
print(f" Components initialized: {len(status['components'])}")
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ System initialization failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_api_endpoints():
|
||||
"""Test API endpoints if server is running"""
|
||||
print("\nTesting API endpoints...")
|
||||
@@ -159,7 +167,7 @@ def test_api_endpoints():
|
||||
response = requests.get("http://localhost:8000/health", timeout=5)
|
||||
if response.status_code == 200:
|
||||
print("✅ API server is running")
|
||||
|
||||
|
||||
# Test system status endpoint
|
||||
try:
|
||||
response = requests.get("http://localhost:8000/system/status", timeout=5)
|
||||
@@ -172,7 +180,7 @@ def test_api_endpoints():
|
||||
print(f"⚠️ System status endpoint returned {response.status_code}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ System status test failed: {e}")
|
||||
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"⚠️ API server returned status {response.status_code}")
|
||||
@@ -184,34 +192,27 @@ def test_api_endpoints():
|
||||
print(f"❌ API test failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("USDA Vision Camera System - Test Suite")
|
||||
print("=" * 50)
|
||||
|
||||
tests = [
|
||||
test_imports,
|
||||
test_configuration,
|
||||
test_camera_discovery,
|
||||
test_storage_setup,
|
||||
test_mqtt_config,
|
||||
test_system_initialization,
|
||||
test_api_endpoints
|
||||
]
|
||||
|
||||
|
||||
tests = [test_imports, test_configuration, test_camera_discovery, test_storage_setup, test_mqtt_config, test_system_initialization, test_api_endpoints]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
|
||||
for test in tests:
|
||||
try:
|
||||
if test():
|
||||
passed += 1
|
||||
except Exception as e:
|
||||
print(f"❌ Test {test.__name__} crashed: {e}")
|
||||
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"Test Results: {passed}/{total} tests passed")
|
||||
|
||||
|
||||
if passed == total:
|
||||
print("🎉 All tests passed! System appears to be working correctly.")
|
||||
return 0
|
||||
@@ -219,5 +220,6 @@ def main():
|
||||
print("⚠️ Some tests failed. Check the output above for details.")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@ from pydantic import BaseModel, Field
|
||||
|
||||
class SystemStatusResponse(BaseModel):
|
||||
"""System status response model"""
|
||||
|
||||
system_started: bool
|
||||
mqtt_connected: bool
|
||||
last_mqtt_message: Optional[str] = None
|
||||
@@ -23,6 +24,7 @@ class SystemStatusResponse(BaseModel):
|
||||
|
||||
class MachineStatusResponse(BaseModel):
|
||||
"""Machine status response model"""
|
||||
|
||||
name: str
|
||||
state: str
|
||||
last_updated: str
|
||||
@@ -30,8 +32,22 @@ class MachineStatusResponse(BaseModel):
|
||||
mqtt_topic: Optional[str] = None
|
||||
|
||||
|
||||
class MQTTStatusResponse(BaseModel):
|
||||
"""MQTT status response model"""
|
||||
|
||||
connected: bool
|
||||
broker_host: str
|
||||
broker_port: int
|
||||
subscribed_topics: List[str]
|
||||
last_message_time: Optional[str] = None
|
||||
message_count: int
|
||||
error_count: int
|
||||
uptime_seconds: Optional[float] = None
|
||||
|
||||
|
||||
class CameraStatusResponse(BaseModel):
|
||||
"""Camera status response model"""
|
||||
|
||||
name: str
|
||||
status: str
|
||||
is_recording: bool
|
||||
@@ -44,6 +60,7 @@ class CameraStatusResponse(BaseModel):
|
||||
|
||||
class RecordingInfoResponse(BaseModel):
|
||||
"""Recording information response model"""
|
||||
|
||||
camera_name: str
|
||||
filename: str
|
||||
start_time: str
|
||||
@@ -57,12 +74,16 @@ class RecordingInfoResponse(BaseModel):
|
||||
|
||||
class StartRecordingRequest(BaseModel):
|
||||
"""Start recording request model"""
|
||||
camera_name: str
|
||||
|
||||
filename: Optional[str] = None
|
||||
exposure_ms: Optional[float] = Field(default=None, description="Exposure time in milliseconds")
|
||||
gain: Optional[float] = Field(default=None, description="Camera gain value")
|
||||
fps: Optional[float] = Field(default=None, description="Target frames per second")
|
||||
|
||||
|
||||
class StartRecordingResponse(BaseModel):
|
||||
"""Start recording response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
filename: Optional[str] = None
|
||||
@@ -70,11 +91,15 @@ class StartRecordingResponse(BaseModel):
|
||||
|
||||
class StopRecordingRequest(BaseModel):
|
||||
"""Stop recording request model"""
|
||||
camera_name: str
|
||||
|
||||
# Note: This model is currently unused as the stop recording endpoint
|
||||
# only requires the camera_name from the URL path parameter
|
||||
pass
|
||||
|
||||
|
||||
class StopRecordingResponse(BaseModel):
|
||||
"""Stop recording response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
duration_seconds: Optional[float] = None
|
||||
@@ -82,6 +107,7 @@ class StopRecordingResponse(BaseModel):
|
||||
|
||||
class StorageStatsResponse(BaseModel):
|
||||
"""Storage statistics response model"""
|
||||
|
||||
base_path: str
|
||||
total_files: int
|
||||
total_size_bytes: int
|
||||
@@ -91,6 +117,7 @@ class StorageStatsResponse(BaseModel):
|
||||
|
||||
class FileListRequest(BaseModel):
|
||||
"""File list request model"""
|
||||
|
||||
camera_name: Optional[str] = None
|
||||
start_date: Optional[str] = None
|
||||
end_date: Optional[str] = None
|
||||
@@ -99,17 +126,20 @@ class FileListRequest(BaseModel):
|
||||
|
||||
class FileListResponse(BaseModel):
|
||||
"""File list response model"""
|
||||
|
||||
files: List[Dict[str, Any]]
|
||||
total_count: int
|
||||
|
||||
|
||||
class CleanupRequest(BaseModel):
|
||||
"""Cleanup request model"""
|
||||
|
||||
max_age_days: Optional[int] = None
|
||||
|
||||
|
||||
class CleanupResponse(BaseModel):
|
||||
"""Cleanup response model"""
|
||||
|
||||
files_removed: int
|
||||
bytes_freed: int
|
||||
errors: List[str]
|
||||
@@ -117,6 +147,7 @@ class CleanupResponse(BaseModel):
|
||||
|
||||
class EventResponse(BaseModel):
|
||||
"""Event response model"""
|
||||
|
||||
event_type: str
|
||||
source: str
|
||||
data: Dict[str, Any]
|
||||
@@ -125,6 +156,7 @@ class EventResponse(BaseModel):
|
||||
|
||||
class WebSocketMessage(BaseModel):
|
||||
"""WebSocket message model"""
|
||||
|
||||
type: str
|
||||
data: Dict[str, Any]
|
||||
timestamp: Optional[str] = None
|
||||
@@ -132,13 +164,53 @@ class WebSocketMessage(BaseModel):
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response model"""
|
||||
|
||||
error: str
|
||||
details: Optional[str] = None
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class CameraRecoveryResponse(BaseModel):
|
||||
"""Camera recovery response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
camera_name: str
|
||||
operation: str
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class CameraTestResponse(BaseModel):
|
||||
"""Camera connection test response model"""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
camera_name: str
|
||||
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
|
||||
|
||||
|
||||
class MQTTEventResponse(BaseModel):
|
||||
"""MQTT event response model"""
|
||||
|
||||
machine_name: str
|
||||
topic: str
|
||||
payload: str
|
||||
normalized_state: str
|
||||
timestamp: str
|
||||
message_number: int
|
||||
|
||||
|
||||
class MQTTEventsHistoryResponse(BaseModel):
|
||||
"""MQTT events history response model"""
|
||||
|
||||
events: List[MQTTEventResponse]
|
||||
total_events: int
|
||||
last_updated: Optional[str] = None
|
||||
|
||||
|
||||
class SuccessResponse(BaseModel):
|
||||
"""Success response model"""
|
||||
|
||||
success: bool = True
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
|
||||
@@ -25,31 +25,31 @@ from .models import *
|
||||
|
||||
class WebSocketManager:
|
||||
"""Manages WebSocket connections for real-time updates"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.active_connections: List[WebSocket] = []
|
||||
self.logger = logging.getLogger(f"{__name__}.WebSocketManager")
|
||||
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
self.logger.info(f"WebSocket connected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
if websocket in self.active_connections:
|
||||
self.active_connections.remove(websocket)
|
||||
self.logger.info(f"WebSocket disconnected. Total connections: {len(self.active_connections)}")
|
||||
|
||||
|
||||
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||
try:
|
||||
await websocket.send_text(json.dumps(message))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error sending personal message: {e}")
|
||||
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
if not self.active_connections:
|
||||
return
|
||||
|
||||
|
||||
disconnected = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
@@ -57,7 +57,7 @@ class WebSocketManager:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error broadcasting to connection: {e}")
|
||||
disconnected.append(connection)
|
||||
|
||||
|
||||
# Remove disconnected connections
|
||||
for connection in disconnected:
|
||||
self.disconnect(connection)
|
||||
@@ -65,9 +65,8 @@ class WebSocketManager:
|
||||
|
||||
class APIServer:
|
||||
"""FastAPI server for the USDA Vision Camera System"""
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem,
|
||||
camera_manager, mqtt_client, storage_manager: StorageManager):
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager, mqtt_client, storage_manager: StorageManager):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
@@ -75,111 +74,101 @@ class APIServer:
|
||||
self.mqtt_client = mqtt_client
|
||||
self.storage_manager = storage_manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# FastAPI app
|
||||
self.app = FastAPI(
|
||||
title="USDA Vision Camera System API",
|
||||
description="API for monitoring and controlling the USDA vision camera system",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
self.app = FastAPI(title="USDA Vision Camera System API", description="API for monitoring and controlling the USDA vision camera system", version="1.0.0")
|
||||
|
||||
# WebSocket manager
|
||||
self.websocket_manager = WebSocketManager()
|
||||
|
||||
|
||||
# Server state
|
||||
self.server_start_time = datetime.now()
|
||||
self.running = False
|
||||
self._server_thread: Optional[threading.Thread] = None
|
||||
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
|
||||
|
||||
# Setup CORS
|
||||
self.app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"], # Configure appropriately for production
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
self.app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) # Configure appropriately for production
|
||||
|
||||
# Setup routes
|
||||
self._setup_routes()
|
||||
|
||||
|
||||
# Subscribe to events for WebSocket broadcasting
|
||||
self._setup_event_subscriptions()
|
||||
|
||||
|
||||
def _setup_routes(self):
|
||||
"""Setup API routes"""
|
||||
|
||||
|
||||
@self.app.get("/", response_model=SuccessResponse)
|
||||
async def root():
|
||||
return SuccessResponse(message="USDA Vision Camera System API")
|
||||
|
||||
|
||||
@self.app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "timestamp": datetime.now().isoformat()}
|
||||
|
||||
|
||||
@self.app.get("/system/status", response_model=SystemStatusResponse)
|
||||
async def get_system_status():
|
||||
"""Get overall system status"""
|
||||
try:
|
||||
summary = self.state_manager.get_system_summary()
|
||||
uptime = (datetime.now() - self.server_start_time).total_seconds()
|
||||
|
||||
return SystemStatusResponse(
|
||||
system_started=summary["system_started"],
|
||||
mqtt_connected=summary["mqtt_connected"],
|
||||
last_mqtt_message=summary["last_mqtt_message"],
|
||||
machines=summary["machines"],
|
||||
cameras=summary["cameras"],
|
||||
active_recordings=summary["active_recordings"],
|
||||
total_recordings=summary["total_recordings"],
|
||||
uptime_seconds=uptime
|
||||
)
|
||||
|
||||
return SystemStatusResponse(system_started=summary["system_started"], mqtt_connected=summary["mqtt_connected"], last_mqtt_message=summary["last_mqtt_message"], machines=summary["machines"], cameras=summary["cameras"], active_recordings=summary["active_recordings"], total_recordings=summary["total_recordings"], uptime_seconds=uptime)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting system status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/machines", response_model=Dict[str, MachineStatusResponse])
|
||||
async def get_machines():
|
||||
"""Get all machine statuses"""
|
||||
try:
|
||||
machines = self.state_manager.get_all_machines()
|
||||
return {
|
||||
name: MachineStatusResponse(
|
||||
name=machine.name,
|
||||
state=machine.state.value,
|
||||
last_updated=machine.last_updated.isoformat(),
|
||||
last_message=machine.last_message,
|
||||
mqtt_topic=machine.mqtt_topic
|
||||
)
|
||||
for name, machine in machines.items()
|
||||
}
|
||||
return {name: MachineStatusResponse(name=machine.name, state=machine.state.value, last_updated=machine.last_updated.isoformat(), last_message=machine.last_message, mqtt_topic=machine.mqtt_topic) for name, machine in machines.items()}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting machines: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/mqtt/status", response_model=MQTTStatusResponse)
|
||||
async def get_mqtt_status():
|
||||
"""Get MQTT client status and statistics"""
|
||||
try:
|
||||
status = self.mqtt_client.get_status()
|
||||
return MQTTStatusResponse(connected=status["connected"], broker_host=status["broker_host"], broker_port=status["broker_port"], subscribed_topics=status["subscribed_topics"], last_message_time=status["last_message_time"], message_count=status["message_count"], error_count=status["error_count"], uptime_seconds=status["uptime_seconds"])
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/mqtt/events", response_model=MQTTEventsHistoryResponse)
|
||||
async def get_mqtt_events(limit: int = Query(default=5, ge=1, le=50, description="Number of recent events to retrieve")):
|
||||
"""Get recent MQTT events history"""
|
||||
try:
|
||||
events = self.state_manager.get_recent_mqtt_events(limit)
|
||||
total_events = self.state_manager.get_mqtt_event_count()
|
||||
|
||||
# Convert events to response format
|
||||
event_responses = [MQTTEventResponse(machine_name=event.machine_name, topic=event.topic, payload=event.payload, normalized_state=event.normalized_state, timestamp=event.timestamp.isoformat(), message_number=event.message_number) for event in events]
|
||||
|
||||
last_updated = events[0].timestamp.isoformat() if events else None
|
||||
|
||||
return MQTTEventsHistoryResponse(events=event_responses, total_events=total_events, last_updated=last_updated)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting MQTT events: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/cameras", response_model=Dict[str, CameraStatusResponse])
|
||||
async def get_cameras():
|
||||
"""Get all camera statuses"""
|
||||
try:
|
||||
cameras = self.state_manager.get_all_cameras()
|
||||
return {
|
||||
name: CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
name: CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
||||
for name, camera in cameras.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting cameras: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/cameras/{camera_name}/status", response_model=CameraStatusResponse)
|
||||
async def get_camera_status(camera_name: str):
|
||||
"""Get specific camera status"""
|
||||
@@ -187,70 +176,158 @@ class APIServer:
|
||||
camera = self.state_manager.get_camera_status(camera_name)
|
||||
if not camera:
|
||||
raise HTTPException(status_code=404, detail=f"Camera not found: {camera_name}")
|
||||
|
||||
return CameraStatusResponse(
|
||||
name=camera.name,
|
||||
status=camera.status.value,
|
||||
is_recording=camera.is_recording,
|
||||
last_checked=camera.last_checked.isoformat(),
|
||||
last_error=camera.last_error,
|
||||
device_info=camera.device_info,
|
||||
current_recording_file=camera.current_recording_file,
|
||||
recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None
|
||||
)
|
||||
|
||||
return CameraStatusResponse(name=camera.name, status=camera.status.value, is_recording=camera.is_recording, last_checked=camera.last_checked.isoformat(), last_error=camera.last_error, device_info=camera.device_info, current_recording_file=camera.current_recording_file, recording_start_time=camera.recording_start_time.isoformat() if camera.recording_start_time else None)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting camera status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/start-recording", response_model=StartRecordingResponse)
|
||||
async def start_recording(camera_name: str, request: StartRecordingRequest):
|
||||
"""Manually start recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name, request.filename)
|
||||
|
||||
|
||||
success = self.camera_manager.manual_start_recording(camera_name=camera_name, filename=request.filename, exposure_ms=request.exposure_ms, gain=request.gain, fps=request.fps)
|
||||
|
||||
if success:
|
||||
return StartRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording started for {camera_name}",
|
||||
filename=request.filename
|
||||
)
|
||||
# Get the actual filename that was used (with datetime prefix)
|
||||
actual_filename = request.filename
|
||||
if request.filename:
|
||||
from ..core.timezone_utils import format_filename_timestamp
|
||||
|
||||
timestamp = format_filename_timestamp()
|
||||
actual_filename = f"{timestamp}_{request.filename}"
|
||||
|
||||
return StartRecordingResponse(success=True, message=f"Recording started for {camera_name}", filename=actual_filename)
|
||||
else:
|
||||
return StartRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to start recording for {camera_name}"
|
||||
)
|
||||
return StartRecordingResponse(success=False, message=f"Failed to start recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/stop-recording", response_model=StopRecordingResponse)
|
||||
async def stop_recording(camera_name: str):
|
||||
"""Manually stop recording for a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
|
||||
success = self.camera_manager.manual_stop_recording(camera_name)
|
||||
|
||||
|
||||
if success:
|
||||
return StopRecordingResponse(
|
||||
success=True,
|
||||
message=f"Recording stopped for {camera_name}"
|
||||
)
|
||||
return StopRecordingResponse(success=True, message=f"Recording stopped for {camera_name}")
|
||||
else:
|
||||
return StopRecordingResponse(
|
||||
success=False,
|
||||
message=f"Failed to stop recording for {camera_name}"
|
||||
)
|
||||
return StopRecordingResponse(success=False, message=f"Failed to stop recording for {camera_name}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping recording: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/test-connection", response_model=CameraTestResponse)
|
||||
async def test_camera_connection(camera_name: str):
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.test_camera_connection(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraTestResponse(success=True, message=f"Camera {camera_name} connection test passed", camera_name=camera_name)
|
||||
else:
|
||||
return CameraTestResponse(success=False, message=f"Camera {camera_name} connection test failed", camera_name=camera_name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing camera connection: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reconnect", response_model=CameraRecoveryResponse)
|
||||
async def reconnect_camera(camera_name: str):
|
||||
"""Reconnect to a camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reconnect_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reconnected successfully", camera_name=camera_name, operation="reconnect")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reconnect camera {camera_name}", camera_name=camera_name, operation="reconnect")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reconnecting camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/restart-grab", response_model=CameraRecoveryResponse)
|
||||
async def restart_camera_grab(camera_name: str):
|
||||
"""Restart camera grab process"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.restart_camera_grab(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} grab process restarted successfully", camera_name=camera_name, operation="restart-grab")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to restart grab process for camera {camera_name}", camera_name=camera_name, operation="restart-grab")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error restarting camera grab: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reset-timestamp", response_model=CameraRecoveryResponse)
|
||||
async def reset_camera_timestamp(camera_name: str):
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reset_camera_timestamp(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} timestamp reset successfully", camera_name=camera_name, operation="reset-timestamp")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reset timestamp for camera {camera_name}", camera_name=camera_name, operation="reset-timestamp")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error resetting camera timestamp: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/full-reset", response_model=CameraRecoveryResponse)
|
||||
async def full_reset_camera(camera_name: str):
|
||||
"""Perform full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.full_reset_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} full reset completed successfully", camera_name=camera_name, operation="full-reset")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to perform full reset for camera {camera_name}", camera_name=camera_name, operation="full-reset")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error performing full camera reset: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.post("/cameras/{camera_name}/reinitialize", response_model=CameraRecoveryResponse)
|
||||
async def reinitialize_camera(camera_name: str):
|
||||
"""Reinitialize a failed camera"""
|
||||
try:
|
||||
if not self.camera_manager:
|
||||
raise HTTPException(status_code=503, detail="Camera manager not available")
|
||||
|
||||
success = self.camera_manager.reinitialize_failed_camera(camera_name)
|
||||
|
||||
if success:
|
||||
return CameraRecoveryResponse(success=True, message=f"Camera {camera_name} reinitialized successfully", camera_name=camera_name, operation="reinitialize")
|
||||
else:
|
||||
return CameraRecoveryResponse(success=False, message=f"Failed to reinitialize camera {camera_name}", camera_name=camera_name, operation="reinitialize")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reinitializing camera: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@self.app.get("/recordings", response_model=Dict[str, RecordingInfoResponse])
|
||||
async def get_recordings():
|
||||
"""Get all recording sessions"""
|
||||
@@ -266,14 +343,14 @@ class APIServer:
|
||||
file_size_bytes=recording.file_size_bytes,
|
||||
frame_count=recording.frame_count,
|
||||
duration_seconds=(recording.end_time - recording.start_time).total_seconds() if recording.end_time else None,
|
||||
error_message=recording.error_message
|
||||
error_message=recording.error_message,
|
||||
)
|
||||
for rid, recording in recordings.items()
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting recordings: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.get("/storage/stats", response_model=StorageStatsResponse)
|
||||
async def get_storage_stats():
|
||||
"""Get storage statistics"""
|
||||
@@ -283,34 +360,26 @@ class APIServer:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting storage stats: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/storage/files", response_model=FileListResponse)
|
||||
async def get_files(request: FileListRequest):
|
||||
"""Get list of recording files"""
|
||||
try:
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
|
||||
if request.start_date:
|
||||
start_date = datetime.fromisoformat(request.start_date)
|
||||
if request.end_date:
|
||||
end_date = datetime.fromisoformat(request.end_date)
|
||||
|
||||
files = self.storage_manager.get_recording_files(
|
||||
camera_name=request.camera_name,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
limit=request.limit
|
||||
)
|
||||
|
||||
return FileListResponse(
|
||||
files=files,
|
||||
total_count=len(files)
|
||||
)
|
||||
|
||||
files = self.storage_manager.get_recording_files(camera_name=request.camera_name, start_date=start_date, end_date=end_date, limit=request.limit)
|
||||
|
||||
return FileListResponse(files=files, total_count=len(files))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting files: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.post("/storage/cleanup", response_model=CleanupResponse)
|
||||
async def cleanup_storage(request: CleanupRequest):
|
||||
"""Clean up old storage files"""
|
||||
@@ -320,7 +389,7 @@ class APIServer:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during cleanup: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@self.app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for real-time updates"""
|
||||
@@ -330,9 +399,7 @@ class APIServer:
|
||||
# Keep connection alive and handle incoming messages
|
||||
data = await websocket.receive_text()
|
||||
# Echo back for now - could implement commands later
|
||||
await self.websocket_manager.send_personal_message(
|
||||
{"type": "echo", "data": data}, websocket
|
||||
)
|
||||
await self.websocket_manager.send_personal_message({"type": "echo", "data": data}, websocket)
|
||||
except WebSocketDisconnect:
|
||||
self.websocket_manager.disconnect(websocket)
|
||||
|
||||
@@ -342,21 +409,12 @@ class APIServer:
|
||||
def broadcast_event(event: Event):
|
||||
"""Broadcast event to all WebSocket connections"""
|
||||
try:
|
||||
message = {
|
||||
"type": "event",
|
||||
"event_type": event.event_type.value,
|
||||
"source": event.source,
|
||||
"data": event.data,
|
||||
"timestamp": event.timestamp.isoformat()
|
||||
}
|
||||
message = {"type": "event", "event_type": event.event_type.value, "source": event.source, "data": event.data, "timestamp": event.timestamp.isoformat()}
|
||||
|
||||
# Schedule the broadcast in the event loop thread-safely
|
||||
if self._event_loop and not self._event_loop.is_closed():
|
||||
# Use call_soon_threadsafe to schedule the coroutine from another thread
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.websocket_manager.broadcast(message),
|
||||
self._event_loop
|
||||
)
|
||||
asyncio.run_coroutine_threadsafe(self.websocket_manager.broadcast(message), self._event_loop)
|
||||
else:
|
||||
self.logger.debug("Event loop not available for broadcasting")
|
||||
|
||||
@@ -411,12 +469,7 @@ class APIServer:
|
||||
self._event_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._event_loop)
|
||||
|
||||
uvicorn.run(
|
||||
self.app,
|
||||
host=self.config.system.api_host,
|
||||
port=self.config.system.api_port,
|
||||
log_level="info"
|
||||
)
|
||||
uvicorn.run(self.app, host=self.config.system.api_host, port=self.config.system.api_port, log_level="info")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error running API server: {e}")
|
||||
finally:
|
||||
@@ -429,11 +482,4 @@ class APIServer:
|
||||
|
||||
def get_server_info(self) -> Dict[str, Any]:
|
||||
"""Get server information"""
|
||||
return {
|
||||
"running": self.running,
|
||||
"host": self.config.system.api_host,
|
||||
"port": self.config.system.api_port,
|
||||
"start_time": self.server_start_time.isoformat(),
|
||||
"uptime_seconds": (datetime.now() - self.server_start_time).total_seconds(),
|
||||
"websocket_connections": len(self.websocket_manager.active_connections)
|
||||
}
|
||||
return {"running": self.running, "host": self.config.system.api_host, "port": self.config.system.api_port, "start_time": self.server_start_time.isoformat(), "uptime_seconds": (datetime.now() - self.server_start_time).total_seconds(), "websocket_connections": len(self.websocket_manager.active_connections)}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -13,7 +13,7 @@ from typing import Dict, List, Optional, Tuple, Any
|
||||
from datetime import datetime
|
||||
|
||||
# Add python demo to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
|
||||
import mvsdk
|
||||
|
||||
from ..core.config import Config, CameraConfig
|
||||
@@ -22,271 +22,233 @@ from ..core.events import EventSystem, EventType, Event, publish_camera_status_c
|
||||
from ..core.timezone_utils import format_filename_timestamp
|
||||
from .recorder import CameraRecorder
|
||||
from .monitor import CameraMonitor
|
||||
from .sdk_config import initialize_sdk_with_suppression
|
||||
|
||||
|
||||
class CameraManager:
|
||||
"""Manages all cameras in the system"""
|
||||
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Initialize SDK early to suppress error messages
|
||||
initialize_sdk_with_suppression()
|
||||
|
||||
# Camera management
|
||||
self.available_cameras: List[Any] = [] # mvsdk camera device info
|
||||
self.camera_recorders: Dict[str, CameraRecorder] = {} # camera_name -> recorder
|
||||
self.camera_monitor: Optional[CameraMonitor] = None
|
||||
|
||||
|
||||
# Threading
|
||||
self._lock = threading.RLock()
|
||||
self.running = False
|
||||
|
||||
|
||||
# Subscribe to machine state changes
|
||||
self.event_system.subscribe(EventType.MACHINE_STATE_CHANGED, self._on_machine_state_changed)
|
||||
|
||||
|
||||
# Initialize camera discovery
|
||||
self._discover_cameras()
|
||||
|
||||
|
||||
# Create camera monitor
|
||||
self.camera_monitor = CameraMonitor(
|
||||
config=config,
|
||||
state_manager=state_manager,
|
||||
event_system=event_system,
|
||||
camera_manager=self
|
||||
)
|
||||
|
||||
self.camera_monitor = CameraMonitor(config=config, state_manager=state_manager, event_system=event_system, camera_manager=self)
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the camera manager"""
|
||||
if self.running:
|
||||
self.logger.warning("Camera manager is already running")
|
||||
return True
|
||||
|
||||
|
||||
self.logger.info("Starting camera manager...")
|
||||
self.running = True
|
||||
|
||||
|
||||
# Start camera monitor
|
||||
if self.camera_monitor:
|
||||
self.camera_monitor.start()
|
||||
|
||||
|
||||
# Initialize camera recorders
|
||||
self._initialize_recorders()
|
||||
|
||||
|
||||
self.logger.info("Camera manager started successfully")
|
||||
return True
|
||||
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the camera manager"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
|
||||
self.logger.info("Stopping camera manager...")
|
||||
self.running = False
|
||||
|
||||
|
||||
# Stop camera monitor
|
||||
if self.camera_monitor:
|
||||
self.camera_monitor.stop()
|
||||
|
||||
|
||||
# Stop all active recordings
|
||||
with self._lock:
|
||||
for recorder in self.camera_recorders.values():
|
||||
if recorder.is_recording():
|
||||
recorder.stop_recording()
|
||||
recorder.cleanup()
|
||||
|
||||
|
||||
self.logger.info("Camera manager stopped")
|
||||
|
||||
|
||||
def _discover_cameras(self) -> None:
|
||||
"""Discover available GigE cameras"""
|
||||
try:
|
||||
self.logger.info("Discovering GigE cameras...")
|
||||
|
||||
|
||||
# Enumerate cameras using mvsdk
|
||||
device_list = mvsdk.CameraEnumerateDevice()
|
||||
self.available_cameras = device_list
|
||||
|
||||
|
||||
self.logger.info(f"Found {len(device_list)} camera(s)")
|
||||
|
||||
|
||||
for i, dev_info in enumerate(device_list):
|
||||
try:
|
||||
name = dev_info.GetFriendlyName()
|
||||
port_type = dev_info.GetPortType()
|
||||
serial = getattr(dev_info, 'acSn', 'Unknown')
|
||||
|
||||
serial = getattr(dev_info, "acSn", "Unknown")
|
||||
|
||||
self.logger.info(f" Camera {i}: {name} ({port_type}) - Serial: {serial}")
|
||||
|
||||
|
||||
# Update state manager with discovered camera
|
||||
camera_name = f"camera{i+1}" # Default naming
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_name,
|
||||
status="available",
|
||||
device_info={
|
||||
"friendly_name": name,
|
||||
"port_type": port_type,
|
||||
"serial_number": serial,
|
||||
"device_index": i
|
||||
}
|
||||
)
|
||||
|
||||
self.state_manager.update_camera_status(name=camera_name, status="available", device_info={"friendly_name": name, "port_type": port_type, "serial_number": serial, "device_index": i})
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing camera {i}: {e}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error discovering cameras: {e}")
|
||||
self.available_cameras = []
|
||||
|
||||
|
||||
def _initialize_recorders(self) -> None:
|
||||
"""Initialize camera recorders for configured cameras"""
|
||||
with self._lock:
|
||||
for camera_config in self.config.cameras:
|
||||
if not camera_config.enabled:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
# Find matching physical camera
|
||||
device_info = self._find_camera_device(camera_config.name)
|
||||
if device_info is None:
|
||||
self.logger.warning(f"No physical camera found for configured camera: {camera_config.name}")
|
||||
# Update state to indicate camera is not available
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_config.name,
|
||||
status="not_found",
|
||||
device_info=None
|
||||
)
|
||||
self.state_manager.update_camera_status(name=camera_config.name, status="not_found", device_info=None)
|
||||
continue
|
||||
|
||||
# Create recorder (this will attempt to initialize the camera)
|
||||
recorder = CameraRecorder(
|
||||
camera_config=camera_config,
|
||||
device_info=device_info,
|
||||
state_manager=self.state_manager,
|
||||
event_system=self.event_system
|
||||
)
|
||||
|
||||
# Check if camera initialization was successful
|
||||
if recorder.hCamera is None:
|
||||
self.logger.warning(f"Camera {camera_config.name} failed to initialize, skipping")
|
||||
# Update state to indicate camera initialization failed
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_config.name,
|
||||
status="initialization_failed",
|
||||
device_info={"error": "Camera initialization failed"}
|
||||
)
|
||||
continue
|
||||
# Create recorder (uses lazy initialization - camera will be initialized when recording starts)
|
||||
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
|
||||
|
||||
# Add recorder to the list (camera will be initialized lazily when needed)
|
||||
self.camera_recorders[camera_config.name] = recorder
|
||||
self.logger.info(f"Successfully initialized recorder for camera: {camera_config.name}")
|
||||
self.logger.info(f"Successfully created recorder for camera: {camera_config.name} (lazy initialization)")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error initializing recorder for {camera_config.name}: {e}")
|
||||
# Update state to indicate error
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_config.name,
|
||||
status="error",
|
||||
device_info={"error": str(e)}
|
||||
)
|
||||
|
||||
self.state_manager.update_camera_status(name=camera_config.name, status="error", device_info={"error": str(e)})
|
||||
|
||||
def _find_camera_device(self, camera_name: str) -> Optional[Any]:
|
||||
"""Find physical camera device for a configured camera"""
|
||||
# For now, use simple mapping: camera1 -> device 0, camera2 -> device 1, etc.
|
||||
# This could be enhanced to use serial numbers or other identifiers
|
||||
|
||||
camera_index_map = {
|
||||
"camera1": 0,
|
||||
"camera2": 1,
|
||||
"camera3": 2,
|
||||
"camera4": 3
|
||||
}
|
||||
|
||||
|
||||
camera_index_map = {"camera1": 0, "camera2": 1, "camera3": 2, "camera4": 3}
|
||||
|
||||
device_index = camera_index_map.get(camera_name)
|
||||
if device_index is not None and device_index < len(self.available_cameras):
|
||||
return self.available_cameras[device_index]
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _on_machine_state_changed(self, event: Event) -> None:
|
||||
"""Handle machine state change events"""
|
||||
try:
|
||||
machine_name = event.data.get("machine_name")
|
||||
new_state = event.data.get("state")
|
||||
|
||||
|
||||
if not machine_name or not new_state:
|
||||
return
|
||||
|
||||
|
||||
self.logger.info(f"Handling machine state change: {machine_name} -> {new_state}")
|
||||
|
||||
|
||||
# Find camera associated with this machine
|
||||
camera_config = None
|
||||
for config in self.config.cameras:
|
||||
if config.machine_topic == machine_name:
|
||||
camera_config = config
|
||||
break
|
||||
|
||||
|
||||
if not camera_config:
|
||||
self.logger.warning(f"No camera configured for machine: {machine_name}")
|
||||
return
|
||||
|
||||
|
||||
# Get the recorder for this camera
|
||||
recorder = self.camera_recorders.get(camera_config.name)
|
||||
if not recorder:
|
||||
self.logger.warning(f"No recorder found for camera: {camera_config.name}")
|
||||
return
|
||||
|
||||
|
||||
# Handle state change
|
||||
if new_state == "on":
|
||||
self._start_recording(camera_config.name, recorder)
|
||||
elif new_state in ["off", "error"]:
|
||||
self._stop_recording(camera_config.name, recorder)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling machine state change: {e}")
|
||||
|
||||
|
||||
def _start_recording(self, camera_name: str, recorder: CameraRecorder) -> None:
|
||||
"""Start recording for a camera"""
|
||||
try:
|
||||
if recorder.is_recording():
|
||||
self.logger.info(f"Camera {camera_name} is already recording")
|
||||
return
|
||||
|
||||
|
||||
# Generate filename with Atlanta timezone timestamp
|
||||
timestamp = format_filename_timestamp()
|
||||
filename = f"{camera_name}_recording_{timestamp}.avi"
|
||||
|
||||
|
||||
# Start recording
|
||||
success = recorder.start_recording(filename)
|
||||
if success:
|
||||
self.logger.info(f"Started recording for camera {camera_name}: {filename}")
|
||||
else:
|
||||
self.logger.error(f"Failed to start recording for camera {camera_name}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording for {camera_name}: {e}")
|
||||
|
||||
|
||||
def _stop_recording(self, camera_name: str, recorder: CameraRecorder) -> None:
|
||||
"""Stop recording for a camera"""
|
||||
try:
|
||||
if not recorder.is_recording():
|
||||
self.logger.info(f"Camera {camera_name} is not recording")
|
||||
return
|
||||
|
||||
|
||||
# Stop recording
|
||||
success = recorder.stop_recording()
|
||||
if success:
|
||||
self.logger.info(f"Stopped recording for camera {camera_name}")
|
||||
else:
|
||||
self.logger.error(f"Failed to stop recording for camera {camera_name}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error stopping recording for {camera_name}: {e}")
|
||||
|
||||
|
||||
def get_camera_status(self, camera_name: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get status of a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
return None
|
||||
|
||||
|
||||
return recorder.get_status()
|
||||
|
||||
|
||||
def get_all_camera_status(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get status of all cameras"""
|
||||
status = {}
|
||||
@@ -294,50 +256,174 @@ class CameraManager:
|
||||
for camera_name, recorder in self.camera_recorders.items():
|
||||
status[camera_name] = recorder.get_status()
|
||||
return status
|
||||
|
||||
def manual_start_recording(self, camera_name: str, filename: Optional[str] = None) -> bool:
|
||||
"""Manually start recording for a camera"""
|
||||
|
||||
def manual_start_recording(self, camera_name: str, filename: Optional[str] = None, exposure_ms: Optional[float] = None, gain: Optional[float] = None, fps: Optional[float] = None) -> bool:
|
||||
"""Manually start recording for a camera with optional camera settings"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
if not filename:
|
||||
timestamp = format_filename_timestamp()
|
||||
|
||||
# Update camera settings if provided
|
||||
if exposure_ms is not None or gain is not None or fps is not None:
|
||||
settings_updated = recorder.update_camera_settings(exposure_ms=exposure_ms, gain=gain, target_fps=fps)
|
||||
if not settings_updated:
|
||||
self.logger.warning(f"Failed to update camera settings for {camera_name}")
|
||||
|
||||
# Generate filename with datetime prefix
|
||||
timestamp = format_filename_timestamp()
|
||||
if filename:
|
||||
# Always prepend datetime to the provided filename
|
||||
filename = f"{timestamp}_{filename}"
|
||||
else:
|
||||
filename = f"{camera_name}_manual_{timestamp}.avi"
|
||||
|
||||
|
||||
return recorder.start_recording(filename)
|
||||
|
||||
|
||||
def manual_stop_recording(self, camera_name: str) -> bool:
|
||||
"""Manually stop recording for a camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
|
||||
return recorder.stop_recording()
|
||||
|
||||
|
||||
def get_available_cameras(self) -> List[Dict[str, Any]]:
|
||||
"""Get list of available physical cameras"""
|
||||
cameras = []
|
||||
for i, dev_info in enumerate(self.available_cameras):
|
||||
try:
|
||||
cameras.append({
|
||||
"index": i,
|
||||
"name": dev_info.GetFriendlyName(),
|
||||
"port_type": dev_info.GetPortType(),
|
||||
"serial_number": getattr(dev_info, 'acSn', 'Unknown')
|
||||
})
|
||||
cameras.append({"index": i, "name": dev_info.GetFriendlyName(), "port_type": dev_info.GetPortType(), "serial_number": getattr(dev_info, "acSn", "Unknown")})
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting info for camera {i}: {e}")
|
||||
|
||||
|
||||
return cameras
|
||||
|
||||
|
||||
def refresh_camera_discovery(self) -> int:
|
||||
"""Refresh camera discovery and return number of cameras found"""
|
||||
self._discover_cameras()
|
||||
return len(self.available_cameras)
|
||||
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if camera manager is running"""
|
||||
return self.running
|
||||
|
||||
def test_camera_connection(self, camera_name: str) -> bool:
|
||||
"""Test connection for a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
return recorder.test_connection()
|
||||
|
||||
def reconnect_camera(self, camera_name: str) -> bool:
|
||||
"""Attempt to reconnect a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
success = recorder.reconnect()
|
||||
|
||||
# Update camera status based on result
|
||||
if success:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
|
||||
else:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="connection_failed", error="Reconnection failed")
|
||||
|
||||
return success
|
||||
|
||||
def restart_camera_grab(self, camera_name: str) -> bool:
|
||||
"""Restart grab process for a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
success = recorder.restart_grab()
|
||||
|
||||
# Update camera status based on result
|
||||
if success:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
|
||||
else:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="grab_failed", error="Grab restart failed")
|
||||
|
||||
return success
|
||||
|
||||
def reset_camera_timestamp(self, camera_name: str) -> bool:
|
||||
"""Reset timestamp for a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
return recorder.reset_timestamp()
|
||||
|
||||
def full_reset_camera(self, camera_name: str) -> bool:
|
||||
"""Perform full reset for a specific camera"""
|
||||
recorder = self.camera_recorders.get(camera_name)
|
||||
if not recorder:
|
||||
self.logger.error(f"Camera not found: {camera_name}")
|
||||
return False
|
||||
|
||||
success = recorder.full_reset()
|
||||
|
||||
# Update camera status based on result
|
||||
if success:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
|
||||
else:
|
||||
self.state_manager.update_camera_status(name=camera_name, status="reset_failed", error="Full reset failed")
|
||||
|
||||
return success
|
||||
|
||||
def reinitialize_failed_camera(self, camera_name: str) -> bool:
|
||||
"""Attempt to reinitialize a camera that failed to initialize"""
|
||||
with self._lock:
|
||||
# Find the camera config
|
||||
camera_config = None
|
||||
for config in self.config.cameras:
|
||||
if config.name == camera_name:
|
||||
camera_config = config
|
||||
break
|
||||
|
||||
if not camera_config:
|
||||
self.logger.error(f"No configuration found for camera: {camera_name}")
|
||||
return False
|
||||
|
||||
if not camera_config.enabled:
|
||||
self.logger.error(f"Camera {camera_name} is disabled in configuration")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Remove existing recorder if any
|
||||
if camera_name in self.camera_recorders:
|
||||
old_recorder = self.camera_recorders[camera_name]
|
||||
try:
|
||||
old_recorder._cleanup_camera()
|
||||
except:
|
||||
pass # Ignore cleanup errors
|
||||
del self.camera_recorders[camera_name]
|
||||
|
||||
# Find matching physical camera
|
||||
device_info = self._find_camera_device(camera_name)
|
||||
if device_info is None:
|
||||
self.logger.warning(f"No physical camera found for configured camera: {camera_name}")
|
||||
self.state_manager.update_camera_status(name=camera_name, status="not_found", device_info=None)
|
||||
return False
|
||||
|
||||
# Create new recorder (uses lazy initialization)
|
||||
recorder = CameraRecorder(camera_config=camera_config, device_info=device_info, state_manager=self.state_manager, event_system=self.event_system)
|
||||
|
||||
# Success - add to recorders (camera will be initialized lazily when needed)
|
||||
self.camera_recorders[camera_name] = recorder
|
||||
self.state_manager.update_camera_status(name=camera_name, status="connected", error=None)
|
||||
|
||||
self.logger.info(f"Successfully reinitialized camera recorder: {camera_name} (lazy initialization)")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error reinitializing camera {camera_name}: {e}")
|
||||
self.state_manager.update_camera_status(name=camera_name, status="error", device_info={"error": str(e)})
|
||||
return False
|
||||
|
||||
@@ -9,240 +9,236 @@ import os
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import contextlib
|
||||
from typing import Dict, List, Optional, Any
|
||||
|
||||
# Add python demo to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
|
||||
import mvsdk
|
||||
|
||||
from ..core.config import Config
|
||||
from ..core.state_manager import StateManager, CameraStatus
|
||||
from ..core.events import EventSystem, publish_camera_status_changed
|
||||
from .sdk_config import ensure_sdk_initialized
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
|
||||
|
||||
class CameraMonitor:
|
||||
"""Monitors camera status and availability"""
|
||||
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem, camera_manager=None):
|
||||
self.config = config
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.camera_manager = camera_manager # Reference to camera manager
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Monitoring settings
|
||||
self.check_interval = config.system.camera_check_interval_seconds
|
||||
|
||||
|
||||
# Threading
|
||||
self.running = False
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
|
||||
# Status tracking
|
||||
self.last_check_time: Optional[float] = None
|
||||
self.check_count = 0
|
||||
self.error_count = 0
|
||||
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start camera monitoring"""
|
||||
if self.running:
|
||||
self.logger.warning("Camera monitor is already running")
|
||||
return True
|
||||
|
||||
|
||||
self.logger.info(f"Starting camera monitor (check interval: {self.check_interval}s)")
|
||||
self.running = True
|
||||
self._stop_event.clear()
|
||||
|
||||
|
||||
# Start monitoring thread
|
||||
self._thread = threading.Thread(target=self._monitoring_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop camera monitoring"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
|
||||
self.logger.info("Stopping camera monitor...")
|
||||
self.running = False
|
||||
self._stop_event.set()
|
||||
|
||||
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
|
||||
self.logger.info("Camera monitor stopped")
|
||||
|
||||
|
||||
def _monitoring_loop(self) -> None:
|
||||
"""Main monitoring loop"""
|
||||
self.logger.info("Camera monitoring loop started")
|
||||
|
||||
|
||||
while self.running and not self._stop_event.is_set():
|
||||
try:
|
||||
self.last_check_time = time.time()
|
||||
self.check_count += 1
|
||||
|
||||
|
||||
# Check all configured cameras
|
||||
self._check_all_cameras()
|
||||
|
||||
|
||||
# Wait for next check
|
||||
if self._stop_event.wait(self.check_interval):
|
||||
break
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.error_count += 1
|
||||
self.logger.error(f"Error in camera monitoring loop: {e}")
|
||||
|
||||
|
||||
# Wait a bit before retrying
|
||||
if self._stop_event.wait(min(self.check_interval, 10)):
|
||||
break
|
||||
|
||||
|
||||
self.logger.info("Camera monitoring loop ended")
|
||||
|
||||
|
||||
def _check_all_cameras(self) -> None:
|
||||
"""Check status of all configured cameras"""
|
||||
for camera_config in self.config.cameras:
|
||||
if not camera_config.enabled:
|
||||
continue
|
||||
|
||||
|
||||
try:
|
||||
self._check_camera_status(camera_config.name)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking camera {camera_config.name}: {e}")
|
||||
|
||||
|
||||
def _check_camera_status(self, camera_name: str) -> None:
|
||||
"""Check status of a specific camera"""
|
||||
try:
|
||||
# Get current status from state manager
|
||||
current_info = self.state_manager.get_camera_status(camera_name)
|
||||
|
||||
|
||||
# Perform actual camera check
|
||||
status, details, device_info = self._perform_camera_check(camera_name)
|
||||
|
||||
|
||||
# Update state if changed
|
||||
old_status = current_info.status.value if current_info else "unknown"
|
||||
if old_status != status:
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_name,
|
||||
status=status,
|
||||
error=details if status == "error" else None,
|
||||
device_info=device_info
|
||||
)
|
||||
|
||||
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
|
||||
|
||||
# Publish status change event
|
||||
publish_camera_status_changed(
|
||||
camera_name=camera_name,
|
||||
status=status,
|
||||
details=details
|
||||
)
|
||||
|
||||
publish_camera_status_changed(camera_name=camera_name, status=status, details=details)
|
||||
|
||||
self.logger.info(f"Camera {camera_name} status changed: {old_status} -> {status}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error checking camera {camera_name}: {e}")
|
||||
|
||||
|
||||
# Update to error state
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_name,
|
||||
status="error",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
self.state_manager.update_camera_status(name=camera_name, status="error", error=str(e))
|
||||
|
||||
def _perform_camera_check(self, camera_name: str) -> tuple[str, str, Optional[Dict[str, Any]]]:
|
||||
"""Perform actual camera availability check"""
|
||||
try:
|
||||
# Get camera device info from camera manager
|
||||
if not self.camera_manager:
|
||||
return "error", "Camera manager not available", None
|
||||
|
||||
|
||||
device_info = self.camera_manager._find_camera_device(camera_name)
|
||||
if not device_info:
|
||||
return "disconnected", "Camera device not found", None
|
||||
|
||||
|
||||
# Check if camera is already opened by another process
|
||||
if mvsdk.CameraIsOpened(device_info):
|
||||
# Camera is opened - check if it's our recorder
|
||||
# Camera is opened - check if it's our recorder that's currently recording
|
||||
recorder = self.camera_manager.camera_recorders.get(camera_name)
|
||||
if recorder and recorder.hCamera:
|
||||
return "available", "Camera initialized and ready", self._get_device_info_dict(device_info)
|
||||
if recorder and recorder.hCamera and recorder.recording:
|
||||
return "available", "Camera recording (in use by system)", self._get_device_info_dict(device_info)
|
||||
else:
|
||||
return "busy", "Camera opened by another process", self._get_device_info_dict(device_info)
|
||||
|
||||
|
||||
# Try to initialize camera briefly to test availability
|
||||
try:
|
||||
hCamera = mvsdk.CameraInit(device_info, -1, -1)
|
||||
|
||||
# Ensure SDK is initialized
|
||||
ensure_sdk_initialized()
|
||||
|
||||
# Suppress output to avoid MVCAMAPI error messages during camera testing
|
||||
with suppress_camera_errors():
|
||||
hCamera = mvsdk.CameraInit(device_info, -1, -1)
|
||||
|
||||
# Quick test - try to get one frame
|
||||
try:
|
||||
mvsdk.CameraSetTriggerMode(hCamera, 0)
|
||||
mvsdk.CameraPlay(hCamera)
|
||||
|
||||
|
||||
# Try to capture with short timeout
|
||||
pRawData, FrameHead = mvsdk.CameraGetImageBuffer(hCamera, 500)
|
||||
mvsdk.CameraReleaseImageBuffer(hCamera, pRawData)
|
||||
|
||||
|
||||
# Success - camera is available
|
||||
mvsdk.CameraUnInit(hCamera)
|
||||
return "available", "Camera test successful", self._get_device_info_dict(device_info)
|
||||
|
||||
|
||||
except mvsdk.CameraException as e:
|
||||
mvsdk.CameraUnInit(hCamera)
|
||||
if e.error_code == mvsdk.CAMERA_STATUS_TIME_OUT:
|
||||
return "available", "Camera available but slow response", self._get_device_info_dict(device_info)
|
||||
else:
|
||||
return "error", f"Camera test failed: {e.message}", self._get_device_info_dict(device_info)
|
||||
|
||||
|
||||
except mvsdk.CameraException as e:
|
||||
return "error", f"Camera initialization failed: {e.message}", self._get_device_info_dict(device_info)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
return "error", f"Camera check failed: {str(e)}", None
|
||||
|
||||
|
||||
def _get_device_info_dict(self, device_info) -> Dict[str, Any]:
|
||||
"""Convert device info to dictionary"""
|
||||
try:
|
||||
return {
|
||||
"friendly_name": device_info.GetFriendlyName(),
|
||||
"port_type": device_info.GetPortType(),
|
||||
"serial_number": getattr(device_info, 'acSn', 'Unknown'),
|
||||
"last_checked": time.time()
|
||||
}
|
||||
return {"friendly_name": device_info.GetFriendlyName(), "port_type": device_info.GetPortType(), "serial_number": getattr(device_info, "acSn", "Unknown"), "last_checked": time.time()}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting device info: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def check_camera_now(self, camera_name: str) -> Dict[str, Any]:
|
||||
"""Manually check a specific camera status"""
|
||||
try:
|
||||
status, details, device_info = self._perform_camera_check(camera_name)
|
||||
|
||||
|
||||
# Update state
|
||||
self.state_manager.update_camera_status(
|
||||
name=camera_name,
|
||||
status=status,
|
||||
error=details if status == "error" else None,
|
||||
device_info=device_info
|
||||
)
|
||||
|
||||
return {
|
||||
"camera_name": camera_name,
|
||||
"status": status,
|
||||
"details": details,
|
||||
"device_info": device_info,
|
||||
"check_time": time.time()
|
||||
}
|
||||
|
||||
self.state_manager.update_camera_status(name=camera_name, status=status, error=details if status == "error" else None, device_info=device_info)
|
||||
|
||||
return {"camera_name": camera_name, "status": status, "details": details, "device_info": device_info, "check_time": time.time()}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Manual camera check failed: {e}"
|
||||
self.logger.error(error_msg)
|
||||
return {
|
||||
"camera_name": camera_name,
|
||||
"status": "error",
|
||||
"details": error_msg,
|
||||
"device_info": None,
|
||||
"check_time": time.time()
|
||||
}
|
||||
|
||||
return {"camera_name": camera_name, "status": "error", "details": error_msg, "device_info": None, "check_time": time.time()}
|
||||
|
||||
def check_all_cameras_now(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Manually check all cameras"""
|
||||
results = {}
|
||||
@@ -250,18 +246,11 @@ class CameraMonitor:
|
||||
if camera_config.enabled:
|
||||
results[camera_config.name] = self.check_camera_now(camera_config.name)
|
||||
return results
|
||||
|
||||
|
||||
def get_monitoring_stats(self) -> Dict[str, Any]:
|
||||
"""Get monitoring statistics"""
|
||||
return {
|
||||
"running": self.running,
|
||||
"check_interval_seconds": self.check_interval,
|
||||
"total_checks": self.check_count,
|
||||
"error_count": self.error_count,
|
||||
"last_check_time": self.last_check_time,
|
||||
"success_rate": (self.check_count - self.error_count) / max(self.check_count, 1) * 100
|
||||
}
|
||||
|
||||
return {"running": self.running, "check_interval_seconds": self.check_interval, "total_checks": self.check_count, "error_count": self.error_count, "last_check_time": self.last_check_time, "success_rate": (self.check_count - self.error_count) / max(self.check_count, 1) * 100}
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if monitor is running"""
|
||||
return self.running
|
||||
|
||||
@@ -11,18 +11,44 @@ import time
|
||||
import logging
|
||||
import cv2
|
||||
import numpy as np
|
||||
import contextlib
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Add python demo to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python demo'))
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
|
||||
import mvsdk
|
||||
|
||||
from ..core.config import CameraConfig
|
||||
from ..core.state_manager import StateManager
|
||||
from ..core.events import EventSystem, publish_recording_started, publish_recording_stopped, publish_recording_error
|
||||
from ..core.timezone_utils import now_atlanta, format_filename_timestamp
|
||||
from .sdk_config import ensure_sdk_initialized
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def suppress_camera_errors():
|
||||
"""Context manager to temporarily suppress camera SDK error output"""
|
||||
# Save original file descriptors
|
||||
original_stderr = os.dup(2)
|
||||
original_stdout = os.dup(1)
|
||||
|
||||
try:
|
||||
# Redirect stderr and stdout to devnull
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, 2) # stderr
|
||||
os.dup2(devnull, 1) # stdout (in case SDK uses stdout)
|
||||
os.close(devnull)
|
||||
|
||||
yield
|
||||
|
||||
finally:
|
||||
# Restore original file descriptors
|
||||
os.dup2(original_stderr, 2)
|
||||
os.dup2(original_stdout, 1)
|
||||
os.close(original_stderr)
|
||||
os.close(original_stdout)
|
||||
|
||||
|
||||
class CameraRecorder:
|
||||
@@ -35,41 +61,46 @@ class CameraRecorder:
|
||||
self.event_system = event_system
|
||||
self.storage_manager = storage_manager
|
||||
self.logger = logging.getLogger(f"{__name__}.{camera_config.name}")
|
||||
|
||||
|
||||
# Camera handle and properties
|
||||
self.hCamera: Optional[int] = None
|
||||
self.cap = None
|
||||
self.monoCamera = False
|
||||
self.frame_buffer = None
|
||||
self.frame_buffer_size = 0
|
||||
|
||||
|
||||
# Recording state
|
||||
self.recording = False
|
||||
self.video_writer: Optional[cv2.VideoWriter] = None
|
||||
self.output_filename: Optional[str] = None
|
||||
self.frame_count = 0
|
||||
self.start_time: Optional[datetime] = None
|
||||
|
||||
|
||||
# Threading
|
||||
self._recording_thread: Optional[threading.Thread] = None
|
||||
self._stop_recording_event = threading.Event()
|
||||
self._lock = threading.RLock()
|
||||
|
||||
# Initialize camera
|
||||
self._initialize_camera()
|
||||
|
||||
|
||||
# Don't initialize camera immediately - use lazy initialization
|
||||
# Camera will be initialized when recording starts
|
||||
self.logger.info(f"Camera recorder created for: {self.camera_config.name} (lazy initialization)")
|
||||
|
||||
def _initialize_camera(self) -> bool:
|
||||
"""Initialize the camera with configured settings"""
|
||||
try:
|
||||
self.logger.info(f"Initializing camera: {self.camera_config.name}")
|
||||
|
||||
# Ensure SDK is initialized
|
||||
ensure_sdk_initialized()
|
||||
|
||||
# Check if device_info is valid
|
||||
if self.device_info is None:
|
||||
self.logger.error("No device info provided for camera initialization")
|
||||
return False
|
||||
|
||||
# Initialize camera
|
||||
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
|
||||
# Initialize camera (suppress output to avoid MVCAMAPI error messages)
|
||||
with suppress_camera_errors():
|
||||
self.hCamera = mvsdk.CameraInit(self.device_info, -1, -1)
|
||||
self.logger.info("Camera initialized successfully")
|
||||
|
||||
# Get camera capabilities
|
||||
@@ -104,9 +135,7 @@ class CameraRecorder:
|
||||
|
||||
# Allocate frame buffer based on bit depth
|
||||
bytes_per_pixel = self._get_bytes_per_pixel()
|
||||
self.frame_buffer_size = (self.cap.sResolutionRange.iWidthMax *
|
||||
self.cap.sResolutionRange.iHeightMax *
|
||||
bytes_per_pixel)
|
||||
self.frame_buffer_size = self.cap.sResolutionRange.iWidthMax * self.cap.sResolutionRange.iHeightMax * bytes_per_pixel
|
||||
self.frame_buffer = mvsdk.CameraAlignMalloc(self.frame_buffer_size, 16)
|
||||
|
||||
# Start camera
|
||||
@@ -124,7 +153,30 @@ class CameraRecorder:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Unexpected error during camera initialization: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _get_bytes_per_pixel(self) -> int:
|
||||
"""Calculate bytes per pixel based on camera type and bit depth"""
|
||||
if self.monoCamera:
|
||||
# Monochrome camera
|
||||
if self.camera_config.bit_depth >= 16:
|
||||
return 2 # 16-bit mono
|
||||
elif self.camera_config.bit_depth >= 12:
|
||||
return 2 # 12-bit mono (stored in 16-bit)
|
||||
elif self.camera_config.bit_depth >= 10:
|
||||
return 2 # 10-bit mono (stored in 16-bit)
|
||||
else:
|
||||
return 1 # 8-bit mono
|
||||
else:
|
||||
# Color camera
|
||||
if self.camera_config.bit_depth >= 16:
|
||||
return 6 # 16-bit RGB (2 bytes × 3 channels)
|
||||
elif self.camera_config.bit_depth >= 12:
|
||||
return 6 # 12-bit RGB (stored as 16-bit)
|
||||
elif self.camera_config.bit_depth >= 10:
|
||||
return 6 # 10-bit RGB (stored as 16-bit)
|
||||
else:
|
||||
return 3 # 8-bit RGB
|
||||
|
||||
def _configure_camera_settings(self) -> None:
|
||||
"""Configure camera settings from config"""
|
||||
try:
|
||||
@@ -174,8 +226,7 @@ class CameraRecorder:
|
||||
if not self.monoCamera:
|
||||
mvsdk.CameraSetSaturation(self.hCamera, self.camera_config.saturation)
|
||||
|
||||
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, "
|
||||
f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
|
||||
self.logger.info(f"Image quality configured - Sharpness: {self.camera_config.sharpness}, " f"Contrast: {self.camera_config.contrast}, Gamma: {self.camera_config.gamma}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error configuring image quality: {e}")
|
||||
@@ -194,8 +245,7 @@ class CameraRecorder:
|
||||
else:
|
||||
mvsdk.CameraSetDenoise3DParams(self.hCamera, False, 2, None)
|
||||
|
||||
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, "
|
||||
f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
|
||||
self.logger.info(f"Noise reduction configured - Filter: {self.camera_config.noise_filter_enabled}, " f"3D Denoise: {self.camera_config.denoise_3d_enabled}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error configuring noise reduction: {e}")
|
||||
@@ -210,8 +260,7 @@ class CameraRecorder:
|
||||
if not self.camera_config.auto_white_balance:
|
||||
mvsdk.CameraSetPresetClrTemp(self.hCamera, self.camera_config.color_temperature_preset)
|
||||
|
||||
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, "
|
||||
f"Color Temp Preset: {self.camera_config.color_temperature_preset}")
|
||||
self.logger.info(f"Color settings configured - Auto WB: {self.camera_config.auto_white_balance}, " f"Color Temp Preset: {self.camera_config.color_temperature_preset}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error configuring color settings: {e}")
|
||||
@@ -225,61 +274,104 @@ class CameraRecorder:
|
||||
# Set light frequency (0=50Hz, 1=60Hz)
|
||||
mvsdk.CameraSetLightFrequency(self.hCamera, self.camera_config.light_frequency)
|
||||
|
||||
# Configure HDR if enabled
|
||||
if self.camera_config.hdr_enabled:
|
||||
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
|
||||
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
|
||||
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
|
||||
else:
|
||||
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
|
||||
# Configure HDR if enabled (check if HDR functions are available)
|
||||
try:
|
||||
if self.camera_config.hdr_enabled:
|
||||
mvsdk.CameraSetHDR(self.hCamera, 1) # Enable HDR
|
||||
mvsdk.CameraSetHDRGainMode(self.hCamera, self.camera_config.hdr_gain_mode)
|
||||
self.logger.info(f"HDR enabled with gain mode: {self.camera_config.hdr_gain_mode}")
|
||||
else:
|
||||
mvsdk.CameraSetHDR(self.hCamera, 0) # Disable HDR
|
||||
except AttributeError:
|
||||
self.logger.info("HDR functions not available in this SDK version, skipping HDR configuration")
|
||||
|
||||
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, "
|
||||
f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
|
||||
self.logger.info(f"Advanced settings configured - Anti-flicker: {self.camera_config.anti_flicker_enabled}, " f"Light Freq: {self.camera_config.light_frequency}Hz, HDR: {self.camera_config.hdr_enabled}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error configuring advanced settings: {e}")
|
||||
|
||||
def update_camera_settings(self, exposure_ms: Optional[float] = None, gain: Optional[float] = None, target_fps: Optional[float] = None) -> bool:
|
||||
"""Update camera settings dynamically"""
|
||||
if not self.hCamera:
|
||||
self.logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
try:
|
||||
settings_updated = False
|
||||
|
||||
# Update exposure if provided
|
||||
if exposure_ms is not None:
|
||||
mvsdk.CameraSetAeState(self.hCamera, 0) # Disable auto exposure
|
||||
exposure_us = int(exposure_ms * 1000) # Convert ms to microseconds
|
||||
mvsdk.CameraSetExposureTime(self.hCamera, exposure_us)
|
||||
self.camera_config.exposure_ms = exposure_ms
|
||||
self.logger.info(f"Updated exposure time: {exposure_ms}ms")
|
||||
settings_updated = True
|
||||
|
||||
# Update gain if provided
|
||||
if gain is not None:
|
||||
gain_value = int(gain * 100) # Convert to camera units
|
||||
mvsdk.CameraSetAnalogGain(self.hCamera, gain_value)
|
||||
self.camera_config.gain = gain
|
||||
self.logger.info(f"Updated gain: {gain}x")
|
||||
settings_updated = True
|
||||
|
||||
# Update target FPS if provided
|
||||
if target_fps is not None:
|
||||
self.camera_config.target_fps = target_fps
|
||||
self.logger.info(f"Updated target FPS: {target_fps}")
|
||||
settings_updated = True
|
||||
|
||||
return settings_updated
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating camera settings: {e}")
|
||||
return False
|
||||
|
||||
def start_recording(self, filename: str) -> bool:
|
||||
"""Start video recording"""
|
||||
with self._lock:
|
||||
if self.recording:
|
||||
self.logger.warning("Already recording!")
|
||||
return False
|
||||
|
||||
|
||||
# Initialize camera if not already initialized (lazy initialization)
|
||||
if not self.hCamera:
|
||||
self.logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
self.logger.info("Camera not initialized, initializing now...")
|
||||
if not self._initialize_camera():
|
||||
self.logger.error("Failed to initialize camera for recording")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Prepare output path
|
||||
output_path = os.path.join(self.camera_config.storage_path, filename)
|
||||
Path(self.camera_config.storage_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Test camera capture before starting recording
|
||||
if not self._test_camera_capture():
|
||||
self.logger.error("Camera capture test failed")
|
||||
return False
|
||||
|
||||
|
||||
# Initialize recording state
|
||||
self.output_filename = output_path
|
||||
self.frame_count = 0
|
||||
self.start_time = now_atlanta() # Use Atlanta timezone
|
||||
self._stop_recording_event.clear()
|
||||
|
||||
|
||||
# Start recording thread
|
||||
self._recording_thread = threading.Thread(target=self._recording_loop, daemon=True)
|
||||
self._recording_thread.start()
|
||||
|
||||
|
||||
# Update state
|
||||
self.recording = True
|
||||
recording_id = self.state_manager.start_recording(self.camera_config.name, output_path)
|
||||
|
||||
|
||||
# Publish event
|
||||
publish_recording_started(self.camera_config.name, output_path)
|
||||
|
||||
|
||||
self.logger.info(f"Started recording to: {output_path}")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error starting recording: {e}")
|
||||
publish_recording_error(self.camera_config.name, str(e))
|
||||
@@ -329,11 +421,11 @@ class CameraRecorder:
|
||||
self.state_manager.stop_recording(self.output_filename, file_size, self.frame_count)
|
||||
|
||||
# Publish event
|
||||
publish_recording_stopped(
|
||||
self.camera_config.name,
|
||||
self.output_filename or "unknown",
|
||||
duration
|
||||
)
|
||||
publish_recording_stopped(self.camera_config.name, self.output_filename or "unknown", duration)
|
||||
|
||||
# Clean up camera resources after recording (lazy cleanup)
|
||||
self._cleanup_camera()
|
||||
self.logger.info("Camera resources cleaned up after recording")
|
||||
|
||||
self.logger.info(f"Stopped recording - Duration: {duration:.1f}s, Frames: {self.frame_count}")
|
||||
return True
|
||||
@@ -402,18 +494,13 @@ class CameraRecorder:
|
||||
mvsdk.CameraReleaseImageBuffer(self.hCamera, pRawData)
|
||||
|
||||
# Set up video writer
|
||||
fourcc = cv2.VideoWriter_fourcc(*'XVID')
|
||||
fourcc = cv2.VideoWriter_fourcc(*"XVID")
|
||||
frame_size = (FrameHead.iWidth, FrameHead.iHeight)
|
||||
|
||||
# Use 30 FPS for video writer if target_fps is 0 (unlimited)
|
||||
video_fps = self.camera_config.target_fps if self.camera_config.target_fps > 0 else 30.0
|
||||
|
||||
self.video_writer = cv2.VideoWriter(
|
||||
self.output_filename,
|
||||
fourcc,
|
||||
video_fps,
|
||||
frame_size
|
||||
)
|
||||
self.video_writer = cv2.VideoWriter(self.output_filename, fourcc, video_fps, frame_size)
|
||||
|
||||
if not self.video_writer.isOpened():
|
||||
self.logger.error(f"Failed to open video writer for {self.output_filename}")
|
||||
@@ -432,15 +519,34 @@ class CameraRecorder:
|
||||
# Convert the frame buffer memory address to a proper buffer
|
||||
# that numpy can work with using mvsdk.c_ubyte
|
||||
frame_data_buffer = (mvsdk.c_ubyte * frame_head.uBytes).from_address(self.frame_buffer)
|
||||
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
|
||||
|
||||
if self.monoCamera:
|
||||
# Monochrome camera - convert to BGR
|
||||
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
|
||||
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
|
||||
# Handle different bit depths
|
||||
if self.camera_config.bit_depth > 8:
|
||||
# For >8-bit, data is stored as 16-bit values
|
||||
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint16)
|
||||
|
||||
if self.monoCamera:
|
||||
# Monochrome camera - convert to 8-bit BGR for video
|
||||
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
|
||||
# Scale down to 8-bit (simple right shift)
|
||||
frame_8bit = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
|
||||
frame_bgr = cv2.cvtColor(frame_8bit, cv2.COLOR_GRAY2BGR)
|
||||
else:
|
||||
# Color camera - convert to 8-bit BGR
|
||||
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
|
||||
# Scale down to 8-bit
|
||||
frame_bgr = (frame >> (self.camera_config.bit_depth - 8)).astype(np.uint8)
|
||||
else:
|
||||
# Color camera - already in BGR format
|
||||
frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
|
||||
# 8-bit data
|
||||
frame_data = np.frombuffer(frame_data_buffer, dtype=np.uint8)
|
||||
|
||||
if self.monoCamera:
|
||||
# Monochrome camera - convert to BGR
|
||||
frame = frame_data.reshape((frame_head.iHeight, frame_head.iWidth))
|
||||
frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
|
||||
else:
|
||||
# Color camera - already in BGR format
|
||||
frame_bgr = frame_data.reshape((frame_head.iHeight, frame_head.iWidth, 3))
|
||||
|
||||
return frame_bgr
|
||||
|
||||
@@ -460,6 +566,175 @@ class CameraRecorder:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during recording cleanup: {e}")
|
||||
|
||||
def test_connection(self) -> bool:
|
||||
"""Test camera connection"""
|
||||
try:
|
||||
if self.hCamera is None:
|
||||
self.logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
# Test connection using SDK function
|
||||
result = mvsdk.CameraConnectTest(self.hCamera)
|
||||
if result == 0: # CAMERA_STATUS_SUCCESS
|
||||
self.logger.info("Camera connection test passed")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Camera connection test failed with code: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error testing camera connection: {e}")
|
||||
return False
|
||||
|
||||
def reconnect(self) -> bool:
|
||||
"""Attempt to reconnect to the camera"""
|
||||
try:
|
||||
if self.hCamera is None:
|
||||
self.logger.error("Camera not initialized, cannot reconnect")
|
||||
return False
|
||||
|
||||
self.logger.info("Attempting to reconnect camera...")
|
||||
|
||||
# Stop any ongoing operations
|
||||
if self.recording:
|
||||
self.logger.info("Stopping recording before reconnect")
|
||||
self.stop_recording()
|
||||
|
||||
# Attempt reconnection using SDK function
|
||||
result = mvsdk.CameraReConnect(self.hCamera)
|
||||
if result == 0: # CAMERA_STATUS_SUCCESS
|
||||
self.logger.info("Camera reconnected successfully")
|
||||
|
||||
# Restart camera if it was playing
|
||||
try:
|
||||
mvsdk.CameraPlay(self.hCamera)
|
||||
self.logger.info("Camera restarted after reconnection")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to restart camera after reconnection: {e}")
|
||||
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Camera reconnection failed with code: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during camera reconnection: {e}")
|
||||
return False
|
||||
|
||||
def restart_grab(self) -> bool:
|
||||
"""Restart the camera grab process"""
|
||||
try:
|
||||
if self.hCamera is None:
|
||||
self.logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
self.logger.info("Restarting camera grab process...")
|
||||
|
||||
# Stop any ongoing recording
|
||||
if self.recording:
|
||||
self.logger.info("Stopping recording before restart")
|
||||
self.stop_recording()
|
||||
|
||||
# Restart grab using SDK function
|
||||
result = mvsdk.CameraRestartGrab(self.hCamera)
|
||||
if result == 0: # CAMERA_STATUS_SUCCESS
|
||||
self.logger.info("Camera grab restarted successfully")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Camera grab restart failed with code: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error restarting camera grab: {e}")
|
||||
return False
|
||||
|
||||
def reset_timestamp(self) -> bool:
|
||||
"""Reset camera timestamp"""
|
||||
try:
|
||||
if self.hCamera is None:
|
||||
self.logger.error("Camera not initialized")
|
||||
return False
|
||||
|
||||
self.logger.info("Resetting camera timestamp...")
|
||||
|
||||
result = mvsdk.CameraRstTimeStamp(self.hCamera)
|
||||
if result == 0: # CAMERA_STATUS_SUCCESS
|
||||
self.logger.info("Camera timestamp reset successfully")
|
||||
return True
|
||||
else:
|
||||
self.logger.error(f"Camera timestamp reset failed with code: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error resetting camera timestamp: {e}")
|
||||
return False
|
||||
|
||||
def full_reset(self) -> bool:
|
||||
"""Perform a full camera reset (uninitialize and reinitialize)"""
|
||||
try:
|
||||
self.logger.info("Performing full camera reset...")
|
||||
|
||||
# Stop any ongoing recording
|
||||
if self.recording:
|
||||
self.logger.info("Stopping recording before reset")
|
||||
self.stop_recording()
|
||||
|
||||
# Store device info for reinitialization
|
||||
device_info = self.device_info
|
||||
|
||||
# Cleanup current camera
|
||||
self._cleanup_camera()
|
||||
|
||||
# Wait a moment
|
||||
time.sleep(1)
|
||||
|
||||
# Reinitialize camera
|
||||
self.device_info = device_info
|
||||
success = self._initialize_camera()
|
||||
|
||||
if success:
|
||||
self.logger.info("Full camera reset completed successfully")
|
||||
return True
|
||||
else:
|
||||
self.logger.error("Full camera reset failed during reinitialization")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during full camera reset: {e}")
|
||||
return False
|
||||
|
||||
def _cleanup_camera(self) -> None:
|
||||
"""Clean up camera resources"""
|
||||
try:
|
||||
# Stop camera if running
|
||||
if self.hCamera is not None:
|
||||
try:
|
||||
mvsdk.CameraStop(self.hCamera)
|
||||
except:
|
||||
pass # Ignore errors during stop
|
||||
|
||||
# Uninitialize camera
|
||||
try:
|
||||
mvsdk.CameraUnInit(self.hCamera)
|
||||
except:
|
||||
pass # Ignore errors during uninit
|
||||
|
||||
self.hCamera = None
|
||||
|
||||
# Free frame buffer
|
||||
if self.frame_buffer is not None:
|
||||
try:
|
||||
mvsdk.CameraAlignFree(self.frame_buffer)
|
||||
except:
|
||||
pass # Ignore errors during free
|
||||
|
||||
self.frame_buffer = None
|
||||
|
||||
self.logger.info("Camera resources cleaned up")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during camera cleanup: {e}")
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""Clean up camera resources"""
|
||||
try:
|
||||
@@ -488,12 +763,4 @@ class CameraRecorder:
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get recorder status"""
|
||||
return {
|
||||
"camera_name": self.camera_config.name,
|
||||
"is_recording": self.recording,
|
||||
"current_file": self.output_filename,
|
||||
"frame_count": self.frame_count,
|
||||
"start_time": self.start_time.isoformat() if self.start_time else None,
|
||||
"camera_initialized": self.hCamera is not None,
|
||||
"storage_path": self.camera_config.storage_path
|
||||
}
|
||||
return {"camera_name": self.camera_config.name, "is_recording": self.recording, "current_file": self.output_filename, "frame_count": self.frame_count, "start_time": self.start_time.isoformat() if self.start_time else None, "camera_initialized": self.hCamera is not None, "storage_path": self.camera_config.storage_path}
|
||||
|
||||
89
usda_vision_system/camera/sdk_config.py
Normal file
89
usda_vision_system/camera/sdk_config.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
SDK Configuration for the USDA Vision Camera System.
|
||||
|
||||
This module handles SDK initialization and configuration to suppress error messages.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
|
||||
# Add python demo to path
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "python demo"))
|
||||
import mvsdk
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Global flag to track SDK initialization
|
||||
_sdk_initialized = False
|
||||
|
||||
|
||||
def initialize_sdk_with_suppression():
|
||||
"""Initialize the camera SDK with error suppression"""
|
||||
global _sdk_initialized
|
||||
|
||||
if _sdk_initialized:
|
||||
return True
|
||||
|
||||
try:
|
||||
# Initialize SDK with English language
|
||||
result = mvsdk.CameraSdkInit(1)
|
||||
if result == 0:
|
||||
logger.info("Camera SDK initialized successfully")
|
||||
|
||||
# Try to set system options to suppress logging
|
||||
try:
|
||||
# These are common options that might control logging
|
||||
# We'll try them and ignore failures since they might not be supported
|
||||
|
||||
# Try to disable debug output
|
||||
try:
|
||||
mvsdk.CameraSetSysOption("DebugLevel", "0")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to disable console output
|
||||
try:
|
||||
mvsdk.CameraSetSysOption("ConsoleOutput", "0")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to disable error logging
|
||||
try:
|
||||
mvsdk.CameraSetSysOption("ErrorLog", "0")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to set log level to none
|
||||
try:
|
||||
mvsdk.CameraSetSysOption("LogLevel", "0")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try to disable verbose mode
|
||||
try:
|
||||
mvsdk.CameraSetSysOption("Verbose", "0")
|
||||
except:
|
||||
pass
|
||||
|
||||
logger.debug("Attempted to configure SDK logging options")
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not configure SDK logging options: {e}")
|
||||
|
||||
_sdk_initialized = True
|
||||
return True
|
||||
else:
|
||||
logger.error(f"SDK initialization failed with code: {result}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SDK initialization failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def ensure_sdk_initialized():
|
||||
"""Ensure the SDK is initialized before camera operations"""
|
||||
if not _sdk_initialized:
|
||||
return initialize_sdk_with_suppression()
|
||||
return True
|
||||
Binary file not shown.
Binary file not shown.
@@ -9,12 +9,13 @@ import threading
|
||||
import logging
|
||||
from typing import Dict, Optional, List, Any
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MachineState(Enum):
|
||||
"""Machine states"""
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
ON = "on"
|
||||
OFF = "off"
|
||||
@@ -23,6 +24,7 @@ class MachineState(Enum):
|
||||
|
||||
class CameraStatus(Enum):
|
||||
"""Camera status"""
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
AVAILABLE = "available"
|
||||
BUSY = "busy"
|
||||
@@ -32,6 +34,7 @@ class CameraStatus(Enum):
|
||||
|
||||
class RecordingState(Enum):
|
||||
"""Recording states"""
|
||||
|
||||
IDLE = "idle"
|
||||
RECORDING = "recording"
|
||||
STOPPING = "stopping"
|
||||
@@ -41,6 +44,7 @@ class RecordingState(Enum):
|
||||
@dataclass
|
||||
class MachineInfo:
|
||||
"""Machine state information"""
|
||||
|
||||
name: str
|
||||
state: MachineState = MachineState.UNKNOWN
|
||||
last_updated: datetime = field(default_factory=datetime.now)
|
||||
@@ -48,9 +52,22 @@ class MachineInfo:
|
||||
mqtt_topic: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class MQTTEvent:
|
||||
"""MQTT event information for history tracking"""
|
||||
|
||||
machine_name: str
|
||||
topic: str
|
||||
payload: str
|
||||
normalized_state: str
|
||||
timestamp: datetime = field(default_factory=datetime.now)
|
||||
message_number: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraInfo:
|
||||
"""Camera state information"""
|
||||
|
||||
name: str
|
||||
status: CameraStatus = CameraStatus.UNKNOWN
|
||||
last_checked: datetime = field(default_factory=datetime.now)
|
||||
@@ -64,6 +81,7 @@ class CameraInfo:
|
||||
@dataclass
|
||||
class RecordingInfo:
|
||||
"""Recording session information"""
|
||||
|
||||
camera_name: str
|
||||
filename: str
|
||||
start_time: datetime
|
||||
@@ -76,21 +94,26 @@ class RecordingInfo:
|
||||
|
||||
class StateManager:
|
||||
"""Thread-safe state manager for the entire system"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self._lock = threading.RLock()
|
||||
|
||||
|
||||
# State dictionaries
|
||||
self._machines: Dict[str, MachineInfo] = {}
|
||||
self._cameras: Dict[str, CameraInfo] = {}
|
||||
self._recordings: Dict[str, RecordingInfo] = {} # Key: recording_id (filename)
|
||||
|
||||
|
||||
# MQTT event history
|
||||
self._mqtt_events: List[MQTTEvent] = []
|
||||
self._mqtt_event_counter = 0
|
||||
self._max_mqtt_events = 100 # Keep last 100 events
|
||||
|
||||
# System state
|
||||
self._mqtt_connected = False
|
||||
self._system_started = False
|
||||
self._last_mqtt_message_time: Optional[datetime] = None
|
||||
|
||||
|
||||
# Machine state management
|
||||
def update_machine_state(self, name: str, state: str, message: Optional[str] = None, topic: Optional[str] = None) -> bool:
|
||||
"""Update machine state"""
|
||||
@@ -99,11 +122,11 @@ class StateManager:
|
||||
except ValueError:
|
||||
self.logger.warning(f"Invalid machine state: {state}")
|
||||
machine_state = MachineState.UNKNOWN
|
||||
|
||||
|
||||
with self._lock:
|
||||
if name not in self._machines:
|
||||
self._machines[name] = MachineInfo(name=name, mqtt_topic=topic)
|
||||
|
||||
|
||||
machine = self._machines[name]
|
||||
old_state = machine.state
|
||||
machine.state = machine_state
|
||||
@@ -111,20 +134,47 @@ class StateManager:
|
||||
machine.last_message = message
|
||||
if topic:
|
||||
machine.mqtt_topic = topic
|
||||
|
||||
|
||||
self.logger.info(f"Machine {name} state: {old_state.value} -> {machine_state.value}")
|
||||
return old_state != machine_state
|
||||
|
||||
|
||||
def get_machine_state(self, name: str) -> Optional[MachineInfo]:
|
||||
"""Get machine state"""
|
||||
with self._lock:
|
||||
return self._machines.get(name)
|
||||
|
||||
|
||||
def get_all_machines(self) -> Dict[str, MachineInfo]:
|
||||
"""Get all machine states"""
|
||||
with self._lock:
|
||||
return self._machines.copy()
|
||||
|
||||
|
||||
# MQTT event management
|
||||
def add_mqtt_event(self, machine_name: str, topic: str, payload: str, normalized_state: str) -> None:
|
||||
"""Add an MQTT event to the history"""
|
||||
with self._lock:
|
||||
self._mqtt_event_counter += 1
|
||||
|
||||
event = MQTTEvent(machine_name=machine_name, topic=topic, payload=payload, normalized_state=normalized_state, timestamp=datetime.now(), message_number=self._mqtt_event_counter)
|
||||
|
||||
self._mqtt_events.append(event)
|
||||
|
||||
# Keep only the last N events
|
||||
if len(self._mqtt_events) > self._max_mqtt_events:
|
||||
self._mqtt_events.pop(0)
|
||||
|
||||
self.logger.debug(f"Added MQTT event #{self._mqtt_event_counter}: {machine_name} -> {normalized_state}")
|
||||
|
||||
def get_recent_mqtt_events(self, limit: int = 5) -> List[MQTTEvent]:
|
||||
"""Get the most recent MQTT events"""
|
||||
with self._lock:
|
||||
# Return the last 'limit' events in reverse chronological order (newest first)
|
||||
return list(reversed(self._mqtt_events[-limit:]))
|
||||
|
||||
def get_mqtt_event_count(self) -> int:
|
||||
"""Get total number of MQTT events processed"""
|
||||
with self._lock:
|
||||
return self._mqtt_event_counter
|
||||
|
||||
# Camera state management
|
||||
def update_camera_status(self, name: str, status: str, error: Optional[str] = None, device_info: Optional[Dict] = None) -> bool:
|
||||
"""Update camera status"""
|
||||
@@ -133,11 +183,11 @@ class StateManager:
|
||||
except ValueError:
|
||||
self.logger.warning(f"Invalid camera status: {status}")
|
||||
camera_status = CameraStatus.UNKNOWN
|
||||
|
||||
|
||||
with self._lock:
|
||||
if name not in self._cameras:
|
||||
self._cameras[name] = CameraInfo(name=name)
|
||||
|
||||
|
||||
camera = self._cameras[name]
|
||||
old_status = camera.status
|
||||
camera.status = camera_status
|
||||
@@ -145,113 +195,106 @@ class StateManager:
|
||||
camera.last_error = error
|
||||
if device_info:
|
||||
camera.device_info = device_info
|
||||
|
||||
|
||||
if old_status != camera_status:
|
||||
self.logger.info(f"Camera {name} status: {old_status.value} -> {camera_status.value}")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def set_camera_recording(self, name: str, recording: bool, filename: Optional[str] = None) -> None:
|
||||
"""Set camera recording state"""
|
||||
with self._lock:
|
||||
if name not in self._cameras:
|
||||
self._cameras[name] = CameraInfo(name=name)
|
||||
|
||||
|
||||
camera = self._cameras[name]
|
||||
camera.is_recording = recording
|
||||
camera.current_recording_file = filename
|
||||
|
||||
|
||||
if recording and filename:
|
||||
camera.recording_start_time = datetime.now()
|
||||
self.logger.info(f"Camera {name} started recording: {filename}")
|
||||
elif not recording:
|
||||
camera.recording_start_time = None
|
||||
self.logger.info(f"Camera {name} stopped recording")
|
||||
|
||||
|
||||
def get_camera_status(self, name: str) -> Optional[CameraInfo]:
|
||||
"""Get camera status"""
|
||||
with self._lock:
|
||||
return self._cameras.get(name)
|
||||
|
||||
|
||||
def get_all_cameras(self) -> Dict[str, CameraInfo]:
|
||||
"""Get all camera statuses"""
|
||||
with self._lock:
|
||||
return self._cameras.copy()
|
||||
|
||||
|
||||
# Recording management
|
||||
def start_recording(self, camera_name: str, filename: str) -> str:
|
||||
"""Start a new recording session"""
|
||||
recording_id = filename # Use filename as recording ID
|
||||
|
||||
|
||||
with self._lock:
|
||||
recording = RecordingInfo(
|
||||
camera_name=camera_name,
|
||||
filename=filename,
|
||||
start_time=datetime.now()
|
||||
)
|
||||
recording = RecordingInfo(camera_name=camera_name, filename=filename, start_time=datetime.now())
|
||||
self._recordings[recording_id] = recording
|
||||
|
||||
|
||||
# Update camera state
|
||||
self.set_camera_recording(camera_name, True, filename)
|
||||
|
||||
|
||||
self.logger.info(f"Started recording session: {recording_id}")
|
||||
return recording_id
|
||||
|
||||
|
||||
def stop_recording(self, recording_id: str, file_size: Optional[int] = None, frame_count: Optional[int] = None) -> bool:
|
||||
"""Stop a recording session"""
|
||||
with self._lock:
|
||||
if recording_id not in self._recordings:
|
||||
self.logger.warning(f"Recording session not found: {recording_id}")
|
||||
return False
|
||||
|
||||
|
||||
recording = self._recordings[recording_id]
|
||||
recording.state = RecordingState.IDLE
|
||||
recording.end_time = datetime.now()
|
||||
recording.file_size_bytes = file_size
|
||||
recording.frame_count = frame_count
|
||||
|
||||
|
||||
# Update camera state
|
||||
self.set_camera_recording(recording.camera_name, False)
|
||||
|
||||
|
||||
duration = (recording.end_time - recording.start_time).total_seconds()
|
||||
self.logger.info(f"Stopped recording session: {recording_id} (duration: {duration:.1f}s)")
|
||||
return True
|
||||
|
||||
|
||||
def set_recording_error(self, recording_id: str, error_message: str) -> bool:
|
||||
"""Set recording error state"""
|
||||
with self._lock:
|
||||
if recording_id not in self._recordings:
|
||||
return False
|
||||
|
||||
|
||||
recording = self._recordings[recording_id]
|
||||
recording.state = RecordingState.ERROR
|
||||
recording.error_message = error_message
|
||||
recording.end_time = datetime.now()
|
||||
|
||||
|
||||
# Update camera state
|
||||
self.set_camera_recording(recording.camera_name, False)
|
||||
|
||||
|
||||
self.logger.error(f"Recording error for {recording_id}: {error_message}")
|
||||
return True
|
||||
|
||||
|
||||
def get_recording(self, recording_id: str) -> Optional[RecordingInfo]:
|
||||
"""Get recording information"""
|
||||
with self._lock:
|
||||
return self._recordings.get(recording_id)
|
||||
|
||||
|
||||
def get_all_recordings(self) -> Dict[str, RecordingInfo]:
|
||||
"""Get all recording sessions"""
|
||||
with self._lock:
|
||||
return self._recordings.copy()
|
||||
|
||||
|
||||
def get_active_recordings(self) -> Dict[str, RecordingInfo]:
|
||||
"""Get currently active recordings"""
|
||||
with self._lock:
|
||||
return {
|
||||
rid: recording for rid, recording in self._recordings.items()
|
||||
if recording.state == RecordingState.RECORDING
|
||||
}
|
||||
|
||||
return {rid: recording for rid, recording in self._recordings.items() if recording.state == RecordingState.RECORDING}
|
||||
|
||||
# System state management
|
||||
def set_mqtt_connected(self, connected: bool) -> None:
|
||||
"""Set MQTT connection state"""
|
||||
@@ -260,31 +303,31 @@ class StateManager:
|
||||
self._mqtt_connected = connected
|
||||
if connected:
|
||||
self._last_mqtt_message_time = datetime.now()
|
||||
|
||||
|
||||
if old_state != connected:
|
||||
self.logger.info(f"MQTT connection: {'connected' if connected else 'disconnected'}")
|
||||
|
||||
|
||||
def is_mqtt_connected(self) -> bool:
|
||||
"""Check if MQTT is connected"""
|
||||
with self._lock:
|
||||
return self._mqtt_connected
|
||||
|
||||
|
||||
def update_mqtt_activity(self) -> None:
|
||||
"""Update last MQTT message time"""
|
||||
with self._lock:
|
||||
self._last_mqtt_message_time = datetime.now()
|
||||
|
||||
|
||||
def set_system_started(self, started: bool) -> None:
|
||||
"""Set system started state"""
|
||||
with self._lock:
|
||||
self._system_started = started
|
||||
self.logger.info(f"System {'started' if started else 'stopped'}")
|
||||
|
||||
|
||||
def is_system_started(self) -> bool:
|
||||
"""Check if system is started"""
|
||||
with self._lock:
|
||||
return self._system_started
|
||||
|
||||
|
||||
# Utility methods
|
||||
def get_system_summary(self) -> Dict[str, Any]:
|
||||
"""Get a summary of the entire system state"""
|
||||
@@ -293,36 +336,28 @@ class StateManager:
|
||||
"system_started": self._system_started,
|
||||
"mqtt_connected": self._mqtt_connected,
|
||||
"last_mqtt_message": self._last_mqtt_message_time.isoformat() if self._last_mqtt_message_time else None,
|
||||
"machines": {name: {
|
||||
"state": machine.state.value,
|
||||
"last_updated": machine.last_updated.isoformat()
|
||||
} for name, machine in self._machines.items()},
|
||||
"cameras": {name: {
|
||||
"status": camera.status.value,
|
||||
"is_recording": camera.is_recording,
|
||||
"last_checked": camera.last_checked.isoformat()
|
||||
} for name, camera in self._cameras.items()},
|
||||
"machines": {name: {"state": machine.state.value, "last_updated": machine.last_updated.isoformat()} for name, machine in self._machines.items()},
|
||||
"cameras": {name: {"status": camera.status.value, "is_recording": camera.is_recording, "last_checked": camera.last_checked.isoformat()} for name, camera in self._cameras.items()},
|
||||
"active_recordings": len(self.get_active_recordings()),
|
||||
"total_recordings": len(self._recordings)
|
||||
"total_recordings": len(self._recordings),
|
||||
}
|
||||
|
||||
|
||||
def cleanup_old_recordings(self, max_age_hours: int = 24) -> int:
|
||||
"""Clean up old recording entries from memory"""
|
||||
cutoff_time = datetime.now() - datetime.timedelta(hours=max_age_hours)
|
||||
cutoff_time = datetime.now() - timedelta(hours=max_age_hours)
|
||||
removed_count = 0
|
||||
|
||||
|
||||
with self._lock:
|
||||
to_remove = []
|
||||
for recording_id, recording in self._recordings.items():
|
||||
if (recording.state != RecordingState.RECORDING and
|
||||
recording.end_time and recording.end_time < cutoff_time):
|
||||
if recording.state != RecordingState.RECORDING and recording.end_time and recording.end_time < cutoff_time:
|
||||
to_remove.append(recording_id)
|
||||
|
||||
|
||||
for recording_id in to_remove:
|
||||
del self._recordings[recording_id]
|
||||
removed_count += 1
|
||||
|
||||
|
||||
if removed_count > 0:
|
||||
self.logger.info(f"Cleaned up {removed_count} old recording entries")
|
||||
|
||||
|
||||
return removed_count
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@ This module provides MQTT connectivity and message handling for machine state up
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
from typing import Dict, Optional, Callable, List
|
||||
from typing import Dict, Optional, Any
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from ..core.config import Config, MQTTConfig
|
||||
@@ -18,207 +18,219 @@ from .handlers import MQTTMessageHandler
|
||||
|
||||
class MQTTClient:
|
||||
"""MQTT client for receiving machine state updates"""
|
||||
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: EventSystem):
|
||||
self.config = config
|
||||
self.mqtt_config = config.mqtt
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# MQTT client
|
||||
self.client: Optional[mqtt.Client] = None
|
||||
self.connected = False
|
||||
self.running = False
|
||||
|
||||
|
||||
# Threading
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
|
||||
# Message handler
|
||||
self.message_handler = MQTTMessageHandler(state_manager, event_system)
|
||||
|
||||
|
||||
# Connection retry settings
|
||||
self.reconnect_delay = 5 # seconds
|
||||
self.max_reconnect_attempts = 10
|
||||
|
||||
|
||||
# Topic mapping (topic -> machine_name)
|
||||
self.topic_to_machine = {
|
||||
topic: machine_name
|
||||
for machine_name, topic in self.mqtt_config.topics.items()
|
||||
}
|
||||
|
||||
self.topic_to_machine = {topic: machine_name for machine_name, topic in self.mqtt_config.topics.items()}
|
||||
|
||||
# Status tracking
|
||||
self.start_time = None
|
||||
self.message_count = 0
|
||||
self.error_count = 0
|
||||
self.last_message_time = None
|
||||
|
||||
def start(self) -> bool:
|
||||
"""Start the MQTT client in a separate thread"""
|
||||
if self.running:
|
||||
self.logger.warning("MQTT client is already running")
|
||||
return True
|
||||
|
||||
|
||||
self.logger.info("Starting MQTT client...")
|
||||
self.running = True
|
||||
self._stop_event.clear()
|
||||
|
||||
self.start_time = time.time()
|
||||
|
||||
# Start in separate thread
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
|
||||
# Wait a moment to see if connection succeeds
|
||||
time.sleep(2)
|
||||
return self.connected
|
||||
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the MQTT client"""
|
||||
if not self.running:
|
||||
return
|
||||
|
||||
|
||||
self.logger.info("Stopping MQTT client...")
|
||||
self.running = False
|
||||
self._stop_event.set()
|
||||
|
||||
|
||||
if self.client and self.connected:
|
||||
self.client.disconnect()
|
||||
|
||||
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(timeout=5)
|
||||
|
||||
|
||||
self.logger.info("MQTT client stopped")
|
||||
|
||||
|
||||
def _run_loop(self) -> None:
|
||||
"""Main MQTT client loop"""
|
||||
reconnect_attempts = 0
|
||||
|
||||
|
||||
while self.running and not self._stop_event.is_set():
|
||||
try:
|
||||
if not self.connected:
|
||||
if self._connect():
|
||||
reconnect_attempts = 0
|
||||
self._subscribe_to_topics()
|
||||
else:
|
||||
reconnect_attempts += 1
|
||||
if reconnect_attempts >= self.max_reconnect_attempts:
|
||||
self.logger.error(f"Max reconnection attempts ({self.max_reconnect_attempts}) reached")
|
||||
break
|
||||
|
||||
|
||||
self.logger.warning(f"Reconnection attempt {reconnect_attempts}/{self.max_reconnect_attempts} in {self.reconnect_delay}s")
|
||||
if self._stop_event.wait(self.reconnect_delay):
|
||||
break
|
||||
continue
|
||||
|
||||
|
||||
# Process MQTT messages
|
||||
if self.client:
|
||||
self.client.loop(timeout=1.0)
|
||||
|
||||
|
||||
# Small delay to prevent busy waiting
|
||||
if self._stop_event.wait(0.1):
|
||||
break
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error in MQTT loop: {e}")
|
||||
self.connected = False
|
||||
if self._stop_event.wait(self.reconnect_delay):
|
||||
break
|
||||
|
||||
|
||||
self.running = False
|
||||
self.logger.info("MQTT client loop ended")
|
||||
|
||||
|
||||
def _connect(self) -> bool:
|
||||
"""Connect to MQTT broker"""
|
||||
try:
|
||||
# Create new client instance
|
||||
self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1)
|
||||
|
||||
|
||||
# Set callbacks
|
||||
self.client.on_connect = self._on_connect
|
||||
self.client.on_disconnect = self._on_disconnect
|
||||
self.client.on_message = self._on_message
|
||||
|
||||
|
||||
# Set authentication if provided
|
||||
if self.mqtt_config.username and self.mqtt_config.password:
|
||||
self.client.username_pw_set(
|
||||
self.mqtt_config.username,
|
||||
self.mqtt_config.password
|
||||
)
|
||||
|
||||
self.client.username_pw_set(self.mqtt_config.username, self.mqtt_config.password)
|
||||
|
||||
# Connect to broker
|
||||
self.logger.info(f"Connecting to MQTT broker at {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port}")
|
||||
self.client.connect(
|
||||
self.mqtt_config.broker_host,
|
||||
self.mqtt_config.broker_port,
|
||||
60
|
||||
)
|
||||
|
||||
self.client.connect(self.mqtt_config.broker_host, self.mqtt_config.broker_port, 60)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to connect to MQTT broker: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _subscribe_to_topics(self) -> None:
|
||||
"""Subscribe to all configured topics"""
|
||||
if not self.client or not self.connected:
|
||||
return
|
||||
|
||||
|
||||
for machine_name, topic in self.mqtt_config.topics.items():
|
||||
try:
|
||||
result, mid = self.client.subscribe(topic)
|
||||
if result == mqtt.MQTT_ERR_SUCCESS:
|
||||
self.logger.info(f"Subscribed to topic: {topic} (machine: {machine_name})")
|
||||
self.logger.info(f"📋 MQTT SUBSCRIBED: {topic} (machine: {machine_name})")
|
||||
print(f"📋 MQTT SUBSCRIBED: {machine_name} → {topic}")
|
||||
else:
|
||||
self.logger.error(f"Failed to subscribe to topic: {topic}")
|
||||
self.logger.error(f"❌ MQTT SUBSCRIPTION FAILED: {topic}")
|
||||
print(f"❌ MQTT SUBSCRIPTION FAILED: {topic}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error subscribing to topic {topic}: {e}")
|
||||
|
||||
|
||||
def _on_connect(self, client, userdata, flags, rc) -> None:
|
||||
"""Callback for when the client connects to the broker"""
|
||||
if rc == 0:
|
||||
self.connected = True
|
||||
self.state_manager.set_mqtt_connected(True)
|
||||
self.event_system.publish(EventType.MQTT_CONNECTED, "mqtt_client")
|
||||
self.logger.info("Successfully connected to MQTT broker")
|
||||
self.logger.info("🔗 MQTT CONNECTED to broker successfully")
|
||||
print(f"🔗 MQTT CONNECTED: {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port}")
|
||||
|
||||
# Subscribe to topics immediately after connection
|
||||
self._subscribe_to_topics()
|
||||
else:
|
||||
self.connected = False
|
||||
self.logger.error(f"Failed to connect to MQTT broker, return code {rc}")
|
||||
|
||||
self.logger.error(f"❌ MQTT CONNECTION FAILED with return code {rc}")
|
||||
print(f"❌ MQTT CONNECTION FAILED: {self.mqtt_config.broker_host}:{self.mqtt_config.broker_port} (code: {rc})")
|
||||
|
||||
def _on_disconnect(self, client, userdata, rc) -> None:
|
||||
"""Callback for when the client disconnects from the broker"""
|
||||
self.connected = False
|
||||
self.state_manager.set_mqtt_connected(False)
|
||||
self.event_system.publish(EventType.MQTT_DISCONNECTED, "mqtt_client")
|
||||
|
||||
|
||||
if rc != 0:
|
||||
self.logger.warning(f"Unexpected MQTT disconnection (rc: {rc})")
|
||||
self.logger.warning(f"⚠️ MQTT DISCONNECTED unexpectedly (rc: {rc})")
|
||||
print(f"⚠️ MQTT DISCONNECTED: Unexpected disconnection (code: {rc})")
|
||||
else:
|
||||
self.logger.info("MQTT client disconnected")
|
||||
|
||||
self.logger.info("🔌 MQTT DISCONNECTED gracefully")
|
||||
print("🔌 MQTT DISCONNECTED: Graceful disconnection")
|
||||
|
||||
def _on_message(self, client, userdata, msg) -> None:
|
||||
"""Callback for when a message is received"""
|
||||
try:
|
||||
topic = msg.topic
|
||||
payload = msg.payload.decode('utf-8').strip()
|
||||
|
||||
payload = msg.payload.decode("utf-8").strip()
|
||||
|
||||
self.logger.debug(f"MQTT message received - Topic: {topic}, Payload: {payload}")
|
||||
|
||||
# Update MQTT activity
|
||||
|
||||
# Update MQTT activity and tracking
|
||||
self.state_manager.update_mqtt_activity()
|
||||
|
||||
self.message_count += 1
|
||||
self.last_message_time = time.time()
|
||||
|
||||
# Get machine name from topic
|
||||
machine_name = self.topic_to_machine.get(topic)
|
||||
if not machine_name:
|
||||
self.logger.warning(f"Unknown topic: {topic}")
|
||||
self.logger.warning(f"❓ MQTT UNKNOWN TOPIC: {topic}")
|
||||
print(f"❓ MQTT UNKNOWN TOPIC: {topic}")
|
||||
return
|
||||
|
||||
|
||||
# Show MQTT message on console
|
||||
print(f"📡 MQTT MESSAGE: {machine_name} → {payload}")
|
||||
|
||||
# Handle the message
|
||||
self.message_handler.handle_message(machine_name, topic, payload)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.error_count += 1
|
||||
self.logger.error(f"Error processing MQTT message: {e}")
|
||||
|
||||
|
||||
def publish_message(self, topic: str, payload: str, qos: int = 0, retain: bool = False) -> bool:
|
||||
"""Publish a message to MQTT broker"""
|
||||
if not self.client or not self.connected:
|
||||
self.logger.warning("Cannot publish: MQTT client not connected")
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
result = self.client.publish(topic, payload, qos, retain)
|
||||
if result.rc == mqtt.MQTT_ERR_SUCCESS:
|
||||
@@ -230,22 +242,26 @@ class MQTTClient:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error publishing message: {e}")
|
||||
return False
|
||||
|
||||
def get_status(self) -> Dict[str, any]:
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get MQTT client status"""
|
||||
return {
|
||||
"connected": self.connected,
|
||||
"running": self.running,
|
||||
"broker_host": self.mqtt_config.broker_host,
|
||||
"broker_port": self.mqtt_config.broker_port,
|
||||
"subscribed_topics": list(self.mqtt_config.topics.values()),
|
||||
"topic_mappings": self.topic_to_machine
|
||||
}
|
||||
|
||||
uptime_seconds = None
|
||||
last_message_time_str = None
|
||||
|
||||
if self.start_time:
|
||||
uptime_seconds = time.time() - self.start_time
|
||||
|
||||
if self.last_message_time:
|
||||
from datetime import datetime
|
||||
|
||||
last_message_time_str = datetime.fromtimestamp(self.last_message_time).isoformat()
|
||||
|
||||
return {"connected": self.connected, "running": self.running, "broker_host": self.mqtt_config.broker_host, "broker_port": self.mqtt_config.broker_port, "subscribed_topics": list(self.mqtt_config.topics.values()), "topic_mappings": self.topic_to_machine, "message_count": self.message_count, "error_count": self.error_count, "last_message_time": last_message_time_str, "uptime_seconds": uptime_seconds}
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Check if MQTT client is connected"""
|
||||
return self.connected
|
||||
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if MQTT client is running"""
|
||||
return self.running
|
||||
|
||||
@@ -14,69 +14,63 @@ from ..core.events import EventSystem, publish_machine_state_changed
|
||||
|
||||
class MQTTMessageHandler:
|
||||
"""Handles MQTT messages and triggers appropriate system actions"""
|
||||
|
||||
|
||||
def __init__(self, state_manager: StateManager, event_system: EventSystem):
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Message processing statistics
|
||||
self.message_count = 0
|
||||
self.last_message_time: Optional[datetime] = None
|
||||
self.error_count = 0
|
||||
|
||||
|
||||
def handle_message(self, machine_name: str, topic: str, payload: str) -> None:
|
||||
"""Handle an incoming MQTT message"""
|
||||
try:
|
||||
self.message_count += 1
|
||||
self.last_message_time = datetime.now()
|
||||
|
||||
|
||||
self.logger.info(f"Processing MQTT message - Machine: {machine_name}, Topic: {topic}, Payload: {payload}")
|
||||
|
||||
|
||||
# Normalize payload
|
||||
normalized_payload = self._normalize_payload(payload)
|
||||
|
||||
|
||||
# Update machine state
|
||||
state_changed = self.state_manager.update_machine_state(
|
||||
name=machine_name,
|
||||
state=normalized_payload,
|
||||
message=payload,
|
||||
topic=topic
|
||||
)
|
||||
|
||||
state_changed = self.state_manager.update_machine_state(name=machine_name, state=normalized_payload, message=payload, topic=topic)
|
||||
|
||||
# Store MQTT event in history
|
||||
self.state_manager.add_mqtt_event(machine_name=machine_name, topic=topic, payload=payload, normalized_state=normalized_payload)
|
||||
|
||||
# Publish state change event if state actually changed
|
||||
if state_changed:
|
||||
publish_machine_state_changed(
|
||||
machine_name=machine_name,
|
||||
state=normalized_payload,
|
||||
source="mqtt_handler"
|
||||
)
|
||||
|
||||
publish_machine_state_changed(machine_name=machine_name, state=normalized_payload, source="mqtt_handler")
|
||||
|
||||
self.logger.info(f"Machine {machine_name} state changed to: {normalized_payload}")
|
||||
|
||||
|
||||
# Log the message for debugging
|
||||
self._log_message_details(machine_name, topic, payload, normalized_payload)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.error_count += 1
|
||||
self.logger.error(f"Error handling MQTT message for {machine_name}: {e}")
|
||||
|
||||
|
||||
def _normalize_payload(self, payload: str) -> str:
|
||||
"""Normalize payload to standard machine states"""
|
||||
payload_lower = payload.lower().strip()
|
||||
|
||||
|
||||
# Map various possible payloads to standard states
|
||||
if payload_lower in ['on', 'true', '1', 'start', 'running', 'active']:
|
||||
return 'on'
|
||||
elif payload_lower in ['off', 'false', '0', 'stop', 'stopped', 'inactive']:
|
||||
return 'off'
|
||||
elif payload_lower in ['error', 'fault', 'alarm']:
|
||||
return 'error'
|
||||
if payload_lower in ["on", "true", "1", "start", "running", "active"]:
|
||||
return "on"
|
||||
elif payload_lower in ["off", "false", "0", "stop", "stopped", "inactive"]:
|
||||
return "off"
|
||||
elif payload_lower in ["error", "fault", "alarm"]:
|
||||
return "error"
|
||||
else:
|
||||
# For unknown payloads, log and return as-is
|
||||
self.logger.warning(f"Unknown payload format: '{payload}', treating as raw state")
|
||||
return payload_lower
|
||||
|
||||
|
||||
def _log_message_details(self, machine_name: str, topic: str, original_payload: str, normalized_payload: str) -> None:
|
||||
"""Log detailed message information"""
|
||||
self.logger.debug(f"MQTT Message Details:")
|
||||
@@ -86,16 +80,11 @@ class MQTTMessageHandler:
|
||||
self.logger.debug(f" Normalized Payload: '{normalized_payload}'")
|
||||
self.logger.debug(f" Timestamp: {self.last_message_time}")
|
||||
self.logger.debug(f" Total Messages Processed: {self.message_count}")
|
||||
|
||||
|
||||
def get_statistics(self) -> Dict[str, any]:
|
||||
"""Get message processing statistics"""
|
||||
return {
|
||||
"total_messages": self.message_count,
|
||||
"error_count": self.error_count,
|
||||
"last_message_time": self.last_message_time.isoformat() if self.last_message_time else None,
|
||||
"success_rate": (self.message_count - self.error_count) / max(self.message_count, 1) * 100
|
||||
}
|
||||
|
||||
return {"total_messages": self.message_count, "error_count": self.error_count, "last_message_time": self.last_message_time.isoformat() if self.last_message_time else None, "success_rate": (self.message_count - self.error_count) / max(self.message_count, 1) * 100}
|
||||
|
||||
def reset_statistics(self) -> None:
|
||||
"""Reset message processing statistics"""
|
||||
self.message_count = 0
|
||||
@@ -106,47 +95,47 @@ class MQTTMessageHandler:
|
||||
|
||||
class MachineStateProcessor:
|
||||
"""Processes machine state changes and determines actions"""
|
||||
|
||||
|
||||
def __init__(self, state_manager: StateManager, event_system: EventSystem):
|
||||
self.state_manager = state_manager
|
||||
self.event_system = event_system
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_state_change(self, machine_name: str, old_state: str, new_state: str) -> None:
|
||||
"""Process a machine state change and determine what actions to take"""
|
||||
self.logger.info(f"Processing state change for {machine_name}: {old_state} -> {new_state}")
|
||||
|
||||
|
||||
# Handle state transitions
|
||||
if old_state != 'on' and new_state == 'on':
|
||||
if old_state != "on" and new_state == "on":
|
||||
self._handle_machine_turned_on(machine_name)
|
||||
elif old_state == 'on' and new_state != 'on':
|
||||
elif old_state == "on" and new_state != "on":
|
||||
self._handle_machine_turned_off(machine_name)
|
||||
elif new_state == 'error':
|
||||
elif new_state == "error":
|
||||
self._handle_machine_error(machine_name)
|
||||
|
||||
|
||||
def _handle_machine_turned_on(self, machine_name: str) -> None:
|
||||
"""Handle machine turning on - should start recording"""
|
||||
self.logger.info(f"Machine {machine_name} turned ON - should start recording")
|
||||
|
||||
|
||||
# The actual recording start will be handled by the camera manager
|
||||
# which listens to the MACHINE_STATE_CHANGED event
|
||||
|
||||
|
||||
# We could add additional logic here, such as:
|
||||
# - Checking if camera is available
|
||||
# - Pre-warming camera settings
|
||||
# - Sending notifications
|
||||
|
||||
|
||||
def _handle_machine_turned_off(self, machine_name: str) -> None:
|
||||
"""Handle machine turning off - should stop recording"""
|
||||
self.logger.info(f"Machine {machine_name} turned OFF - should stop recording")
|
||||
|
||||
|
||||
# The actual recording stop will be handled by the camera manager
|
||||
# which listens to the MACHINE_STATE_CHANGED event
|
||||
|
||||
|
||||
def _handle_machine_error(self, machine_name: str) -> None:
|
||||
"""Handle machine error state"""
|
||||
self.logger.warning(f"Machine {machine_name} in ERROR state")
|
||||
|
||||
|
||||
# Could implement error handling logic here:
|
||||
# - Stop recording if active
|
||||
# - Send alerts
|
||||
|
||||
Binary file not shown.
@@ -19,7 +19,7 @@ from ..core.events import EventSystem, EventType, Event
|
||||
|
||||
class StorageManager:
|
||||
"""Manages storage and file organization for recorded videos"""
|
||||
|
||||
|
||||
def __init__(self, config: Config, state_manager: StateManager, event_system: Optional[EventSystem] = None):
|
||||
self.config = config
|
||||
self.storage_config = config.storage
|
||||
@@ -37,20 +37,20 @@ class StorageManager:
|
||||
# Subscribe to recording events if event system is available
|
||||
if self.event_system:
|
||||
self._setup_event_subscriptions()
|
||||
|
||||
|
||||
def _ensure_storage_structure(self) -> None:
|
||||
"""Ensure storage directory structure exists"""
|
||||
try:
|
||||
# Create base storage directory
|
||||
Path(self.storage_config.base_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Create camera-specific directories
|
||||
for camera_config in self.config.cameras:
|
||||
Path(camera_config.storage_path).mkdir(parents=True, exist_ok=True)
|
||||
self.logger.debug(f"Ensured storage directory: {camera_config.storage_path}")
|
||||
|
||||
|
||||
self.logger.info("Storage directory structure verified")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error creating storage structure: {e}")
|
||||
raise
|
||||
@@ -66,12 +66,7 @@ class StorageManager:
|
||||
camera_name = event.data.get("camera_name")
|
||||
filename = event.data.get("filename")
|
||||
if camera_name and filename:
|
||||
self.register_recording_file(
|
||||
camera_name=camera_name,
|
||||
filename=filename,
|
||||
start_time=event.timestamp,
|
||||
machine_trigger=event.data.get("machine_trigger")
|
||||
)
|
||||
self.register_recording_file(camera_name=camera_name, filename=filename, start_time=event.timestamp, machine_trigger=event.data.get("machine_trigger"))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling recording started event: {e}")
|
||||
|
||||
@@ -81,64 +76,48 @@ class StorageManager:
|
||||
filename = event.data.get("filename")
|
||||
if filename:
|
||||
file_id = os.path.basename(filename)
|
||||
self.finalize_recording_file(
|
||||
file_id=file_id,
|
||||
end_time=event.timestamp,
|
||||
duration_seconds=event.data.get("duration_seconds")
|
||||
)
|
||||
self.finalize_recording_file(file_id=file_id, end_time=event.timestamp, duration_seconds=event.data.get("duration_seconds"))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error handling recording stopped event: {e}")
|
||||
|
||||
# Subscribe to recording events
|
||||
self.event_system.subscribe(EventType.RECORDING_STARTED, on_recording_started)
|
||||
self.event_system.subscribe(EventType.RECORDING_STOPPED, on_recording_stopped)
|
||||
|
||||
|
||||
def _load_file_index(self) -> Dict[str, Any]:
|
||||
"""Load file index from disk"""
|
||||
try:
|
||||
if os.path.exists(self.file_index_path):
|
||||
with open(self.file_index_path, 'r') as f:
|
||||
with open(self.file_index_path, "r") as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return {"files": {}, "last_updated": None}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading file index: {e}")
|
||||
return {"files": {}, "last_updated": None}
|
||||
|
||||
|
||||
def _save_file_index(self) -> None:
|
||||
"""Save file index to disk"""
|
||||
try:
|
||||
self.file_index["last_updated"] = datetime.now().isoformat()
|
||||
with open(self.file_index_path, 'w') as f:
|
||||
with open(self.file_index_path, "w") as f:
|
||||
json.dump(self.file_index, f, indent=2)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error saving file index: {e}")
|
||||
|
||||
def register_recording_file(self, camera_name: str, filename: str, start_time: datetime,
|
||||
machine_trigger: Optional[str] = None) -> str:
|
||||
|
||||
def register_recording_file(self, camera_name: str, filename: str, start_time: datetime, machine_trigger: Optional[str] = None) -> str:
|
||||
"""Register a new recording file"""
|
||||
try:
|
||||
file_id = os.path.basename(filename)
|
||||
|
||||
file_info = {
|
||||
"camera_name": camera_name,
|
||||
"filename": filename,
|
||||
"file_id": file_id,
|
||||
"start_time": start_time.isoformat(),
|
||||
"end_time": None,
|
||||
"file_size_bytes": None,
|
||||
"duration_seconds": None,
|
||||
"machine_trigger": machine_trigger,
|
||||
"status": "recording",
|
||||
"created_at": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
|
||||
file_info = {"camera_name": camera_name, "filename": filename, "file_id": file_id, "start_time": start_time.isoformat(), "end_time": None, "file_size_bytes": None, "duration_seconds": None, "machine_trigger": machine_trigger, "status": "recording", "created_at": datetime.now().isoformat()}
|
||||
|
||||
self.file_index["files"][file_id] = file_info
|
||||
self._save_file_index()
|
||||
|
||||
|
||||
self.logger.info(f"Registered recording file: {file_id}")
|
||||
return file_id
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error registering recording file: {e}")
|
||||
return ""
|
||||
@@ -169,52 +148,50 @@ class StorageManager:
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finalizing recording file: {e}")
|
||||
return False
|
||||
|
||||
def finalize_recording_file(self, file_id: str, end_time: datetime,
|
||||
duration_seconds: float, frame_count: Optional[int] = None) -> bool:
|
||||
|
||||
def finalize_recording_file(self, file_id: str, end_time: datetime, duration_seconds: float, frame_count: Optional[int] = None) -> bool:
|
||||
"""Finalize a recording file after recording stops"""
|
||||
try:
|
||||
if file_id not in self.file_index["files"]:
|
||||
self.logger.warning(f"File ID not found in index: {file_id}")
|
||||
return False
|
||||
|
||||
|
||||
file_info = self.file_index["files"][file_id]
|
||||
filename = file_info["filename"]
|
||||
|
||||
|
||||
# Update file information
|
||||
file_info["end_time"] = end_time.isoformat()
|
||||
file_info["duration_seconds"] = duration_seconds
|
||||
file_info["status"] = "completed"
|
||||
|
||||
|
||||
# Get file size if file exists
|
||||
if os.path.exists(filename):
|
||||
file_info["file_size_bytes"] = os.path.getsize(filename)
|
||||
|
||||
|
||||
if frame_count is not None:
|
||||
file_info["frame_count"] = frame_count
|
||||
|
||||
|
||||
self._save_file_index()
|
||||
|
||||
|
||||
self.logger.info(f"Finalized recording file: {file_id} (duration: {duration_seconds:.1f}s)")
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error finalizing recording file: {e}")
|
||||
return False
|
||||
|
||||
def get_recording_files(self, camera_name: Optional[str] = None,
|
||||
start_date: Optional[datetime] = None,
|
||||
end_date: Optional[datetime] = None,
|
||||
limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
|
||||
def get_recording_files(self, camera_name: Optional[str] = None, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, limit: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""Get list of recording files with optional filters"""
|
||||
try:
|
||||
files = []
|
||||
|
||||
|
||||
# First, get files from the index (if available)
|
||||
indexed_files = set()
|
||||
for file_id, file_info in self.file_index["files"].items():
|
||||
# Filter by camera name
|
||||
if camera_name and file_info["camera_name"] != camera_name:
|
||||
continue
|
||||
|
||||
|
||||
# Filter by date range
|
||||
if start_date or end_date:
|
||||
file_start = datetime.fromisoformat(file_info["start_time"])
|
||||
@@ -222,88 +199,106 @@ class StorageManager:
|
||||
continue
|
||||
if end_date and file_start > end_date:
|
||||
continue
|
||||
|
||||
|
||||
files.append(file_info.copy())
|
||||
|
||||
indexed_files.add(file_info["filename"])
|
||||
|
||||
# Then, scan filesystem for files not in the index
|
||||
for camera_config in self.config.cameras:
|
||||
# Skip if filtering by camera name and this isn't the one
|
||||
if camera_name and camera_config.name != camera_name:
|
||||
continue
|
||||
|
||||
storage_path = Path(camera_config.storage_path)
|
||||
if storage_path.exists():
|
||||
for video_file in storage_path.glob("*.avi"):
|
||||
if video_file.is_file() and str(video_file) not in indexed_files:
|
||||
# Get file stats
|
||||
stat = video_file.stat()
|
||||
file_mtime = datetime.fromtimestamp(stat.st_mtime)
|
||||
|
||||
# Apply date filters
|
||||
if start_date and file_mtime < start_date:
|
||||
continue
|
||||
if end_date and file_mtime > end_date:
|
||||
continue
|
||||
|
||||
# Create file info for unindexed file
|
||||
file_info = {"camera_name": camera_config.name, "filename": str(video_file), "file_id": video_file.name, "start_time": file_mtime.isoformat(), "end_time": None, "file_size_bytes": stat.st_size, "duration_seconds": None, "machine_trigger": None, "status": "unknown", "created_at": file_mtime.isoformat()} # We don't know if it's completed or not
|
||||
files.append(file_info)
|
||||
|
||||
# Sort by start time (newest first)
|
||||
files.sort(key=lambda x: x["start_time"], reverse=True)
|
||||
|
||||
|
||||
# Apply limit
|
||||
if limit:
|
||||
files = files[:limit]
|
||||
|
||||
|
||||
return files
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting recording files: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def get_storage_statistics(self) -> Dict[str, Any]:
|
||||
"""Get storage usage statistics"""
|
||||
try:
|
||||
stats = {
|
||||
"base_path": self.storage_config.base_path,
|
||||
"total_files": 0,
|
||||
"total_size_bytes": 0,
|
||||
"cameras": {},
|
||||
"disk_usage": {}
|
||||
}
|
||||
|
||||
stats = {"base_path": self.storage_config.base_path, "total_files": 0, "total_size_bytes": 0, "cameras": {}, "disk_usage": {}}
|
||||
|
||||
# Get disk usage for base path
|
||||
if os.path.exists(self.storage_config.base_path):
|
||||
disk_usage = shutil.disk_usage(self.storage_config.base_path)
|
||||
stats["disk_usage"] = {
|
||||
"total_bytes": disk_usage.total,
|
||||
"used_bytes": disk_usage.used,
|
||||
"free_bytes": disk_usage.free,
|
||||
"used_percent": (disk_usage.used / disk_usage.total) * 100
|
||||
}
|
||||
|
||||
# Analyze files by camera
|
||||
stats["disk_usage"] = {"total_bytes": disk_usage.total, "used_bytes": disk_usage.used, "free_bytes": disk_usage.free, "used_percent": (disk_usage.used / disk_usage.total) * 100}
|
||||
|
||||
# Scan actual filesystem for all video files
|
||||
# This ensures we count all files, not just those in the index
|
||||
for camera_config in self.config.cameras:
|
||||
camera_name = camera_config.name
|
||||
storage_path = Path(camera_config.storage_path)
|
||||
|
||||
if camera_name not in stats["cameras"]:
|
||||
stats["cameras"][camera_name] = {"file_count": 0, "total_size_bytes": 0, "total_duration_seconds": 0}
|
||||
|
||||
# Scan for video files in camera directory
|
||||
if storage_path.exists():
|
||||
for video_file in storage_path.glob("*.avi"):
|
||||
if video_file.is_file():
|
||||
stats["total_files"] += 1
|
||||
stats["cameras"][camera_name]["file_count"] += 1
|
||||
|
||||
# Get file size
|
||||
try:
|
||||
file_size = video_file.stat().st_size
|
||||
stats["total_size_bytes"] += file_size
|
||||
stats["cameras"][camera_name]["total_size_bytes"] += file_size
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Could not get size for {video_file}: {e}")
|
||||
|
||||
# Add duration information from index if available
|
||||
for file_info in self.file_index["files"].values():
|
||||
camera_name = file_info["camera_name"]
|
||||
|
||||
if camera_name not in stats["cameras"]:
|
||||
stats["cameras"][camera_name] = {
|
||||
"file_count": 0,
|
||||
"total_size_bytes": 0,
|
||||
"total_duration_seconds": 0
|
||||
}
|
||||
|
||||
stats["total_files"] += 1
|
||||
stats["cameras"][camera_name]["file_count"] += 1
|
||||
|
||||
if file_info.get("file_size_bytes"):
|
||||
size = file_info["file_size_bytes"]
|
||||
stats["total_size_bytes"] += size
|
||||
stats["cameras"][camera_name]["total_size_bytes"] += size
|
||||
|
||||
if file_info.get("duration_seconds"):
|
||||
if camera_name in stats["cameras"] and file_info.get("duration_seconds"):
|
||||
duration = file_info["duration_seconds"]
|
||||
stats["cameras"][camera_name]["total_duration_seconds"] += duration
|
||||
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error getting storage statistics: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def cleanup_old_files(self, max_age_days: Optional[int] = None) -> Dict[str, Any]:
|
||||
"""Clean up old recording files"""
|
||||
if max_age_days is None:
|
||||
max_age_days = self.storage_config.cleanup_older_than_days
|
||||
|
||||
|
||||
cutoff_date = datetime.now() - timedelta(days=max_age_days)
|
||||
|
||||
cleanup_stats = {
|
||||
"files_removed": 0,
|
||||
"bytes_freed": 0,
|
||||
"errors": []
|
||||
}
|
||||
|
||||
|
||||
cleanup_stats = {"files_removed": 0, "bytes_freed": 0, "errors": []}
|
||||
|
||||
try:
|
||||
files_to_remove = []
|
||||
|
||||
|
||||
# Find files older than cutoff date
|
||||
for file_id, file_info in self.file_index["files"].items():
|
||||
try:
|
||||
@@ -312,81 +307,74 @@ class StorageManager:
|
||||
files_to_remove.append((file_id, file_info))
|
||||
except Exception as e:
|
||||
cleanup_stats["errors"].append(f"Error parsing date for {file_id}: {e}")
|
||||
|
||||
|
||||
# Remove old files
|
||||
for file_id, file_info in files_to_remove:
|
||||
try:
|
||||
filename = file_info["filename"]
|
||||
|
||||
|
||||
# Remove physical file
|
||||
if os.path.exists(filename):
|
||||
file_size = os.path.getsize(filename)
|
||||
os.remove(filename)
|
||||
cleanup_stats["bytes_freed"] += file_size
|
||||
self.logger.info(f"Removed old file: {filename}")
|
||||
|
||||
|
||||
# Remove from index
|
||||
del self.file_index["files"][file_id]
|
||||
cleanup_stats["files_removed"] += 1
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error removing file {file_id}: {e}"
|
||||
cleanup_stats["errors"].append(error_msg)
|
||||
self.logger.error(error_msg)
|
||||
|
||||
|
||||
# Save updated index
|
||||
if cleanup_stats["files_removed"] > 0:
|
||||
self._save_file_index()
|
||||
|
||||
self.logger.info(f"Cleanup completed: {cleanup_stats['files_removed']} files removed, "
|
||||
f"{cleanup_stats['bytes_freed']} bytes freed")
|
||||
|
||||
|
||||
self.logger.info(f"Cleanup completed: {cleanup_stats['files_removed']} files removed, " f"{cleanup_stats['bytes_freed']} bytes freed")
|
||||
|
||||
return cleanup_stats
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during cleanup: {e}")
|
||||
cleanup_stats["errors"].append(str(e))
|
||||
return cleanup_stats
|
||||
|
||||
|
||||
def get_file_info(self, file_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get information about a specific file"""
|
||||
return self.file_index["files"].get(file_id)
|
||||
|
||||
|
||||
def delete_file(self, file_id: str) -> bool:
|
||||
"""Delete a specific recording file"""
|
||||
try:
|
||||
if file_id not in self.file_index["files"]:
|
||||
self.logger.warning(f"File ID not found: {file_id}")
|
||||
return False
|
||||
|
||||
|
||||
file_info = self.file_index["files"][file_id]
|
||||
filename = file_info["filename"]
|
||||
|
||||
|
||||
# Remove physical file
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
self.logger.info(f"Deleted file: {filename}")
|
||||
|
||||
|
||||
# Remove from index
|
||||
del self.file_index["files"][file_id]
|
||||
self._save_file_index()
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error deleting file {file_id}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def verify_storage_integrity(self) -> Dict[str, Any]:
|
||||
"""Verify storage integrity and fix issues"""
|
||||
integrity_report = {
|
||||
"total_files_in_index": len(self.file_index["files"]),
|
||||
"missing_files": [],
|
||||
"orphaned_files": [],
|
||||
"corrupted_entries": [],
|
||||
"fixed_issues": 0
|
||||
}
|
||||
|
||||
integrity_report = {"total_files_in_index": len(self.file_index["files"]), "missing_files": [], "orphaned_files": [], "corrupted_entries": [], "fixed_issues": 0}
|
||||
|
||||
try:
|
||||
# Check for missing files (in index but not on disk)
|
||||
for file_id, file_info in list(self.file_index["files"].items()):
|
||||
@@ -396,7 +384,7 @@ class StorageManager:
|
||||
# Remove from index
|
||||
del self.file_index["files"][file_id]
|
||||
integrity_report["fixed_issues"] += 1
|
||||
|
||||
|
||||
# Check for orphaned files (on disk but not in index)
|
||||
for camera_config in self.config.cameras:
|
||||
storage_path = Path(camera_config.storage_path)
|
||||
@@ -405,15 +393,15 @@ class StorageManager:
|
||||
file_id = video_file.name
|
||||
if file_id not in self.file_index["files"]:
|
||||
integrity_report["orphaned_files"].append(str(video_file))
|
||||
|
||||
|
||||
# Save updated index if fixes were made
|
||||
if integrity_report["fixed_issues"] > 0:
|
||||
self._save_file_index()
|
||||
|
||||
|
||||
self.logger.info(f"Storage integrity check completed: {integrity_report['fixed_issues']} issues fixed")
|
||||
|
||||
|
||||
return integrity_report
|
||||
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error during integrity check: {e}")
|
||||
integrity_report["error"] = str(e)
|
||||
|
||||
442
uv.lock
generated
442
uv.lock
generated
@@ -33,6 +33,24 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "appnope"
|
||||
version = "0.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asttokens"
|
||||
version = "3.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.7.14"
|
||||
@@ -42,6 +60,51 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
@@ -111,6 +174,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comm"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "contourpy"
|
||||
version = "1.3.2"
|
||||
@@ -174,6 +246,45 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
version = "1.8.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279, upload-time = "2025-07-15T16:43:29.135Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442, upload-time = "2025-07-15T16:43:36.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215, upload-time = "2025-07-15T16:43:38.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037, upload-time = "2025-07-15T16:43:39.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133, upload-time = "2025-07-15T16:43:40.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/4a/4508d256e52897f5cdfee6a6d7580974811e911c6d01321df3264508a5ac/debugpy-1.8.15-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:3dcc7225cb317469721ab5136cda9ff9c8b6e6fb43e87c9e15d5b108b99d01ba", size = 2511197, upload-time = "2025-07-15T16:43:42.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/8d/7f6ef1097e7fecf26b4ef72338d08e41644a41b7ee958a19f494ffcffc29/debugpy-1.8.15-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:047a493ca93c85ccede1dbbaf4e66816794bdc214213dde41a9a61e42d27f8fc", size = 4229517, upload-time = "2025-07-15T16:43:44.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/e8/e8c6a9aa33a9c9c6dacbf31747384f6ed2adde4de2e9693c766bdf323aa3/debugpy-1.8.15-cp312-cp312-win32.whl", hash = "sha256:b08e9b0bc260cf324c890626961dad4ffd973f7568fbf57feb3c3a65ab6b6327", size = 5276132, upload-time = "2025-07-15T16:43:45.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ad/231050c6177b3476b85fcea01e565dac83607b5233d003ff067e2ee44d8f/debugpy-1.8.15-cp312-cp312-win_amd64.whl", hash = "sha256:e2a4fe357c92334272eb2845fcfcdbec3ef9f22c16cf613c388ac0887aed15fa", size = 5317645, upload-time = "2025-07-15T16:43:46.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/70/2928aad2310726d5920b18ed9f54b9f06df5aa4c10cf9b45fa18ff0ab7e8/debugpy-1.8.15-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:f5e01291ad7d6649aed5773256c5bba7a1a556196300232de1474c3c372592bf", size = 2495538, upload-time = "2025-07-15T16:43:48.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c6/9b8ffb4ca91fac8b2877eef63c9cc0e87dd2570b1120054c272815ec4cd0/debugpy-1.8.15-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dc0f0d00e528d915e0ce1c78e771475b2335b376c49afcc7382ee0b146bab6", size = 4221874, upload-time = "2025-07-15T16:43:50.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/8a/9b8d59674b4bf489318c7c46a1aab58e606e583651438084b7e029bf3c43/debugpy-1.8.15-cp313-cp313-win32.whl", hash = "sha256:fcf0748d4f6e25f89dc5e013d1129ca6f26ad4da405e0723a4f704583896a709", size = 5275949, upload-time = "2025-07-15T16:43:52.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/83/9e58e6fdfa8710a5e6ec06c2401241b9ad48b71c0a7eb99570a1f1edb1d3/debugpy-1.8.15-cp313-cp313-win_amd64.whl", hash = "sha256:73c943776cb83e36baf95e8f7f8da765896fd94b05991e7bc162456d25500683", size = 5317720, upload-time = "2025-07-15T16:43:53.703Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697, upload-time = "2025-07-15T16:44:07.996Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "decorator"
|
||||
version = "5.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.116.1"
|
||||
@@ -252,6 +363,106 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/bd/b394387b598ed84d8d0fa90611a90bee0adc2021820ad5729f7ced74a8e2/imageio-2.37.0-py3-none-any.whl", hash = "sha256:11efa15b87bc7871b61590326b2d635439acc321cf7f8ce996f812543ce10eed", size = 315796, upload-time = "2025-01-20T02:42:34.931Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipykernel"
|
||||
version = "6.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "appnope", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "comm" },
|
||||
{ name = "debugpy" },
|
||||
{ name = "ipython" },
|
||||
{ name = "jupyter-client" },
|
||||
{ name = "jupyter-core" },
|
||||
{ name = "matplotlib-inline" },
|
||||
{ name = "nest-asyncio" },
|
||||
{ name = "packaging" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pyzmq" },
|
||||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/38/27/9e6e30ed92f2ac53d29f70b09da8b2dc456e256148e289678fa0e825f46a/ipykernel-6.30.0.tar.gz", hash = "sha256:b7b808ddb2d261aae2df3a26ff3ff810046e6de3dfbc6f7de8c98ea0a6cb632c", size = 165125, upload-time = "2025-07-21T10:36:09.259Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/3d/00813c3d9b46e3dcd88bd4530e0a3c63c0509e5d8c9eff34723ea243ab04/ipykernel-6.30.0-py3-none-any.whl", hash = "sha256:fd2936e55c4a1c2ee8b1e5fa6a372b8eecc0ab1338750dee76f48fa5cca1301e", size = 117264, upload-time = "2025-07-21T10:36:06.854Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "9.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "decorator" },
|
||||
{ name = "ipython-pygments-lexers" },
|
||||
{ name = "jedi" },
|
||||
{ name = "matplotlib-inline" },
|
||||
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "pygments" },
|
||||
{ name = "stack-data" },
|
||||
{ name = "traitlets" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/80/406f9e3bde1c1fd9bf5a0be9d090f8ae623e401b7670d8f6fdf2ab679891/ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270", size = 4385338, upload-time = "2025-07-01T11:11:30.606Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/f8/0031ee2b906a15a33d6bfc12dd09c3dfa966b3cb5b284ecfb7549e6ac3c4/ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066", size = 611021, upload-time = "2025-07-01T11:11:27.85Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipython-pygments-lexers"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jedi"
|
||||
version = "0.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "parso" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-client"
|
||||
version = "8.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jupyter-core" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pyzmq" },
|
||||
{ name = "tornado" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jupyter-core"
|
||||
version = "5.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kiwisolver"
|
||||
version = "1.4.8"
|
||||
@@ -361,6 +572,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib-inline"
|
||||
version = "0.1.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nest-asyncio"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.2"
|
||||
@@ -477,6 +709,27 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parso"
|
||||
version = "0.8.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pexpect"
|
||||
version = "4.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "ptyprocess" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "11.3.0"
|
||||
@@ -561,6 +814,69 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.51"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptyprocess"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pure-eval"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
@@ -641,6 +957,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.2.3"
|
||||
@@ -671,6 +996,70 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "311"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyzmq"
|
||||
version = "27.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "implementation_name == 'pypy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718, upload-time = "2025-06-13T14:07:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248, upload-time = "2025-06-13T14:07:18.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647, upload-time = "2025-06-13T14:07:19.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600, upload-time = "2025-06-13T14:07:20.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748, upload-time = "2025-06-13T14:07:22.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311, upload-time = "2025-06-13T14:07:23.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630, upload-time = "2025-06-13T14:07:25.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706, upload-time = "2025-06-13T14:07:27.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322, upload-time = "2025-06-13T14:07:28.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435, upload-time = "2025-06-13T14:07:30.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/62/876b27c4ff777db4ceba1c69ea90d3c825bb4f8d5e7cd987ce5802e33c55/pyzmq-27.0.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c36ad534c0c29b4afa088dc53543c525b23c0797e01b69fef59b1a9c0e38b688", size = 1340826, upload-time = "2025-06-13T14:07:46.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/69/58ef8f4f59d3bcd505260c73bee87b008850f45edca40ddaba54273c35f4/pyzmq-27.0.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:67855c14173aec36395d7777aaba3cc527b393821f30143fd20b98e1ff31fd38", size = 897283, upload-time = "2025-06-13T14:07:49.562Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/15/93a0d0396700a60475ad3c5d42c5f1c308d3570bc94626b86c71ef9953e0/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8617c7d43cd8ccdb62aebe984bfed77ca8f036e6c3e46dd3dddda64b10f0ab7a", size = 660567, upload-time = "2025-06-13T14:07:51.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/b3/fe055513e498ca32f64509abae19b9c9eb4d7c829e02bd8997dd51b029eb/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67bfbcbd0a04c575e8103a6061d03e393d9f80ffdb9beb3189261e9e9bc5d5e9", size = 847681, upload-time = "2025-06-13T14:07:52.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/4f/ff15300b00b5b602191f3df06bbc8dd4164e805fdd65bb77ffbb9c5facdc/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5cd11d46d7b7e5958121b3eaf4cd8638eff3a720ec527692132f05a57f14341d", size = 1650148, upload-time = "2025-06-13T14:07:54.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/6f/84bdfff2a224a6f26a24249a342e5906993c50b0761e311e81b39aef52a7/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b801c2e40c5aa6072c2f4876de8dccd100af6d9918d4d0d7aa54a1d982fd4f44", size = 2023768, upload-time = "2025-06-13T14:07:55.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/39/dc2db178c26a42228c5ac94a9cc595030458aa64c8d796a7727947afbf55/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef", size = 1885199, upload-time = "2025-06-13T14:07:57.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/dae7b06a1f8cdee5d8e7a63d99c5d129c401acc40410bef2cbf42025e26f/pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad", size = 575439, upload-time = "2025-06-13T14:07:58.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/bc/1709dc55f0970cf4cb8259e435e6773f9946f41a045c2cb90e870b7072da/pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f", size = 639933, upload-time = "2025-06-13T14:08:00.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948, upload-time = "2025-06-13T14:08:43.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874, upload-time = "2025-06-13T14:08:45.017Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.4"
|
||||
@@ -704,6 +1093,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stack-data"
|
||||
version = "0.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asttokens" },
|
||||
{ name = "executing" },
|
||||
{ name = "pure-eval" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.47.2"
|
||||
@@ -717,6 +1120,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tornado"
|
||||
version = "6.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
@@ -729,6 +1151,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "traitlets"
|
||||
version = "5.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.14.1"
|
||||
@@ -766,6 +1197,7 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "imageio" },
|
||||
{ name = "ipykernel" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "opencv-python" },
|
||||
@@ -782,6 +1214,7 @@ dependencies = [
|
||||
requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.104.0" },
|
||||
{ name = "imageio", specifier = ">=2.37.0" },
|
||||
{ name = "ipykernel", specifier = ">=6.30.0" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.3" },
|
||||
{ name = "numpy", specifier = ">=2.3.2" },
|
||||
{ name = "opencv-python", specifier = ">=4.11.0.86" },
|
||||
@@ -807,6 +1240,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "15.0.1"
|
||||
|
||||
Reference in New Issue
Block a user