8d4a698144
Run the dev linters over Sprint 0's core/library/shared modules and address every finding. Behaviour unchanged; tests still 80/80 green. Changes: - Replace `class Foo(str, Enum)` with `class Foo(StrEnum)` (PEP 663 / Python 3.11+) in 7 enum classes: ActuatorType, AlarmSeverity, AlarmType, KnobMode, KnobFunction, AutopilotMode, AccessLevel, VesselType. Pydantic v2 serialises StrEnum the same way, so YAML/JSON round-trips are byte-identical. - Use `datetime.UTC` alias in place of `datetime.timezone.utc` (UP017) across alarms.py, knob_state.py, project_config.py, and test_knob_state.py. - Remove now-unnecessary forward-reference quotes from method return type annotations (UP037) — `from __future__ import annotations` is already in scope everywhere. - Tighten `_read_json_resource` / `_read_yaml_resource` in the library loader: validate that the deserialised payload is actually a dict before returning, instead of leaking `Any` from json.loads / yaml.safe_load. Fixes the only two `mypy --strict` findings. - Add `.claude/settings.local.json` to .gitignore (personal Claude Code overrides are not committed). Verification: ruff check arautopilot/ -> All checks passed mypy arautopilot/core library shared -> Success, 0 issues, 12 files pytest -> 80 passed in 0.25s Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
141 lines
4.6 KiB
Python
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 StrEnum
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
class ActuatorType(StrEnum):
|
|
"""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)
|