sprint-0: foundations -- data model, seed library, tests, demo
Initial commit. Delivers what the brief calls 'Sprint 0 - Foundations' (see docs/AR_Autopilot_brief.md section 12): - Complete repository structure (arautopilot package + firmware, display, installer, tools placeholders + docs). - Core data model (Pydantic v2): modes, alarms, actuator config, PID config + gain scheduling, vessel config, knob state machine, project config with YAML/JSON serialisation. - Seed library: 2 actuator profiles (hydraulic & electric DC reversible) and 2 default tunings (yacht motor planeo 30 m and 40 m). Conservative literature values, NOT the integrator's production tuning IP. - Firmware skeleton: only src/hal/pinout.h with the 21 I/O contract for the AR-NMEA-IO v1.0 board. No drivers, no main loop. - Studio stubs (real PySide6 app starts in Sprint 4). - pytest suite (80 tests, all green): modes, alarms, actuator, PID (incl. gain interpolation and the +/-50% adaptive bound from brief section 6), vessel, knob state, project config, library loader, end-to-end roundtrip. - examples/sprint0_demo.py - the acceptance demo from the brief. Acceptance criteria met: - pytest green (80/80) - demo creates, saves (YAML + JSON), reloads, and verifies a full ProjectConfig using the seed library - repository ready for tag `sprint-0-approved` See CHANGELOG.md for the detailed scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
"""Alarm types, severities, and the ``Alarm`` runtime record.
|
||||
|
||||
The catalogue mirrors section 7 of the brief. Each ``AlarmType`` has a
|
||||
canonical default ``AlarmSeverity`` and a hard-coded flag stating whether
|
||||
that alarm, when fired, should also trigger automatic disengagement of
|
||||
the autopilot (return to STANDBY).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class AlarmSeverity(str, Enum):
|
||||
"""IEC-style four-level severity scheme."""
|
||||
|
||||
EMERGENCY = "emergency"
|
||||
"""Immediate danger; loudest annunciation, blocking modal on display."""
|
||||
|
||||
HIGH = "high"
|
||||
"""Operator must act within a few seconds."""
|
||||
|
||||
LOW = "low"
|
||||
"""Warning; degraded operation but no immediate danger."""
|
||||
|
||||
INFO = "info"
|
||||
"""Informational only; ack with one tap."""
|
||||
|
||||
|
||||
class AlarmType(str, Enum):
|
||||
"""The fixed catalogue of alarms emitted by the firmware.
|
||||
|
||||
Adding an entry here is a deliberate, reviewed change — the firmware
|
||||
state machine and the display strings must be updated in lockstep.
|
||||
"""
|
||||
|
||||
OFF_COURSE = "off_course"
|
||||
OFF_COURSE_SEVERE = "off_course_severe"
|
||||
RUDDER_NOT_RESPONDING = "rudder_not_responding"
|
||||
HEADING_SENSOR_LOST = "heading_sensor_lost"
|
||||
ACTUATOR_OVERCURRENT = "actuator_overcurrent"
|
||||
VOLTAGE_LOW = "voltage_low"
|
||||
LIMIT_SWITCH_REACHED = "limit_switch_reached"
|
||||
WATCHDOG_TRIPPED = "watchdog_tripped"
|
||||
VMS_CRITICAL = "vms_critical"
|
||||
|
||||
|
||||
# Canonical metadata per alarm type. Source of truth referenced by both
|
||||
# the firmware (via code-generated header) and the Studio (via the GUI).
|
||||
_CATALOGUE: dict[AlarmType, tuple[AlarmSeverity, bool, str]] = {
|
||||
AlarmType.OFF_COURSE: (AlarmSeverity.LOW, False, "Heading deviates from setpoint beyond threshold"),
|
||||
AlarmType.OFF_COURSE_SEVERE: (AlarmSeverity.EMERGENCY, True, "Heading deviation >30 deg for >5 s — auto-disengage"),
|
||||
AlarmType.RUDDER_NOT_RESPONDING:(AlarmSeverity.EMERGENCY, True, "Rudder command sent but no feedback motion — auto-disengage"),
|
||||
AlarmType.HEADING_SENSOR_LOST: (AlarmSeverity.EMERGENCY, True, "NMEA 2000 PGN 127250 not received for >5 s — auto-disengage"),
|
||||
AlarmType.ACTUATOR_OVERCURRENT: (AlarmSeverity.HIGH, True, "Actuator current exceeded configured limit"),
|
||||
AlarmType.VOLTAGE_LOW: (AlarmSeverity.HIGH, True, "Supply voltage below safe operating threshold"),
|
||||
AlarmType.LIMIT_SWITCH_REACHED: (AlarmSeverity.LOW, False, "Rudder reached mechanical end-stop"),
|
||||
AlarmType.WATCHDOG_TRIPPED: (AlarmSeverity.EMERGENCY, True, "Firmware watchdog fired — controller reset to STANDBY"),
|
||||
AlarmType.VMS_CRITICAL: (AlarmSeverity.EMERGENCY, True, "VMS reported blackout or critical electrical overload"),
|
||||
}
|
||||
|
||||
|
||||
def default_severity(alarm_type: AlarmType) -> AlarmSeverity:
|
||||
"""Return the canonical severity for ``alarm_type``."""
|
||||
return _CATALOGUE[alarm_type][0]
|
||||
|
||||
|
||||
def triggers_auto_disengage(alarm_type: AlarmType) -> bool:
|
||||
"""Return ``True`` iff this alarm forces the pilot into STANDBY."""
|
||||
return _CATALOGUE[alarm_type][1]
|
||||
|
||||
|
||||
def default_message(alarm_type: AlarmType) -> str:
|
||||
"""Return the canonical default human-readable description."""
|
||||
return _CATALOGUE[alarm_type][2]
|
||||
|
||||
|
||||
class Alarm(BaseModel):
|
||||
"""A single alarm event raised by the firmware at runtime.
|
||||
|
||||
Persisted to the immutable audit log (brief section 14, rule #14).
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(frozen=True, extra="forbid")
|
||||
|
||||
type: AlarmType
|
||||
severity: AlarmSeverity
|
||||
message: str = Field(min_length=1, max_length=240)
|
||||
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
source: str = Field(
|
||||
default="firmware",
|
||||
description="Subsystem that raised the alarm (e.g. 'firmware', 'display', 'vms').",
|
||||
max_length=64,
|
||||
)
|
||||
acknowledged: bool = False
|
||||
auto_disengage_triggered: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_type(
|
||||
cls,
|
||||
alarm_type: AlarmType,
|
||||
*,
|
||||
message: str | None = None,
|
||||
source: str = "firmware",
|
||||
) -> "Alarm":
|
||||
"""Convenience constructor using catalogue defaults."""
|
||||
return cls(
|
||||
type=alarm_type,
|
||||
severity=default_severity(alarm_type),
|
||||
message=message or default_message(alarm_type),
|
||||
source=source,
|
||||
auto_disengage_triggered=triggers_auto_disengage(alarm_type),
|
||||
)
|
||||
Reference in New Issue
Block a user