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>
111 lines
3.6 KiB
Python
111 lines
3.6 KiB
Python
"""Rudder angle sensor configuration.
|
||
|
||
Supports single and dual (redundant) sensor setups. The primary sensor
|
||
is mandatory for closed-loop operation. The redundant sensor is optional
|
||
but strongly recommended for Phase-1 marine deployments.
|
||
|
||
Sensor families supported in Phase 1:
|
||
|
||
- ``AS5048A_SPI`` — contactless magnetic absolute encoder, 14-bit, SPI.
|
||
Recommended for new installations (brief session 2026-05-18).
|
||
- ``POTENTIOMETER`` — resistive potentiometer 0-10 V or 4-20 mA.
|
||
Supported for legacy actuators.
|
||
|
||
Both sensors use a polycarbonate linkage arm to the rudder stock, giving
|
||
a proportional rotation to the rudder angle.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from enum import StrEnum
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
|
||
class RudderSensorType(StrEnum):
|
||
AS5048A_SPI = "as5048a_spi"
|
||
"""AMS AS5048A contactless magnetic encoder — 14-bit, SPI interface.
|
||
No mechanical contact; requires diametrically magnetised 6 mm magnet
|
||
on the rotating shaft. Recommended for all new Phase-1 installations."""
|
||
|
||
POTENTIOMETER = "potentiometer"
|
||
"""Resistive potentiometer with 0-10 V or 4-20 mA signal conditioning.
|
||
Suitable for retrofit on existing actuators that already carry a pot."""
|
||
|
||
|
||
class RudderSensorConfig(BaseModel):
|
||
"""Configuration for one physical rudder angle sensor.
|
||
|
||
The ``full_scale_deg`` / ``zero_offset_deg`` pair maps the electrical
|
||
range of the sensor to real rudder degrees. Calibration during
|
||
commissioning (Sprint 7) will update these values in NVS.
|
||
"""
|
||
|
||
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
||
|
||
type: RudderSensorType
|
||
"""Physical sensor technology."""
|
||
|
||
label: str = Field(
|
||
default="",
|
||
max_length=60,
|
||
description="Free-form label, e.g. 'Primary – rudder stock' or 'Redundant – actuator arm'.",
|
||
)
|
||
|
||
# SPI-specific (ignored for POTENTIOMETER)
|
||
spi_cs_gpio: int = Field(
|
||
default=10,
|
||
ge=0,
|
||
le=39,
|
||
description="ESP32 GPIO pin used as SPI chip-select for AS5048A.",
|
||
)
|
||
|
||
# Calibration
|
||
full_scale_deg: float = Field(
|
||
default=35.0,
|
||
gt=0.0,
|
||
le=45.0,
|
||
description="Rudder angle (degrees) corresponding to full electrical output.",
|
||
)
|
||
zero_offset_deg: float = Field(
|
||
default=0.0,
|
||
ge=-10.0,
|
||
le=10.0,
|
||
description="Offset applied after scaling to correct mechanical amidships misalignment.",
|
||
)
|
||
|
||
# Cross-validation (used by dual-sensor arbitrator)
|
||
divergence_alarm_deg: float = Field(
|
||
default=3.0,
|
||
gt=0.0,
|
||
le=15.0,
|
||
description="Alarm threshold: raise SENSOR_DIVERGE if |A - B| exceeds this value.",
|
||
)
|
||
divergence_failover_deg: float = Field(
|
||
default=6.0,
|
||
gt=0.0,
|
||
le=20.0,
|
||
description="Failover threshold: switch to the other sensor if |A - B| exceeds this.",
|
||
)
|
||
|
||
|
||
class DualRudderSensorConfig(BaseModel):
|
||
"""Primary + optional redundant rudder sensor pair.
|
||
|
||
When ``redundant`` is ``None`` the system operates in single-sensor
|
||
mode (feedback_required still enforced). When ``redundant`` is
|
||
present the firmware enables cross-validation and automatic failover.
|
||
|
||
Divergence thresholds are taken from the *primary* sensor's config so
|
||
there is one canonical place to tune them.
|
||
"""
|
||
|
||
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
||
|
||
primary: RudderSensorConfig
|
||
redundant: RudderSensorConfig | None = None
|
||
|
||
@property
|
||
def has_redundancy(self) -> bool:
|
||
return self.redundant is not None
|