feat(collector): add collector service

This commit is contained in:
2025-12-26 18:04:17 +03:00
parent a7e5670d7c
commit cfec8d0ff6
8 changed files with 569 additions and 40 deletions

144
services/collector/main.py Normal file
View 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()