Files
AR-Autopilot/arautopilot/core/actuator_config.py
T
alro65 700756c16f sprint-0: foundations -- data model, seed library, tests, demo
Initial commit. Delivers what the brief calls 'Sprint 0 - Foundations'
(see docs/AR_Autopilot_brief.md section 12):

- Complete repository structure (arautopilot package + firmware, display,
  installer, tools placeholders + docs).
- Core data model (Pydantic v2): modes, alarms, actuator config, PID
  config + gain scheduling, vessel config, knob state machine, project
  config with YAML/JSON serialisation.
- Seed library: 2 actuator profiles (hydraulic & electric DC reversible)
  and 2 default tunings (yacht motor planeo 30 m and 40 m). Conservative
  literature values, NOT the integrator's production tuning IP.
- Firmware skeleton: only src/hal/pinout.h with the 21 I/O contract for
  the AR-NMEA-IO v1.0 board. No drivers, no main loop.
- Studio stubs (real PySide6 app starts in Sprint 4).
- pytest suite (80 tests, all green): modes, alarms, actuator, PID
  (incl. gain interpolation and the +/-50% adaptive bound from brief
  section 6), vessel, knob state, project config, library loader,
  end-to-end roundtrip.
- examples/sprint0_demo.py - the acceptance demo from the brief.

Acceptance criteria met:
- pytest green (80/80)
- demo creates, saves (YAML + JSON), reloads, and verifies a full
  ProjectConfig using the seed library
- repository ready for tag `sprint-0-approved`

See CHANGELOG.md for the detailed scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 23:57:18 -04:00

141 lines
4.6 KiB
Python

"""Rudder actuator configuration.
Supports the actuator families enumerated in section 3 of the brief.
Sterndrive, IPS and Zeus interfaces are reserved for Phase 3 and are
intentionally **not** enabled in Phase 1 configurations.
"""
from __future__ import annotations
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, field_validator
class ActuatorType(str, Enum):
"""Actuator family driving the rudder.
Phase-1 supported families have a regular value. Reserved (Phase 3)
families also live here so that configurations and the UI can model
them, but the firmware refuses to operate them in Phase 1.
"""
HYDRAULIC_REVERSIBLE = "hydraulic_reversible"
"""Reversible hydraulic pump (Hynautic, Hypro, Octopus, Vetus, L&S). Phase 1."""
ELECTRIC_DC_REVERSIBLE = "electric_dc_reversible"
"""Reversible DC motor with mechanical end-stops (Lewmar, Simpson Lawrence). Phase 1."""
SERVOMOTOR_FEEDBACK = "servomotor_feedback"
"""Servomotor with built-in position feedback. Phase 1."""
STERNDRIVE_ANALOG = "sterndrive_analog"
"""Analog directional sterndrive. Phase 1."""
VOLVO_IPS = "volvo_ips"
"""Volvo IPS via EVC. Phase 3 — reserved."""
MERCURY_ZEUS = "mercury_zeus"
"""Mercury Zeus via SmartCraft. Phase 3 — reserved."""
_PHASE_1_ACTUATORS: frozenset[ActuatorType] = frozenset(
{
ActuatorType.HYDRAULIC_REVERSIBLE,
ActuatorType.ELECTRIC_DC_REVERSIBLE,
ActuatorType.SERVOMOTOR_FEEDBACK,
ActuatorType.STERNDRIVE_ANALOG,
}
)
def is_phase_1(actuator_type: ActuatorType) -> bool:
"""Return ``True`` iff the firmware is allowed to drive this actuator in Phase 1."""
return actuator_type in _PHASE_1_ACTUATORS
class ActuatorConfig(BaseModel):
"""Configuration of the rudder actuator and its calibration.
All angles are in degrees. ``deadband_pct`` and ``min_useful_pwm_pct``
are percentages of full command. Asymmetry is the ratio of starboard
speed to port speed: ``1.0`` means symmetric, ``1.10`` means the pump
pushes 10 % faster to starboard than to port.
"""
model_config = ConfigDict(extra="forbid", validate_assignment=True)
type: ActuatorType
"""Actuator family."""
name: str = Field(
default="",
max_length=120,
description="Free-form label, e.g. 'Hynautic K-21 + Capilano cylinder'.",
)
# -- Calibration / non-linearity compensation ------------------------------
deadband_pct: float = Field(
default=5.0,
ge=0.0,
le=30.0,
description="Initial percentage of command that produces no motion (static friction).",
)
min_useful_pwm_pct: float = Field(
default=8.0,
ge=0.0,
le=50.0,
description="Minimum PWM where the actuator actually moves; commands below this snap up to it.",
)
asymmetry_stbd_over_port: float = Field(
default=1.0,
ge=0.50,
le=2.00,
description="Ratio of starboard speed to port speed; 1.0 = symmetric.",
)
# -- Mechanical / electrical limits ---------------------------------------
max_rudder_angle_deg: float = Field(
default=35.0,
gt=0.0,
le=45.0,
description="Mechanical limit on either side from amidships; typically 35 deg.",
)
max_rate_dps: float = Field(
default=4.5,
gt=0.0,
le=15.0,
description="Maximum slew rate in degrees per second (typical 3-6 dps).",
)
max_current_a: float = Field(
default=15.0,
gt=0.0,
le=200.0,
description="Overcurrent trip threshold (A) for the actuator power line.",
)
# -- Safety policy --------------------------------------------------------
feedback_required: bool = Field(
default=True,
description=(
"Closed loop only. Open-loop operation requires explicit integrator "
"override (brief section 3, Phase 1). Default True."
),
)
@field_validator("min_useful_pwm_pct")
@classmethod
def _min_useful_must_cover_deadband(cls, v: float, info: object) -> float:
# info.data is provided by pydantic-core for cross-field validation
data = getattr(info, "data", {}) or {}
deadband = data.get("deadband_pct")
if deadband is not None and v < deadband:
raise ValueError(
f"min_useful_pwm_pct ({v}) must be >= deadband_pct ({deadband})"
)
return v
def is_phase_1_supported(self) -> bool:
"""Return ``True`` iff this actuator can be driven in Phase 1."""
return is_phase_1(self.type)