"""Orquestador del Runtime server: ensambla servicios. Sprint 4: tag_store + historian + alarm engine + simulator + API. Sprint 5: + log book (engine_log_writer + snapshot_writer) + nmea2000 stub. """ from __future__ import annotations import logging from dataclasses import dataclass, field from pathlib import Path from vmssailor.core.alarm import Alarm from vmssailor.core.enums import AlarmState from vmssailor.core.project import Project from vmssailor.runtime.server.alarm_engine import AlarmEngine from vmssailor.runtime.server.drivers import SimulatorDriver from vmssailor.runtime.server.historian import Historian from vmssailor.runtime.server.logbook import ( EngineLogWriter, LogBook, LogEntryKind, SnapshotLogWriter, ) from vmssailor.runtime.server.nmea2000 import N2KPublisher, N2KSubscriber from vmssailor.runtime.server.tag_store import TagStore logger = logging.getLogger(__name__) @dataclass(slots=True) class RuntimeApp: """Conjunto de servicios del Runtime.""" project: Project tag_store: TagStore historian: Historian alarm_engine: AlarmEngine simulator: SimulatorDriver logbook: LogBook engine_log_writer: EngineLogWriter snapshot_writer: SnapshotLogWriter n2k_publisher: N2KPublisher n2k_subscriber: N2KSubscriber alarm_events: list[Alarm] = field(default_factory=list) async def start(self) -> None: await self.historian.start(self.tag_store) await self.alarm_engine.start() await self.simulator.start() await self.engine_log_writer.start() await self.snapshot_writer.start() await self.n2k_publisher.start() await self.n2k_subscriber.start() logger.info("RuntimeApp started — %d tags", len(self.tag_store)) async def stop(self) -> None: await self.n2k_subscriber.stop() await self.n2k_publisher.stop() await self.snapshot_writer.stop() await self.engine_log_writer.stop() await self.simulator.stop() await self.alarm_engine.stop() await self.historian.stop() self.logbook.close() logger.info("RuntimeApp stopped") def build_runtime( project: Project, *, historian_db: Path | str | None = None, logbook_db: Path | str | None = None, simulator_tick_s: float = 0.5, snapshot_period_s: float = 900.0, ) -> RuntimeApp: """Construye un Runtime listo para usar (no arrancado todavía).""" tag_store = TagStore() tag_store.register_many(project.tags) historian = Historian(historian_db) logbook = LogBook(logbook_db) alarm_events: list[Alarm] = [] # Strong refs para tareas async que persisten más allá del callback _alarm_log_tasks: set = set() def _on_alarm_event(alarm: Alarm) -> None: alarm_events.append(alarm) # Auto-log al log book (fire and forget) import asyncio kind_map = { AlarmState.ACTIVE: LogEntryKind.ALARM_ACTIVE, AlarmState.ACK: LogEntryKind.ALARM_ACK, AlarmState.CLEARED: LogEntryKind.ALARM_CLEARED, } kind = kind_map.get(alarm.state, LogEntryKind.ALARM_ACTIVE) try: loop = asyncio.get_running_loop() task = loop.create_task( logbook.append( kind, f"[{alarm.priority.value}] {alarm.tag_id}: {alarm.message}", payload={"alarm_id": alarm.id, "value": alarm.value_at_trigger}, user=alarm.acknowledged_by or "system", timestamp=alarm.timestamp_ack or alarm.timestamp_active, ) ) _alarm_log_tasks.add(task) task.add_done_callback(_alarm_log_tasks.discard) except RuntimeError: # No event loop — silencioso (puede ocurrir en teardown) pass alarm_engine = AlarmEngine(tag_store, on_alarm_event=_on_alarm_event) simulator = SimulatorDriver(tag_store, tick_period_s=simulator_tick_s) engine_log_writer = EngineLogWriter(tag_store, logbook) snapshot_writer = SnapshotLogWriter( tag_store, logbook, period_s=snapshot_period_s ) n2k_publisher = N2KPublisher(tag_store) n2k_subscriber = N2KSubscriber(tag_store) return RuntimeApp( project=project, tag_store=tag_store, historian=historian, alarm_engine=alarm_engine, simulator=simulator, logbook=logbook, engine_log_writer=engine_log_writer, snapshot_writer=snapshot_writer, n2k_publisher=n2k_publisher, n2k_subscriber=n2k_subscriber, alarm_events=alarm_events, )