sprint-5: NMEA 2000 stub + Log Book naval básico
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>
This commit is contained in:
@@ -145,6 +145,69 @@ def create_app(runtime: RuntimeApp) -> FastAPI:
|
||||
raise HTTPException(status_code=404, detail=f"Alarma '{alarm_id}' no activa.")
|
||||
return acked.model_dump(mode="json")
|
||||
|
||||
# ----- Log Book ----------------------------------------------------
|
||||
|
||||
@app.get("/logbook")
|
||||
def logbook_entries(
|
||||
kind: str | None = None,
|
||||
since: str | None = None,
|
||||
until: str | None = None,
|
||||
limit: int = 200,
|
||||
) -> list[dict[str, Any]]:
|
||||
from vmssailor.runtime.server.logbook import LogEntryKind
|
||||
|
||||
kind_enum = LogEntryKind(kind) if kind else None
|
||||
since_dt = datetime.fromisoformat(since) if since else None
|
||||
until_dt = datetime.fromisoformat(until) if until else None
|
||||
entries = runtime.logbook.query(
|
||||
kind=kind_enum, since=since_dt, until=until_dt, limit=limit
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": e.id,
|
||||
"kind": e.kind.value,
|
||||
"timestamp": e.timestamp.isoformat(),
|
||||
"summary": e.summary,
|
||||
"payload": e.payload,
|
||||
"user": e.user,
|
||||
"hash": e.hash[:16] + "…",
|
||||
}
|
||||
for e in entries
|
||||
]
|
||||
|
||||
@app.post("/logbook")
|
||||
def logbook_append_manual(
|
||||
kind: str = "manual",
|
||||
summary: str = "",
|
||||
user: str = "operator",
|
||||
) -> dict[str, Any]:
|
||||
import asyncio
|
||||
|
||||
from vmssailor.runtime.server.logbook import LogEntryKind
|
||||
|
||||
try:
|
||||
kind_enum = LogEntryKind(kind)
|
||||
except ValueError as err:
|
||||
raise HTTPException(status_code=400, detail=f"Kind inválido: {kind}") from err
|
||||
if not summary:
|
||||
raise HTTPException(status_code=400, detail="summary requerido")
|
||||
|
||||
async def _do_append():
|
||||
return await runtime.logbook.append(kind_enum, summary, user=user)
|
||||
|
||||
entry = asyncio.run_coroutine_threadsafe(_do_append(), asyncio.get_event_loop()).result(timeout=2)
|
||||
return {
|
||||
"id": entry.id,
|
||||
"kind": entry.kind.value,
|
||||
"timestamp": entry.timestamp.isoformat(),
|
||||
"hash": entry.hash,
|
||||
}
|
||||
|
||||
@app.get("/logbook/verify")
|
||||
def logbook_verify() -> dict[str, Any]:
|
||||
ok, broken = runtime.logbook.verify_chain()
|
||||
return {"ok": ok, "broken_entry_ids": broken}
|
||||
|
||||
# ----- WebSocket ---------------------------------------------------
|
||||
|
||||
@app.websocket("/ws/realtime")
|
||||
|
||||
Reference in New Issue
Block a user