df56f52091
vmssailor/runtime/server/nmea2000.py
- PgnFrame dataclass + parsers/encoders para PGNs clave:
- 127257 Attitude (yaw/pitch/roll en radianes)
- 127488 Engine Parameters Rapid (RPM, boost, trim)
- 127489 Engine Dynamic (oil, coolant, alternator, hours)
- 127505 Fluid Level (tanks)
- 127508 Battery Status
- 129025 Position Rapid
- N2KPublisher: tag updates -> PGNs publicados via transport callable
- Sprint 5: callable de testing/observabilidad
- Sprint 12+: integracion real con python-can / Actisense / YDWG-02
- N2KSubscriber: inyecta PGNs entrantes y los traduce a tag updates
- PGN_ATTITUDE -> VESSEL.ROLL_DEG / VESSEL.PITCH_DEG (PGN 127257 = AR-ECDIS source)
- PGN_ENGINE_RAPID -> ME_INST_{N}.RPM
vmssailor/runtime/server/logbook.py
- LogBook con cadena SHA-256 inmutable (prev_hash -> hash chain)
- LogEntryKind: ENGINE_START, ENGINE_STOP, ALARM_ACTIVE/ACK/CLEARED,
AUTHORITY_TRANSFER, OVERRIDE, SNAPSHOT, MANUAL
- verify_chain() detecta tampering (bit flip o reescritura)
- query() con filtros por kind/since/until/limit
- EngineLogWriter: auto-detecta start/stop via cruces de umbral RPM
con persistencia configurable (default 5s)
- SnapshotLogWriter: snapshots periodicos (default 15 min) condicional
a motor corriendo
API actualizada:
- GET /logbook con filtros
- POST /logbook (entrada manual)
- GET /logbook/verify (integridad de cadena)
RuntimeApp integra todos los servicios. El alarm engine ahora
auto-anota al log book via callback con strong-ref task tracking.
Tests (tests/runtime/, 10 nuevos, total 152/152):
- test_logbook: append, chain, tamper detection, engine start/stop,
snapshot writer con/sin motor corriendo
- test_nmea2000: PGN encode/decode roundtrip, publisher emits engine
rapid frames, subscriber handles attitude
152/152 pytest verde, ruff clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.6 KiB
Python
139 lines
4.6 KiB
Python
"""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,
|
|
)
|