Files
alro65 e4812e9b44 sprint-4: Project Editor + .appack compiler + IRudderActuator HAL + dual sensor config
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>
2026-05-20 10:29:17 -04:00

99 lines
3.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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