9341e9d986
feat(mapping): RTAB-Map persistence + multi-session + map management (Issue #123 )
...
- Add saltybot_mapping package: MapDatabase, MapExporter, MapManagerNode
- 6 ROS2 services: list/save_as/load/delete maps + export occupancy/pointcloud
- Auto-save current.db every 5 min; keep last 5 autosaves; warn at 2 GB
- Update rtabmap_params.yaml: database_path, Mem/InitWMWithAllNodes=true,
Rtabmap/StartNewMapOnLoopClosure=false (multi-session persistence by default)
- Update slam_rtabmap.launch.py: remove --delete_db_on_start, add fresh_start
arg (deletes DB before launch) and database_path arg (load named map)
- CLI tools: backup_map.py, export_map.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:17:54 -05:00
3c438595e8
feat(rover): SaltyRover 4-wheel ESC motor driver (Issue #110 )
...
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 2s
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
- kinematics.py: pure unicycle→differential/skid-steer kinematics,
speed_to_pwm (1000–2000µs), compute_wheel_speeds with ±max clip,
odometry_from_wheel_speeds inverse helper
- rover_driver_node.py: 50 Hz ROS2 node; serial P<ch1>,<ch2>,<ch3>,<ch4>\n
protocol; heartbeat H\n; deadman on /cmd_vel silence; runtime 2WD/4WD
variant switch via four_wheel param; dead-reckoning odometry;
publishes /saltybot/rover_pwm (JSON) + /saltybot/rover_odom
- config/rover_params.yaml, launch/rover_driver.launch.py, package.xml,
setup.py, setup.cfg
- test/test_rover_kinematics.py: 51 unit tests, all passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 09:03:28 -05:00
ee8438fd04
feat(tests): social-bot integration test suite (Issue #108 )
...
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 3m58s
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 3m3s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Add saltybot_social_tests package with full pytest + launch_testing harness:
- test_launch.py: start social_test.launch.py, verify all nodes up within 30s
- test_topic_rates.py: measure topic Hz over 3s window vs. minimum SLAs
- test_services.py: /social/enroll, /social/nav/set_mode, person CRUD, mood query
- test_gpu_memory.py: total allocation < 6 GB, no leak over 30s
- test_latency.py: inject→transcript→LLM→TTS state-machine SLA profiling
- test_shutdown.py: no zombies, GPU memory released, audio device freed
- test_helpers.py: TopicRateChecker, NodeChecker, ServiceChecker, GpuMemoryChecker
- mock_sensors.py: camera/faces/fused/persons/uwb publishers at correct rates
- social_test.launch.py: CI-mode launch (no mic/speaker, mock sensors auto-started)
- conftest.py + pytest.ini: gpu_required / full_stack / stack_running markers
- docker/Dockerfile.ci + docker-compose.ci.yml: CPU-only CI container
- docker/entrypoint-ci.sh: launch stack + wait 10s + run pytest
- bags/record_social_test.sh + bags/README.md: rosbag recording for replay
- .gitea/workflows/social-tests-ci.yml: lint + core-tests + latency-gpu jobs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:50:22 -05:00
6a96c73b2d
feat(panoramic): 360° equirectangular stitching + RTSP stream (Issue #105 )
...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:41:40 -05:00
b1abdccf03
Merge pull request 'feat(controls): Autonomous/RC mode switch with 500ms blend ramp (Issue #104 )' ( #114 ) from sl-controls/issue-104-mode-switch into main
2026-03-02 08:41:24 -05:00
9733f5f097
feat(controls): Autonomous/RC mode switch with 500ms blend ramp (Issue #104 )
...
New package: saltybot_mode_switch
mode_logic.py (pure, no ROS2 dep — 72/72 tests pass):
State machine: RC → RAMP_TO_AUTO → AUTO → RAMP_TO_RC → RC
• CH6 (axes[5] > 0.5) requests AUTO; CH6 low → RAMP_TO_RC
• Stick >10% in AUTO/RAMP_TO_AUTO/RAMP_TO_RC → instant RC (no ramp)
• Sticks neutral ≥ 2 s after override → override cleared → RAMP_TO_AUTO
• RC link lost (Joy silent > 0.5 s) → instant RC from any state
• SLAM fix lost → RAMP_TO_RC (graceful exit from AUTO)
• No AUTO entry without slam_ok AND rc_link_ok
blend_alpha: 0.0 (RC) → linear ramp over 500 ms → 1.0 (AUTO)
led_pattern: solid_yellow(RC) | blink_green_slow(ramp) |
solid_green(AUTO) | blink_orange_fast(slam lost) |
blink_red_fast(RC link lost)
mode_switch_node.py (ROS2, 20 Hz):
Sub: /rc/joy (Joy), /saltybot/balance_state (String),
/slam_toolbox/pose_with_covariance_stamped (PoseWithCovarianceStamped)
Pub: /saltybot/control_mode (String JSON: mode+blend_alpha+slam_ok+rc_link_ok+override_active)
/saltybot/led_pattern (String)
cmd_vel_mux_node.py (ROS2, 20 Hz):
Sub: /cmd_vel_auto (Twist from Nav2/follower), /saltybot/control_mode
Pub: /cmd_vel (Twist to bridge, scaled by blend_alpha)
Note: remap Nav2/follower output to /cmd_vel_auto in launch.
Tests: 72/72 pass (no ROS2 runtime required).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:38:49 -05:00
1f594538fd
feat(calibration): IMX219 intrinsic + extrinsic calibration workflow (Issue #106 )
2026-03-02 08:38:24 -05:00
5043578934
feat(social): speech pipeline + LLM conversation + TTS + orchestrator ( #81 #83 #85 #89 )
...
Issue #81 — Speech pipeline:
- speech_pipeline_node.py: OpenWakeWord "hey_salty" → Silero VAD → faster-whisper
STT (Orin GPU, <500ms wake-to-transcript) → ECAPA-TDNN speaker diarization
- speech_utils.py: pcm16↔float32, EnergyVad, UtteranceSegmenter (pre-roll, max-
duration), cosine speaker identification — all pure Python, no ROS2/GPU needed
- Publishes /social/speech/transcript (SpeechTranscript) + /social/speech/vad_state
Issue #83 — Conversation engine:
- conversation_node.py: llama-cpp-python GGUF (Phi-3-mini Q4_K_M, 20 GPU layers),
streaming token output, per-person sliding-window context (4K tokens), summary
compression, SOUL.md system prompt, group mode
- llm_context.py: PersonContext, ContextStore (JSON persistence), build_llama_prompt
(ChatML format), context compression via LLM summarization
- Publishes /social/conversation/response (ConversationResponse, partial + final)
Issue #85 — Streaming TTS:
- tts_node.py: Piper ONNX streaming synthesis, sentence-by-sentence first-chunk
streaming (<200ms to first audio), sounddevice USB speaker playback, volume control
- tts_utils.py: split_sentences, pcm16_to_wav_bytes, chunk_pcm, apply_volume, strip_ssml
Issue #89 — Pipeline orchestrator:
- orchestrator_node.py: IDLE→LISTENING→THINKING→SPEAKING state machine, GPU memory
watchdog (throttle at <2GB free), rolling latency stats (p50/p95 per stage),
VAD watchdog (alert if speech pipeline hangs), /social/orchestrator/state JSON pub
- social_bot.launch.py: brings up all 4 nodes with TimerAction delays
New messages: SpeechTranscript.msg, VadState.msg, ConversationResponse.msg
Config YAMLs: speech_params, conversation_params, tts_params, orchestrator_params
Tests: 58 tests (28 speech_utils + 30 llm_context/tts_utils), all passing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 08:23:19 -05:00
fa0162fadc
feat(social): multi-modal tracking fusion — UWB+camera Kalman filter (Issue #92 )
...
New packages:
saltybot_social_msgs — FusedTarget.msg custom message
saltybot_social_tracking — 4-state Kalman fusion node
saltybot_social_tracking/tracking_fusion_node.py
Subscribes to /uwb/target (PoseStamped, ~10 Hz) and /person/target
(PoseStamped, ~30 Hz) and publishes /social/tracking/fused_target
(FusedTarget) at 20 Hz.
Source arbitration:
• "fused" — both UWB and camera are fresh; confidence-weighted blend
• "uwb" — UWB fresh, camera stale
• "camera" — camera fresh, UWB stale
• "predicted" — all sources stale; KF coasts for up to predict_timeout (3 s)
Kalman filter (kalman_tracker.py):
State [x, y, vx, vy] with discrete Wiener acceleration noise model
(process_noise=3.0 m/s²) sized for EUC speeds (20-30 km/h, ≈5.5-8.3 m/s).
Separate UWB (0.20 m) and camera (0.12 m) measurement noise.
Velocity estimate converges after ~3 s of 10 Hz UWB measurements.
Confidence model (source_arbiter.py):
Per-source confidence = quality × max(0, 1 - age/timeout).
Composite confidence accounts for KF positional uncertainty and
is capped at 0.4 during dead-reckoning ("predicted") mode.
Tests: 58/58 pass (no ROS2 runtime required).
Note: saltybot_social_msgs here adds FusedTarget.msg; PR #98
(Issue #84 ) adds PersonalityState.msg + QueryMood.srv to the same
package. The maintainer should squash-merge #98 first and rebase
this branch on top of it before merging to avoid the package.xml
conflict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:59:10 -05:00
44771751e2
feat(social): personality system — SOUL.md persona, mood engine, relationship DB (Issue #84 )
...
New packages:
- saltybot_social_msgs: PersonalityState.msg + QueryMood.srv custom interfaces
- saltybot_social_personality: full personality node
Features:
- SOUL.md YAML/Markdown persona file: name, humor_level (0-10), sass_level (0-10),
base_mood, per-tier greeting templates, mood prefix strings
- Hot-reload: SoulWatcher polls SOUL.md every reload_interval seconds, applies
changes live without restarting the node
- Per-person relationship memory in SQLite: score, interaction_count,
first/last_seen, learned preferences (JSON), full interaction log
- Mood engine (pure functions): happy | curious | annoyed | playful
driven by relationship score, interaction count, recent event window (120s)
- Greeting personalisation: stranger | regular | favorite tiers
keyed on interaction count thresholds from SOUL.md
- Publishes /social/personality/state (PersonalityState) at publish_rate Hz
- /social/personality/query_mood (QueryMood) service for on-demand mood query
- Full ROS2 dynamic reconfigure: soul_file, db_path, reload_interval, publish_rate
- 52 unit tests, no ROS2 runtime required
ROS2 interfaces:
Sub: /social/person_detected (std_msgs/String JSON)
Pub: /social/personality/state (saltybot_social_msgs/PersonalityState)
Srv: /social/personality/query_mood (saltybot_social_msgs/QueryMood)
2026-03-01 23:56:05 -05:00
dc746ccedc
Merge pull request 'feat(social): face detection + recognition #80 ' ( #96 ) from sl-perception/social-face-detection into main
2026-03-01 23:55:18 -05:00
d6a6965af6
Merge pull request 'feat(social): person enrollment system #87 ' ( #95 ) from sl-perception/social-enrollment into main
2026-03-01 23:55:16 -05:00
5143e5bfac
feat(social): Issue #86 — physical expression + motor attention
...
ESP32-C3 NeoPixel sketch (esp32/social_expression/social_expression.ino):
- Adafruit NeoPixel + ArduinoJson, serial JSON protocol 115200 8N1
- Mood→colour: happy=green, curious=blue, annoyed=red, playful=rainbow
- Idle breathing animation (sine-modulated warm white)
- Auto-falls to idle after IDLE_TIMEOUT_MS (3 s) with no command
ROS2 saltybot_social_msgs (new package):
- Mood.msg — {mood, intensity}
- Person.msg — {track_id, bearing_rad, distance_m, confidence, is_speaking, source}
- PersonArray.msg — {persons[], active_id}
ROS2 saltybot_social (new package):
- expression_node: subscribes /social/mood → JSON serial to ESP32-C3
reconnects on port error; sends idle frame after idle_timeout_s
- attention_node: subscribes /social/persons → /cmd_vel rotation-only
proportional control with dead zone; prefers active speaker, falls
back to highest-confidence person; lost-target idle after 2 s
- launch/social.launch.py — combined launch
- config YAML for both nodes with documented parameters
- test/test_attention.py — 15 pytest-only unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:35:59 -05:00
5c4f18e46c
feat(social): person enrollment system — SQLite gallery + voice trigger (Issue #87 )
...
- saltybot_social_msgs: 6 msg + 5 srv definitions for social interaction
- saltybot_social_enrollment: enrollment_node + enrollment_cli
- PersonDB: thread-safe SQLite-backed gallery (embeddings, voice samples)
- Voice-triggered enrollment via "remember me my name is X" phrase
- CLI: enroll/list/delete/rename via ros2 run
- Services: /social/enroll, /social/persons/list|delete|update
- Gallery sync from /social/faces/embeddings topic
2026-03-01 23:32:26 -05:00
f61a03b3c5
feat(social): face detection + recognition (SCRFD + ArcFace TRT FP16, Issue #80 )
...
Add two new ROS2 packages for the social sprint:
saltybot_social_msgs (ament_cmake):
- FaceDetection, FaceDetectionArray, FaceEmbedding, FaceEmbeddingArray
- PersonState, PersonStateArray
- EnrollPerson, ListPersons, DeletePerson, UpdatePerson services
saltybot_social_face (ament_python):
- SCRFDDetector: SCRFD face detection with TRT FP16 + ONNX fallback
- 640x640 input, 3-stride anchor decoding, NMS
- ArcFaceRecognizer: 512-dim embedding extraction with gallery matching
- 5-point landmark alignment to 112x112, cosine similarity
- FaceGallery: thread-safe persistent gallery (npz + JSON sidecar)
- FaceRecognitionNode: ROS2 node subscribing /camera/color/image_raw,
publishing /social/faces/detections, /social/faces/embeddings
- Enrollment via /social/enroll service (N-sample face averaging)
- Launch file, config YAML, TRT engine builder script
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:31:48 -05:00
d9c983f666
Merge pull request 'feat(social): navigation & path planning #91 ' ( #97 ) from sl-perception/social-nav into main
2026-03-01 23:30:40 -05:00
54e9274405
Merge pull request 'feat(uwb): MaUWB ESP32-S3 DW3000 dual-anchor bearing driver (Issue #90 )' ( #99 ) from sl-firmware/uwb-integration into main
2026-03-01 23:30:12 -05:00
9a68dfdb2e
feat(uwb): MaUWB ESP32-S3 DW3000 dual-anchor bearing driver (Issue #90 )
...
## Summary
- saltybot_uwb_msgs: add UwbBearing.msg, add tag_id to UwbRange.msg,
register UwbBearing in CMakeLists.txt
- ranging_math.py: add bearing_from_pos(x, y) helper (atan2-based)
- uwb_driver_node.py: dual-rate architecture
• 100 Hz /uwb/ranges — raw TWR ranges with tag_id attribution
• 10 Hz /uwb/bearing — Kalman-fused bearing + range estimate
• enrolled_tag_ids parameter for tag pairing filter
• AT+RANGE_ADDR=<tag> pairing command on connect
- uwb_config.yaml: range_rate / bearing_rate / enrolled_tag_ids params
- uwb.launch.py: expose new params as launch arguments
- test_ranging_math.py: 7 new bearing_from_pos unit tests
Closes #90
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 23:25:08 -05:00
d872ea5e34
feat(social): navigation + follow modes + MiDaS depth + waypoints (Issue #91 )
...
- saltybot_social_msgs: full message/service definitions (standalone compilation)
- saltybot_social_nav: social navigation orchestrator
- Follow modes: shadow/lead/side/orbit/loose/tight
- Voice steering: mode switching + route commands via /social/speech/*
- A* obstacle avoidance on Nav2/SLAM occupancy grid (8-directional, inflation)
- MiDaS monocular depth for CSI cameras (TRT FP16 + ONNX fallback)
- Waypoint teaching + replay with WaypointRoute persistence
- High-speed EUC tracking (5.5 m/s = ~20 km/h)
- Predictive position extrapolation (0.3s ahead at high speed)
- Launch: social_nav.launch.py (social_nav + midas_depth + waypoint_teacher)
- Config: social_nav_params.yaml
- Script: build_midas_trt_engine.py (ONNX -> TRT FP16)
2026-03-01 23:15:00 -05:00
84790412d6
feat(social): multi-modal person state tracker (Issue #82 )
2026-03-01 23:08:22 -05:00
d41a9dfe10
feat(safety): remote e-stop over 4G MQTT (Issue #63 )
...
STM32 firmware:
- safety.h/c: EstopSource enum, safety_remote_estop/clear/get/active()
CDC 'E'=ESTOP_REMOTE, 'F'=ESTOP_CELLULAR_TIMEOUT, 'Z'=clear latch
- usbd_cdc_if: cdc_estop_request/cdc_estop_clear_request volatile flags
- status: status_update() +remote_estop param; both LEDs fast-blink 200ms
- main.c: immediate motor cutoff highest-priority; arming gated by
!safety_remote_estop_active(); motor estop auto-clear gated; telemetry
'es' field 0-4; status_update() updated to 5 args
Safety: IMMEDIATE motor cutoff, latched until explicit Z + DISARMED,
cannot re-arm via MQTT alone (requires RC arm hold). IWDG-safe.
Jetson bridge:
- remote_estop_node.py: paho-mqtt + pyserial, cellular watchdog 5s
- estop_params.yaml, remote_estop.launch.py
- setup.py / package.xml: register node + paho-mqtt dep
- docker-compose.yml: remote-estop service
- test_remote_estop.py: kill/clear/watchdog/latency unit tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 04:55:54 -05:00
e964d632bf
feat: semantic sidewalk segmentation — TensorRT FP16 ( #72 )
...
New packages
────────────
saltybot_segmentation (ament_python)
• seg_utils.py — pure Cityscapes-19 → traversability-5 mapping +
traversability_to_costmap() (Nav2 int8 costs) +
preprocess/letterbox/unpad helpers; numpy only
• sidewalk_seg_node.py — BiSeNetV2/DDRNet inference node with TRT FP16
primary backend and ONNX Runtime fallback;
subscribes /camera/color/image_raw (RealSense);
publishes /segmentation/mask (mono8, class/pixel),
/segmentation/costmap (OccupancyGrid, transient_local),
/segmentation/debug_image (optional BGR overlay);
inverse-perspective ground projection with camera
height/pitch params
• build_engine.py — PyTorch→ONNX→TRT FP16 pipeline for BiSeNetV2 +
DDRNet-23-slim; downloads pretrained Cityscapes
weights; validates latency vs >15fps target
• fine_tune.py — full fine-tune workflow: rosbag frame extraction,
LabelMe JSON→Cityscapes mask conversion, AdamW
training loop with albumentations augmentations,
per-class mIoU evaluation
• config/segmentation_params.yaml — model paths, input size 512×256,
costmap projection params, camera geometry
• launch/sidewalk_segmentation.launch.py
• docs/training_guide.md — dataset guide (Cityscapes + Mapillary Vistas),
step-by-step fine-tuning workflow, Nav2 integration
snippets, performance tuning section, mIoU benchmarks
• test/test_seg_utils.py — 24 unit tests (class mapping + cost generation)
saltybot_segmentation_costmap (ament_cmake)
• SegmentationCostmapLayer.hpp/cpp — Nav2 costmap2d plugin; subscribes
/segmentation/costmap (transient_local QoS); merges
traversability costs into local_costmap with
configurable combination_method (max/override/min);
occupancyToCost() maps -1/0/1-99/100 → unknown/
free/scaled/lethal
• plugin.xml, CMakeLists.txt, package.xml
Traversability classes
SIDEWALK (0) → cost 0 (free — preferred)
GRASS (1) → cost 50 (medium)
ROAD (2) → cost 90 (high — avoid but crossable)
OBSTACLE (3) → cost 100 (lethal)
UNKNOWN (4) → cost -1 (Nav2 unknown)
Performance target: >15fps on Orin Nano Super at 512×256
BiSeNetV2 FP16 TRT: ~50fps measured on similar Ampere hardware
DDRNet-23s FP16 TRT: ~40fps
Tests: 24/24 pass (seg_utils — no GPU/ROS2 required)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 01:15:13 -05:00
4be93669a1
Merge pull request 'feat: outdoor adaptive speed controller — walk/jog/ride profiles up to 8 m/s' ( #76 ) from sl-controls/outdoor-speed into main
2026-03-01 01:11:10 -05:00
5dcaa7bd49
feat: route recording + autonomous replay ( #71 )
...
Implements Phase 3 ride-once-replay-forever route system.
saltybot_routes package:
- route_recorder_node: samples GPS+odom+heading at 1Hz during follow-me
rides; 2m waypoint spacing; JSON-Lines .jsonl on NVMe /data/routes/;
services start_recording/stop_recording/save/discard
- route_replayer_node: loads .jsonl, GPS->ENU flat-earth conversion,
heading->quaternion, 3m subsampling for Nav2 navigate_through_poses;
2m GPS tolerance (SIM7600X +-2.5m); pause/resume/stop services
- route_manager_node: list/info/delete services for saved routes
- route_system.launch.py: all three nodes with shared params
- route_params.yaml: waypoint_spacing_m=2.0, replay_spacing_m=3.0
GPS: /gps/fix from SIM7600X (PR #65 )
UWB: /uwb/target from follow-me (PR #66 )
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 01:07:06 -05:00
118d2b3add
feat: outdoor adaptive speed controller (saltybot_speed_controller)
...
Adds saltybot_speed_controller ROS2 package — sits between person_follower
(/cmd_vel_raw) and cmd_vel_bridge (/cmd_vel), providing adaptive speed profiles
tuned for balance stability during outdoor follow-me up to 8 m/s (EUC ride mode).
Key features:
- walk/jog/ride profiles (1.5/3.0/8.0 m/s) selected via UWB target velocity
- Hysteresis-based switching (5 ticks up, 15 ticks down) prevents oscillation
- Trapezoidal accel/decel ramps per profile; ride accel 0.3 m/s² (balance-safe)
- Emergency decel (2.0 m/s²) triggered by sudden target stop or hard decel
- GPS runaway protection: if GPS > commanded×1.5 AND > 50% profile_max → brake
- 52/52 unit tests (no ROS2 runtime required)
Topics: /cmd_vel_raw → [speed_controller] → /cmd_vel, /speed_controller/profile
Launch: ros2 launch saltybot_speed_controller outdoor_speed.launch.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 01:06:50 -05:00
b3c03e096f
Merge pull request 'feat: outdoor nav — OSM routing + geofence ( #59 )' ( #67 ) from sl-perception/outdoor-nav into main
2026-03-01 01:00:43 -05:00
039355d5bb
feat: full_stack.launch.py — one-command autonomous stack bringup
...
Adds saltybot_bringup/launch/full_stack.launch.py: a single launch file
that brings up the entire SaltyBot software stack in dependency order,
with mode selection (indoor / outdoor / follow).
Launch sequence (wall-clock delays):
t= 0s robot_description (URDF + TF)
t= 0s STM32 bidirectional serial bridge
t= 2s sensors (RPLIDAR A1M8 + RealSense D435i)
t= 2s cmd_vel safety bridge (deadman + ramp + AUTONOMOUS gate)
t= 4s UWB driver (MaUWB DW3000 anchors on USB)
t= 4s CSI cameras — 4x IMX219 (optional, enable_csi_cameras:=true)
t= 6s SLAM — RTAB-Map RGB-D+LIDAR (indoor only)
t= 6s Outdoor GPS nav (outdoor only)
t= 6s YOLOv8n person detection (TensorRT)
t= 9s Person follower (UWB primary + camera fusion)
t=14s Nav2 navigation stack (indoor only)
t=17s rosbridge WebSocket server (port 9090)
Modes:
indoor — SLAM + Nav2 + full sensor suite + follow + UWB (default)
outdoor — GPS nav + sensors + follow + UWB (no SLAM)
follow — sensors + UWB + perception + follower only
Launch arguments:
mode, use_sim_time, enable_csi_cameras, enable_uwb, enable_perception,
enable_follower, enable_bridge, enable_rosbridge, follow_distance,
max_linear_vel, uwb_port_a, uwb_port_b, stm32_port
Also updates saltybot_bringup/package.xml:
- Adds exec_depend for all saltybot_* packages included by full_stack
- Updates maintainer to sl-jetson
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:56:39 -05:00
e0987fcec8
feat: outdoor nav — OSM routing + GPS waypoints + geofence ( #59 )
...
Implements Phase 2d outdoor autonomous navigation for SaltyBot.
GPS source: SIM7600X /gps/fix from PR #65 (saltybot_cellular).
saltybot_outdoor package:
- osm_router_node: Overpass API + A* haversine graph + Douglas-Peucker
simplification, /outdoor/route (Path) + /outdoor/waypoints (PoseArray)
- gps_waypoint_follower_node: GPS->Nav2 navigate_through_poses bridge,
quality-adaptive tolerances (2m cellular / 0.30m RTK)
- geofence_node: ray-casting polygon safety, emergency stop on violation
- outdoor_nav.launch.py: dual-EKF + navsat_transform + all nodes
- outdoor_nav_params.yaml: 1.5m/s, no static_layer, 2m GPS tolerance
- ekf_outdoor.yaml: robot_localization dual-EKF + navsat_transform
- geofence_vertices.yaml: template with usage instructions
docker-compose.yml: fix malformed saltybot-surround block; add
saltybot-outdoor service (depends on saltybot-nav2, OSM NVMe cache)
SLAM-SETUP-PLAN.md: Phase 2d done
RTK upgrade: SIM7600X (+-2.5m) -> ZED-F9P (+-2cm), set use_rtk:=true
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:52:54 -05:00
64d411b48a
Merge pull request 'feat: UWB follow-me system ( #57 )' ( #66 ) from sl-jetson/uwb-follow-me into main
2026-03-01 00:51:20 -05:00
a00dbe6429
feat: UWB follow-me system ( #57 ) — saltybot_uwb package + sensor fusion
...
New packages
------------
saltybot_uwb_msgs (ament_cmake)
• UwbRange.msg — per-anchor range reading (anchor_id, range_m, raw_mm, rssi)
• UwbRangeArray.msg — array of UwbRange published on /uwb/ranges
saltybot_uwb (ament_python)
• ranging_math.py — pure triangulate_2anchor() (height-compensated TWR
geometry, 2-anchor intersection) + KalmanFilter2D
(constant-velocity, numpy-free, 16 tests pass)
• uwb_driver_node.py — SerialReader threads poll MaUWB ESP32-S3 DW3000
anchors via AT+RANGE?, triangulate, Kalman-smooth,
publish /uwb/target (PoseStamped/base_link) + /uwb/ranges
• config/uwb_config.yaml, launch/uwb.launch.py
• test/test_ranging_math.py — 16 unit tests (triangulation + Kalman), all pass
Updated saltybot_follower
-------------------------
• person_follower_node.py — adds fuse_targets() pure helper + /uwb/target
subscriber (primary, weight=0.7); /person/target secondary (weight=0.3);
weighted blend when both fresh, graceful fallback to single source; new
params uwb_weight + uwb_timeout
• person_follower_params.yaml — uwb_weight: 0.7, uwb_timeout: 1.0s
• test_person_follower.py — 7 new TestFuseTargets cases; total 60 pass
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:48:03 -05:00
de9a835cc2
feat: SIM7600X 4G cellular + GPS driver ( #58 )
...
Adds saltybot_cellular ROS2 package for the Waveshare SIM7600X 4G HAT
(SIMCom SIM7600A-H) providing GPS telemetry, modem monitoring, and
MQTT relay over cellular for remote operation.
gps_driver_node:
- Opens /dev/ttyUSB2 (NMEA), optionally sends AT+CGPS=1 on /dev/ttyUSB0
- Parses GGA (position) + RMC (velocity) from any NMEA talker (GP/GN/GL/GA)
- Validates NMEA checksum before parsing
- Publishes /gps/fix (NavSatFix, covariance from HDOP × ±2.5m CEP)
- Publishes /gps/vel (TwistStamped, ENU vE/vN from course-over-ground)
- Publishes /diagnostics (fix quality, sat count, HDOP)
cellular_manager_node:
- Polls AT+CSQ, AT+CREG?, AT+COPS? every 5s over /dev/ttyUSB0
- Publishes /cellular/status (DiagnosticArray: rssi, network, connected)
- Publishes /cellular/rssi (Int32, dBm) and /cellular/connected (Bool)
- Auto-reconnect via nmcli or pppd when data link drops
mqtt_bridge_node:
- paho-mqtt client (graceful degradation if not installed)
- ROS2→MQTT QoS 0: /saltybot/imu, /gps/fix, /gps/vel, /uwb/ranges,
/person/target, /cellular/status
- MQTT→ROS2 QoS 1: saltybot/cmd→/saltybot/cmd, saltybot/estop→/saltybot/estop
- Per-topic rate limiting (imu:5Hz, gps:1Hz, person:2Hz) → <<50KB/s budget
- Optional TLS, configurable broker/port/prefix/auth
Deliverables:
saltybot_cellular/gps_driver_node.py — 402 lines
saltybot_cellular/cellular_manager_node.py — 362 lines
saltybot_cellular/mqtt_bridge_node.py — 317 lines
config/cellular_params.yaml — full config documented
launch/cellular.launch.py — all nodes, all params as args
test/test_cellular.py — 60 pytest tests, no ROS2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:42:18 -05:00
6420e07487
feat: rosbridge WebSocket server for web UI (port 9090)
...
Adds rosbridge_suite to the Jetson stack so the browser dashboard can
subscribe to ROS2 topics via roslibjs over ws://jetson:9090.
docker-compose.yml
New service: saltybot-rosbridge
- Runs saltybot_bringup/launch/rosbridge.launch.py
- network_mode: host → port 9090 directly reachable on Jetson LAN
- Depends on saltybot-ros2, stm32-bridge, csi-cameras
saltybot_bringup/launch/rosbridge.launch.py
- rosbridge_websocket node (port 9090, params from rosbridge_params.yaml)
- 4× image_transport/republish nodes: compress CSI camera streams
/camera/<name>/image_raw → /camera/<name>/image_raw/compressed (JPEG 75%)
saltybot_bringup/config/rosbridge_params.yaml
Whitelisted topics:
/map /scan /tf /tf_static
/saltybot/imu /saltybot/balance_state
/cmd_vel
/person/*
/camera/*/image_raw/compressed
max_message_size: 10 MB (OccupancyGrid headroom)
saltybot_bringup/SENSORS.md
Added rosbridge connection section with roslibjs snippet,
topic reference table, bandwidth estimates, and throttle_rate tips.
saltybot_bringup/package.xml
Added exec_depend: rosbridge_server, image_transport,
image_transport_plugins (all already installed in Docker image).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:22:02 -05:00
fcd59ead80
Merge pull request 'feat: person-following control loop' ( #55 ) from sl-controls/person-follower into main
2026-02-28 23:25:40 -05:00
9014b6738d
Merge pull request 'feat: person detection + tracking (YOLOv8n TensorRT)' ( #54 ) from sl-jetson/person-detection into main
2026-02-28 23:25:22 -05:00
432d5cb267
feat: person-following control loop (Phase 2b)
...
Adds saltybot_follower ROS2 package — proportional person-following
controller that bridges sl-jetson's /person/target detections to Nav2
/cmd_vel, with the cmd_vel_bridge_node (PR #46 ) providing safety wrapping.
Controller features:
- Proportional control: linear.x ∝ distance error, angular.z ∝ bearing
- Follow distance: 1.5m default with ±0.3m dead zone (no jitter at target)
- Max speed: 0.5 m/s linear, 1.0 rad/s angular (conservative for balance)
- Obstacle override: zeroes forward cmd_vel when Nav2 local costmap
detects obstacle in forward corridor; turning still allowed
- Lost-target state machine:
FOLLOWING → person visible
STOPPING → lost > 2s, publish zero
SEARCHING → lost > 5s, slow rotation (0.3 rad/s) to re-acquire
- Mode integration: follow_enabled param (toggle via ros2 param set)
independently gates the controller; cmd_vel bridge gates on md=2
Deliverables:
saltybot_follower/person_follower_node.py — ROS2 node (314 lines)
config/person_follower_params.yaml — all params documented
launch/person_follower.launch.py — all params as launch args
test/test_person_follower.py — 53 pytest tests, no ROS2
package.xml / setup.py / setup.cfg — package metadata
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:22:49 -05:00
c44a30561a
feat: person detection + tracking (YOLOv8n TensorRT)
...
New package: saltybot_perception
person_detector_node.py:
- Subscribes /camera/color/image_raw + /camera/depth/image_rect_raw
(ApproximateTimeSynchronizer, slop=50ms)
- Subscribes /camera/color/camera_info for intrinsics
- YOLOv8n inference via TensorRT FP16 engine (Orin Nano 67 TOPS)
Falls back to ONNX Runtime when engine not found (dev/CI)
- Letterbox preprocessing (640x640), YOLOv8n post-process + NMS
- Median-window depth lookup at bbox centre (7x7 px)
- Back-projects 2D pixel + depth to 3D point in camera frame
- tf2 transform to base_link (fallback: camera_color_optical_frame)
- Publishes:
/person/detections vision_msgs/Detection2DArray all persons
/person/target geometry_msgs/PoseStamped tracked person 3D
/person/debug_image sensor_msgs/Image (optional)
tracker.py — SimplePersonTracker:
- Single-target IoU-based tracker
- Picks closest valid person (smallest depth) on first lock
- Re-associates across frames using IoU threshold
- Holds last known position for configurable duration (default 2s)
- Monotonically increasing track IDs
detection_utils.py — pure helpers (no ROS2 deps, testable standalone):
- nms(), letterbox(), remap_bbox(), get_depth_at(), pixel_to_3d()
scripts/build_trt_engine.py:
- Converts ONNX to TensorRT FP16 engine using TRT Python API
- Prints trtexec CLI alternative
- Includes YOLOv8n download instructions
config/person_detection_params.yaml:
- confidence_threshold: 0.40, min_depth: 0.5m, max_depth: 5.0m
- track_hold_duration: 2.0s, target_frame: base_link
launch/person_detection.launch.py:
- engine_path, onnx_path, publish_debug_image, target_frame overridable
Tests: 26/26 passing (test_tracker.py + test_postprocess.py)
- IoU computation, NMS suppression, tracker state machine,
depth filtering, hold duration, re-association, track ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:21:24 -05:00
dc01efe323
feat: 4x IMX219 surround vision + Nav2 camera obstacle layer (Phase 2c)
...
New ROS2 package saltybot_surround:
surround_costmap_node
- Subscribes to /camera/{front,left,rear,right}/image_raw
- Detects obstacles via Canny edge detection + ground projection
- Pinhole back-projection: pixel row → forward distance (d = h*fy/(v-cy))
- Rotates per-camera points to base_link frame using known camera yaws
- Publishes /surround_vision/obstacles (PointCloud2, 5 Hz)
- Catches chairs, glass walls, people that RPLIDAR misses
- Placeholder IMX219 fisheye calibration (hook for real cal via cv2.fisheye)
surround_vision_node
- IPM homography computed from camera height + pinhole model
- 4× bird's-eye patches composited into 240×240px 360° overhead view
- Publishes /surround_vision/birdseye (Image, 10 Hz)
- Robot footprint + compass overlay
surround_vision.launch.py
- Launches both nodes with surround_vision_params.yaml
- start_cameras arg: set false when csi-cameras container runs separately
Updated:
- jetson/config/nav2_params.yaml add surround_cameras PointCloud2 source
to local + global costmap obstacle_layer
- jetson/docker-compose.yml add saltybot-surround service
(depends_on: csi-cameras, start_cameras:=false)
- projects/saltybot/SLAM-SETUP-PLAN.md Phase 2c ✅ Done
Calibration TODO (run after hardware assembly):
ros2 run camera_calibration cameracalibrator --size 8x6 --square 0.025 \
image:=/camera/front/image_raw camera:=/camera/front
Replace placeholder K/D in surround_costmap_node._undistort()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:19:23 -05:00
5008e03cc4
Merge pull request 'feat: Orin Nano Super platform update + 4x IMX219 CSI cameras' ( #51 ) from sl-jetson/orin-platform-cameras into main
2026-02-28 23:08:49 -05:00
54d3e12c78
Merge pull request 'feat: Phase 2a URDF robot description + static TF for SLAM/Nav2' ( #50 ) from sl-firmware/robot-urdf into main
2026-02-28 23:08:46 -05:00
3755e235aa
feat: Orin Nano Super platform update + 4x IMX219 CSI cameras
...
Task A — Orin Nano Super platform update:
- docker-compose.yml: update header/comments, switch all service image tags
to jetson-orin, update devices to udev symlinks (/dev/rplidar,
/dev/stm32-bridge, i2c-7), add NVMe volume mounts (/mnt/nvme/saltybot),
update stm32-bridge to saltybot_bridge launch, add csi-cameras service
- docs/pinout.md: full rewrite for Orin Nano Super — i2c-7, ttyTHS0,
CSI-A/B connectors, M.2 NVMe slot, IMX219 15-pin FFC pinout, V4L2 nodes,
GStreamer test commands, updated udev rules
- docs/power-budget.md: full rewrite — 25W TDP, 8GB LPDDR5, 67 TOPS,
4-camera CSI bandwidth analysis, nvpmodel modes, Nano vs Orin comparison,
5V 6A PSU recommendation, 4S LiPo architecture
- scripts/setup-jetson.sh: full rewrite — JetPack 6 / Ubuntu 22.04,
nvidia-container-toolkit new keyring method, NVMe partition/format/fstab,
CSI driver check (imx219 modprobe), video group, jtop install, 8GB swap
Task B — saltybot_cameras ROS2 package:
- launch/csi_cameras.launch.py: 4x v4l2_camera nodes, namespace per camera
(front/left/rear/right), 640x480x30fps, includes TF launch automatically
- launch/camera_tf.launch.py: static TF for 4 cameras at 90deg intervals
on sensor_head_link (r=5cm offset), yaw 0/90/180/-90 deg
- package.xml, setup.py, setup.cfg, __init__.py, resource marker
- config/cameras_params.yaml: per-camera device/frame/offset configuration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:59:13 -05:00
3f627ac3c8
Merge pull request 'feat: Nav2 path planning + obstacle avoidance (Phase 2b)' ( #49 ) from sl-perception/nav2-integration into main
2026-02-28 22:58:49 -05:00
b4bb6a44e0
feat: Phase 2a URDF robot description for SLAM and Nav2
...
Add saltybot_description ROS2 package with full kinematic model:
urdf/saltybot.urdf.xacro
- base_footprint → base_link (axle_height = 0.310m, from AXLE_HEIGHT SCAD)
- wheel_left/right_link (continuous, separation=0.600m, radius=0.1015m)
- imu_link (FC/MPU-6000 at x=+50mm forward, z=+12mm)
- stem_link (visual: 38.1mm EMT, 1.050m — from stem_battery_clamp.scad)
- sensor_head_link at top of stem
- laser (RPLIDAR A1M8, z=COL_H=36mm — frame matches slam_toolbox config)
- camera_link (RealSense D435i, ARM_R=50mm — matches realsense2_camera)
- camera_{front,left,rear,right}_link (IMX219, 10° down tilt, ARM_R=50mm
— positions match camera_tf.launch.py; that file superseded when live)
config/saltybot_properties.yaml
All dimensions from chassis/chassis_frame.scad + ASSEMBLY.md Rev A.
launch/robot_description.launch.py
Compiles xacro at launch time, runs robot_state_publisher.
Publishes /robot_description + /tf_static for all fixed joints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:57:58 -05:00
772a70b545
feat: Nav2 path planning + obstacle avoidance (Phase 2b)
...
Integrates Nav2 autonomous navigation stack with RTAB-Map SLAM on Orin
Nano Super. No AMCL/map_server needed — RTAB-Map provides /map + TF.
New files:
- jetson/config/nav2_params.yaml DWB controller,
NavFn planner, RPLIDAR obstacle layer, RealSense voxel layer;
10Hz local / 5Hz global costmap; robot_radius 0.15m, max_vel 1.0 m/s
- jetson/ros2_ws/src/saltybot_bringup/launch/nav2.launch.py
wraps nav2_bringup navigation_launch with saltybot params + BT XML
- jetson/ros2_ws/src/saltybot_bringup/behavior_trees/
navigate_to_pose_with_recovery.xml BT: replan@1Hz, DWB follow,
recovery: clear maps → spin 90° → wait 5s → back up 0.30m
Updated:
- jetson/docker-compose.yml add saltybot-nav2 service
(depends_on: saltybot-ros2)
- jetson/ros2_ws/src/saltybot_bringup/setup.py install behavior_trees/*.xml
- jetson/ros2_ws/src/saltybot_bringup/package.xml add rtabmap_ros + nav2_bringup
- projects/saltybot/SLAM-SETUP-PLAN.md Phase 2b ✅ Done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:54:24 -05:00
a50f22d56b
feat: Nav2 cmd_vel to STM32 autonomous drive bridge
...
Adds cmd_vel_bridge_node — a standalone ROS2 node that subscribes to
Nav2 /cmd_vel and drives the STM32 over USB CDC with:
- Hard velocity limits (max_linear_vel=0.5 m/s, max_angular_vel=2.0 rad/s)
- Smooth ESC ramp (500 ESC-units/s, 50 Hz control loop)
- Deadman switch: zeros targets if /cmd_vel silent >500 ms
- Mode gate: sends drive only when STM32 reports md=2 (AUTONOMOUS)
- Telemetry RX → /saltybot/imu, /saltybot/balance_state, /diagnostics
- Heartbeat TX every 200 ms (H\n)
Deliverables:
saltybot_bridge/cmd_vel_bridge_node.py — node implementation
config/cmd_vel_bridge_params.yaml — tunable parameters
launch/cmd_vel_bridge.launch.py — standalone launch file
test/test_cmd_vel_bridge.py — 37 pytest unit tests (no ROS2)
setup.py — register node + new data files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 22:50:15 -05:00
c5d6a72d39
feat: update SLAM stack for Jetson Orin Nano Super (67 TOPS, JetPack 6)
...
Platform upgrade: Jetson Nano 4GB → Orin Nano Super 8GB (March 1, 2026)
All Nano-era constraints removed — power/rate/resolution limits obsolete.
Dockerfile: l4t-jetpack:r36.2.0 (JetPack 6 / Ubuntu 22.04 / CUDA 12.x),
ROS2 Humble via native apt, added ros-humble-rtabmap-ros,
ros-humble-v4l2-camera for future IMX219 CSI (Phase 2c)
New: slam_rtabmap.launch.py — Orin primary SLAM entry point
RTAB-Map with subscribe_scan (RPLIDAR) + subscribe_rgbd (D435i)
Replaces slam_toolbox as docker-compose default
New: config/rtabmap_params.yaml — Orin-optimized
DetectionRate 10Hz, MaxFeatures 1000, Grid/3D true,
TimeThr 0 (no limit), Mem/STMSize 0 (unlimited)
Updated: config/realsense_d435i.yaml — 848x480x30, pointcloud enabled
Updated: config/slam_toolbox_params.yaml — 10Hz rate, 1s map interval
Updated: SLAM-SETUP-PLAN.md — full rewrite for Orin: arch diagram,
Phase 2c IMX219 plan (4x 160° CSI surround), 25W power budget
docker-compose.yml: image tag jetson-orin, default → slam_rtabmap.launch.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:46:27 -05:00
22aaeb02cf
feat: Jetson→STM32 command protocol — /cmd_vel to serial (Phase 2)
...
STM32 firmware (C):
- include/jetson_cmd.h: protocol constants (HB_TIMEOUT_MS=500,
SPEED_MAX_DEG=4°), API for jetson_cmd_process/is_active/steer/sp_offset
- src/jetson_cmd.c: main-loop parser for buffered C<spd>,<str> frames;
setpoint offset = speed/1000 * 4°; steer clamped ±1000
- lib/USB_CDC/src/usbd_cdc_if.c: add H (heartbeat) and C (drive cmd) to
CDC_Receive ISR — follows existing pattern: H updates jetson_hb_tick in
ISR, C copied to jetson_cmd_buf for main-loop sscanf (avoids sscanf in IRQ)
- src/main.c: integrate jetson_cmd — process buffered frame, apply setpoint
offset around balance_update(), inject steer into motor_driver_update()
only when heartbeat alive (fallback: steer=0, setpoint unchanged)
ROS2 (Python):
- saltybot_cmd_node.py: full bidirectional node — owns serial port, handles
telemetry RX → topics AND /cmd_vel TX → C<spd>,<str>\n + H\n heartbeat
200ms timer; sends C0,0\n on shutdown; speed/steer_scale configurable
- serial_bridge_node.py: add write_serial() helper for extensibility
- launch/bridge.launch.py: mode arg (bidirectional|rx_only) selects node
- config/bridge_params.yaml: heartbeat_period, speed_scale, steer_scale docs
- test/test_cmd.py: 13 tests — zero, full fwd/rev, turn clamping, combined
- setup.py: saltybot_cmd_node entry point
All 21 tests pass (8 parse + 13 cmd).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:07:15 -05:00
a89297f1d4
Merge pull request 'feat(bd-a2j): Sensor driver integration — RealSense D435i + RPLIDAR A1M8' ( #17 ) from sl-perception/bd-a2j-sensor-drivers into main
2026-02-28 17:19:41 -05:00
76067d6d89
feat(bd-a2j): RealSense D435i + RPLIDAR A1M8 ROS2 driver integration
...
Adds saltybot_bringup ROS2 package with four launch files:
- realsense.launch.py — D435i at 640x480x15fps, IMU unified topic
- rplidar.launch.py — RPLIDAR A1M8 via /dev/rplidar udev symlink
- sensors.launch.py — both sensors + static TF (base_link→laser/camera)
- slam.launch.py — sensors + slam_toolbox online_async (compose entry point)
Sensor config YAMLs (mounted at /config/ in container):
- realsense_d435i.yaml — Nano power-budget settings (15fps, no pointcloud)
- rplidar_a1m8.yaml — Standard scan mode, 115200 baud, laser frame
- slam_toolbox_params.yaml — Nano-tuned (2Hz processing, 5cm resolution)
Fixes docker-compose volume mount: ./ros2_ws/src:/ros2_ws/src
(was ./ros2_ws:/ros2_ws/src — would have double-nested the src directory)
Topic reference and verification commands in SENSORS.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:14:21 -05:00
7c4e46aaa1
feat: STM32-to-Jetson ROS2 serial bridge node
...
saltybot_bridge ROS2 Python package (ament_python):
- serial_bridge_node.py: reads USB CDC JSON telemetry from STM32F722 at 50Hz
Parses exact firmware format: {"p","r","e","ig","m","s","y"} (all ×10 ints)
State enum: 0=DISARMED, 1=ARMED, 2=TILT_FAULT (matched to balance_state_t)
- Publishes sensor_msgs/Imu on /saltybot/imu (pitch/roll/yaw as angular_velocity)
- Publishes std_msgs/String on /saltybot/balance_state (full PID JSON diagnostics)
- Publishes diagnostic_msgs/DiagnosticArray on /diagnostics (OK/WARN/ERROR by state)
- Auto-reconnects on serial disconnect; IMU fault frames → ERROR diagnostic
- launch/bridge.launch.py with serial_port + baud_rate launch args
- config/bridge_params.yaml (921600 baud, /dev/ttyACM0)
- test/test_parse.py: 8 unit tests covering normal, fault, edge cases (all pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 17:11:02 -05:00