"""Abstract robot + audio contracts. The state machine and main loop talk *only* to ``RobotInterface``; concrete implementations are :class:`gowelcome.robot.go2_robot.Go2Robot` (real hardware via the official Unitree SDK) and :class:`gowelcome.robot.mock_robot.MockRobot` (webcam / video file, for off-robot development). Velocity convention (matches Unitree ``SportClient.Move``): vx forward (+) / backward (-) metres/second, body frame vy left (+) / right (-) metres/second, body frame vyaw turn-left/CCW (+) / right/CW (-) radians/second Gesture names accepted by :meth:`RobotInterface.gesture` (mapped to the SDK on real hardware): ``hello``, ``heart``, ``stretch``, ``dance1``, ``dance2``, ``scrape``, ``content``, ``sit``, ``rise_sit``. """ from __future__ import annotations from abc import ABC, abstractmethod from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: import numpy as np # Canonical gesture vocabulary the state machine may request. Backends map # these to whatever the underlying hardware supports (unknown -> no-op + warn). GESTURES = ( "hello", "heart", "stretch", "dance1", "dance2", "scrape", "content", "sit", "rise_sit", # dog-play actions (supported by the WebRTC backend; no-op where unsupported) "wiggle", "wallow", "pounce", ) class AudioBackend(ABC): """Plays the greeting clip. Pluggable because the Go2 has no first-party SDK audio path (see ``gowelcome/robot/audio.py`` for the gory details).""" @abstractmethod def play(self, wav_path: str, blocking: bool = False) -> bool: """Play ``wav_path``. Returns ``True`` if playback was dispatched. ``blocking=True`` waits for the clip to finish; ``False`` returns immediately (fire-and-forget). Implementations must never raise on a missing file or backend error -- log and return ``False`` instead. """ def close(self) -> None: # optional cleanup hook """Release any audio resources. Default: no-op.""" class RobotInterface(ABC): """Everything the behaviour layer needs from a robot.""" # --- perception input ------------------------------------------------- @abstractmethod def get_frame(self) -> "Optional[np.ndarray]": """Return the latest BGR camera frame, or ``None`` if unavailable.""" @abstractmethod def frame_size(self) -> "tuple[int, int]": """Current ``(width, height)``; ``(0, 0)`` until the first frame.""" # --- locomotion ------------------------------------------------------- @abstractmethod def drive(self, vx: float, vy: float, vyaw: float) -> None: """Command a body-frame velocity (one tick). Call repeatedly to keep moving. On real hardware this goes through the firmware LiDAR obstacle-avoidance layer when avoidance is enabled.""" @abstractmethod def stop(self) -> None: """Command zero velocity (stay standing).""" @abstractmethod def set_avoidance(self, on: bool) -> None: """Enable/disable the on-robot LiDAR hard-stop avoidance layer.""" # --- posture / expression -------------------------------------------- @abstractmethod def balance_stand(self) -> None: """Enter balanced standing (ready-to-move).""" @abstractmethod def stand_up(self) -> None: """Stiff stand up.""" @abstractmethod def damp(self) -> None: """Emergency soft-stop: limp motors. Safe failure posture.""" @abstractmethod def gesture(self, name: str) -> None: """Perform an expressive gesture (see ``GESTURES``).""" # --- greeting payload ------------------------------------------------- @abstractmethod def play_greeting(self) -> None: """Play the configured greeting audio clip (non-blocking).""" # --- lifecycle -------------------------------------------------------- @abstractmethod def shutdown(self) -> None: """Stop motion, release avoidance API, close camera/audio. Idempotent and safe to call from a signal handler / ``finally`` block.""" # context-manager sugar so callers can ``with build_robot(...) as bot:`` def __enter__(self) -> "RobotInterface": return self def __exit__(self, exc_type, exc, tb) -> None: self.shutdown()