Files
AR-Autopilot/arautopilot/core/modes.py
T
alro65 8d4a698144 polish(sprint-0): clean code per ruff + mypy strict
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>
2026-05-18 07:26:37 -04:00

77 lines
2.1 KiB
Python

"""Autopilot operating modes.
The brief (section 3) defines five Phase 1 modes plus three Phase 2 sail
modes that appear greyed-out in the UI but are reserved here for forward
compatibility.
"""
from __future__ import annotations
from enum import StrEnum
class AutopilotMode(StrEnum):
"""The complete set of autopilot operating modes, across both phases.
Use :func:`is_available_in_phase` to query phase gating instead of
hard-coding lists in the UI.
"""
# --- Phase 1 (Sprint 1+) ---
STANDBY = "standby"
"""Pilot disengaged, helm is manual."""
HEADING_HOLD = "heading_hold"
"""Holds a fixed magnetic/true compass heading."""
TRUE_COURSE = "true_course"
"""Holds COG, compensating drift due to current and wind."""
TRACK_KEEPING = "track_keeping"
"""Follows an ECDIS waypoint route with smooth XTE correction."""
DODGE = "dodge"
"""Temporary deviation without losing the route; auto-returns when released."""
# --- Phase 2 (Sprint 10+, greyed in Phase 1 UI) ---
APPARENT_WIND = "apparent_wind"
"""Holds constant apparent wind angle (vane mode). Sailboats only."""
TRUE_WIND = "true_wind"
"""Holds constant true wind angle. Sailboats only."""
AUTO_TACK = "auto_tack"
"""Automatically tacks at a target relative wind angle. Sailboats only."""
_PHASE_1: frozenset[AutopilotMode] = frozenset(
{
AutopilotMode.STANDBY,
AutopilotMode.HEADING_HOLD,
AutopilotMode.TRUE_COURSE,
AutopilotMode.TRACK_KEEPING,
AutopilotMode.DODGE,
}
)
_PHASE_2: frozenset[AutopilotMode] = frozenset(
{
AutopilotMode.APPARENT_WIND,
AutopilotMode.TRUE_WIND,
AutopilotMode.AUTO_TACK,
}
)
def is_available_in_phase(mode: AutopilotMode, phase: int) -> bool:
"""Return ``True`` if ``mode`` is available in the given product phase.
Phase 1 is the launch product. Phase 2 adds the sailboat wind modes.
Asking for any other phase number returns ``False``.
"""
if phase == 1:
return mode in _PHASE_1
if phase == 2:
return mode in _PHASE_1 or mode in _PHASE_2
return False