Skip to content

Latest commit

 

History

History
538 lines (438 loc) · 26.7 KB

File metadata and controls

538 lines (438 loc) · 26.7 KB

System Architecture — SpotCommandCenter + hetero_follow

Version: 2.0 | SDK: Spot 4.1.1 | ROS 2: Jazzy (GCS) / Galactic (V60)


Table of Contents

  1. System Overview
  2. Component Inventory
  3. SpotCommandCenter — Web Dashboard
  4. hetero_follow — Multi-Robot ROS 2 Demo
  5. Data Flow Diagrams
  6. Network Topology
  7. ROS 2 Topic Graph
  8. Control Law — Pure Pursuit
  9. Safety Architecture
  10. Simulation Mode
  11. Deployment Model
  12. Key Design Decisions
  13. Limitations & Known Issues
  14. Future Roadmap

1. System Overview

The system has two co-existing subsystems operating on overlapping hardware:

┌─────────────────────────────────────────────────────────────────────────────┐
│                          GROUND CONTROL STATION (GCS)                       │
│                             192.168.168.x / Ubuntu 24.04                    │
│                                                                              │
│  ┌─────────────────────────────┐   ┌──────────────────────────────────────┐ │
│  │   SpotCommandCenter          │   │     hetero_follow (ROS 2 Jazzy)      │ │
│  │                              │   │                                      │ │
│  │  React UI  ←→  Flask API     │   │  spot_bridge  ←→  v60_follower       │ │
│  │     :3000       :8000        │   │  (Spot SDK)       (pure-pursuit)     │ │
│  └──────────┬──────────────────┘   └──────────────┬───────────────────────┘ │
│             │ HTTP/WS                              │ ROS 2 / CycloneDDS     │
└─────────────┼──────────────────────────────────────┼────────────────────────┘
              │ Spot SDK gRPC                         │ DDS (domain 123)
              ▼                                       ▼
   ┌──────────────────┐                  ┌────────────────────────┐
   │  Boston Dynamics  │                  │   Ghost Robotics V60   │
   │       Spot        │                  │   192.168.168.105      │
   │  192.168.168.y    │                  │   ROS 2 Galactic       │
   └──────────────────┘                  └────────────────────────┘
  • SpotCommandCenter is a browser-based dashboard for direct control, live video, and telemetry of Spot.
  • hetero_follow is a ROS 2 workspace that makes the Ghost Robotics V60 autonomously track Spot using a pure-pursuit controller.
  • Both subsystems connect to Spot simultaneously but use different channels (HTTP/SDK vs ROS 2 DDS bus).

2. Component Inventory

Component Tech Location Port / Topic
React Frontend React 18, Axios, WebSocket frontend/ :3000
Flask Backend Python 3.12, Flask, Spot SDK 4.1.1 backend/ :8000
spot_pose_publisher ROS 2 Node, Spot SDK spot_bridge/ /spot/pose
spot_estop_monitor ROS 2 Node, Spot SDK spot_bridge/ /estop
pursuit_controller ROS 2 Node v60_follower/ /mcu/command/manual_twist
safety_monitor ROS 2 Node v60_follower/ /mcu/command/manual_twist
figure8_publisher ROS 2 Node (sim only) v60_follower/ /spot/pose, /odom
Ghost V60 MCU ROS 2 Galactic (onboard) 192.168.168.105 /odom, /mcu/command/manual_twist

3. SpotCommandCenter — Web Dashboard

3.1 Three-Tier Architecture

┌────────────────────────────────────────────────────────────────────┐
│  TIER 1 — Spot Robot                                                │
│  7 cameras  ·  IMU  ·  Odometry  ·  Joint States  ·  Battery       │
└──────────────────────────────┬─────────────────────────────────────┘
                               │ Spot SDK gRPC (protobuf)
