124 lines
4.3 KiB
Python
124 lines
4.3 KiB
Python
"""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()
|