358 lines
15 KiB
Python
358 lines
15 KiB
Python
"""Central configuration for GoWelcome.
|
|
|
|
Every tunable lives here so field tuning never means hunting through modules.
|
|
Grouped by subsystem; all values are plain Python (no heavy imports) so this
|
|
file is safe to import anywhere, including unit tests.
|
|
|
|
Paths are derived from this file's location (``PROJECT_ROOT``) -- never
|
|
hardcode an absolute project path. Override at runtime via ``main.py`` CLI
|
|
flags or by editing the dataclass defaults below.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Tuple
|
|
|
|
# Resolve the project root dynamically (this file lives at the repo root).
|
|
PROJECT_ROOT: Path = Path(__file__).resolve().parent
|
|
ASSETS_DIR: Path = PROJECT_ROOT / "assets"
|
|
|
|
|
|
@dataclass
|
|
class NetworkConfig:
|
|
"""How to reach the Go2 over CycloneDDS (official SDK, the ``dds`` transport)."""
|
|
|
|
interface: str = "eth0" # host NIC on the robot LAN (e.g. eth0 / wlan0 / enpXs0)
|
|
domain_id: int = 0 # DDS domain; 0 for a physical Go2
|
|
|
|
|
|
@dataclass
|
|
class WebRTCConfig:
|
|
"""How to reach the Go2 over WebRTC (the default ``webrtc`` transport).
|
|
|
|
Uses ``unitree_webrtc_connect`` -- the same protocol as the Unitree app, so
|
|
it works on Go2 AIR/PRO/EDU over wifi and (uniquely) can play the greeting
|
|
from the dog's own speaker via AudioHub. No jailbreak required.
|
|
"""
|
|
|
|
connection_method: str = "localsta" # localsta | localap | remote
|
|
ip: str = "" # robot IP for localsta (e.g. 192.168.x.x); "" -> discover by serial
|
|
serial_number: str = "" # for localsta discovery / remote signaling
|
|
aes_128_key: str = "" # 32-hex per-device key; REQUIRED on Go2 firmware >= 1.1.15
|
|
username: str = "" # remote (cloud) signaling only
|
|
password: str = "" # remote (cloud) signaling only
|
|
region: str = "global" # global | cn
|
|
# How to play the greeting clip on the dog:
|
|
# "audiohub" -> upload greeting.wav to the robot once, play by uuid (default)
|
|
# "stream" -> stream the file live each greeting via an aiortc MediaPlayer
|
|
audio_method: str = "audiohub"
|
|
|
|
|
|
@dataclass
|
|
class PerceptionConfig:
|
|
"""YOLO detection + HSV road masking."""
|
|
|
|
# --- YOLO ---
|
|
# Resolve a bundled weights file relative to the project so GoWelcome runs
|
|
# from ANY working directory; fall back to the bare name (ultralytics then
|
|
# auto-downloads it) when no local copy is present.
|
|
model_path: str = (
|
|
str(PROJECT_ROOT / "yolov8n.pt")
|
|
if (PROJECT_ROOT / "yolov8n.pt").exists()
|
|
else "yolov8n.pt"
|
|
)
|
|
device: str = "cpu" # "cpu", "cuda", "cuda:0" ... ("cuda" on Jetson)
|
|
use_half: bool = False # FP16 (discrete-GPU only; leave off on Jetson/CPU)
|
|
use_tracking: bool = False # model.track(persist=True) vs model.predict
|
|
tracker: str = "bytetrack.yaml"
|
|
infer_imgsz: int = 640
|
|
person_conf: float = 0.80 # spec: APPROACH only when person conf > 0.80
|
|
danger_conf: float = 0.40 # lower bar for vehicles (safety-biased)
|
|
person_classes: Tuple[str, ...] = ("person",)
|
|
danger_classes: Tuple[str, ...] = ("car", "truck", "bus", "motorcycle", "train")
|
|
# A danger box is only "near enough to matter" once it fills this much of
|
|
# the frame height (ignore tiny far-away cars on the horizon).
|
|
danger_min_height_ratio: float = 0.25
|
|
|
|
# --- HSV road / asphalt mask ---
|
|
road_enabled: bool = True
|
|
road_crop_frac: float = 0.30 # analyse the bottom 30% of the frame
|
|
# Asphalt/concrete is low-saturation grey. HSV ranges (OpenCV: H 0-179).
|
|
road_hsv_lower: Tuple[int, int, int] = (0, 0, 40)
|
|
road_hsv_upper: Tuple[int, int, int] = (179, 60, 200)
|
|
road_morph_kernel: int = 5 # opening kernel to de-speckle the mask
|
|
# Coverage of the crop above which we consider "road ahead".
|
|
road_trigger_coverage: float = 0.35
|
|
|
|
|
|
@dataclass
|
|
class CameraConfig:
|
|
"""Frame source sizing (mock backend / resize)."""
|
|
|
|
width: int = 640
|
|
height: int = 480
|
|
# Mock backend frame source: integer webcam index, or a path to a video file.
|
|
mock_source: str = "0"
|
|
target_fps: float = 15.0
|
|
|
|
|
|
@dataclass
|
|
class ServoConfig:
|
|
"""Visual-servoing P(ID) controller: bbox centroid -> velocity."""
|
|
|
|
# Yaw: turn to centre the target horizontally.
|
|
kp_yaw: float = 1.2 # gain on normalised horizontal error (-1..1)
|
|
ki_yaw: float = 0.0
|
|
kd_yaw: float = 0.05
|
|
max_yaw_rate: float = 0.9 # rad/s clamp
|
|
yaw_deadband: float = 0.06 # |norm err| below this -> no turn
|
|
# Sign: with default Unitree convention (+vyaw = CCW/left), a target to the
|
|
# RIGHT (err>0) needs a right turn (vyaw<0), so vyaw = -kp*err. Flip if your
|
|
# robot/camera mounting inverts this.
|
|
yaw_sign: float = -1.0
|
|
|
|
# Forward: approach until the box fills the frame vertically.
|
|
kp_forward: float = 1.0
|
|
max_forward: float = 0.45 # m/s (gentle backyard pace)
|
|
min_forward: float = 0.06 # below this, treat as stopped
|
|
# spec: trigger stop when the box fills ~50% of the vertical frame.
|
|
stop_height_ratio: float = 0.50
|
|
# Throttle forward speed when heading error is large (don't charge sideways):
|
|
# scale = exp(-forward_heading_falloff * |norm_yaw_err|).
|
|
forward_heading_falloff: float = 2.0
|
|
|
|
|
|
@dataclass
|
|
class WanderConfig:
|
|
"""Open-loop roaming when no person is in view."""
|
|
|
|
forward_speed: float = 0.30 # m/s cruise
|
|
# Gentle sinusoidal yaw sweep so the dog scans the yard.
|
|
yaw_sweep_rate: float = 0.35 # rad/s peak
|
|
yaw_sweep_period: float = 6.0 # s per full left-right cycle
|
|
|
|
|
|
@dataclass
|
|
class AvoidConfig:
|
|
"""Road/car avoidance: a soft 'keep away' steer in WANDER plus the hard
|
|
AVOID_DANGER reaction up close."""
|
|
|
|
# --- hard reaction (AVOID_DANGER) ---
|
|
backup_speed: float = 0.15 # m/s reverse while turning away
|
|
turn_rate: float = 0.7 # rad/s turn-away
|
|
# Stay in AVOID until the road/danger has been clear this many frames.
|
|
clear_frames: int = 8
|
|
backup_duration: float = 0.8 # s of reverse before pivoting in place
|
|
|
|
# --- soft 'stay away from the road/cars' steer (applied during WANDER) ---
|
|
# This is the vision-only area-containment: the dog veers away from pavement
|
|
# and slows down BEFORE it reaches the hard AVOID_DANGER trigger, so it keeps
|
|
# its distance from the road/cars rather than only reacting at the edge.
|
|
soft_avoid_enabled: bool = True
|
|
# Road coverage (in the bottom-crop thirds) above which the soft steer kicks
|
|
# in. Must be below perception.road_trigger_coverage (the hard trigger).
|
|
soft_road_coverage: float = 0.12
|
|
road_repulsion_gain: float = 1.4 # rad/s of yaw bias per unit (left-right) road imbalance
|
|
road_slowdown_gain: float = 1.5 # forward-speed reduction per unit centre-road coverage
|
|
# Steer away from any detected vehicle (even far/small ones) by this much
|
|
# yaw per unit of its normalised horizontal offset; near cars still hard-avoid.
|
|
car_repulsion_gain: float = 0.7
|
|
|
|
|
|
@dataclass
|
|
class GreetConfig:
|
|
"""The greeting payload + re-greet gating."""
|
|
|
|
wav_path: str = str(ASSETS_DIR / "greeting.wav")
|
|
# Gestures performed during a greeting, in order.
|
|
gestures: Tuple[str, ...] = ("hello", "heart")
|
|
gesture_gap: float = 2.5 # s between gestures (let each finish)
|
|
audio_volume: int = 85 # 0-100 (applied where the backend supports it)
|
|
# After greeting, ignore people for this long so we don't loop on one person.
|
|
cooldown: float = 12.0 # s
|
|
settle_time: float = 0.6 # s to come to a full stop before gesturing
|
|
|
|
|
|
@dataclass
|
|
class AudioConfig:
|
|
"""Greeting audio backend selection.
|
|
|
|
backend:
|
|
"host" -> play on the machine running GoWelcome (aplay/simpleaudio).
|
|
Reliable; works regardless of robot model. DEFAULT.
|
|
"go2" -> EXPERIMENTAL: stream to the Go2 'audiohub' over DDS. Unverified
|
|
against firmware; may be a no-op. Test on hardware first.
|
|
"null" -> log only, no sound (used by the mock backend / CI).
|
|
"""
|
|
|
|
backend: str = "host"
|
|
# Host backend: external player command (used if simpleaudio is absent or
|
|
# when output_device is set). Default is aplay (ALSA); paplay is used
|
|
# automatically when output_device names a PulseAudio sink.
|
|
host_player_cmd: Tuple[str, ...] = ("aplay", "-q")
|
|
# Optional output device to pin a *specific* speaker (e.g. a USB/Bluetooth
|
|
# speaker mounted on the Go2). When set, HostSpeakerAudio plays via
|
|
# `paplay --device=<output_device>` so playback always lands on that sink
|
|
# regardless of the system default. This mirrors the proven pattern from
|
|
# the team's G1 "Sanad" stack (a USB Anker PowerConf speaker targeted by
|
|
# its PulseAudio sink name). Empty -> use the system default sink.
|
|
# Example: "alsa_output.usb-Anker_PowerConf_A3321-DEV-SN1-01.analog-stereo"
|
|
output_device: str = ""
|
|
|
|
|
|
@dataclass
|
|
class SafetyConfig:
|
|
"""Cross-cutting safety limits + the firmware avoidance toggle."""
|
|
|
|
use_lidar_avoidance: bool = True # route drive() through ObstaclesAvoidClient
|
|
# Hard caps applied to *every* velocity command, after the controllers.
|
|
max_vx: float = 0.5
|
|
max_vy: float = 0.3
|
|
max_vyaw: float = 1.0
|
|
# If perception goes stale (no new frame) longer than this, stop the robot.
|
|
perception_timeout: float = 0.8 # s
|
|
# Watchdog: if drive() isn't called within this period the robot decays to
|
|
# stop on its own (Unitree firmware behaviour); we also explicitly stop.
|
|
command_timeout: float = 0.5 # s
|
|
|
|
|
|
@dataclass
|
|
class WebConfig:
|
|
"""Optional MJPEG web viewer -- watch the live annotated camera in a browser.
|
|
|
|
When enabled, GoWelcome serves the latest frame (with YOLO person/danger
|
|
boxes, road coverage and the current state drawn on it) as an MJPEG stream
|
|
over plain HTTP. Open ``http://<host-ip>:<port>/`` in any browser. Useful
|
|
for headless operation on the dog (no local display needed).
|
|
|
|
Stdlib-only (``http.server`` + ``cv2.imencode``); no extra dependency.
|
|
"""
|
|
|
|
enabled: bool = False
|
|
host: str = "0.0.0.0" # bind all interfaces so other machines can view
|
|
port: int = 8080
|
|
fps: float = 10.0 # stream pacing (cap; never exceeds perception rate)
|
|
jpeg_quality: int = 80 # 1-100; lower = less bandwidth
|
|
|
|
|
|
@dataclass
|
|
class GpsConfig:
|
|
"""External GPS receiver used for the area geofence.
|
|
|
|
NOTE: the Go2 has NO built-in GPS -- this requires a USB/serial GPS receiver
|
|
on the onboard computer (read via gpsd or NMEA over serial). Standard GPS is
|
|
accurate to ~2-5 m, so keep the geofence well inside the real edge; use RTK
|
|
for tight bounds near roads.
|
|
"""
|
|
|
|
enabled: bool = False # OFF by default -- GoWelcome is vision-only unless GPS is added
|
|
source: str = "auto" # auto | gpsd | serial | mock
|
|
serial_port: str = "/dev/ttyACM0"
|
|
serial_baud: int = 9600
|
|
gpsd_host: str = "127.0.0.1"
|
|
gpsd_port: int = 2947
|
|
min_satellites: int = 4 # reject fixes with fewer sats
|
|
max_hdop: float = 5.0 # reject fixes worse (larger) than this
|
|
stale_after: float = 3.0 # s; a fix older than this counts as "no fix"
|
|
# Mock source (off-robot testing): a virtual receiver that integrates the
|
|
# commanded velocity into a fake lat/lon so the geofence can be exercised.
|
|
mock_start_lat: float = 25.2048
|
|
mock_start_lon: float = 55.2708
|
|
|
|
|
|
@dataclass
|
|
class GeoFenceConfig:
|
|
"""Optional GPS keep-in-area geofence + the BOUNDARY return behaviour.
|
|
|
|
OFF by default: GoWelcome keeps the dog in the area with vision (road/car
|
|
avoidance). Enable this only when an external GPS receiver is fitted.
|
|
"""
|
|
|
|
enabled: bool = False
|
|
center_mode: str = "onstart" # onstart (first good fix) | fixed
|
|
center_lat: float = 0.0 # used when center_mode == "fixed"
|
|
center_lon: float = 0.0
|
|
radius_m: float = 15.0 # keep within this radius of the centre
|
|
margin_m: float = 3.0 # start homing back this far before the edge
|
|
release_m: float = 2.0 # extra hysteresis: only resume once this far back inside
|
|
return_speed: float = 0.35 # m/s while homing toward centre
|
|
homing_kp: float = 1.2 # vyaw per rad of (bearing-to-centre - course)
|
|
max_homing_yaw: float = 0.9 # rad/s clamp while homing
|
|
min_speed_for_course: float = 0.15 # m/s; below this GPS course is unreliable
|
|
# What to do when GPS is unavailable/stale while the fence is enabled:
|
|
# "stop" -> halt (safe; don't roam blind near a boundary). DEFAULT.
|
|
# "ignore" -> keep operating on perception-only avoidance (riskier).
|
|
no_fix_behavior: str = "stop"
|
|
|
|
|
|
@dataclass
|
|
class PlayConfig:
|
|
"""Idle 'act like a dog' behaviour scheduler (runs during WANDER).
|
|
|
|
``mode`` is runtime-settable from the dashboard. Each mode sets the mean
|
|
seconds between random dog actions; actions are gestures the active robot
|
|
backend supports (unsupported ones are skipped).
|
|
"""
|
|
|
|
mode: str = "moderate" # calm | moderate | playful
|
|
calm_interval: float = 75.0
|
|
moderate_interval: float = 30.0
|
|
playful_interval: float = 15.0
|
|
jitter: float = 0.4 # +/- fraction randomisation of the interval
|
|
action_hold: float = 3.0 # s the dog pauses (stops roaming) to perform a trick
|
|
actions: Tuple[str, ...] = ("stretch", "wiggle", "scrape", "dance1", "wallow")
|
|
|
|
def interval_for(self, mode: str) -> float:
|
|
return {
|
|
"calm": self.calm_interval,
|
|
"moderate": self.moderate_interval,
|
|
"playful": self.playful_interval,
|
|
}.get(mode, self.moderate_interval)
|
|
|
|
|
|
@dataclass
|
|
class LoopConfig:
|
|
"""Main control-loop cadence + lost-target handling."""
|
|
|
|
rate_hz: float = 15.0
|
|
# Frames a locked person may be missing before APPROACH falls back to WANDER.
|
|
person_lost_frames: int = 12
|
|
|
|
|
|
@dataclass
|
|
class GoWelcomeConfig:
|
|
"""Top-level config aggregating every subsystem."""
|
|
|
|
# Which robot transport to use on real hardware:
|
|
# "webrtc" -> Go2WebRTCRobot (default; app protocol, wifi, dog-speaker audio)
|
|
# "dds" -> Go2Robot (official unitree_sdk2py over CycloneDDS, wired/EDU)
|
|
transport: str = "webrtc"
|
|
|
|
network: NetworkConfig = field(default_factory=NetworkConfig)
|
|
webrtc: WebRTCConfig = field(default_factory=WebRTCConfig)
|
|
perception: PerceptionConfig = field(default_factory=PerceptionConfig)
|
|
camera: CameraConfig = field(default_factory=CameraConfig)
|
|
servo: ServoConfig = field(default_factory=ServoConfig)
|
|
wander: WanderConfig = field(default_factory=WanderConfig)
|
|
avoid: AvoidConfig = field(default_factory=AvoidConfig)
|
|
greet: GreetConfig = field(default_factory=GreetConfig)
|
|
audio: AudioConfig = field(default_factory=AudioConfig)
|
|
safety: SafetyConfig = field(default_factory=SafetyConfig)
|
|
loop: LoopConfig = field(default_factory=LoopConfig)
|
|
web: WebConfig = field(default_factory=WebConfig)
|
|
gps: GpsConfig = field(default_factory=GpsConfig)
|
|
geofence: GeoFenceConfig = field(default_factory=GeoFenceConfig)
|
|
play: PlayConfig = field(default_factory=PlayConfig)
|
|
|
|
# Runtime mode flags (set by main.py).
|
|
mock: bool = False # True -> MockRobot (no hardware)
|
|
headless: bool = False # True -> no cv2 debug window
|
|
dry_run: bool = False # True -> perception + decisions, but never move
|
|
|
|
|
|
def default_config() -> GoWelcomeConfig:
|
|
"""Return a fresh fully-defaulted config."""
|
|
return GoWelcomeConfig()
|