sl-firmware 291dd689f8 feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only
Archive STM32 firmware to legacy/stm32/:
- src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py
- test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c
- USB_CDC_BUG.md

Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol,
  stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params,
  stm32_cmd.launch.py → esp32_cmd.launch.py,
  test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node

Content cleanup across all files:
- Mamba F722S → ESP32-S3 BALANCE
- BlackPill → ESP32-S3 IO
- STM32F722/F7xx → ESP32-S3
- stm32Mode/Version/Port → esp32Mode/Version/Port
- STM32 State/Mode labels → ESP32 State/Mode
- Jetson Nano → Jetson Orin Nano Super
- /dev/stm32 → /dev/esp32
- stm32_bridge → esp32_bridge
- STM32 HAL → ESP-IDF

docs/SALTYLAB.md:
- Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28)
- Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md

TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF

No new functionality — cleanup only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:41:26 -04:00

624 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
full_stack.launch.py — One-command full autonomous stack bringup for SaltyBot.
Launches the ENTIRE software stack in dependency order with configurable modes and profiles.
Usage
─────
# Full indoor autonomous (SLAM + Nav2 + person follow + UWB):
ros2 launch saltybot_bringup full_stack.launch.py
# Indoor profile (conservative 0.2 m/s, tight geofence, no GPS):
ros2 launch saltybot_bringup full_stack.launch.py profile:=indoor
# Outdoor profile (0.5 m/s, wide geofence, GPS-enabled):
ros2 launch saltybot_bringup full_stack.launch.py profile:=outdoor
# Demo profile (tricks, dancing, social features, 0.3 m/s):
ros2 launch saltybot_bringup full_stack.launch.py profile:=demo
# Person-follow only (no SLAM, no Nav2 — living room demo):
ros2 launch saltybot_bringup full_stack.launch.py mode:=follow
# Outdoor GPS nav + person follow:
ros2 launch saltybot_bringup full_stack.launch.py mode:=outdoor
# Indoor, no CSI cameras (RealSense + RPLIDAR only):
ros2 launch saltybot_bringup full_stack.launch.py mode:=indoor enable_csi_cameras:=false
# Simulation / rosbag replay:
ros2 launch saltybot_bringup full_stack.launch.py use_sim_time:=true enable_bridge:=false
Modes
─────
indoor (default)
─ RPLIDAR + RealSense D435i sensors
─ RTAB-Map SLAM (RGB-D + LIDAR, fresh map each boot)
─ Nav2 navigation stack (planners, controllers, BT navigator)
─ CSI 4× IMX219 surround cameras (optional, default on)
─ UWB driver (2-anchor DW3000, publishes /uwb/target)
─ YOLOv8n person detection (TensorRT)
─ Person follower with UWB+camera fusion
─ cmd_vel bridge → ESP32-S3 (deadman + ramp + AUTONOMOUS gate)
─ rosbridge WebSocket (port 9090)
outdoor
─ RPLIDAR + RealSense D435i sensors (no SLAM)
─ GPS-based outdoor navigation (robot_localization EKF + Nav2)
─ UWB + person detection + follower
─ cmd_vel bridge + rosbridge
follow
─ RealSense D435i + RPLIDAR only (no SLAM, no Nav2)
─ UWB + person detection + follower
─ cmd_vel bridge + rosbridge
─ Note: obstacle avoidance disabled (no Nav2 local costmap)
Launch sequence (wall-clock delays — conservative for cold start)
─────────────────────────────────────────────────────────────────
t= 0s robot_description (URDF + TF tree)
t= 0s ESP32-S3 bridge (serial port owner — must be first)
t= 2s cmd_vel bridge (consumes /cmd_vel, needs ESP32-S3 bridge up)
t= 2s sensors (RPLIDAR + RealSense)
t= 4s UWB driver (independent serial device)
t= 4s CSI cameras (optional, independent)
t= 5s audio_pipeline (Jabra SPEAK 810: wake word + STT + TTS; Issue #503)
t= 6s SLAM / outdoor nav (needs sensors; indoor/outdoor mode only)
t= 6s person detection (needs RealSense up)
t=14s Nav2 (needs SLAM to have partial map; indoor only)
t= 9s person follower (needs perception + UWB)
t=17s rosbridge (last — exposes all topics over WebSocket)
Safety wiring
─────────────
• ESP32-S3 bridge must be up before cmd_vel bridge sends any command.
• cmd_vel bridge has 500ms deadman: stops robot if /cmd_vel goes silent.
• ESP32-S3 AUTONOMOUS mode gate (md=2) in cmd_vel bridge — robot stays still
until ESP32-S3 firmware is in AUTONOMOUS mode regardless of /cmd_vel.
• follow_enabled:=false disables person follower without stopping the node.
• To e-stop at runtime: ros2 topic pub /saltybot/estop std_msgs/Bool '{data: true}'
Topics published by this stack
───────────────────────────────
/scan RPLIDAR LaserScan
/camera/color/image_raw RealSense RGB
/camera/depth/image_rect_raw RealSense depth
/camera/imu RealSense IMU
/rtabmap/map OccupancyGrid (indoor only)
/rtabmap/odom Odometry (indoor only)
/uwb/target PoseStamped (UWB position, base_link)
/uwb/ranges UwbRangeArray (raw anchor ranges)
/person/target PoseStamped (camera position, base_link)
/person/detections Detection2DArray
/cmd_vel Twist (from follower or Nav2)
/saltybot/cmd String (to ESP32-S3)
/saltybot/imu Imu
/saltybot/balance_state String
"""
import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import (
DeclareLaunchArgument,
GroupAction,
IncludeLaunchDescription,
LogInfo,
TimerAction,
)
from launch.conditions import IfCondition
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PythonExpression
# ── Convenience: package share paths ──────────────────────────────────────────
def _pkg(name: str) -> str:
return get_package_share_directory(name)
def _launch(pkg: str, *parts: str) -> PythonLaunchDescriptionSource:
return PythonLaunchDescriptionSource(os.path.join(_pkg(pkg), *parts))
# ── Mode helpers ───────────────────────────────────────────────────────────────
def _mode_is(mode_str: str):
"""IfCondition: true when 'mode' arg == mode_str."""
return IfCondition(
PythonExpression(["'", LaunchConfiguration("mode"), f"' == '{mode_str}'"])
)
def _mode_in(*modes):
"""IfCondition: true when 'mode' arg is one of the given strings."""
mode_set = repr(set(modes))
return IfCondition(
PythonExpression(["'", LaunchConfiguration("mode"), f"' in {mode_set}"])
)
# ── Main ──────────────────────────────────────────────────────────────────────
def generate_launch_description():
# ── Launch arguments ──────────────────────────────────────────────────────
# Issue #506: Profile argument — overrides mode-based defaults
profile_arg = DeclareLaunchArgument(
"profile",
default_value="indoor",
choices=["indoor", "outdoor", "demo"],
description=(
"Launch profile (Issue #506) — overrides nav2/costmap/behavior params: "
"indoor (0.2 m/s, tight geofence, no GPS); "
"outdoor (0.5 m/s, wide geofence, GPS); "
"demo (0.3 m/s, tricks, social features)"
),
)
mode_arg = DeclareLaunchArgument(
"mode",
default_value="indoor",
choices=["indoor", "outdoor", "follow"],
description=(
"Stack mode (legacy) — indoor: SLAM+Nav2+follow; "
"outdoor: GPS nav+follow; "
"follow: sensors+UWB+perception+follower only. "
"Profiles (profile arg) take precedence over mode."
),
)
use_sim_time_arg = DeclareLaunchArgument(
"use_sim_time",
default_value="false",
description="Use /clock from rosbag/simulator instead of wall clock",
)
enable_csi_cameras_arg = DeclareLaunchArgument(
"enable_csi_cameras",
default_value="true",
description="Launch 4× IMX219 CSI surround cameras",
)
enable_uwb_arg = DeclareLaunchArgument(
"enable_uwb",
default_value="true",
description="Launch UWB driver (requires MaUWB anchors on USB)",
)
enable_perception_arg = DeclareLaunchArgument(
"enable_perception",
default_value="true",
description="Launch YOLOv8n person detection (TensorRT)",
)
enable_object_detection_arg = DeclareLaunchArgument(
"enable_object_detection",
default_value="true",
description="Launch YOLOv8n general object detection with depth (Issue #468)",
)
enable_follower_arg = DeclareLaunchArgument(
"enable_follower",
default_value="true",
description="Launch proportional person-follower controller",
)
enable_bridge_arg = DeclareLaunchArgument(
"enable_bridge",
default_value="true",
description="Launch ESP32-S3 serial bridge + cmd_vel bridge (disable for sim/rosbag)",
)
enable_rosbridge_arg = DeclareLaunchArgument(
"enable_rosbridge",
default_value="true",
description="Launch rosbridge WebSocket server (port 9090)",
)
enable_mission_logging_arg = DeclareLaunchArgument(
"enable_mission_logging",
default_value="true",
description="Launch ROS2 bag recorder for mission logging (Issue #488)",
)
enable_docking_arg = DeclareLaunchArgument(
"enable_docking",
default_value="true",
description="Launch autonomous docking behavior (auto-dock at 20% battery, Issue #489)",
)
enable_audio_pipeline_arg = DeclareLaunchArgument(
"enable_audio_pipeline",
default_value="true",
description="Audio pipeline: Jabra SPEAK 810 + wake word + STT + TTS (Issue #503)",
)
audio_stt_backend_arg = DeclareLaunchArgument(
"audio_stt_backend",
default_value="whisper",
choices=["whisper", "vosk"],
description="STT backend: whisper (CUDA) or vosk (CPU, low-latency)",
)
follow_distance_arg = DeclareLaunchArgument(
"follow_distance",
default_value="1.5",
description="Person-follower target distance (m)",
)
max_linear_vel_arg = DeclareLaunchArgument(
"max_linear_vel",
default_value="0.5",
description="Max forward velocity cap (m/s) — forwarded to both follower and cmd_vel bridge",
)
uwb_port_a_arg = DeclareLaunchArgument(
"uwb_port_a",
default_value="/dev/uwb-anchor0",
description="UWB anchor-0 serial port (port/left side)",
)
uwb_port_b_arg = DeclareLaunchArgument(
"uwb_port_b",
default_value="/dev/uwb-anchor1",
description="UWB anchor-1 serial port (starboard/right side)",
)
esp32_port_arg = DeclareLaunchArgument(
"esp32_port",
default_value="/dev/esp32-bridge",
description="ESP32-S3 USB CDC serial port",
)
# ── Shared substitution handles ───────────────────────────────────────────
# Profile argument for parameter override (Issue #506)
profile = LaunchConfiguration("profile")
mode = LaunchConfiguration("mode")
use_sim_time = LaunchConfiguration("use_sim_time")
follow_distance = LaunchConfiguration("follow_distance")
max_linear_vel = LaunchConfiguration("max_linear_vel")
uwb_port_a = LaunchConfiguration("uwb_port_a")
uwb_port_b = LaunchConfiguration("uwb_port_b")
esp32_port = LaunchConfiguration("esp32_port")
# ── t=0s Robot description (URDF + TF tree) ──────────────────────────────
robot_description = IncludeLaunchDescription(
_launch("saltybot_description", "launch", "robot_description.launch.py"),
launch_arguments={"use_sim_time": use_sim_time}.items(),
)
# ── t=0s ESP32-S3 bidirectional serial bridge ────────────────────────────────
esp32_bridge = GroupAction(
condition=IfCondition(LaunchConfiguration("enable_bridge")),
actions=[
IncludeLaunchDescription(
_launch("saltybot_bridge", "launch", "bridge.launch.py"),
launch_arguments={
"mode": "bidirectional",
"serial_port": esp32_port,
}.items(),
),
],
)
# ── t=0.5s Mission logging (bag recorder; Issue #488) ───────────────────
mission_logging = TimerAction(
period=0.5,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_mission_logging")),
actions=[
LogInfo(msg="[full_stack] Starting mission logging recorder"),
IncludeLaunchDescription(
_launch("saltybot_bag_recorder", "launch", "bag_recorder.launch.py"),
),
],
),
],
)
# ── t=2s cmd_vel safety bridge (depends on ESP32-S3 bridge) ────────────────
cmd_vel_bridge = TimerAction(
period=2.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_bridge")),
actions=[
IncludeLaunchDescription(
_launch("saltybot_bridge", "launch", "cmd_vel_bridge.launch.py"),
launch_arguments={
"max_linear_vel": max_linear_vel,
}.items(),
),
],
),
],
)
# ── t=2s Sensors: RPLIDAR + RealSense D435i ──────────────────────────────
sensors = TimerAction(
period=2.0,
actions=[
IncludeLaunchDescription(
_launch("saltybot_bringup", "launch", "sensors.launch.py"),
),
],
)
# ── t=3s LIDAR obstacle avoidance (360° safety with RPLIDAR A1M8) ────────
lidar_avoidance = TimerAction(
period=3.0,
actions=[
IncludeLaunchDescription(
_launch("saltybot_lidar_avoidance", "launch", "lidar_avoidance.launch.py"),
),
],
)
# ── t=4s Optional: 4× IMX219 CSI surround cameras ───────────────────────
csi_cameras = TimerAction(
period=4.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_csi_cameras")),
actions=[
IncludeLaunchDescription(
_launch("saltybot_cameras", "launch", "csi_cameras.launch.py"),
),
],
),
],
)
# ── t=4s UWB driver (independent serial — can start early) ──────────────
uwb_driver = TimerAction(
period=4.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_uwb")),
actions=[
IncludeLaunchDescription(
_launch("saltybot_uwb", "launch", "uwb.launch.py"),
launch_arguments={
"port_a": uwb_port_a,
"port_b": uwb_port_b,
}.items(),
),
],
),
],
)
# ── t=6s SLAM — RTAB-Map (indoor only; needs sensors up for ~4s) ─────────
audio_pipeline = TimerAction(
period=5.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_audio_pipeline")),
actions=[
LogInfo(msg="[full_stack] Starting audio pipeline (Jabra SPEAK 810)"),
IncludeLaunchDescription(
_launch("saltybot_audio_pipeline", "launch", "audio_pipeline.launch.py"),
),
],
),
],
)
slam = TimerAction(
period=6.0,
actions=[
GroupAction(
condition=_mode_is("indoor"),
actions=[
LogInfo(msg="[full_stack] Starting RTAB-Map SLAM (indoor mode)"),
IncludeLaunchDescription(
_launch("saltybot_bringup", "launch", "slam_rtabmap.launch.py"),
launch_arguments={
"use_sim_time": use_sim_time,
}.items(),
),
],
),
],
)
# ── t=6s Outdoor navigation (outdoor only; no SLAM) ─────────────────────
outdoor_nav = TimerAction(
period=6.0,
actions=[
GroupAction(
condition=_mode_is("outdoor"),
actions=[
LogInfo(msg="[full_stack] Starting outdoor GPS navigation"),
IncludeLaunchDescription(
_launch("saltybot_outdoor", "launch", "outdoor_nav.launch.py"),
launch_arguments={
"use_sim_time": use_sim_time,
}.items(),
),
],
),
],
)
# ── t=6s Person detection (needs RealSense up for ~4s) ──────────────────
perception = TimerAction(
period=6.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_perception")),
actions=[
LogInfo(msg="[full_stack] Starting YOLOv8n person detection"),
IncludeLaunchDescription(
_launch("saltybot_perception", "launch", "person_detection.launch.py"),
launch_arguments={
"use_sim_time": use_sim_time,
}.items(),
),
],
),
],
)
# ── t=6s Object detection (needs RealSense up for ~4s; Issue #468) ──────
object_detection = TimerAction(
period=6.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_object_detection")),
actions=[
LogInfo(msg="[full_stack] Starting YOLOv8n general object detection"),
IncludeLaunchDescription(
_launch("saltybot_object_detection", "launch", "object_detection.launch.py"),
),
],
),
],
)
# ── t=9s Person follower (needs perception + UWB; ~3s after both start) ─
follower = TimerAction(
period=9.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_follower")),
actions=[
LogInfo(msg="[full_stack] Starting person follower"),
IncludeLaunchDescription(
_launch("saltybot_follower", "launch", "person_follower.launch.py"),
launch_arguments={
"follow_distance": follow_distance,
"max_linear_vel": max_linear_vel,
}.items(),
),
],
),
],
)
# ── t=7s Docking (auto-dock at 20% battery, Issue #489) ──────────────────
docking = TimerAction(
period=7.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_docking")),
actions=[
LogInfo(msg="[full_stack] Starting autonomous docking (auto-trigger at 20% battery)"),
IncludeLaunchDescription(
_launch("saltybot_docking", "launch", "docking.launch.py"),
launch_arguments={
"battery_low_pct": "20.0",
}.items(),
),
],
),
],
)
# ── t=14s Nav2 (indoor only; SLAM needs ~8s to build initial map) ────────
nav2 = TimerAction(
period=14.0,
actions=[
GroupAction(
condition=_mode_is("indoor"),
actions=[
LogInfo(msg="[full_stack] Starting Nav2 navigation stack"),
IncludeLaunchDescription(
_launch("saltybot_bringup", "launch", "nav2.launch.py"),
),
],
),
],
)
# ── t=17s rosbridge WebSocket (last — all topics must be alive) ──────────
rosbridge = TimerAction(
period=17.0,
actions=[
GroupAction(
condition=IfCondition(LaunchConfiguration("enable_rosbridge")),
actions=[
LogInfo(msg="[full_stack] Starting rosbridge (ws://0.0.0.0:9090)"),
IncludeLaunchDescription(
_launch("saltybot_bringup", "launch", "rosbridge.launch.py"),
),
],
),
],
)
# ── Startup banner ────────────────────────────────────────────────────────
banner = LogInfo(
msg=["[full_stack] SaltyBot bringup starting — mode=", mode,
" sim_time=", use_sim_time]
)
# ── Assemble ──────────────────────────────────────────────────────────────
return LaunchDescription([
# Arguments
mode_arg,
use_sim_time_arg,
enable_csi_cameras_arg,
enable_uwb_arg,
enable_perception_arg,
enable_object_detection_arg,
enable_follower_arg,
enable_bridge_arg,
enable_rosbridge_arg,
enable_mission_logging_arg,
enable_docking_arg,
enable_audio_pipeline_arg,
audio_stt_backend_arg,
follow_distance_arg,
max_linear_vel_arg,
uwb_port_a_arg,
uwb_port_b_arg,
esp32_port_arg,
# Startup banner
banner,
# t=0s
robot_description,
esp32_bridge,
# t=0.5s
mission_logging,
# t=2s
sensors,
cmd_vel_bridge,
# t=3s
lidar_avoidance,
# t=4s
csi_cameras,
uwb_driver,
# t=5s
audio_pipeline,
# t=6s
slam,
outdoor_nav,
perception,
object_detection,
# t=7s
docking,
# t=9s
follower,
# t=14s
nav2,
# t=17s
rosbridge,
])