e4812e9b44
Cherry-pick of ab28cb7 onto main (was missing from main branch history). Python: - arautopilot/core/sensor_config.py: RudderSensorConfig + DualRudderSensorConfig (AS5048A SPI / potentiometer, dual with cross-validation thresholds) - arautopilot/core/vessel_config.py: add sensors: DualRudderSensorConfig field - arautopilot/studio/compiler/appack.py: .appack compiler (ZIP with manifest.json + project.yaml + firmware_config.h + install_notes.txt) - arautopilot/studio/editors/project_editor.py: full project config editor (vessel / actuator / sensors / PID, RBAC-gated fields, save/load .yaml/.json) - arautopilot/studio/main_window.py: wire ProjectEditorWidget into Project tab - tests: test_sensor_config.py (11 tests) + test_appack_compiler.py (10 tests) Firmware: - hal/rudder_actuator_iface.h: IRudderActuator abstract interface - hal/rudder_actuator_hydraulic.h: reversible hydraulic pump (LEDC PWM) - hal/rudder_actuator_electric.h: reversible DC motor + deadband compensation - hal/rudder_actuator_factory.h: build-time type selection via AR_ACTUATOR_TYPE Tests: 483 passed (was 462, +21 Sprint 4) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
"""Tests for RudderSensorConfig and DualRudderSensorConfig — Sprint 4."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from arautopilot.core.sensor_config import (
|
||
DualRudderSensorConfig,
|
||
RudderSensorConfig,
|
||
RudderSensorType,
|
||
)
|
||
|
||
|
||
def _primary(
|
||
sensor_type: RudderSensorType = RudderSensorType.AS5048A_SPI,
|
||
cs: int = 10,
|
||
fsd: float = 35.0,
|
||
) -> RudderSensorConfig:
|
||
return RudderSensorConfig(type=sensor_type, spi_cs_gpio=cs, full_scale_deg=fsd)
|
||
|
||
|
||
def _redundant(cs: int = 11) -> RudderSensorConfig:
|
||
return RudderSensorConfig(
|
||
type=RudderSensorType.AS5048A_SPI,
|
||
label="Redundant – actuator arm",
|
||
spi_cs_gpio=cs,
|
||
full_scale_deg=35.0,
|
||
)
|
||
|
||
|
||
class TestRudderSensorConfig:
|
||
def test_as5048a_defaults(self) -> None:
|
||
s = _primary()
|
||
assert s.type == RudderSensorType.AS5048A_SPI
|
||
assert s.spi_cs_gpio == 10
|
||
assert s.full_scale_deg == 35.0
|
||
assert s.zero_offset_deg == 0.0
|
||
assert s.divergence_alarm_deg == 3.0
|
||
assert s.divergence_failover_deg == 6.0
|
||
|
||
def test_potentiometer_type(self) -> None:
|
||
s = _primary(sensor_type=RudderSensorType.POTENTIOMETER)
|
||
assert s.type == RudderSensorType.POTENTIOMETER
|
||
|
||
def test_gpio_bounds(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
RudderSensorConfig(type=RudderSensorType.AS5048A_SPI, spi_cs_gpio=40)
|
||
with pytest.raises(ValidationError):
|
||
RudderSensorConfig(type=RudderSensorType.AS5048A_SPI, spi_cs_gpio=-1)
|
||
|
||
def test_full_scale_bounds(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
_primary(fsd=0.0)
|
||
with pytest.raises(ValidationError):
|
||
_primary(fsd=46.0)
|
||
|
||
def test_label_optional(self) -> None:
|
||
s = RudderSensorConfig(type=RudderSensorType.AS5048A_SPI)
|
||
assert s.label == ""
|
||
|
||
def test_extra_fields_forbidden(self) -> None:
|
||
with pytest.raises(ValidationError):
|
||
RudderSensorConfig(
|
||
type=RudderSensorType.AS5048A_SPI,
|
||
unknown_field="boom", # type: ignore[call-arg]
|
||
)
|
||
|
||
def test_roundtrip_json(self) -> None:
|
||
s = _primary()
|
||
restored = RudderSensorConfig.model_validate_json(s.model_dump_json())
|
||
assert restored == s
|
||
|
||
|
||
class TestDualRudderSensorConfig:
|
||
def test_single_sensor_mode(self) -> None:
|
||
d = DualRudderSensorConfig(primary=_primary())
|
||
assert d.redundant is None
|
||
assert not d.has_redundancy
|
||
|
||
def test_dual_sensor_mode(self) -> None:
|
||
d = DualRudderSensorConfig(primary=_primary(), redundant=_redundant())
|
||
assert d.redundant is not None
|
||
assert d.has_redundancy
|
||
|
||
def test_different_cs_pins(self) -> None:
|
||
d = DualRudderSensorConfig(primary=_primary(cs=10), redundant=_redundant(cs=11))
|
||
assert d.primary.spi_cs_gpio == 10
|
||
assert d.redundant is not None
|
||
assert d.redundant.spi_cs_gpio == 11
|
||
|
||
def test_roundtrip_yaml(self) -> None:
|
||
import yaml
|
||
|
||
d = DualRudderSensorConfig(primary=_primary(), redundant=_redundant())
|
||
data = yaml.safe_dump(d.model_dump(mode="json"))
|
||
restored = DualRudderSensorConfig.model_validate(yaml.safe_load(data))
|
||
assert restored == d
|