625 Commits

Author SHA1 Message Date
salty
2a78201a2d fix: time-division multiplexing for 2-anchor collision avoidance
Each anchor only runs DW1000Ranging during its 50ms time slot.
Anchor 0 responds in even slots, anchor 1 in odd slots.
Prevents RF collisions that caused wild range readings.
2026-03-14 13:01:04 -04:00
salty
9ed3e06fb8 fix: disable e-stop — GPIO 0 floats LOW on Makerfabs display board 2026-03-14 12:41:06 -04:00
salty
35d43f3173 fix: use LOWPOWER mode + add delay(1) to prevent overheating
- MODE_LONGDATA_RANGE_ACCURACY → MODE_LONGDATA_RANGE_LOWPOWER (both anchor+tag)
- Add delay(1) in tag loop to prevent tight-loop when no anchors respond
- ACCURACY was unnecessary and kept DW1000 radio hot
2026-03-14 12:38:28 -04:00
salty
b7dcc00c5e fix: ESP-NOW callback signature for new ESP-IDF, volatile qualifier 2026-03-14 12:32:11 -04:00
salty
30e012255f fix: char[] for DW1000 addr, suppress -Werror=return-type in lib 2026-03-14 12:31:40 -04:00
salty
ed1542ae11 feat: rewrite UWB firmware for DW1000 (all 3 boards)
Anchor (esp32/uwb_anchor):
- DW1000Ranging library (200m range, MODE_LONGDATA_RANGE_ACCURACY)
- Unique addresses per anchor (anchor0/anchor1 build envs)
- +RANGE output: anchor_id, tag_addr, range_mm, rssi
- ESP-NOW receiver: forwards tag packets + priority E-STOP to Jetson
- AT+ID? command

Tag with Display (esp32/uwb_tag):
- DW1000Ranging as tag, auto-discovers anchors
- SSD1306 OLED: big distance, per-anchor ranges, RSSI bars, link status
- ESP-NOW broadcast: range/heartbeat/estop packets
- E-Stop on GPIO 0 (BOOT button), 10Hz TX while held
- Display at 5Hz, ranging driven by DW1000Ranging.loop()

Shared:
- lib/DW1000/ extracted from mf_DW1000.zip (Makerfabs fork)
- lib_extra_dirs for PlatformIO to find local library
2026-03-14 12:30:26 -04:00
salty
a9e1b9fae5 feat: add OLED display, ESP-NOW wireless, and E-Stop to UWB tag
Tag firmware (esp32/uwb_tag):
- SSD1306 128x64 OLED: shows distance, anchor ranges, RSSI bars, link status
- ESP-NOW broadcast: sends range/heartbeat/estop packets (20 bytes, peer-to-peer)
- Emergency stop: GPIO 0 (BOOT), active LOW, 10Hz TX while held, 3x clear on release
- Display updates at 5Hz, ranging still at 20Hz round-robin
- Added Adafruit SSD1306 + GFX lib_deps to platformio.ini

Anchor firmware (esp32/uwb_anchor):
- ESP-NOW receiver: captures tag packets via ISR ring buffer
- Forwards to Jetson serial as +ESPNOW: and +ESTOP: lines
- E-STOP packets get priority immediate output
- Zero impact on existing TWR ranging loop

