Refactor: done phase 5
This commit is contained in:
parent
5176f99ba7
commit
476f19cabe
3 changed files with 740 additions and 109 deletions
282
core/storage/license_plate.py
Normal file
282
core/storage/license_plate.py
Normal file
|
@ -0,0 +1,282 @@
|
|||
"""
|
||||
License Plate Manager Module.
|
||||
Handles Redis subscription to license plate results from LPR service.
|
||||
"""
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Dict, Optional, Any, Callable
|
||||
import redis.asyncio as redis
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LicensePlateManager:
|
||||
"""
|
||||
Manages license plate result subscription from Redis channel.
|
||||
Subscribes to 'license_results' channel for license plate data from LPR service.
|
||||
"""
|
||||
|
||||
def __init__(self, redis_config: Dict[str, Any]):
|
||||
"""
|
||||
Initialize license plate manager with Redis configuration.
|
||||
|
||||
Args:
|
||||
redis_config: Redis configuration dictionary
|
||||
"""
|
||||
self.config = redis_config
|
||||
self.redis_client: Optional[redis.Redis] = None
|
||||
self.pubsub = None
|
||||
self.subscription_task = None
|
||||
self.callback = None
|
||||
|
||||
# Connection parameters
|
||||
self.host = redis_config.get('host', 'localhost')
|
||||
self.port = redis_config.get('port', 6379)
|
||||
self.password = redis_config.get('password')
|
||||
self.db = redis_config.get('db', 0)
|
||||
|
||||
# License plate data cache - store recent results by session_id
|
||||
self.license_plate_cache: Dict[str, Dict[str, Any]] = {}
|
||||
self.cache_ttl = 300 # 5 minutes TTL for cached results
|
||||
|
||||
logger.info(f"LicensePlateManager initialized for {self.host}:{self.port}")
|
||||
|
||||
async def initialize(self, callback: Optional[Callable] = None) -> bool:
|
||||
"""
|
||||
Initialize Redis connection and start subscription to license_results channel.
|
||||
|
||||
Args:
|
||||
callback: Optional callback function for processing license plate results
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Create Redis connection
|
||||
self.redis_client = redis.Redis(
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
password=self.password,
|
||||
db=self.db,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
# Test connection
|
||||
await self.redis_client.ping()
|
||||
logger.info(f"Connected to Redis for license plate subscription")
|
||||
|
||||
# Set callback
|
||||
self.callback = callback
|
||||
|
||||
# Start subscription
|
||||
await self._start_subscription()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to initialize license plate manager: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def _start_subscription(self):
|
||||
"""Start Redis subscription to license_results channel."""
|
||||
try:
|
||||
if not self.redis_client:
|
||||
logger.error("Redis client not initialized")
|
||||
return
|
||||
|
||||
# Create pubsub and subscribe
|
||||
self.pubsub = self.redis_client.pubsub()
|
||||
await self.pubsub.subscribe('license_results')
|
||||
|
||||
logger.info("Subscribed to Redis channel: license_results")
|
||||
|
||||
# Start listening task
|
||||
self.subscription_task = asyncio.create_task(self._listen_for_messages())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting license plate subscription: {e}", exc_info=True)
|
||||
|
||||
async def _listen_for_messages(self):
|
||||
"""Listen for messages on the license_results channel."""
|
||||
try:
|
||||
if not self.pubsub:
|
||||
return
|
||||
|
||||
async for message in self.pubsub.listen():
|
||||
if message['type'] == 'message':
|
||||
try:
|
||||
# Log the raw message from Redis channel
|
||||
logger.info(f"[LICENSE PLATE RAW] Received from 'license_results' channel: {message['data']}")
|
||||
|
||||
# Parse the license plate result message
|
||||
data = json.loads(message['data'])
|
||||
logger.info(f"[LICENSE PLATE PARSED] Parsed JSON data: {data}")
|
||||
await self._process_license_plate_result(data)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"[LICENSE PLATE ERROR] Invalid JSON in license plate message: {e}")
|
||||
logger.error(f"[LICENSE PLATE ERROR] Raw message was: {message['data']}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing license plate message: {e}", exc_info=True)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("License plate subscription task cancelled")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in license plate message listener: {e}", exc_info=True)
|
||||
|
||||
async def _process_license_plate_result(self, data: Dict[str, Any]):
|
||||
"""
|
||||
Process incoming license plate result from LPR service.
|
||||
|
||||
Expected message format (from actual LPR service):
|
||||
{
|
||||
"session_id": "511",
|
||||
"license_character": "ข3184"
|
||||
}
|
||||
or
|
||||
{
|
||||
"session_id": "508",
|
||||
"display_id": "test3",
|
||||
"license_plate_text": "ABC-123",
|
||||
"confidence": 0.95,
|
||||
"timestamp": "2025-09-24T21:10:00Z"
|
||||
}
|
||||
|
||||
Args:
|
||||
data: License plate result data
|
||||
"""
|
||||
try:
|
||||
session_id = data.get('session_id')
|
||||
if not session_id:
|
||||
logger.warning("License plate result missing session_id")
|
||||
return
|
||||
|
||||
# Handle different message formats
|
||||
# Format 1: {"session_id": "511", "license_character": "ข3184"}
|
||||
# Format 2: {"session_id": "508", "license_plate_text": "ABC-123", "confidence": 0.95, ...}
|
||||
license_plate_text = data.get('license_plate_text') or data.get('license_character')
|
||||
confidence = data.get('confidence', 1.0) # Default confidence for LPR service results
|
||||
display_id = data.get('display_id', '')
|
||||
timestamp = data.get('timestamp', '')
|
||||
|
||||
logger.info(f"[LICENSE PLATE] Received result for session {session_id}: "
|
||||
f"text='{license_plate_text}', confidence={confidence:.3f}")
|
||||
|
||||
# Store in cache
|
||||
self.license_plate_cache[session_id] = {
|
||||
'license_plate_text': license_plate_text,
|
||||
'confidence': confidence,
|
||||
'display_id': display_id,
|
||||
'timestamp': timestamp,
|
||||
'received_at': asyncio.get_event_loop().time()
|
||||
}
|
||||
|
||||
# Call callback if provided
|
||||
if self.callback:
|
||||
await self.callback(session_id, {
|
||||
'license_plate_text': license_plate_text,
|
||||
'confidence': confidence,
|
||||
'display_id': display_id,
|
||||
'timestamp': timestamp
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing license plate result: {e}", exc_info=True)
|
||||
|
||||
def get_license_plate_result(self, session_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Get cached license plate result for a session.
|
||||
|
||||
Args:
|
||||
session_id: Session identifier
|
||||
|
||||
Returns:
|
||||
License plate result dictionary or None if not found
|
||||
"""
|
||||
if session_id not in self.license_plate_cache:
|
||||
return None
|
||||
|
||||
result = self.license_plate_cache[session_id]
|
||||
|
||||
# Check TTL
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
if current_time - result.get('received_at', 0) > self.cache_ttl:
|
||||
# Expired, remove from cache
|
||||
del self.license_plate_cache[session_id]
|
||||
return None
|
||||
|
||||
return {
|
||||
'license_plate_text': result.get('license_plate_text'),
|
||||
'confidence': result.get('confidence'),
|
||||
'display_id': result.get('display_id'),
|
||||
'timestamp': result.get('timestamp')
|
||||
}
|
||||
|
||||
def cleanup_expired_results(self):
|
||||
"""Remove expired license plate results from cache."""
|
||||
try:
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
expired_sessions = []
|
||||
|
||||
for session_id, result in self.license_plate_cache.items():
|
||||
if current_time - result.get('received_at', 0) > self.cache_ttl:
|
||||
expired_sessions.append(session_id)
|
||||
|
||||
for session_id in expired_sessions:
|
||||
del self.license_plate_cache[session_id]
|
||||
logger.debug(f"Removed expired license plate result for session {session_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up expired license plate results: {e}", exc_info=True)
|
||||
|
||||
async def close(self):
|
||||
"""Close Redis connection and cleanup resources."""
|
||||
try:
|
||||
# Cancel subscription task first
|
||||
if self.subscription_task and not self.subscription_task.done():
|
||||
self.subscription_task.cancel()
|
||||
try:
|
||||
await self.subscription_task
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("License plate subscription task cancelled successfully")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error waiting for subscription task cancellation: {e}")
|
||||
|
||||
# Close pubsub connection properly
|
||||
if self.pubsub:
|
||||
try:
|
||||
# First unsubscribe from channels
|
||||
await self.pubsub.unsubscribe('license_results')
|
||||
# Then close the pubsub connection
|
||||
await self.pubsub.aclose()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing pubsub connection: {e}")
|
||||
finally:
|
||||
self.pubsub = None
|
||||
|
||||
# Close Redis connection
|
||||
if self.redis_client:
|
||||
try:
|
||||
await self.redis_client.aclose()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error closing Redis connection: {e}")
|
||||
finally:
|
||||
self.redis_client = None
|
||||
|
||||
# Clear cache
|
||||
self.license_plate_cache.clear()
|
||||
|
||||
logger.info("License plate manager closed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing license plate manager: {e}", exc_info=True)
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get license plate manager statistics."""
|
||||
return {
|
||||
'cached_results': len(self.license_plate_cache),
|
||||
'connected': self.redis_client is not None,
|
||||
'subscribed': self.pubsub is not None,
|
||||
'host': self.host,
|
||||
'port': self.port
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue