"""Who is allowed to drive the arm. Two sources want one arm, and the arm cannot take both. This is the whole of that decision, in one place, so there is exactly one answer to "why did it not move" rather than one per input path. The model comes from the monolithic repo's `RobotService` lease, and is kept because the web console is built on it: its controls are gated on owning the arm and it renders an owner chip. A simpler last-writer-wins scheme would mean rewriting the console to match the node rather than the other way round. The arm node itself has no idea any of this exists. A lease held here binds only what goes through this node — another node publishing `request` to the arm will preempt an operator mid-motion, and nothing here can stop it. """ from dora_teleop.log import log GAMEPAD = "gamepad" WEB = "web" class Arbiter: """The lease on the arm, and the rules for handing it between sources.""" def __init__(self, idle_release_s: float): self.idle_release_s = idle_release_s self._owner: str | None = None self._last_activity = 0.0 @property def owner(self) -> str | None: """Who holds the arm, or None if it is free.""" return self._owner def take(self, who: str, now: float, preempt: bool = False) -> bool: """Give ``who`` the arm if it is theirs to take, and say whether it worked. A free arm goes to whoever asks. A held one stays held unless the caller asked to preempt, which the console's TAKE CONTROL button offers deliberately: an operator who can see the arm is better placed than this code to decide they should have it. """ if self._owner not in (None, who) and not preempt: return False if self._owner != who: log(f"{who} has the arm" + (f", preempting {self._owner}" if self._owner else "")) self._owner = who self._last_activity = now return True def release(self, who: str) -> None: """Give the arm back, if this source had it.""" if self._owner == who: log(f"{who} released the arm") self._owner = None def note_activity(self, who: str, now: float) -> None: """Record that the holder is still there, so the idle timer does not fire.""" if self._owner == who: self._last_activity = now def expire_idle_gamepad(self, now: float) -> None: """Hand the arm back after the pad has sat untouched long enough. Only the gamepad expires. It takes the arm implicitly, on the first nudge of a stick, so it has to give it back the same way or a pad left on the bench would lock the browser out forever. The web takes the arm by pressing a button and gives it back by pressing another. """ if self._owner != GAMEPAD: return if now - self._last_activity >= self.idle_release_s: self.release(GAMEPAD) def may_drive(self, who: str) -> bool: """Whether this source's jog should reach the arm.""" return self._owner == who