700756c16f
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>
102 lines
2.8 KiB
Python
102 lines
2.8 KiB
Python
"""Tests for ``arautopilot.core.knob_state``."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from arautopilot.core.knob_state import (
|
|
KnobFunction,
|
|
KnobMode,
|
|
KnobState,
|
|
)
|
|
|
|
|
|
def test_idle_state_defaults() -> None:
|
|
s = KnobState.idle()
|
|
assert s.mode is KnobMode.LIBRE
|
|
assert s.function is KnobFunction.NONE
|
|
assert s.pending_value is None
|
|
assert s.armed_at is None
|
|
assert s.timeout_remaining_s == 0.0
|
|
|
|
|
|
def test_libre_state_is_immutable() -> None:
|
|
s = KnobState.idle()
|
|
with pytest.raises((TypeError, ValueError)):
|
|
s.mode = KnobMode.ARMADO # type: ignore[misc]
|
|
|
|
|
|
def test_arm_transitions_to_armado() -> None:
|
|
s = KnobState.idle().arm(KnobFunction.RUMBO, current_value=180.0)
|
|
assert s.mode is KnobMode.ARMADO
|
|
assert s.function is KnobFunction.RUMBO
|
|
assert s.current_value == pytest.approx(180.0)
|
|
assert s.armed_at is not None
|
|
assert s.timeout_remaining_s > 0
|
|
|
|
|
|
def test_arming_with_none_function_rejected() -> None:
|
|
with pytest.raises(ValueError):
|
|
KnobState.idle().arm(KnobFunction.NONE, current_value=0.0)
|
|
|
|
|
|
def test_arming_from_non_libre_rejected() -> None:
|
|
armed = KnobState.idle().arm(KnobFunction.RUMBO, current_value=90.0)
|
|
with pytest.raises(ValueError):
|
|
armed.arm(KnobFunction.BRILLO, current_value=50.0)
|
|
|
|
|
|
def test_propose_then_confirm_round_trip() -> None:
|
|
armed = KnobState.idle().arm(KnobFunction.RUMBO, current_value=180.0)
|
|
pending = armed.propose(185.0)
|
|
assert pending.mode is KnobMode.CONFIRMANDO
|
|
assert pending.pending_value == pytest.approx(185.0)
|
|
assert pending.current_value == pytest.approx(180.0)
|
|
|
|
confirmed = pending.confirm()
|
|
assert confirmed.mode is KnobMode.ARMADO
|
|
assert confirmed.pending_value is None
|
|
assert confirmed.current_value == pytest.approx(185.0)
|
|
|
|
|
|
def test_propose_from_libre_rejected() -> None:
|
|
with pytest.raises(ValueError):
|
|
KnobState.idle().propose(10.0)
|
|
|
|
|
|
def test_confirm_without_pending_rejected() -> None:
|
|
armed = KnobState.idle().arm(KnobFunction.RUMBO, current_value=180.0)
|
|
with pytest.raises(ValueError):
|
|
armed.confirm()
|
|
|
|
|
|
def test_disarm_returns_to_libre() -> None:
|
|
s = (
|
|
KnobState.idle()
|
|
.arm(KnobFunction.RUMBO, current_value=180.0)
|
|
.propose(190.0)
|
|
.disarm()
|
|
)
|
|
assert s == KnobState.idle()
|
|
|
|
|
|
def test_libre_with_armed_at_set_invalid() -> None:
|
|
from datetime import datetime, timezone
|
|
|
|
with pytest.raises(ValueError):
|
|
KnobState(
|
|
mode=KnobMode.LIBRE,
|
|
function=KnobFunction.NONE,
|
|
armed_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
|
|
def test_armado_without_armed_at_invalid() -> None:
|
|
with pytest.raises(ValueError):
|
|
KnobState(
|
|
mode=KnobMode.ARMADO,
|
|
function=KnobFunction.RUMBO,
|
|
current_value=180.0,
|
|
armed_at=None,
|
|
)
|