For Makerfabs ESP32 UWB Pro with Display (DW3000 chip).
2026-03-14 12:12:31 -04:00
salty
66a4bbe25b Merge remote-tracking branch 'origin/main' into salty/uwb-tag-display-wireless 2026-03-14 12:11:28 -04:00
35440b7463 Merge pull request 'feat: ROS2 sensor health monitor (Issue #566)' (#572) from sl-jetson/issue-566-health-monitor into main 2026-03-14 11:49:55 -04:00
d36b79371d Merge pull request 'feat: ESP32 UWB Pro anchor firmware — DS-TWR responder (Issue #544)' (#570) from sl-uwb/issue-544-anchor-firmware into main 2026-03-14 11:49:51 -04:00
3b0b9d0f16 Merge pull request 'feat: UWB tag firmware (Issue #545)' (#568) from sl-perception/issue-546-uwb-ros2 into main 2026-03-14 11:49:43 -04:00
4116232b27 Merge pull request 'feat: WebUI diagnostics dashboard (Issue #562)' (#567) from sl-webui/issue-562-diagnostics into main 2026-03-14 11:49:39 -04:00
sl-uwb
c6e479ef3f feat: ESP32 UWB Pro tag firmware — DS-TWR initiator (Issue #545)
Tag firmware for Makerfabs ESP32 UWB Pro worn by person being tracked.
Initiates DS-TWR with each robot anchor in 20 Hz round-robin.

- DS-TWR initiator: Poll→(RESP received)→Final with timestamps Ra/Da/Db
- Anchor side computes authoritative range; tag computes local estimate
- Round-robin across NUM_ANCHORS anchors (default 2) at 20 Hz
- Streams +RANGE:<id>,<mm>,<rssi> over USB serial (bench debug)
- LED blink on each successful range
- TAG_ID, NUM_ANCHORS, RANGE_INTERVAL_MS configurable in platformio.ini
- Pin map: SCK=18 MISO=19 MOSI=23 CS=21 RST=27 IRQ=34

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:48:12 -04:00
c7dcce18c2 feat: UWB anchor mount bracket wall/ceiling design (Issue #564) 2026-03-14 11:47:07 -04:00
8e03a209be feat: ROS2 sensor health monitor (Issue #566)
Add sensor_health_node to saltybot_health_monitor package. Monitors 8
sensor topics for staleness, publishing DiagnosticArray on
/saltybot/diagnostics and MQTT JSON on saltybot/health.

Sensors monitored (configurable thresholds):
  /camera/color/image_raw, /camera/depth/image_rect_raw,
  /camera/color/camera_info, /scan, /imu/data,
  /saltybot/uwb/range, /saltybot/battery, /saltybot/motor_daemon/status

Each sensor: OK/WARN/ERROR based on topic age vs warn_s/error_s thresholds.
Critical sensors (camera, lidar, imu, motor_daemon) escalate overall status.

Files added:
  sensor_health_node.py — SensorWatcher + SensorHealthNode
  config/sensor_health_params.yaml — per-sensor thresholds
  launch/sensor_health.launch.py
  test/test_sensor_health.py — 35 tests, all passing

setup.py/package.xml updated: sensor_msgs, diagnostic_msgs deps + new entry point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:47:01 -04:00
sl-uwb
a4879b6b3f feat: ESP32 UWB Pro anchor firmware — DS-TWR responder (Issue #544)
Anchor firmware for Makerfabs ESP32 UWB Pro (DW3000 chip). Two anchors
mount on SaltyBot (port/starboard), USB-connected to Jetson Orin.

- DS-TWR responder: Poll→Resp→Final with ±10cm accuracy
- Streams +RANGE:<id>,<mm>,<rssi_dbm> on Serial 115200
- AT command interface: AT+RANGE?, AT+RANGE_ADDR=, AT+ID?
- ANCHOR_ID 0/1 set at build time (env:anchor0 / env:anchor1)
- PlatformIO config for Makerfabs MaUWB_DW3000 library
- udev rules for /dev/uwb-anchor0 /dev/uwb-anchor1 USB symlinks
- Pin map: SCK=18 MISO=19 MOSI=23 CS=21 RST=27 IRQ=34

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:45:29 -04:00
2180b61440 feat: ROS2 UWB position node (Issue #546)
Add saltybot_uwb_position — ROS2 Python package that reads JSON range
measurements from an ESP32 DW3000 UWB tag over USB serial, trilaterates
the robot's absolute position from 3+ fixed infrastructure anchors, and
publishes position + TF2 to the rest of the stack.

Serial protocol (one JSON line per frame):
  Full frame: {"ts":…, "ranges": [{"id":0,"d_mm":1500,"rssi":-65}, …]}
  Per-anchor: {"id":0, "d_mm":1500, "rssi":-65.0}
  Accepts both "d_mm" and "range_mm" field names.

Trilateration (trilateration.py, numpy, no ROS deps):
  Linear least-squares: linearise sphere equations around anchor 0,
  solve (N-1)x2 (2D) or (N-1)x3 (3D) system via np.linalg.lstsq.
  2D mode (default): robot_z fixed, needs >=3 anchors.
  3D mode (solve_z=true): full 3D, needs >=4 anchors.

Outlier rejection:
  After initial solve, compute per-anchor residual |r_meas - r_pred|.
  Reject anchors with residual > outlier_threshold_m (0.4 m default).
  Re-solve with inliers if >= min_anchors remain.
  Track consecutive outlier strikes; flag in /status after N strikes.

Kalman filter (KalmanFilter3D, constant-velocity, 6-state, numpy):
  Predict-only coasting when anchors drop below minimum.
  Q=0.05, R=0.10 (tunable).

Topics:
  /saltybot/uwb/pose       PoseStamped  10 Hz Kalman-filtered position
  /saltybot/uwb/range/<id> UwbRange     on arrival, raw per-anchor ranges
  /saltybot/uwb/status     String/JSON  10 Hz state+residuals+flags

TF2: uwb_link -> map (identity rotation)

Anchor config: flat float arrays in YAML.
Default layout: 4-anchor 5x5m room at 2m height.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:43:22 -04:00
c2d9adad25 feat: WebUI diagnostics dashboard (Issue #562)
Standalone 3-file diagnostics dashboard (ui/diagnostics_panel.{html,js,css}).
No build step — serve the ui/ directory directly. roslib.js via CDN.

Panels:
- Battery: voltage (V), SOC (%), current (A) with large readouts + gauge bars
  + 2-minute sparkline history canvas, 4S LiPo thresholds
- Temperatures: CPU/GPU (Jetson tegrastats) + Board/STM32 + Motor L/R
  color-coded temp boxes with mini progress bars (green<60 amber<75 red>75°C)
- Motor current: per-wheel current gauge bars + CMD value + balance_state label
  Thresholds: warn 8A / crit 12A
- Resources: RAM / GPU memory / Disk — gauge bars with used/total display
  Thresholds: warn 80% / crit 95%
- WiFi / Network: RSSI signal bars (5-level) + dBm readout + latency (ms)
  MQTT broker status via mqtt_connected KeyValue
- ROS2 node health: full DiagnosticArray node list with OK/WARN/ERROR/STALE
  badges, per-node message preview, MutationObserver count badge

Features:
- Auto 2 Hz refresh via rosbridge subscriptions (throttle_rate: 500ms)
- Pulsing refresh indicator dot on each update
- System status bar: HEALTHY/DEGRADED/FAULT/STALE badge + battery/thermal/net
- Alert thresholds: red/amber/green for every metric
- Responsive CSS grid: 3-col → 2-col → 1-col via media queries
- WS URL persisted in localStorage

ROS topics:
  SUB /diagnostics              diagnostic_msgs/DiagnosticArray
  SUB /saltybot/balance_state   std_msgs/String (JSON)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:41:43 -04:00
76668d8346 Merge pull request 'feat: RPLIDAR A1 mount bracket (Issue #561)' (#563) from sl-mechanical/issue-561-rplidar-mount into main 2026-03-14 11:41:10 -04:00
d8e5490a0e feat: RPLIDAR A1 mount bracket (Issue #561) 2026-03-14 11:40:17 -04:00
6409360428 Merge pull request 'feat: Pan/tilt gimbal servo driver for ST3215 bus servos (Issue #547)' (#559) from sl-firmware/issue-547-gimbal-servo into main 2026-03-14 11:40:02 -04:00
6c5ecc9e00 Merge pull request 'feat: ROS2 gimbal control node (Issue #548)' (#558) from sl-jetson/issue-548-gimbal-ros2 into main 2026-03-14 11:39:58 -04:00
df6b79d676 Merge pull request 'feat: WebUI gimbal control panel (Issue #551)' (#557) from sl-webui/issue-551-gimbal-webui into main 2026-03-14 11:36:49 -04:00
0dbd64a6f4 Merge pull request 'feat: Camera gimbal mount for RealSense D435i (Issue #552)' (#556) from sl-mechanical/issue-552-gimbal-mount into main 2026-03-14 11:36:31 -04:00
8e21201dd4 Merge pull request 'feat: Person-following head tracking (Issue #549)' (#555) from sl-perception/issue-549-head-tracking into main 2026-03-14 11:36:29 -04:00
80e3b23aec Merge pull request 'feat: Phone voice command interface (Issue #553)' (#554) from sl-android/issue-553-voice-command into main 2026-03-14 11:36:27 -04:00
36643dd652 feat: Pan/tilt gimbal servo driver for ST3215 bus servos (Issue #547)
- servo_bus.c/h: half-duplex USART3 driver for Feetech ST3215 servos at
  1 Mbps; blocking TX/RX with CRC checksum; read/write position, torque
  enable, speed; deg<->raw conversion (center=2048, 4096 counts/360°)
- gimbal.c/h: gimbal_t controller; 50 Hz feedback poll alternating pan/tilt
  at 25 Hz each; clamps to ±GIMBAL_PAN/TILT_LIMIT_DEG soft limits
- jlink.c: dispatch JLINK_CMD_GIMBAL_POS (0x0B, 6-byte payload int16+int16+
  uint16); jlink_send_gimbal_state() for JLINK_TLM_GIMBAL_STATE (0x84)
- main.c: servo_bus_init() + gimbal_init() on boot; gimbal_tick() in main
  loop; gimbal_updated flag handler; GIMBAL_STATE telemetry at 50 Hz
- config.h: SERVO_BUS_UART/PORT/PIN/BAUD, GIMBAL_PAN/TILT_ID, GIMBAL_TLM_HZ,
  GIMBAL_PAN/TILT_LIMIT_DEG
- jlink.h: CMD_GIMBAL_POS, TLM_GIMBAL_STATE, jlink_tlm_gimbal_state_t (10 B),
  gimbal_updated/pan_x10/tilt_x10/speed volatile fields in JLinkState

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:38:06 -04:00
da6a17cdcb feat: ROS2 gimbal control node (Issue #548)
saltybot_gimbal ROS2 Python package for pan/tilt camera head control
via JLINK binary protocol over serial to STM32 (Issue #547 C side).

- gimbal_node.py: subscribes /saltybot/gimbal/cmd (Vector3: pan, tilt,
  speed), publishes /saltybot/gimbal/state (JSON), /saltybot/gimbal/cmd_echo
- Services: /saltybot/gimbal/home (Trigger), /saltybot/gimbal/look_at
  (Trigger + /saltybot/gimbal/look_at_target PointStamped)
- jlink_gimbal.py: JLINK codec matching jlink.h — CMD_GIMBAL_POS=0x0B,
  TLM_GIMBAL_STATE=0x84, CRC16-CCITT, deg*10 encoding, speed register
- MotionAxis: trapezoidal velocity profile (configurable accel + speed)
- Configurable limits: pan ±150°, tilt ±45° (gimbal_params.yaml)
- Serial reconnect with configurable retry delay
- 48 unit tests — all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:34:06 -04:00
cc3a65f4a4 feat: WebUI gimbal control panel (Issue #551)
Adds a full gimbal control panel with live camera preview:

Standalone page (ui/gimbal_panel.html + .js + .css):
- Self-contained HTML page, no build step, served directly
- roslib.js via CDN, connects to rosbridge WebSocket
- 2-D canvas pan/tilt pad: click-drag + touch pointer capture
- Live camera stream (front/rear/left/right selector, base64 CompressedImage)
- FPS badge + angle overlay on video feed
- Preset positions: CENTER / LEFT / RIGHT / UP / DOWN
- Home button (0° / 0°)
- Person-tracking toggle → /gimbal/tracking_enabled
- Current angle display from /gimbal/state feedback
- WS URL persisted in localStorage

React component (GimbalPanel.jsx) + App.jsx integration:
- Same features in dashboard — TELEOP group → Gimbal tab
- Shares rosbridge connection from parent
- Mobile-responsive: stacks vertically on mobile, side-by-side on lg+

ROS topics:
  PUB /gimbal/cmd              geometry_msgs/Vector3
  SUB /gimbal/state            geometry_msgs/Vector3
  PUB /gimbal/tracking_enabled std_msgs/Bool
  SUB /camera/*/image_raw/compressed sensor_msgs/CompressedImage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:29:29 -04:00
c68b751590 feat: Person-following head tracking (Issue #549)
Add saltybot_head_tracking — ROS2 Python node for automatic person-
following using dual-axis PID control targeting the pan/tilt camera head.

Pipeline:
  1. Subscribe to /saltybot/objects (DetectedObjectArray from YOLOv8n)
  2. Filter for class_id==0 (person); select best target by score:
       score = 0.6 * 1/(1+dist_m)  +  0.4 * confidence
     (falls back to confidence-only when distance_m==0 / unknown)
  3. Compute pixel error of bbox centre from image centre
  4. Apply dead-zone (10 px default) to suppress micro-jitter
  5. Convert pixel error to angle error via camera FOV
  6. Independent PID controllers for pan and tilt axes
  7. Accumulate PID output into absolute angle setpoint
  8. Publish geometry_msgs/Point to /saltybot/gimbal/cmd:
       x = pan_angle_deg, y = tilt_angle_deg, z = confidence

State machine:
  IDLE      -> waiting for first detection
  TRACKING  -> active PID
  LOST      -> hold last angle for hold_duration_s (3 s)
  CENTERING -> return to (0, 0) at 20 deg/s -> IDLE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:28:17 -04:00
0fd6ea92b0 feat: Camera gimbal mount bracket for RealSense D435i (Issue #552) 2026-03-14 10:28:03 -04:00
sl-android
c249b2d74e feat: Phone voice command interface (Issue #553)
Add phone/voice_commander.py — Termux-based voice command listener for SaltyBot:
- Continuous wake word detection ('Hey Salty') via Whisper STT on short audio clips
- Command recording after wake word, transcribed with local Whisper (tiny/base/small)
- Parses go forward/back/left/right, stop, follow me, go home, look at me
- Publishes JSON to /saltybot/voice/cmd via ROS2 (rclpy) or rosbridge WebSocket
- TTS confirmation via termux-tts-speak; 'Yes?' prompt on wake word
- Fuzzy token-overlap fallback for wake word matching
- Flags: --host, --port, --model, --threshold, --record-sec, --no-tts, --debug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 10:26:38 -04:00
59d164944d Merge origin/sl-controls/issue-533-battery-adc — resolve jlink conflicts
Keep both Issue #531 (PID_RESULT telemetry) and Issue #533 (BATTERY
telemetry) additions in include/jlink.h and src/jlink.c.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:10:26 -05:00
6e09d13dfc Merge pull request 'feat: Audio pipeline — wake word + STT + TTS on Jabra SPEAK 810 (Issue #503)' (#543) from sl-jetson/issue-503-audio-pipeline into main 2026-03-07 10:07:16 -05:00
salty
02217443ea chore: merge CAD files and design docs from seb/saltylab seed repo
Consolidating seb/saltylab into saltylab-firmware before deleting the seed repo.
- 16 OpenSCAD CAD models → cad/
- Design docs (SALTYLAB.md, PLATFORM.md, AGENTS.md, board-viz.html) → docs/
2026-03-07 10:04:24 -05: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
6d316514da Merge remote-tracking branch 'origin/sl-firmware/issue-531-pid-autotune' 2026-03-07 10:03:24 -05:00
2d97033539 Merge remote-tracking branch 'origin/sl-perception/issue-532-depth-costmap' 2026-03-07 10:03:24 -05:00
71ec357c93 Merge remote-tracking branch 'origin/sl-webui/issue-534-teleop-webui' 2026-03-07 10:03:24 -05:00
cc0ffd1999 feat: Battery voltage ADC driver with DMA sampling (Issue #533)
STM32F7 ADC driver for battery voltage/current monitoring using
DMA-based continuous sampling, IIR low-pass filter, voltage divider
calibration, and USART telemetry to Jetson. Integrates with power
management for low-battery sleep (Issue #467).

Implementation:
- include/battery_adc.h: New driver header with calibration struct and
  public API (init, tick, get_voltage_mv, get_current_ma, calibrate,
  publish, check_pm, is_low, is_critical)
- src/battery_adc.c: ADC3 continuous-scan DMA (DMA2_Stream0/Ch2), 4x
  hardware oversampling of both Vbat (PC1/IN11) and Ibat (PC3/IN13),
  IIR LPF (alpha=1/8, cutoff ~4 Hz at 100 Hz tick rate), calibration
  with ±500 mV offset clamp, 3S/4S auto-detection, 1 Hz USART publish
- include/jlink.h + src/jlink.c: Add JLINK_TLM_BATTERY (0x82) telemetry
  type and jlink_tlm_battery_t (10-byte packed struct), implement
  jlink_send_battery_telemetry() using CRC16-XModem framing
- include/power_mgmt.h + src/power_mgmt.c: Add
  power_mgmt_notify_battery() — triggers STOP-mode sleep when Vbat
  sustains critical level (Issue #467)
- test/test_battery_adc.c: 27 unit tests (27/27 passing): voltage
  conversion, calibration offset/scale, IIR LPF convergence, SoC
  estimation (3S/4S), low/critical flags, PM notification timing,
  calibration reset, publish rate-limiting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 10:01:02 -05:00
19a30a1c4f feat: PID auto-tune for balance mode (Issue #531)
Implement Ziegler-Nichols relay feedback auto-tuning with flash persistence:

Firmware (STM32F722):
- pid_flash.c/h: erase+write Kp/Ki/Kd to flash sector 7 (0x0807FFC0),
  magic-validated; load on boot to restore saved tune
- jlink.h: add JLINK_CMD_PID_SAVE (0x0A) and JLINK_TLM_PID_RESULT (0x83)
  with jlink_tlm_pid_result_t struct and pid_save_req flag in JLinkState
- jlink.c: dispatch JLINK_CMD_PID_SAVE -> pid_save_req; add
  jlink_send_pid_result() to confirm flash write outcome over USART1
- main.c: load saved PID from flash after balance_init(); handle
  pid_save_req in main loop (disarmed-only, erase stalls CPU ~1s)

Jetson ROS2 (saltybot_pid_autotune):
- pid_autotune_node.py: add Ki to Ziegler-Nichols formula (ZN PID:
  Kp=0.6Ku, Ki=1.2Ku/Tu, Kd=0.075KuTu); add JLink serial client that
  sends JLINK_CMD_PID_SET + JLINK_CMD_PID_SAVE after tuning completes
- autotune_config.yaml: add jlink_serial_port and jlink_baud_rate params

Trigger: ros2 service call /saltybot/autotune_pid std_srvs/srv/Trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:56:19 -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
916ad36ad5 feat(webui): teleop web interface with live camera stream (Issue #534)
Adds TeleopWebUI component — a dedicated browser-based remote control
panel combining live video and joystick teleoperation in one view:

- Live camera stream (front/rear/left/right) via rosbridge CompressedImage
- Virtual joystick (canvas-based, touch + mouse, 10% deadzone)
- WASD / arrow-key keyboard fallback, Space for quick stop
- Speed presets: SLOW (20%), NORMAL (50%), FAST (100%)
- Latching E-stop button with pulsing visual indicator
- Real-time linear/angular velocity display
- Mobile-responsive: stacks vertically on small screens, side-by-side on lg+
- Added TELEOP tab group → Drive tab in App.jsx

Topics: /camera/<name>/image_raw/compressed (subscribe)
        /cmd_vel geometry_msgs/Twist (publish)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:51:14 -05:00
fc43135144 feat: Spring-loaded phone mount bracket for T-slot rail (Issue #535)
Parametric OpenSCAD design for 2020 T-slot rail phone mount bracket.
Adjustable width 60-85mm, spring-loaded cam quick-release lever,
vibration-dampening flexure rib grip pads. PETG 3D-printable, no supports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 09:49:10 -05:00
e67783f313 Merge remote-tracking branch 'origin/sl-android/issue-521-esc-debug-cleanup' 2026-03-06 23:34:45 -05:00
68b6410b24 Merge remote-tracking branch 'origin/sl-jetson/issue-523-motor-daemon' 2026-03-06 23:34:45 -05:00
5b7ee63d1e Merge remote-tracking branch 'origin/sl-controls/issue-522-usart6-truncation' 2026-03-06 23:34:45 -05:00
4f33e4e88d Merge pull request 'fix: USB CDC TX investigation (Issue #524)' (#525) from sl-mechanical/issue-524-usb-cdc-tx into main 2026-03-06 23:34:17 -05:00
54bc2509c1 Merge pull request 'fix: IMU calibration (Issue #520)' (#530) from sl-firmware/issue-520-imu-calibration into main 2026-03-06 23:34:16 -05:00
b0a5041261 fix: MPU6000 IMU calibration SPI/DCache issue (Issue #520)
Three bugs prevented mpu6000_is_calibrated() from returning true,
blocking arming and balance mode:

1. WHO_AM_I single-attempt: one SPI glitch returning 0x00 caused
   icm42688_init() to return -128, skipping mpu6000_calibrate()
   entirely. Fix: retry WHO_AM_I up to 3 times with 10ms gaps.

2. icm42688_read() rx[15] uninitialized: if HAL_SPI_TransmitReceive()
   failed, garbage stack data was accumulated as gyro bias. Fix: zero-
   init rx[15] so failed transfers produce zero data.

3. mpu6000_calibrate() raw uninitialized: UB if icm42688_read() is
   a no-op (imu_type mismatch). Fix: zero-init raw each iteration.

Also add SCB_InvalidateDCache_by_Addr() on SPI rx buffers in rreg()
and icm42688_read() for DCache coherency. Currently a no-op (DCache
is not enabled), but required if SCB_EnableDCache() is added — stack
buffers in SRAM2 are in the cacheable memory region on STM32F7.

Fix misleading DCache comment in icm42688.c (claimed DCache was
disabled by main.c; actually SCB_EnableDCache() is never called).

Build: 59904 bytes Flash (+512), 17100 bytes RAM — SUCCESS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:14:49 -05:00