0f00ad10da
- 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>
101 lines
2.9 KiB
Python
101 lines
2.9 KiB
Python
"""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
|