Source code for cubemarspycan.policy

"""Caller-chosen safety behaviour.

Policy is deliberately *not* part of :class:`~cubemarspycan.spec.MotorSpec`. A spec holds
sourced facts about a motor; a policy holds decisions about how your application wants to
treat them. Mixing the two is how TMotorCANControl ended up with an empirical 0.59
current fudge factor - measured once for one AK80-9 - baked into the constants of every
motor it supports.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum


[docs] class ClampMode(Enum): """What to do with a command outside the usable range.""" EFFECTIVE = "effective" """Clamp to ``min(wire field, physical limit)`` per field. The default.""" FIELD = "field" """Clamp only to what the wire can express. Lets you exceed the motor's rating.""" RAISE = "raise" """Refuse out-of-range commands instead of clamping."""
[docs] class FaultAction(Enum): """What ``update()`` does when the driver reports a non-zero fault code.""" RAISE = "raise" """Send a safe-stop frame, then raise MotorFault on the control thread. Default.""" WARN = "warn" """Emit a warning and keep going. For diagnostics only.""" IGNORE = "ignore" """Latch it for inspection and say nothing."""
[docs] @dataclass(frozen=True, slots=True) class ClampReport: """What a command had to be changed to before it could be sent.""" field_name: str requested: float applied: float limit: float reason: str @property def clamped(self) -> bool: """Whether the value was actually altered. ``requested != applied``. Reported rather than raised, so a saturating controller is visible in the log instead of being silently trimmed. """ return self.requested != self.applied def __str__(self) -> str: return ( f"{self.field_name}: requested {self.requested:g}, sent {self.applied:g} " f"({self.reason} {self.limit:g})" )
[docs] @dataclass(frozen=True, slots=True) class SafetyPolicy: """Limits and reactions chosen by the caller, not read off the motor.""" max_temp_c: float = 75.0 """Board temperature above which update() stops. The manual allows 100 C.""" clamp: ClampMode = ClampMode.EFFECTIVE on_fault: FaultAction = FaultAction.RAISE stale_warn_s: float = 0.1 """No feedback for this long while commanding -> warn.""" stale_fatal_s: float = 0.5 """No feedback for this long -> safe-stop and raise StaleFeedbackError.""" current_ceiling_a: float | None = None """Hard ceiling for servo current commands. The servo current field spans +/-60 A while an AK40-10 peaks at 7.3 A, so a typo can ask for eight times the motor's rating. Mandatory for any variant whose ``limits.peak_current_a`` is still unknown; otherwise it tightens that value. """ supply_voltage_v: float | None = None """Declared supply. Used only to warn when the motor can outrun the velocity field.""" warn_on_estimates: bool = True forbidden: frozenset[str] = field(default_factory=frozenset) """Named operations to refuse outright, e.g. {"permanent_zero"}."""
DEFAULT_POLICY = SafetyPolicy()