a506989af6
feat: CANable 2.0 bringup with udev rule and systemd service (Issue #643 )
...
- udev: 70-canable.rules — gs_usb VID/PID 1d50:606f, names iface can0 and brings it up at 500 kbps on plug-in
- systemd: can-bringup.service — oneshot service bound to sys-subsystem-net-devices-can0.device
- scripts: can_setup.sh — manual up/down/verify helper; candump verify for VESC IDs 61 (0x3D) and 79 (0x4F)
- install_systemd.sh updated to install can-bringup.service and all udev rules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:49:21 -04:00
4035b4cfc3
feat: ROS2 launch orchestrator for full SaltyBot bringup (Issue #577 )
...
Adds saltybot_bringup.launch.py with ordered startup groups (drivers→
perception→navigation→UI), timer-based health gates, configurable
profiles (minimal/full/debug), and estop on Ctrl-C shutdown.
Also adds launch_profiles.py dataclass module and 53-test coverage for
profile hierarchy, timing gates, safety bounds, and to_dict serialization.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:57:57 -04:00
14164089dc
feat: Audio pipeline end-to-end (Issue #503 )
...
- Add VoskSTT class to audio_utils.py: offline Vosk STT backend as
low-latency CPU alternative to Whisper for Jetson deployments
- Update audio_pipeline_node.py: stt_backend param ("whisper"/"vosk"),
Vosk loading with Whisper fallback, CPU auto-detection for Whisper,
dual-backend _process_utterance dispatch, STT/<backend> log prefix
- Update audio_pipeline_params.yaml: add stt_backend and vosk_model_path
- Add test/test_audio_pipeline.py: 40 unit tests covering EnergyVAD,
PCM conversion, AudioBuffer, UtteranceSegmenter, VoskSTT, JabraAudioDevice,
AudioMetrics, AudioState
- Integrate into full_stack.launch.py: audio_pipeline at t=5s with
enable_audio_pipeline and audio_stt_backend args
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:03:31 -05:00
f71fdae747
feat: Depth-to-costmap plugin for RealSense D435i (Issue #532 )
...
Add saltybot_depth_costmap — a Nav2 costmap2d plugin that converts
D435i depth images directly into obstacle markings on both local and
global costmaps.
Pipeline:
1. Subscribe to /camera/depth/image_rect_raw (16UC1 mm) + camera_info
2. Back-project depth pixels to 3D using pinhole camera intrinsics
3. Transform points to costmap global_frame via TF2
4. Apply configurable height filter (min_height..max_height above ground)
5. Mark obstacle cells as LETHAL_OBSTACLE
6. Inflate neighbours within inflation_radius as INSCRIBED_INFLATED_OBSTACLE
Parameters:
min_height: 0.05 m — floor clearance (ignores ground returns)
max_height: 0.80 m — ceiling cutoff (ignores lights/ceiling)
obstacle_range: 3.5 m — max marking distance from camera
clearing_range: 4.0 m — max distance processed at all
inflation_radius: 0.10 m — in-layer inflation (works before inflation_layer)
downsample_factor: 4 — process 1 of N rows+cols (~19k pts @ 640×480)
Integration (#478 ):
- Added depth_costmap_layer to local_costmap plugins list
- Added depth_costmap_layer to global_costmap plugins list
- Plugin registered via pluginlib (plugin.xml)
Files:
jetson/ros2_ws/src/saltybot_depth_costmap/
CMakeLists.txt, package.xml, plugin.xml
include/saltybot_depth_costmap/depth_costmap_layer.hpp
src/depth_costmap_layer.cpp
jetson/ros2_ws/src/saltybot_bringup/config/nav2_params.yaml (updated)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:52:18 -05:00
8d67d06857
feat: Integration test suite (Issue #504 )
...
Add comprehensive integration testing for complete ROS2 system stack:
Integration Tests (test_integration_full_stack.py):
- Verifies all ROS2 nodes launch successfully
- Checks critical topics are published (sensors, nav, control)
- Validates system component health and stability
- Tests launch file validity and configuration
- Covers indoor/outdoor/follow modes
Launch Testing (test_launch_full_stack.py):
- Validates launch file syntax and configuration
- Verifies all required packages are installed
- Checks launch sequence timing
- Validates conditional logic for optional components
Test Coverage:
✓ SLAM/RTAB-Map (indoor mode)
✓ Nav2 navigation stack
✓ Perception (YOLOv8n person detection)
✓ Control (cmd_vel bridge, STM32 bridge)
✓ Audio pipeline and monitoring
✓ Sensors (LIDAR, RealSense, UWB, CSI cameras)
✓ Battery and temperature monitoring
✓ Autonomous docking behavior
✓ TF2 tree and odometry
Usage:
pytest test/test_integration_full_stack.py -v
pytest test/test_launch_full_stack.py -v
Documentation:
See test/README_INTEGRATION_TESTS.md for detailed information.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
e5329391bc
feat: Add parameter profile YAML files for Nav2 (Issue #506 )
...
- profile_indoor.yaml: Conservative settings (0.4 m/s, 0.35m inflation)
- profile_outdoor.yaml: Moderate settings (0.8 m/s, 0.3m inflation)
- profile_demo.yaml: Agile settings (0.6 m/s, 0.32m inflation)
Each profile customizes velocity limits, costmap inflation, and obstacle detection.
2026-03-06 16:42:31 -05:00
5d17b6c501
feat: Issue #506 — Update nav2.launch.py for profile support
...
Add profile argument to nav2.launch.py to accept launch profile parameter
and log profile selection for debugging/monitoring.
Changes:
- Add profile_arg declaration with choices (indoor/outdoor/demo)
- Add profile substitution and log output
- Update docstring with profile documentation
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
b5acb32ee6
feat: Issue #506 — Update full_stack.launch.py for profile support
...
Add profile argument and documentation to full_stack.launch.py for
Issue #506 launch parameter profiles. Updated to support:
- profile:=indoor (conservative)
- profile:=outdoor (moderate)
- profile:=demo (agile with tricks/social features)
Changes:
- Add profile_arg declaration
- Add profile substitution handle
- Update docstring with profile examples
- Ready for profile-based Nav2 parameter overrides
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
bbfcd2a9d1
feat: Issue #506 — Launch parameter profiles (indoor/outdoor/demo)
...
Implement profile-based parameter overrides for Nav2, costmap, and behavior
server configurations. Profiles predefine parameter sets for different
deployment scenarios.
New files:
- config/profiles/indoor.yaml: Conservative (0.2 m/s, tight geofence, no GPS)
- config/profiles/outdoor.yaml: Moderate (0.5 m/s, wide geofence, GPS-enabled)
- config/profiles/demo.yaml: Agile (0.3 m/s, tricks/social features enabled)
- saltybot_bringup/profile_loader.py: YAML loader and parameter merger utility
Supports: ros2 launch saltybot_bringup full_stack.launch.py profile:=<profile>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
d97fa5fab0
Merge remote-tracking branch 'origin/sl-webui/issue-482-behavior-tree'
...
# Conflicts:
# jetson/ros2_ws/src/saltybot_bringup/behavior_trees/autonomous_coordinator.xml
# jetson/ros2_ws/src/saltybot_bringup/launch/autonomous_mode.launch.py
2026-03-06 11:43:26 -05:00
a3d3ea1471
Merge remote-tracking branch 'origin/sl-perception/issue-478-costmaps'
...
# Conflicts:
# jetson/ros2_ws/src/saltybot_bringup/config/nav2_params.yaml
2026-03-06 11:43:11 -05:00
Sebastien Vayrette
868b453777
fix: resolve merge conflicts for voice router PR #499 (keep both docking + mission logging)
2026-03-05 19:25:23 -05:00
b986702aed
feat: Docking station behavior for auto-charging (Issue #489 )
...
- Integrate saltybot_docking package into full_stack.launch.py
- Auto-trigger docking when battery drops to 20% (configurable via battery_low_pct)
- Launch docking at t=7s (after sensors, before Nav2)
- Add /saltybot/docking_state publisher (std_msgs/String) for state monitoring
- Update docking_params.yaml:
- battery_low_pct: 15% → 20% per Issue #489
- Add references to Issue #475 for conservative FC+hoverboard speeds
- Docking behavior includes:
- ArUco marker or IR beacon detection for dock location
- Nav2-based approach to pre-dock pose (~1m away)
- Visual servoing final alignment with contact detection
- Auto-undocking on full charge (80%) or command
- Integration with power management for mission interruption/resumption
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:08:21 -05:00
6d6909d9d9
feat: Voice command router (Issue #491 )
...
Natural language voice command routing with fuzzy matching for speech variations.
Supported Commands:
- Follow me / Come with me
- Stop / Halt / Freeze
- Go home / Return to dock / Charge
- Patrol / Autonomous mode
- Come here / Approach
- Sit / Sit down
- Spin / Rotate / Turn around
- Dance / Groove
- Take photo / Picture / Smile
- What's that / Identify / Recognize
- Battery status / Battery level
Features:
- Fuzzy matching (rapidfuzz token_set_ratio) with 75% threshold
- Multiple pattern support per command for natural variations
- Three routing types: velocity (/cmd_vel), actions (/saltybot/action_command), services
- Command monitoring via /saltybot/voice_command
- Graceful handling of unrecognized speech
Architecture:
- Input: /saltybot/speech/transcribed_text (lowercase text)
- Fuzzy match against 11 command groups with 40+ patterns
- Route to: /cmd_vel (velocity), /saltybot/action_command (actions), or services
Files:
- saltybot_voice_router_node.py: Main router with fuzzy matching
- launch/voice_router.launch.py: Launch configuration
- VOICE_ROUTER_README.md: Usage documentation
Dependencies:
- rapidfuzz: Fuzzy string matching for natural speech handling
- rclpy, std_msgs, geometry_msgs: ROS2 core
Performance: <100ms per command (fuzzy matching + routing)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:05:02 -05:00
285178b2f9
feat: Configure Nav2 navigation stack conservative speeds (Issue #475 )
...
- Update max_vel_x to 0.3 m/s (conservative for FC + hoverboard ESC)
- Update max_vel_theta to 0.5 rad/s (conservative for FC + hoverboard ESC)
- Set robot_radius to 0.22 m for 0.4m x 0.4m footprint
- Configure velocity smoother with conservative limits
- Both DWB local planner and velocity smoother updated for consistency
- RPLIDAR (/scan) + depth_to_laserscan (/depth_scan) costmap layers enabled
- NavFn global planner, DWB local planner configured
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:50:41 -05:00
56a48b4e25
Merge PR #486 : Issue #480 - Map save/load
2026-03-05 14:48:59 -05:00
f5877756d5
feat: Map save/load service for SLAM Toolbox persistence (Issue #480 )
...
Implement automatic map serialization and persistence for slam_toolbox:
- New SlamToolboxPersistenceNode with auto-save every 5 minutes
- Auto-load most recent map on startup
- Services: /saltybot/save_map, /saltybot/load_map, /saltybot/list_maps
- Export to Nav2-compatible YAML + PGM format
- Stores maps in ~/.saltybot-data/maps/ with .posegraph format
- Integrates with slam_toolbox serialize/deserialize services
Changes:
- Created saltybot_mapping/slam_toolbox_persistence.py
- Added slam_toolbox_persistence.launch.py
- Updated slam.launch.py to include persistence service
- Updated CMakeLists.txt to install new executable
- Added slam_toolbox dependency to package.xml
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:46:33 -05:00
50d1ebbdcc
feat: Behavior tree coordinator for autonomous mode (Issue #482 )
...
State machine: idle → patrol → investigate → interact → return
- IDLE: Waiting for activation or battery recovery
- PATROL: Autonomous patrolling with curiosity-driven exploration (#470 )
- INVESTIGATE: Approach and track detected persons
- INTERACT: Social interaction with face recognition and gestures
- RETURN: Navigate back to home/dock with recovery behaviors
Integrations:
- Patrol mode (#446 ): Waypoint routes with geofence (#441 )
- Curiosity behavior (#470 ): Autonomous exploration
- Person following: Approach detected persons
- Emergency stop cascade (#459 ): Highest priority safety
- Geofence constraint (#441 ): Keep patrol within boundaries
Blackboard variables:
- battery_level: Current battery percentage
- person_detected: Person detection state
- current_mode: Current behavior tree state
- home_pose: Home/dock location
Files:
- behavior_trees/autonomous_coordinator.xml: BehaviorTree definition
- launch/autonomous_mode.launch.py: Full launch setup
- saltybot_bringup/bt_nodes.py: Custom BT node plugins
- BEHAVIOR_TREE_README.md: Documentation
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:42:57 -05:00
e66bcc2ab0
feat: Configure Nav2 costmaps (Issue #478 )
...
- Create nav2_params.yaml in saltybot_bringup/config/
- Global costmap: static layer + obstacle layer with /scan + /depth_scan
- Local costmap: rolling window 3m×3m, voxel layer + obstacle layer
- Inflation radius: 0.3m (robot_radius 0.15m + 0.15m padding)
- Robot footprint: 0.4m × 0.4m square [-0.2, +0.2] in x, y
- RPLIDAR A1M8 at /scan (360° LaserScan)
- RealSense D435i via depth_to_laserscan at /depth_scan
- Surround vision support for dynamic obstacles
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:41:41 -05:00
fd742f6890
feat: Configure Nav2 recovery behaviors (Issue #479 )
...
Implement conservative recovery behaviors for autonomous navigation on FC + Hoverboard ESC drivetrain.
Recovery Sequence (round-robin, 6 retries):
1. Clear costmaps (local + global)
2. Spin 90° @ 0.5 rad/s max (conservative for self-balancer)
3. Wait 5 seconds (allow dynamic obstacles to move)
4. Backup 0.3m @ 0.1 m/s (deadlock escape, very conservative)
Configuration:
- backup: 0.3m reverse, 0.1 m/s speed, 5s timeout
- spin: 90° rotation, 0.5 rad/s max angular velocity
- wait: 5-second pause for obstacle clearing
- progress_checker: 20cm minimum movement threshold in 10s window
Safety:
- E-stop (Issue #459 ) takes priority over recovery behaviors
- Emergency stop system runs independently on STM32 firmware
- Conservative speeds for FC + Hoverboard ESC stability
Files Modified:
- jetson/config/nav2_params.yaml: behavior_server parameters
- jetson/ros2_ws/src/saltybot_bringup/behavior_trees/navigate_to_pose_with_recovery.xml: BT updates
- jetson/config/RECOVERY_BEHAVIORS.md: Configuration documentation
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:40:45 -05:00
d7051fe854
feat: Add Issue #467 - Power management supervisor with battery protection
...
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 11s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
- New ROS2 node: power_supervisor_node for battery state monitoring
- Battery thresholds: 30% warning, 20% dock search, 10% graceful shutdown, 5% force kill
- Charge cycle tracking and battery health estimation
- CSV logging to battery_log.csv for external analysis
- Publishes /saltybot/power_state for MQTT relay
- Graceful shutdown cascade: save state, stop motors, disarm on critical low battery
- Replaces/extends Issue #125 battery_node with supervisor-level power management
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:34:37 -05:00
c96c68a7c4
feat: YOLOv8n object detection with RealSense depth integration (Issue #468 )
...
- saltybot_object_detection_msgs: DetectedObject, DetectedObjectArray, QueryObjects.srv
- saltybot_object_detection: YOLOv8n TensorRT FP16 node with depth projection
- Message filters for RGB-depth sync, TF2 transform to base_link
- Configurable confidence and class filtering (COCO 80 classes)
- Query service for voice integration ("whats in front of you")
- TensorRT build script with ONNX fallback
- Launch file with parameter configuration
- Full stack integration at t=6s (30 FPS target alongside person tracker)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:14:50 -05:00
ab5dd97fcb
feat: add consolidated params and stack state monitor (Issue #447 )
2026-03-05 09:07:17 -05:00
e35bd949c0
feat: Integrate 360° LIDAR obstacle avoidance into full_stack (Issue #364 )
...
Adds saltybot_lidar_avoidance node to the launch sequence at t=3s.
The RPLIDAR A1M8 node was already fully implemented with:
- Emergency stop if obstacle detected within 0.5m
- Speed-dependent safety zones (0.6m @ 0 m/s → 3.0m @ 5.56 m/s)
- Forward scanning window (±30° cone)
- Debounced obstacle detection (2-frame debounce)
- Publishes /cmd_vel_safe with filtered velocity commands
Integrates seamlessly with Nav2 stack and cmd_vel_mux multiplexer.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:23:43 -05:00
cfa8ee111d
Merge pull request 'feat: Replace GNOME with Cage+Chromium kiosk (Issue #374 )' ( #377 ) from sl-webui/issue-374-cage-kiosk into main
2026-03-03 17:46:14 -05:00
b04fd916ff
Merge pull request 'feat: MageDok 7in display setup for Orin (Issue #369 )' ( #373 ) from sl-webui/issue-369-display-setup into main
2026-03-03 17:20:15 -05:00
042c0529a1
feat: Add Issue #375 — adaptive camera power mode manager
...
Implements a 5-mode FSM for dynamic sensor activation based on speed,
scenario, and battery level — avoids running all 4 CSI cameras + full
sensor suite when unnecessary, saving ~1 GB RAM and significant compute.
Five modes (sensor sets):
SLEEP — no sensors (~150 MB RAM)
SOCIAL — webcam only (~400 MB RAM, parked/socialising)
AWARE — front CSI + RealSense + LIDAR (~850 MB RAM, indoor/<5km/h)
ACTIVE — front+rear CSI + RealSense + LIDAR + UWB (~1.15 GB, 5-15km/h)
FULL — all 4 CSI + RealSense + LIDAR + UWB (~1.55 GB, >15km/h)
Core library — _camera_power_manager.py (pure Python, no ROS2 deps)
- CameraPowerFSM.update(speed_mps, scenario, battery_pct) → ModeDecision
- Speed-driven upgrades: instant (safety-first)
- Speed-driven downgrades: held for downgrade_hold_s (default 5s, anti-flap)
- Scenario overrides (instant, bypass hysteresis):
· CROSSING / EMERGENCY → FULL always
· PARKED → SOCIAL immediately
· INDOOR → cap at AWARE (never ACTIVE/FULL indoors)
- Battery low cap: battery_pct < threshold → cap at AWARE
- Idle timer: near-zero speed holds at AWARE for idle_to_social_s (30s)
before dropping to SOCIAL (avoids cycling at traffic lights)
ROS2 node — camera_power_node.py
- Subscribes: /saltybot/speed, /saltybot/scenario, /saltybot/battery_pct
- Publishes: /saltybot/camera_mode (CameraPowerMode, latched, 2 Hz)
- Publishes: /saltybot/camera_cmd/{front,rear,left,right,realsense,lidar,uwb,webcam}
(std_msgs/Bool, TRANSIENT_LOCAL so late subscribers get last state)
- Logs mode transitions with speed/scenario/battery context
Tests — test/test_camera_power_manager.py: 64/64 passing
- Sensor configs: counts, correct flags per mode, safety invariants
- Speed upgrades: instantaneous at all thresholds, no hold required
- Downgrade hysteresis: hold timer, cancellation on speed spike, hold=0 instant
- Scenario overrides: CROSSING/EMERGENCY/PARKED/INDOOR, all CSIs on crossing
- Battery low: cap at AWARE, threshold boundary
- Idle timer: delay AWARE→SOCIAL, motion resets timer
- Reset, labels, ModeDecision fields
- Integration: full ride scenario (walk→jog→sprint→crossing→indoor→park→low bat)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:48:17 -05:00
e2587b60fb
feat: SaltyFace web app UI for Chromium kiosk (Issue #370 )
...
Animated robot expression interface as lightweight web application:
**Architecture:**
- HTML5 Canvas rendering engine
- Node.js HTTP server (localhost:3000)
- ROSLIB WebSocket bridge for ROS2 topics
- Fullscreen responsive design (1024×600)
**Features:**
- 8 emotional states (happy, alert, confused, sleeping, excited, emergency, listening, talking)
- Real-time ROS2 subscriptions:
- /saltybot/state (emotion triggers)
- /saltybot/battery (status display)
- /saltybot/target_track (EXCITED emotion)
- /saltybot/obstacles (ALERT emotion)
- /social/speech/is_speaking (TALKING emotion)
- /social/speech/is_listening (LISTENING emotion)
- Tap-to-toggle status overlay
- 60fps Canvas animation on Wayland
- ~80MB total memory (Node.js + browser)
**Files:**
- public/index.html — Main page (1024×600 fullscreen)
- public/salty-face.js — Canvas rendering + ROS2 integration
- server.js — Node.js HTTP server with CORS support
- systemd/salty-face-server.service — Auto-start systemd service
- docs/SALTY_FACE_WEB_APP.md — Complete setup & API documentation
**Integration:**
- Runs in Chromium kiosk (Issue #374 )
- Depends on rosbridge_server for WebSocket bridge
- Serves on localhost:3000 (configurable)
**Next:** Issue #371 (Accessibility enhancements)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 16:42:41 -05:00
82b8f40b39
feat: Replace GNOME with Cage + Chromium kiosk (Issue #374 )
...
Lightweight fullscreen kiosk for MageDok 7" display:
**Architecture:**
- Cage: Minimal Wayland compositor (replaces GNOME)
- Chromium: Fullscreen kiosk browser for SaltyFace web UI
- PulseAudio: HDMI audio routing (from Issue #369 )
- Touch: HID input from MageDok USB device
**Memory Savings:**
- GNOME desktop: ~650MB RAM
- Cage + Chromium: ~200MB RAM
- Net gain: ~450MB for ROS2 workloads
**Files:**
- config/cage-magedok.ini — Cage display settings (1024×600@60Hz)
- config/wayland-magedok.conf — Wayland output configuration
- scripts/chromium_kiosk.sh — Cage + Chromium launcher
- systemd/chromium-kiosk.service — Auto-start systemd service
- launch/cage_display.launch.py — ROS2 launch configuration
- docs/CAGE_CHROMIUM_KIOSK.md — Complete setup & troubleshooting guide
**Next:** Issue #370 (Salty Face as web app in Chromium kiosk)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 16:41:00 -05:00
6592b58f65
feat: Add Issue #350 — smooth velocity ramp controller
...
Adds a rate-limiting shim between raw /cmd_vel and the drive stack to
prevent wheel slip, tipping, and jerky motion from step velocity inputs.
Core library — _velocity_ramp.py (pure Python, no ROS2 deps)
- VelocityRamp: applies independent accel/decel limits to linear-x and
angular-z with configurable max_lin_accel, max_lin_decel,
max_ang_accel, max_ang_decel
- _ramp_axis(): per-axis rate limiter with correct accel/decel selection
(decel when |target| < |current| or sign reversal; accel otherwise)
- Emergency stop: step(0.0, 0.0) bypasses ramp → immediate zero output
- Asymmetric limits supported (e.g. faster decel than accel)
ROS2 node — velocity_ramp_node.py
- Subscribes /cmd_vel, publishes /cmd_vel_smooth at configurable rate_hz
- Parameters: max_lin_accel (0.5 m/s²), max_lin_decel (0.5 m/s²),
max_ang_accel (1.0 rad/s²), max_ang_decel (1.0 rad/s²), rate_hz (50)
Tests — test/test_velocity_ramp.py: 50/50 passing
- _ramp_axis: accel/decel selection, sign reversal, overshoot prevention
- Construction: invalid params raise ValueError, defaults verified
- Linear/angular ramp-up: step size, target reached, no overshoot
- Deceleration: asymmetric limits, partial decel (non-zero target)
- Emergency stop: immediate zero, state cleared, resume from zero
- Sign reversal: passes through zero without jumping
- Reset: state cleared, next ramp starts from zero
- Monotonicity: linear and angular outputs are monotone toward target
- Rate accuracy: 50Hz/10Hz step sizes, 100-step convergence verified
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:45:05 -05:00
45d456049a
feat: MageDok 7in display setup for Jetson Orin (Issue #369 )
...
Add complete display integration for MageDok 7" IPS touchscreen:
Configuration Files:
- X11 display config (xorg-magedok.conf) — 1024×600 @ 60Hz
- PulseAudio routing (pulseaudio-magedok.conf) — HDMI audio to speakers
- Udev rules (90-magedok-touch.rules) — USB touch device permissions
- Systemd service (magedok-display.service) — auto-start on boot
ROS2 Launch:
- magedok_display.launch.py — coordinate display/touch/audio setup
Helper Scripts:
- verify_display.py — validate 1024×600 resolution via xrandr
- touch_monitor.py — detect MageDok USB touch, publish status
- audio_router.py — configure PulseAudio HDMI sink routing
Documentation:
- MAGEDOK_DISPLAY_SETUP.md — complete installation and troubleshooting guide
Features:
✓ DisplayPort → HDMI video from Orin DP connector
✓ USB touch input as HID device (driver-free)
✓ HDMI audio routing to built-in speakers
✓ 1024×600 native resolution verification
✓ Systemd auto-launch on boot (no login prompt)
✓ Headless fallback when display disconnected
✓ ROS2 status monitoring (touch/audio/resolution)
Supports Salty Face UI (Issue #370 ) and accessibility features (Issue #371 )
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 15:44:03 -05:00
0ecf341c57
feat: Add Issue #365 — UWB DW3000 anchor/tag tracking (bearing + distance)
...
Software-complete implementation of the two-anchor UWB ranging stack.
All ROS2 / serial code written against an abstract interface so tests run
without physical hardware (anchors on order).
New message
- UwbTarget.msg: valid, bearing_deg, distance_m, confidence,
anchor0/1_dist_m, baseline_m, fix_quality (0=none 1=single 2=dual)
Core library — _uwb_tracker.py (pure Python, no ROS2/runtime deps)
- parse_frame(): ASCII RANGE,<id>,<tag>,<mm> protocol decoder
- bearing_from_ranges(): law-of-cosines 2-anchor bearing with confidence
(penalises extreme angles + close-range geometry)
- bearing_single_anchor(): fallback bearing=0, conf≤0.3
- BearingKalman: 1-D constant-velocity Kalman filter [bearing, rate]
- UwbRangingState: thread-safe per-anchor state + stale timeout + Kalman
- AnchorSerialReader: background thread, readline() interface (real or mock)
ROS2 node — uwb_node.py
- Opens /dev/ttyUSB0 + /dev/ttyUSB1 (configurable)
- Non-fatal serial open failure (will publish FIX_NONE until plugged in)
- Publishes /saltybot/uwb_target at 10 Hz (configurable)
- Graceful shutdown: stops reader threads
Tests — test/test_uwb_tracker.py: 64/64 passing
- Frame parsing: valid, malformed, STATUS, CR/LF, mm→m conversion
- Bearing geometry: straight-ahead, ±45°, ±30°, symmetry, confidence
- Kalman: seeding, smoothing, convergence, rate tracking
- UwbRangingState: single/dual fix, stale timeout, thread safety
- AnchorSerialReader: mock serial, bytes decode, stop()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:25:23 -05:00
c620dc51a7
feat: Add Issue #363 — P0 person tracking for follow-me mode
...
Implements real-time person detection + tracking pipeline for the
follow-me motion controller on Jetson Orin Nano Super (D435i).
Core components
- TargetTrack.msg: bearing_deg, distance_m, confidence, bbox, vel_bearing_dps,
vel_dist_mps, depth_quality (0-3)
- _person_tracker.py (pure-Python, no ROS2/runtime deps):
· 8-state constant-velocity Kalman filter [cx,cy,w,h,vcx,vcy,vw,vh]
· Greedy IoU data association
· HSV torso colour histogram re-ID (16H×8S, Bhattacharyya similarity)
with fixed saturation clamping (s = (cmax−cmin)/cmax, clipped to [0,1])
· FollowTargetSelector: nearest person auto-lock, hold_frames hysteresis
· TENTATIVE→ACTIVE after min_hits; LOST track removal after max_lost_frames
with per-frame lost_age increment across all LOST tracks
· bearing_from_pixel, depth_at_bbox (median, quality flags)
- person_tracking_node.py:
· YOLOv8n via ultralytics (TRT FP16 on first run) → HOG+SVM fallback
· Subscribes colour + depth + camera_info + follow_start/stop
· Publishes /saltybot/target_track at ≤30 fps
- test/test_person_tracker.py: 59/59 tests passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:19:02 -05:00
672120bb50
feat(perception): geometric face emotion classifier (Issue #359 )
...
Classifies facial expressions into neutral/happy/surprised/angry/sad
using geometric rules over MediaPipe Face Mesh landmarks — no ML model
required at runtime.
Rules
-----
surprised: brow_raise > 0.12 AND eye_open > 0.07 AND mouth_open > 0.07
happy: smile > 0.025 (lip corners above lip midpoint)
angry: brow_furl > 0.02 AND smile < 0.01
sad: smile < -0.025 AND brow_furl < 0.015
neutral: default
Changes
-------
- saltybot_scene_msgs/msg/FaceEmotion.msg — per-face emotion + features
- saltybot_scene_msgs/msg/FaceEmotionArray.msg
- saltybot_scene_msgs/CMakeLists.txt — register new msgs
- _face_emotion.py — pure-Python: FaceLandmarks, compute_features,
classify_emotion, detect_emotion, from_mediapipe
- face_emotion_node.py — subscribes /camera/color/image_raw,
publishes /saltybot/face_emotions (≤15 fps)
- test/test_face_emotion.py — 48 tests, all passing
- setup.py — add face_emotion entry point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:39:49 -05:00
677e6eb75e
feat(perception): MFCC nearest-centroid audio scene classifier (Issue #353 )
...
Classifies ambient audio into indoor/outdoor/traffic/park at 1 Hz using
a 16-d feature vector (13 MFCC + spectral centroid + rolloff + ZCR) with
a normalised nearest-centroid classifier. Centroids are computed at import
time from seeded synthetic prototypes, ensuring deterministic behaviour.
Changes
-------
- saltybot_scene_msgs/msg/AudioScene.msg — label + confidence + features[16]
- saltybot_scene_msgs/CMakeLists.txt — register AudioScene.msg
- _audio_scene.py — pure-numpy feature extraction + NearestCentroidClassifier
- audio_scene_node.py — subscribes /audio/audio, publishes /saltybot/audio_scene
- test/test_audio_scene.py — 53 tests (all passing) with synthetic audio
- setup.py — add audio_scene entry point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:03:11 -05:00
2a9b03dd76
feat(perception): depth-based obstacle size estimator (Issue #348 )
...
Projects LIDAR clusters into the D435i depth image to estimate 3-D
obstacle width and height in metres.
- saltybot_scene_msgs/msg/ObstacleSize.msg — new message
- saltybot_scene_msgs/msg/ObstacleSizeArray.msg — array wrapper
- saltybot_scene_msgs/CMakeLists.txt — register new msgs
- saltybot_bringup/_obstacle_size.py — pure-Python helper:
CameraParams (intrinsics + LIDAR→camera extrinsics)
ObstacleSizeEstimate (NamedTuple)
lidar_to_camera() LIDAR frame → camera frame transform
project_to_pixel() pinhole projection + bounds check
sample_depth_median() uint16 depth image window → median metres
estimate_height() vertical strip scan for row extent → height_m
estimate_cluster_size() full pipeline: cluster → size estimate
- saltybot_bringup/obstacle_size_node.py — ROS2 node
sub: /scan, /camera/depth/image_rect_raw, /camera/depth/camera_info
pub: /saltybot/obstacle_sizes (ObstacleSizeArray)
width from LIDAR bbox; height from depth strip back-projection;
graceful fallback (LIDAR-only) when depth image unavailable;
intrinsics latched from CameraInfo on first arrival
- test/test_obstacle_size.py — 33 tests, 33 passing
- setup.py — add obstacle_size entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:32:41 -05:00
bd9cb6da35
feat(perception): lane/path edge detector (Issue #339 )
...
Adds Canny+Hough+bird-eye perspective pipeline for detecting left/right
path edges from the forward camera. Pure-Python helper (_path_edges.py)
is fully tested; ROS2 node publishes PathEdges on /saltybot/path_edges.
- saltybot_scene_msgs/msg/PathEdges.msg — new message
- saltybot_scene_msgs/CMakeLists.txt — register PathEdges.msg
- saltybot_bringup/_path_edges.py — PathEdgeConfig, PathEdgesResult,
build/apply_homography, canny_edges,
hough_lines, classify_lines,
average_line, warp_segments,
process_frame
- saltybot_bringup/path_edges_node.py — ROS2 node (sensor_msgs/Image →
PathEdges, parameters for all
tunable Canny/Hough/birdseye params)
- test/test_path_edges.py — 38 tests, 38 passing
- setup.py — add path_edges console_script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 11:33:22 -05:00
eb61207532
feat(perception): dynamic obstacle velocity estimator (Issue #326 )
...
Adds ObstacleVelocity/ObstacleVelocityArray msgs and an
ObstacleVelocityNode that clusters /scan points, tracks each centroid
with a constant-velocity Kalman filter, and publishes velocity vectors
on /saltybot/obstacle_velocities.
New messages (saltybot_scene_msgs):
msg/ObstacleVelocity.msg — obstacle_id, centroid, velocity,
speed_mps, width_m, depth_m,
point_count, confidence, is_static
msg/ObstacleVelocityArray.msg — array wrapper with header
New files (saltybot_bringup):
saltybot_bringup/_obstacle_velocity.py — pure helpers (no ROS2 deps)
KalmanTrack constant-velocity 2-D KF: predict(dt) / update(centroid)
coasting counter → alive flag; confidence = age/n_init
associate() greedy nearest-centroid matching (O(N·M), strict <)
ObstacleTracker predict-all → associate → update/spawn → prune cycle
saltybot_bringup/obstacle_velocity_node.py
Subscribes /scan (BEST_EFFORT); reuses _lidar_clustering helpers;
publishes ObstacleVelocityArray on /saltybot/obstacle_velocities
Parameters: distance_threshold_m=0.20, min_points=3, range 0.05–12m,
max_association_dist_m=0.50, max_coasting_frames=5,
n_init_frames=3, q_pos=0.05, q_vel=0.50, r_pos=0.10,
static_speed_threshold=0.10
test/test_obstacle_velocity.py — 48 tests, all passing
Modified:
saltybot_scene_msgs/CMakeLists.txt — register new msgs
saltybot_bringup/setup.py — add obstacle_velocity console_script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 06:53:04 -05:00
4dbb4c6f0d
feat(perception): appearance-based person re-identification (Issue #322 )
...
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 12s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Adds PersonTrack/PersonTrackArray msgs and a PersonReidNode that matches
individuals across camera views using HSV colour histogram appearance
features and cosine similarity, with EMA gallery update and 30s stale timeout.
New messages (saltybot_scene_msgs):
msg/PersonTrack.msg — track_id, camera_id, bbox, confidence,
first_seen, last_seen, is_stale
msg/PersonTrackArray.msg — array wrapper with header
New files (saltybot_bringup):
saltybot_bringup/_person_reid.py — pure kinematics (no ROS2 deps)
extract_hsv_histogram() 2-D HS histogram (H=16, S=8 → 128-dim, L2-norm)
cosine_similarity() handles zero/non-unit vectors
match_track() best gallery match above threshold (strict >)
TrackGallery add/update/match/mark_stale/prune_stale
TrackEntry mutable dataclass; EMA feature blend (α=0.3)
saltybot_bringup/person_reid_node.py
Subscribes /camera/color/image_raw + /saltybot/scene/objects (BEST_EFFORT)
Crops COCO person (class_id=0) ROIs; extracts features; matches gallery
Publishes PersonTrackArray on /saltybot/person_tracks at 5 Hz
Parameters: camera_id, similarity_threshold=0.75, stale_timeout_s=30,
max_tracks=20, publish_hz=5.0
test/test_person_reid.py — 50 tests, all passing
Modified:
saltybot_scene_msgs/CMakeLists.txt — register PersonTrack/Array msgs
saltybot_bringup/setup.py — add person_reid console_script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 06:45:43 -05:00
067a871103
feat(perception): wheel encoder differential drive odometry (Issue #184 )
...
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 7s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Adds saltybot_bridge_msgs package with WheelTicks.msg (int32 left/right
encoder counts) and a WheelOdomNode that subscribes to
/saltybot/wheel_ticks, integrates midpoint-Euler differential drive
kinematics (handling int32 counter rollover), and publishes
nav_msgs/Odometry on /odom_wheel at 50 Hz with optional TF broadcast.
New files:
jetson/ros2_ws/src/saltybot_bridge_msgs/
msg/WheelTicks.msg
CMakeLists.txt, package.xml
jetson/ros2_ws/src/saltybot_bringup/
saltybot_bringup/_wheel_odom.py — pure kinematics (no ROS2 deps)
saltybot_bringup/wheel_odom_node.py — 50 Hz timer node + TF broadcast
test/test_wheel_odom.py — 42 tests, all passing
Modified:
saltybot_bringup/package.xml — add saltybot_bridge_msgs, nav_msgs deps
saltybot_bringup/setup.py — add wheel_odom console_script entry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:41:39 -05:00
e24c0b2e26
feat(perception): sky detector for outdoor navigation — Issue #307
...
- Add _sky_detector.py: SkyResult NamedTuple; detect_sky() with dual HSV
band masking (blue sky H∈[90,130]/S∈[40,255]/V∈[80,255] OR overcast
S∈[0,50]/V∈[185,255]), cv2.bitwise_or combined mask; sky_fraction over
configurable top scan_frac region; horizon_y = bottommost row where
per-row sky fraction ≥ row_threshold (−1 when no sky detected)
- Add sky_detect_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
publishes Float32 /saltybot/sky_fraction and Int32 /saltybot/horizon_y
per frame; scan_frac (default 0.60) and row_threshold (default 0.30) params
- Register sky_detector console script in setup.py
- 33/33 unit tests pass (no ROS2 required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:39:28 -05:00
3bf603f685
feat(perception): terrain roughness estimator via Gabor + LBP — Issue #296
...
- Add _terrain_roughness.py: RoughnessResult NamedTuple; gabor_energy() with
4-orientation × 2-wavelength (5px, 10px) quadrature Gabor bank, DC removal
via image mean subtraction (prevents false high energy on uniform surfaces);
lbp_variance() using 8-point radius-1 LBP in vectorised numpy slice
comparisons (no sklearn); estimate_roughness() with bottom roi_frac crop,
normalised blend roughness = 0.5*(gabor/500) + 0.5*(lbp/5000) clipped [0,1]
- Add terrain_rough_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
publishes Float32 /saltybot/terrain_roughness at 2Hz (configurable via
publish_hz param); roi_frac param default 0.40 (bottom 40% = floor region)
- Register terrain_roughness console script in setup.py
- 37/37 unit tests pass (no ROS2 required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:12:51 -05:00
c5f3a5b2ce
feat(perception): motion blur detector via Laplacian variance — Issue #286
...
- Add _blur_detector.py: BlurResult NamedTuple, laplacian_variance() (ksize=3
Laplacian on greyscale, with optional ROI crop), detect_blur() returning
variance + is_blurred flag + threshold; handles greyscale and BGR inputs,
empty ROI returns 0.0
- Add blur_detect_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
publishes Bool /saltybot/image_blurred and Float32 /saltybot/blur_score per
frame; threshold and roi_frac ROS params
- Register blur_detector console script in setup.py
- 25/25 unit tests pass (no ROS2 required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:46:37 -05:00
f5093ecd34
feat(perception): HSV color object segmenter — Issue #274
...
- Add ColorDetection.msg + ColorDetectionArray.msg to saltybot_scene_msgs
- Add _color_segmenter.py: HsvRange/ColorBlob types, COLOR_RANGES defaults,
mask_for_color() (dual-band red wrap), find_color_blobs() with morph open,
contour extraction, area filter and max-blob-per-color limit
- Add color_segment_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
publishes /saltybot/color_objects (ColorDetectionArray) per frame;
active_colors, min_area_px, max_blobs_per_color params
- Add saltybot_scene_msgs exec_depend to saltybot_bringup/package.xml
- Register color_segmenter console_script in setup.py
- 34/34 unit tests pass (no ROS2 required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:32:41 -05:00
f0e11fe7ca
feat(bringup): depth image hole filler via bilateral interpolation (Issue #268 )
...
Adds multi-pass spatial-Gaussian hole filler for D435i depth images.
Each pass replaces zero/NaN pixels with the Gaussian-weighted mean of valid
neighbours in a growing kernel (×1, ×2.5, ×6 default); original valid
pixels are never modified. Handles uint16 mm → float32 m conversion,
border pixels via BORDER_REFLECT, and above-d_max pixels as holes.
Publishes filled float32 depth on /camera/depth/filled at camera rate.
37/37 pure-Python tests pass (no ROS2 required).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 14:19:27 -05:00
9d12805843
feat(bringup): visual odometry drift detector (Issue #260 )
...
Adds sliding-window drift detector that compares cumulative path lengths
of visual odom and wheel odom over a configurable window (default 10 s).
Drift = |vo_path − wheel_path|; flagged when ≥ 0.5 m (configurable).
OdomBuffer handles per-source rolling storage with automatic age eviction.
Publishes Bool on /saltybot/vo_drift_detected and Float32 on
/saltybot/vo_drift_magnitude at 2 Hz. 27/27 pure-Python tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 13:26:07 -05:00
32857435a1
feat(bringup): floor surface type classifier on D435i RGB (Issue #249 )
...
Adds multi-feature nearest-centroid classifier for 6 surface types:
carpet, tile, wood, concrete, grass, gravel. Features: circular hue mean,
saturation mean/std, brightness, Laplacian texture variance, Sobel edge
density — all extracted from the bottom 40% of each frame (floor ROI).
Majority-vote temporal smoother (window=5) suppresses single-frame noise.
Publishes std_msgs/String on /saltybot/floor_type at 2 Hz.
34/34 pure-Python tests pass (no ROS2 required).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:51:14 -05:00
ff34f5ac43
feat(bringup): LIDAR Euclidean object clustering + RViz visualisation (Issue #239 )
...
Adds gap-based Euclidean distance clustering of /scan LaserScan points.
Each cluster is published as a labelled semi-transparent CUBE + TEXT marker
in /saltybot/lidar_clusters (MarkerArray), sorted nearest-first. Stale
markers from shrinking cluster counts are explicitly deleted each cycle.
22/22 pure-Python tests pass (no ROS2 required).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:21:35 -05:00
b722279739
feat(bringup): scan height filter with IMU pitch compensation (Issue #211 )
...
Two files added to saltybot_bringup:
- _scan_height_filter.py: pure-Python helpers (no rclpy) —
filter_scan_by_height() projects each LIDAR ray to world-frame height
using pitch/roll from the IMU and filters ground/ceiling returns;
pitch_roll_from_accel() uses convention-agnostic atan2 formula;
AttitudeEstimator low-pass filters the accelerometer attitude.
- scan_height_filter_node.py: subscribes /scan + /camera/imu, publishes
/scan_filtered (LaserScan) for Nav2 at source rate (up to 20 Hz).
setup.py: adds scan_height_filter entry point.
18/18 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:50:56 -05:00
e6065e1531
feat(jetson): camera health watchdog node (issue #198 )
...
Adds camera_health_node.py + _camera_state.py to saltybot_bringup:
• _camera_state.py — pure-Python CameraState dataclass (no ROS2):
on_frame(), age_s, fps(window_s), status(),
should_reset() + mark_reset() with 30s cooldown
• camera_health_node.py — subscribes 6 image topics (D435i color/depth
+ 4× IMX219 CSI front/right/rear/left);
1 Hz tick: WARNING at >2s silence, ERROR at
>10s + v4l2 stream-off/on reset for CSI cams;
publishes /saltybot/camera_health JSON with
per-camera status, age_s, fps, total_frames
• test/test_camera_health.py — 15 unit tests (15/15 pass, no ROS2 needed)
• setup.py — adds camera_health_monitor console_scripts entry point
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:11:48 -05:00