Files
AR-Autopilot/arautopilot/core/sensor_config.py
T
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

111 lines
3.6 KiB
Python
Raw 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.
"""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