┌──────────────────────────────▼─────────────────────────────────────┐
│  TIER 2 — Backend  (Python Flask  :8000)                            │
│                                                                     │
│  ┌──────────────┐  ┌────────────────┐  ┌──────────────────────┐    │
│  │ SpotRobot    │  │ PARVS Streaming│  │  REST API            │    │
│  │ Controller   │  │ Engine         │  │  /api/robot/*        │    │
│  │ (SDK Bridge) │  │ (WebSocket)    │  │  /api/camera/*       │    │
│  └──────┬───────┘  └───────┬────────┘  └──────────────────────┘    │
│         │ robot_state       │ binary tile packets                   │
│  ┌──────▼───────────────────▼──────────────────────────────────┐   │
│  │               RobotManager (multi-robot registry)            │   │
│  │   robots: { spot: SpotController, v60: GhostController }    │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  Optional: GestureController · VisionProcessor · VideoRecorder      │
│            CoreIOClient · ArmController · GhostSDKProxy             │
└──────────────────────────────┬─────────────────────────────────────┘
                               │ HTTP JSON / WebSocket binary
┌──────────────────────────────▼─────────────────────────────────────┐
│  TIER 3 — Frontend  (React  :3000)                                  │
│                                                                     │
│  Login → JWT → Authenticated routes                                 │
│                                                                     │
│  ┌─────────────┐ ┌────────────┐ ┌────────────┐ ┌───────────────┐  │
│  │ CameraFeed  │ │ControlPanel│ │ RobotStatus│ │ GhostDashboard│  │
│  │ CameraGrid  │ │ ArmControl │ │ Telemetry  │ │ GhostSensor   │  │
│  │ SurroundView│ │ GripperCtrl│ │ FaultLog   │ │   Panel       │  │
│  └─────────────┘ └────────────┘ └────────────┘ └───────────────┘  │
│                                                                     │
│  Also: MissionControl · GestureControl · DataAcquisition           │
│        VisionPanel · PoseHistory · VideoRecording · WiFiPanel       │
└────────────────────────────────────────────────────────────────────┘

3.2 Backend Modules

Module Responsibility
app.py Flask app factory, route registration, init_robot() startup
robot_controller.py SpotRobotController — wraps all Spot SDK calls
robot_manager.py RobotManager — registry for Spot + Ghost; active_robot() routing
ghost_controller.py GhostRobotController — Ghost SDK proxy
ghost_sdk_proxy.py Low-level Ghost SDK HTTP bridge
camera_stream.py Camera frame acquisition + PARVS pipeline entrypoint
arm_controller.py Spot arm + gripper kinematics
auth.py JWT issue / verify decorators
config.py ROBOT_IP, ROBOT_USERNAME, ROBOT_PASSWORD, JWT_SECRET from .env
vision/ YOLO / SAM2 / RT-DETR inference pipeline
gesture/ MediaPipe-based gesture recognition → movement mapping
video_recorder.py Frame capture to disk (recordings/)
coreio_client.py CoreIO payload HTTP client

3.3 API Surface (key routes)

POST /api/auth/login            → JWT token
GET  /api/robot/status          → battery, pose, e-stop state
POST /api/robot/command         → move / stand / sit / self-right
GET  /api/robot/select/<id>     → switch active robot (spot | ghost)
GET  /api/camera/<source>       → JPEG frame (MJPEG polling)
WS   /ws/stream                 → PARVS binary tile stream
POST /api/arm/move              → joint-space arm command
POST /api/mission/execute       → waypoint mission
POST /api/data/acquire          → trigger data acquisition service

3.4 Authentication Flow

Browser → POST /api/auth/login {user, pass}
        ← 200 { token: "eyJ..." }
Browser → GET  /api/robot/status
          Authorization: Bearer eyJ...
        ← 200 { battery: 87, ... }

4. hetero_follow — Multi-Robot ROS 2 Demo

4.1 Package Structure

hetero_follow/
├── spot_bridge/          ─── ROS 2 package — Spot SDK → DDS
│   └── spot_bridge/
│       ├── spot_pose_publisher.py    (Node)
│       └── spot_estop_monitor.py     (Node)
├── v60_follower/         ─── ROS 2 package — pursuit + safety
│   └── v60_follower/
│       ├── pursuit_math.py           (pure Python — no ROS deps)
│       ├── pursuit_controller.py     (Node)
│       ├── safety_monitor.py         (Node)
│       └── figure8_publisher.py      (Node — sim only)
├── config/
│   └── follow_params.yaml
├── launch/
│   └── follow_demo.launch.py
└── tests/
    ├── test_pursuit_math.py
    └── test_pose_filtering.py

4.2 Node Descriptions

spot_pose_publisher (spot_bridge)

  • Credentials: SPOT_IP, SPOT_USERNAME, SPOT_PASSWORD env vars
  • Acquires a Spot SDK lease on startup; releases on shutdown
  • Calls robot_state_client.get_robot_state() at 20 Hz
  • Extracts body → odom transform via get_a_tform_b(ODOM_FRAME_NAME, BODY_FRAME_NAME)
  • Publishes geometry_msgs/PoseStamped on /spot/pose
  • Reconnect with exponential backoff (1 s → 2 s → … → 30 s cap)

spot_estop_monitor (spot_bridge)

  • Polls estop_client.get_status() at 5 Hz
  • Publishes std_msgs/Bool on /estop
  • Conservative default: publishes True (stopped) on any connectivity failure

pursuit_controller (v60_follower)

  • Subscribes: /spot/pose (PoseStamped), /odom (Odometry), /estop (Bool)
  • Publishes: /mcu/command/manual_twist (Twist) at 20 Hz
  • Stale-pose guard: if /spot/pose timestamp > stale_pose_timeout (0.5 s) → zero vel
  • E-stop guard: if /estop = True → zero vel (safety_monitor also overrides)
  • Math kernel in pursuit_math.py — fully testable without ROS 2

safety_monitor (v60_follower)

  • Subscribes /estop
  • On estop engage: floods /mcu/command/manual_twist with zero Twist at 50 Hz for 2.0 s
  • Separate node — overrides pursuit_controller output even if that node hangs
  • Publisher on the same topic wins by recency (last-write-wins on V60 MCU subscriber)

figure8_publisher (v60_follower — sim only)

  • Publishes synthetic Spot pose on a Lissajous figure-8 path to /spot/pose
  • Publishes static V60 odometry (at −1.5 m) to /odom
  • Runs at 20 Hz; parameter OMEGA controls figure-8 speed

5. Data Flow Diagrams

5.1 Real-Robot Mode

Spot Hardware
    │
    │ gRPC (Spot SDK 4.1.1)
    ▼
spot_pose_publisher ──── /spot/pose (PoseStamped, 20 Hz) ───────────────┐
                                                                          │
spot_estop_monitor ───── /estop (Bool, 5 Hz) ──────────────────────┐     │
                                                                    │     │
V60 onboard MCU                                                     │     │
    │                                                               │     │
    │ CycloneDDS (domain 123)                                       │     │
    ▼                                                               ▼     ▼
/odom (Odometry, ~50 Hz) ──────────────────────► pursuit_controller ─────┐
                                                  safety_monitor ─────────┤
                                                                         │
                                                        /mcu/command/manual_twist
                                                        (Twist, 20 Hz)   │
                                                                         ▼
                                                              V60 MCU actuators

5.2 Simulation Mode

figure8_publisher
    │
    ├── /spot/pose  (synthetic Lissajous figure-8)
    │
    └── /odom       (static V60 start position)
           │
           ▼
    pursuit_controller
           │
           └── /mcu/command/manual_twist  (velocity commands, echoed to terminal)

5.3 SpotCommandCenter Web Flow

Browser
  │
  ├─(1) POST /api/auth/login ──► Flask ──► JWT issued
  │
  ├─(2) GET  /api/robot/status ──► Flask ──► SpotSDK ──► Spot
  │                              ◄──────── robot state ◄────────
  │
  ├─(3) POST /api/robot/command { "command": "stand" }
  │       ──► Flask ──► robot_controller.stand() ──► Spot gRPC
  │
  └─(4) WebSocket /ws/stream
            ──► PARVS engine tiles ──► Canvas compositor ──► display

6. Network Topology

                   Layer 2 flat subnet: 192.168.168.0/24
                   ROS_DOMAIN_ID = 123
                   RMW = rmw_cyclonedds_cpp

    ┌────────────────────────────────────────────────────────┐
    │                                                        │
    │   GCS Laptop            Spot              Ghost V60    │
    │   192.168.168.x    192.168.168.y    192.168.168.105    │
    │                                                        │
    │   ←────────── Spot SDK gRPC (TCP) ──────────►         │
    │                                                        │
    │   ←─────────── DDS multicast (UDP) ────────────────►  │
    │       /spot/pose  /estop → GCS publishes              │
    │       /odom  /mcu/command/manual_twist ← GCS controls │
    │                                                        │
    └────────────────────────────────────────────────────────┘

Ports used:
  :8000   Flask REST API (GCS)
  :3000   React dev server (GCS)
  :7402   Spot SDK gRPC (Spot onboard)
  UDP     CycloneDDS discovery + data (ephemeral, multicast)

7. ROS 2 Topic Graph

                 ┌─────────────────────┐
                 │  spot_pose_publisher │
                 │  (spot_bridge pkg)   │
                 └──────────┬──────────┘
                            │ /spot/pose
                            │ geometry_msgs/PoseStamped  @20Hz
                 ┌─────────────────────┐
                 │   spot_estop_monitor │
                 │   (spot_bridge pkg)  │
                 └──────────┬──────────┘
                            │ /estop
                            │ std_msgs/Bool  @5Hz
                            │
        ┌───────────────────┼───────────────────┐
        │                   │                   │
        ▼                   ▼                   ▼
┌──────────────┐   ┌────────────────┐   ┌───────────────┐
│ /odom        │   │pursuit_         │   │ safety_monitor│
│ (V60 MCU)    │──►│controller      │   │               │
│ nav_msgs/    │   │                │   │               │
│ Odometry     │   └───────┬────────┘   └───────┬───────┘
└──────────────┘           │                    │
                           │ /mcu/command/manual_twist
                           │ geometry_msgs/Twist  @20Hz / @50Hz
                           ▼
                    ┌──────────────┐
                    │  V60 MCU     │
                    │  actuators   │
                    └──────────────┘

Topic registry:

Topic Type Publisher Subscribers Rate
/spot/pose geometry_msgs/PoseStamped spot_pose_publisher pursuit_controller 20 Hz
/estop std_msgs/Bool spot_estop_monitor pursuit_controller, safety_monitor 5 Hz
/odom nav_msgs/Odometry V60 MCU pursuit_controller ~50 Hz
/mcu/command/manual_twist geometry_msgs/Twist pursuit_controller, safety_monitor V60 MCU 20–50 Hz
/gx5/nav/odom nav_msgs/Odometry V60 IMU unit — (backup, unused v0.1) ~100 Hz

8. Control Law — Pure Pursuit

All math lives in pursuit_math.py (zero ROS dependencies, fully unit-tested).

Goal Point Computation

$$ \text{goal} = \begin{pmatrix} x_{spot} - d \cos\psi_{spot} \ y_{spot} - d \sin\psi_{spot} \end{pmatrix} $$

where $d$ = follow_distance (default 1.5 m) and $\psi_{spot}$ is Spot's yaw.

Error Terms

$$ \Delta = \text{goal} - \text{pos}_{V60}, \quad \theta_{goal} = \text{atan2}(\Delta_y, \Delta_x), \quad e_\theta = \text{wrap_to_pi}(\theta_{goal} - \psi_{V60}) $$

Control Output

Arrival zone ($|\Delta| &lt; \epsilon_{arrival}$): $$ v = 0, \quad \omega = \text{clamp}(k_{align} \cdot \text{wrap_to_pi}(\psi_{spot} - \psi_{V60}),\ \pm\omega_{max}) $$

Normal tracking: $$ v = \text{clamp}(k_v \cdot |\Delta|,\ 0,\ v_{max}) $$ $$ \omega = \text{clamp}(k_\omega \cdot e_\theta,\ \pm\omega_{max}) $$

Large heading penalty ($|e_\theta| &gt; \pi/4$): $$ v \leftarrow 0.3 \cdot v \quad \text{(30% speed cap to prevent corner-cutting)} $$

Default Gains

Parameter Value Description
follow_distance 1.5 m Desired gap
arrival_threshold 0.2 m Arrival zone radius
stale_pose_timeout 0.5 s Comms loss cutoff
max_linear_vel 1.0 m/s Speed cap
max_angular_vel 1.2 rad/s Turn rate cap
k_linear 0.8 Distance gain
k_angular 1.5 Heading gain
k_heading_align 0.8 Final alignment gain

9. Safety Architecture

Safety is implemented in independent, overlapping layers:

Layer 1 — Stale Pose Guard (pursuit_controller)
  If /spot/pose timestamp > 0.5s old → publish zero Twist immediately

Layer 2 — E-Stop Subscriber (pursuit_controller)
  If /estop = True → publish zero Twist, skip control loop

Layer 3 — SafetyMonitor node (independent process)
  If /estop = True → flood zero Twist at 50 Hz for 2.0s
  Operates even if pursuit_controller is deadlocked or crashing

Layer 4 — V60 Hardware Watchdog
  V60 MCU has an onboard watchdog — stops motors if no cmd_vel received for ~500ms

Layer 5 — Spot SDK E-Stop
  spot_estop_monitor publishes True conservatively (on any SDK failure)
  Triggers Layers 2+3 automatically on Spot connectivity loss

Why two zero-vel publishers? Layers 2 and 3 publish on the same topic. The V60 MCU processes the last received message — if pursuit_controller is stuck in a Python exception, safety_monitor still floods zeros independently.


10. Simulation Mode

ros2 launch v60_follower follow_demo.launch.py sim_mode:=true

In simulation mode:

  • figure8_publisher replaces spot_pose_publisher + spot_estop_monitor
  • No Spot SDK calls, no Spot robot needed
  • No actual V60 connection needed — /mcu/command/manual_twist is published but not consumed by real hardware
  • pursuit_controller + safety_monitor run identically to real mode

Lissajous figure-8 path: $$ x(t) = A\sin(t), \quad y(t) = B\sin(2t) $$ where $A$ = 3.0 m, $B$ = 1.5 m, $\dot{t}$ = 0.3 rad/s.


11. Deployment Model

Development (current)

GCS (bare metal Ubuntu 24.04)
  └── backend/         python app.py        → :8000
  └── frontend/        npm start             → :3000
  └── hetero_follow/   ros2 launch ...       → DDS bus

Production (Docker Compose)

services:
  backend:   spot-backend:latest   → :5000
  frontend:  spot-frontend:latest  → :3000
  nginx:     nginx:alpine          → :80 / :443  (reverse proxy)

Environment Variables

Variable Used By Default
ROBOT_IP Flask backend 192.168.1.100
ROBOT_USERNAME Flask backend
ROBOT_PASSWORD Flask backend
JWT_SECRET Flask auth
SPOT_IP spot_bridge nodes
SPOT_USERNAME spot_bridge nodes
SPOT_PASSWORD spot_bridge nodes
ROS_DOMAIN_ID all ROS 2 nodes 123
RMW_IMPLEMENTATION all ROS 2 nodes rmw_cyclonedds_cpp
CYCLONEDDS_URI CycloneDDS /home/ghost/.cyclonedds.xml (V60)

12. Key Design Decisions

D1 — Spot SDK held by both Flask and spot_bridge simultaneously

The Flask backend uses the Spot SDK for direct control and video streaming. spot_pose_publisher (ROS 2) also holds a lease for pose reading. These coexist because spot_pose_publisher acquires an independent lease and only reads state — it does not issue commands. Both can run simultaneously.

D2 — CycloneDDS over FastDDS for V60 interop

The Ghost V60 runs ROS 2 Galactic with CycloneDDS. The GCS runs ROS 2 Jazzy. CycloneDDS on both ends is the only RMW that wire-compatible across these versions on the same L2 subnet. ROS_DOMAIN_ID=123 isolates the demo bus from any other ROS 2 traffic on the network.

D3 — pursuit_math.py extracted from pursuit_controller.py

The control law math (compute_twist, wrap_to_pi, clamp, yaw_from_quaternion) has no ROS dependency. Keeping it in a separate file enables 24 unit tests to run with plain pytest — no ROS 2 runtime, no DDS daemon, no mock nodes needed.

D4 — Safety monitor as a separate process

safety_monitor is a second ROS 2 node that publishes on the same /mcu/command/manual_twist topic. Publishing zeros from a separate process means the V60 stops even if pursuit_controller's Python thread is blocked on an exception or stuck in a slow control loop.

D5 — Frame Option A (co-location assumption)

Both robots' odometry frames are assumed to coincide at launch (physical co-location). This avoids a TF2 calibration setup at the cost of a pre-flight requirement. Option B (static transform from tape-measure or AprilTag) is tracked as a future enhancement.

D6 — Mock robot fallback in Flask backend

On startup, init_robot() tries to connect to real Spot. On any failure (SDK not installed, robot unreachable, wrong credentials) it falls back to MockSpotRobotController with deterministic fake data. This allows the dashboard to be developed and demonstrated without a physical robot.


13. Limitations & Known Issues

Issue Impact Mitigation
Frame co-location (Option A) Drift accumulates over time Pre-flight alignment; Option B in roadmap
V60 odom drift Path error grows over long runs GPS-aided /gx5/nav/odom wiring planned
Spot lease exclusivity Dashboard + ROS 2 bridge both need Spot simultaneously spot_bridge uses read-only state; dashboard holds control lease
No obstacle avoidance V60 follows blindly Clear the path; hardware estop available
ROS 2 version skew (Jazzy vs Galactic) Potential message ABI break on custom types Only using standard msgs; confirmed working
sim_mode does not simulate V60 movement Controller output is ignored Open-loop verification only for now

14. Future Roadmap

  • Option B frame calibration — AprilTag or tape-measure static TF between Spot odom and V60 odom
  • GPS-aided fallback — wire /gx5/nav/odom as backup odometry source
  • n-follower support — namespaced topics for multiple V60s tracking one Spot
  • RViz2 visualization — TF tree + path visualization for demo
  • SpotCommandCenter ROS 2 bridge panel — live ROS 2 topic monitor embedded in the React dashboard
  • CI pipeline — GitHub Actions running pytest tests/ on every push (no ROS 2 required)
  • Closed-loop sim — integrate V60 kinematics so pursuit_controller output drives the simulated /odom