"""In-process arbitration between Nav2 (web_nav3) and the manual LocoController. Both stacks can drive the G1's legs via different command paths: - Nav2 (web_nav3) publishes cmd_vel from a navigation goal/mission. - LocoController issues LocoClient.Move()/step() from the Controller tab and Gemini movement dispatch. The documented hazard is "two stacks must never both drive the legs at once". This module is a tiny thread-safe gate that lets ONE commander own the legs at a time. controller.py sets loco_active for arm/move/step and refuses when nav is active; navigation.py sets nav_active for goto/missions/run and refuses when loco is active. The E-STOP / cancel paths clear the relevant flag. Pure in-process state (no DDS, no HTTP) — both routers share this single module instance, so the flags are coherent across the dashboard process. """ from __future__ import annotations import threading _lock = threading.Lock() _loco_active = False _nav_active = False def loco_active() -> bool: with _lock: return _loco_active def nav_active() -> bool: with _lock: return _nav_active def acquire_loco() -> bool: """Claim the legs for manual loco. Returns False if Nav2 holds them.""" global _loco_active with _lock: if _nav_active: return False _loco_active = True return True def release_loco() -> None: global _loco_active with _lock: _loco_active = False def acquire_nav() -> bool: """Claim the legs for Nav2. Returns False if manual loco holds them.""" global _nav_active with _lock: if _loco_active: return False _nav_active = True return True def release_nav() -> None: global _nav_active with _lock: _nav_active = False