feat(collector): add collector service
This commit is contained in:
144
services/collector/main.py
Normal file
144
services/collector/main.py
Normal file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FR-2: Audio Data Collector Service
|
||||
Reads audio metrics from STM32, validates, and writes to TimescaleDB
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from serial_reader import SerialReader
|
||||
from audio_validator import AudioValidator
|
||||
from db_writer import DatabaseWriter
|
||||
from protocol_parser import AudioMetrics
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CollectorService:
|
||||
"""Main collector service orchestrating serial reading and database writing"""
|
||||
|
||||
def __init__(self, serial_port: str, db_url: str, baudrate: int = 115200):
|
||||
self.serial_reader = SerialReader(
|
||||
port=serial_port, baudrate=baudrate, on_packet=self._handle_packet
|
||||
)
|
||||
self.db_writer = DatabaseWriter(db_url=db_url)
|
||||
self.validator = AudioValidator()
|
||||
|
||||
self._shutdown_event = asyncio.Event()
|
||||
|
||||
async def _handle_packet(self, packet: AudioMetrics):
|
||||
"""
|
||||
Process received audio packet: validate and write to database.
|
||||
|
||||
Args:
|
||||
packet: Parsed audio metrics packet
|
||||
"""
|
||||
# Validate packet
|
||||
validation = self.validator.validate_packet(packet.rms_db, packet.freq_hz)
|
||||
|
||||
if not validation.valid:
|
||||
logger.warning(
|
||||
f"Invalid packet: {validation.error} "
|
||||
f"(rms={packet.rms_db:.1f}dB freq={packet.freq_hz}Hz)"
|
||||
)
|
||||
return
|
||||
|
||||
# Write to database
|
||||
try:
|
||||
await self.db_writer.add_record(
|
||||
timestamp_ms=packet.timestamp_ms,
|
||||
rms_db=packet.rms_db,
|
||||
freq_hz=packet.freq_hz,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add record to database: {e}")
|
||||
|
||||
async def start(self):
|
||||
"""Start collector service"""
|
||||
logger.info("Starting Audio Data Collector Service")
|
||||
|
||||
try:
|
||||
# Connect to database
|
||||
await self.db_writer.connect()
|
||||
await self.db_writer.start_auto_flush()
|
||||
|
||||
# Connect to serial port
|
||||
await self.serial_reader.connect()
|
||||
await self.serial_reader.start_reading()
|
||||
|
||||
logger.info("Service started successfully")
|
||||
|
||||
# Wait for shutdown signal
|
||||
await self._shutdown_event.wait()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Service startup failed: {e}")
|
||||
raise
|
||||
finally:
|
||||
await self.stop()
|
||||
|
||||
async def stop(self):
|
||||
"""Stop collector service gracefully"""
|
||||
logger.info("Stopping Audio Data Collector Service")
|
||||
|
||||
# Disconnect serial reader
|
||||
await self.serial_reader.disconnect()
|
||||
|
||||
# Close database writer (flushes remaining data)
|
||||
await self.db_writer.close()
|
||||
|
||||
logger.info("Service stopped")
|
||||
|
||||
def shutdown(self):
|
||||
"""Trigger graceful shutdown"""
|
||||
logger.info("Shutdown requested")
|
||||
self._shutdown_event.set()
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
# Read configuration from environment
|
||||
SERIAL_PORT = os.getenv("SERIAL_PORT", "/dev/ttyACM0")
|
||||
BAUDRATE = int(os.getenv("BAUDRATE", "115200"))
|
||||
DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
DB_PORT = os.getenv("DB_PORT", "5432")
|
||||
DB_NAME = os.getenv("DB_NAME", "audio_analyzer")
|
||||
DB_USER = os.getenv("DB_USER", "postgres")
|
||||
DB_PASSWORD = os.getenv("DB_PASSWORD", "postgres")
|
||||
|
||||
db_url = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
|
||||
|
||||
# Create service
|
||||
service = CollectorService(
|
||||
serial_port=SERIAL_PORT, db_url=db_url, baudrate=BAUDRATE
|
||||
)
|
||||
|
||||
# Setup signal handlers for graceful shutdown
|
||||
loop = asyncio.get_event_loop()
|
||||
for sig in (signal.SIGTERM, signal.SIGINT):
|
||||
loop.add_signal_handler(sig, service.shutdown)
|
||||
|
||||
try:
|
||||
# Run service
|
||||
loop.run_until_complete(service.start())
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Interrupted by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Service error: {e}")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user