sprint-5: True Course + Track Keeping + XTE + PGN 129026/129284
- Python: NmeaNavData (COG/SOG/XTE data models with staleness tracking) - Python: TrueCoursePilot with TRUE_COURSE and TRACK_KEEPING modes - Python: 26 new tests (test_nmea_data, test_true_course) - Modbus: COG/SOG/XTE input registers + TC setpoint/XTE-gain holdings - Firmware: nmea2000_consumer handles PGN 129026 + 129284 - Firmware: pid_outer_task wired for TC + TK modes with live SOG scheduling - YAML regenerated; 284 tests pass, firmware compiles clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""Tests for arautopilot.core.nmea_data -- Sprint 5."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from arautopilot.core.nmea_data import CogSogData, HeadingData, NmeaNavData, XteData
|
||||
|
||||
|
||||
class TestCogSogData:
|
||||
def test_initial_state_invalid(self):
|
||||
d = CogSogData()
|
||||
assert not d.is_valid
|
||||
assert math.isnan(d.cog_deg)
|
||||
assert math.isnan(d.sog_kn)
|
||||
|
||||
def test_update_marks_valid(self):
|
||||
d = CogSogData()
|
||||
d.update(270.0, 8.5)
|
||||
assert d.is_valid
|
||||
assert d.cog_deg == pytest.approx(270.0)
|
||||
assert d.sog_kn == pytest.approx(8.5)
|
||||
|
||||
def test_cog_wraps_360(self):
|
||||
d = CogSogData()
|
||||
d.update(370.0, 5.0)
|
||||
assert d.cog_deg == pytest.approx(10.0)
|
||||
|
||||
def test_cog_zero_stays_zero(self):
|
||||
d = CogSogData()
|
||||
d.update(0.0, 5.0)
|
||||
assert d.cog_deg == pytest.approx(0.0)
|
||||
|
||||
def test_stale_after_max_age(self):
|
||||
d = CogSogData(max_age_s=1.0)
|
||||
d.update(90.0, 6.0)
|
||||
assert d.is_valid
|
||||
with patch("arautopilot.core.nmea_data.time") as mock_time:
|
||||
mock_time.monotonic.return_value = d.timestamp + 2.0
|
||||
assert not d.is_valid
|
||||
|
||||
def test_age_ms(self):
|
||||
d = CogSogData()
|
||||
d.update(180.0, 10.0)
|
||||
assert d.age_ms >= 0
|
||||
|
||||
|
||||
class TestXteData:
|
||||
def test_initial_invalid(self):
|
||||
d = XteData()
|
||||
assert not d.is_valid
|
||||
assert math.isnan(d.xte_m)
|
||||
|
||||
def test_update_positive_xte(self):
|
||||
"""Positive XTE = vessel to starboard of track."""
|
||||
d = XteData()
|
||||
d.update(xte_m=5.0, dtw_m=200.0, waypoint_name="WP01")
|
||||
assert d.is_valid
|
||||
assert d.xte_m == pytest.approx(5.0)
|
||||
assert d.dtw_m == pytest.approx(200.0)
|
||||
assert d.waypoint_name == "WP01"
|
||||
|
||||
def test_update_negative_xte(self):
|
||||
d = XteData()
|
||||
d.update(xte_m=-3.5)
|
||||
assert d.xte_m == pytest.approx(-3.5)
|
||||
|
||||
def test_stale_after_max_age(self):
|
||||
d = XteData(max_age_s=2.0)
|
||||
d.update(1.0)
|
||||
with patch("arautopilot.core.nmea_data.time") as mock_time:
|
||||
mock_time.monotonic.return_value = d.timestamp + 3.0
|
||||
assert not d.is_valid
|
||||
|
||||
def test_update_without_optional_fields(self):
|
||||
d = XteData()
|
||||
d.update(xte_m=0.0)
|
||||
assert d.is_valid
|
||||
assert math.isnan(d.dtw_m)
|
||||
assert d.waypoint_name == ""
|
||||
|
||||
|
||||
class TestNmeaNavData:
|
||||
def test_aggregate_has_all_fields(self):
|
||||
nav = NmeaNavData()
|
||||
assert isinstance(nav.heading, HeadingData)
|
||||
assert isinstance(nav.cog_sog, CogSogData)
|
||||
assert isinstance(nav.xte, XteData)
|
||||
|
||||
def test_independent_updates(self):
|
||||
nav = NmeaNavData()
|
||||
nav.cog_sog.update(45.0, 7.0)
|
||||
nav.xte.update(2.5)
|
||||
assert nav.cog_sog.is_valid
|
||||
assert nav.xte.is_valid
|
||||
assert not nav.heading.is_valid
|
||||
Reference in New Issue
Block a user