317 Commits

Author SHA1 Message Date
d48edf4092 Merge pull request 'feat(social): personality system — SOUL.md persona, mood engine, relationship DB (Issue #84)' (#98) from sl-controls/social-personality into main 2026-03-01 23:58:43 -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
35b940e1f5 Merge pull request 'feat(social): Issue #86 — physical expression + motor attention' (#94) from sl-firmware/social-expression into main 2026-03-01 23:55:14 -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
b432492785 Merge pull request 'feat(social): multi-modal person state tracker #82' (#93) from sl-perception/social-person-state into main 2026-03-01 23:30:04 -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
Sebastien Vayrette
ac6fcb9a42 docs: add Leap Motion, fix ESC step inset 12mm 2026-03-01 17:49:17 -05:00
Sebastien Vayrette
ea18b9ad72 docs: add ReSpeaker 2-Mic + SIM7600A 4G to wiring diagram 2026-03-01 16:32:34 -05:00
Sebastien Vayrette
0c40a1c4f4 docs: add full wiring diagram (Orin ↔ FC ↔ ESC) 2026-03-01 14:51:56 -05:00
fc8faa0dab Merge pull request 'feat(safety): remote e-stop over 4G MQTT (Issue #63)' (#69) from sl-firmware/remote-estop into main 2026-03-01 04:58:58 -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
f7acf1554c Merge pull request 'feat: semantic sidewalk segmentation — TensorRT FP16 (#72)' (#78) from sl-jetson/sidewalk-segmentation into main 2026-03-01 01:20:50 -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
seb
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
seb
d3168b9c07 Merge pull request 'feat: route recording + autonomous replay (#71)' (#75) from sl-perception/route-record-replay into main 2026-03-01 01:10:02 -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
seb
dcc26e6937 Merge pull request 'feat: SIM7600X mount + LTE/GNSS antenna brackets' (#70) from sl-mechanical/cellular-mount into main 2026-03-01 01:00:50 -05:00
seb
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
seb
b97ce09d80 Merge pull request 'feat: full_stack.launch.py — one-command autonomous stack bringup' (#68) from sl-jetson/full-stack-launch into main 2026-03-01 01:00:30 -05:00
0daac970c3 feat: SIM7600X mount + LTE/GNSS antenna brackets
chassis/sim7600x_mount.scad
  Platform bracket for Waveshare SIM7600X-H 4G HAT (65×56 mm,
  RPi HAT M2.5 pattern 58×49 mm). Standoffs height = HAT underside
  component clearance + 4 mm. Three walls (X−, X+, Y+); Y− edge
  fully open for SIM card tray access without disassembly. Floor-plate
  notch wider than SIM slot so card inserts/ejects with board in situ.
  USB port notch same open edge. u.FL pigtail exit slot in Y+ wall.
  4× M3 flat-head countersunk holes for base plate bolt-down.
  RENDER: bracket / assembly / bracket_2d (DXF for base plate layout).

chassis/antenna_mount.scad
  Two bracket types on shared 25 mm stem split-collar (M4 bolts,
  set screw height lock, cable-tie grooves on rear half):

  lte_bracket() — arm with 2× SMA bulkhead holes (6.6 mm clearance,
  hex-nut capture on underside). u.FL pigtail relief grooves on arm
  underside. Antennas point skyward. Recommended: 500–600 mm stem.

  gnss_platform() — upward-facing tray (≤40×40 mm patch antenna),
  4-sided retention lip, central GNSS coax slot, optional M2 bolt-down
  holes at 30×30 mm. Mount as high as practical for clear sky view:
  750–800 mm stem height. Recommended: active 35×35 mm patch antenna.

  RENDER "full_stem" shows both at 80 mm spacing on stem stub.
  Individual RENDER modes for each printable piece.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:59:47 -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
seb
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
seb
528034fe69 Merge pull request 'feat: SIM7600X 4G cellular + GPS (#58)' (#65) from sl-controls/cellular-gps into main 2026-03-01 00:51:16 -05:00
seb
c1582b1382 Merge pull request 'feat: UWB tag enclosure + anchor mounts (#57, #61, #62)' (#64) from sl-mechanical/uwb-enclosures into main 2026-03-01 00:51:12 -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
61c716ee58 feat: UWB tag enclosure + stem anchor mounts (#57, #61, #62)
3× MaUWB ESP32-S3 follow-me UWB system: 1 wearable tag, 2 robot anchors.

chassis/uwb_tag_enclosure.scad
  Belt-clip enclosure for MaUWB PCB (~50×25×10 mm) + TP4056 micro-USB
  charger + 18650 cell. Snap-fit PETG shell + TPU 95A bumper sleeve.
  IP44-ish 4 mm overlap + 2-turn labyrinth seam. Open antenna window in
  lid (no PLA within 10 mm of UWB antenna). Power switch cutout (Y− face),
  micro-USB port (X− face), LED window hole (Y+ face). Belt clip integrated
  (PETG spring arm, 42 mm belt slot). RENDER: body/lid/tpu_bumper/assembly.

chassis/uwb_anchor_mount.scad
  Stem-mounted anchor bracket for 25 mm OD stem. Split D-collar with M4
  thumbscrews (tool-free), M4 hex nut pockets, M4 set screw height lock.
  Anti-rotation flat tab on front half prevents axial rotation without stem
  modification. USB cable routing channel in rear half. Module bracket tilted
  10° outward — antenna faces horizon, clears stem metal. Back-wall cutout
  behind antenna section (10 mm clearance). 250 mm anchor spacing (RENDER
  "pair" shows both on stem section). RENDER: collar_front/collar_rear/
  bracket/assembly/pair.

chassis/uwb_assembly.md
  Full assembly notes: antenna clearance rules, IP44 seam description, stem
  positioning diagram (anchors at 450 mm + 700 mm), USB cable routing,
  complete BOM (~300 g total, tag ~130 g).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 00:41:45 -05:00
seb
226e653e7c Merge pull request 'feat: rosbridge WebSocket for web UI (port 9090)' (#53) from sl-firmware/rosbridge into main 2026-03-01 00:22:22 -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
seb
3d2d317431 Merge pull request 'feat: bumper + frame crash protection (roll cage, base bumper, stem sleeves)' (#56) from sl-mechanical/bumper-protection into main 2026-03-01 00:21:13 -05:00
beb0af7ce6 feat: bumper + frame crash protection for SaltyBot
Protects expensive sensors when robot tips over during testing.

Files added:
  chassis/bumper_frame.scad   — roll cage (25 mm stem collar + 4 inclined
                                posts + crown ring + TPU snap pad). Posts at
                                45/135/225/315° between cameras. Crown at
                                Z=148 mm above cage collar, 18 mm above RPLIDAR
                                top. RPLIDAR FOV unobstructed.
  chassis/base_bumper.scad    — clip-on bumper ring for 270×240 mm base plate.
                                50 mm tall, 15 mm standoff, rounded corners.
                                4× corner + 4× side clip brackets. TPU snap
                                edge caps. FC status LEDs remain visible.
  chassis/stem_protector.scad — split TPU sleeve for 25 mm stem. Snap-fit
                                closure (no hardware). Install 3–4 sleeves
                                along stem at ~200 mm intervals.
  chassis/bumper_BOM.md       — full BOM, print settings, install heights,
                                mass estimate (~500 g total, balanced).

All parts print without supports. Fully removable (clip/snap/clamp).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 23:30:06 -05:00
seb
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
seb
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
seb
2ffd462223 Merge pull request 'feat: 4x IMX219 surround vision + Nav2 costmap layer (Phase 2c)' (#52) from sl-perception/surround-vision into main 2026-02-28 23:25:17 -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
seb
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
seb
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