- Tag: strip all ESP-NOW/WiFi code
- Tag: add BLE GPS characteristic (def3) for iPhone binary GPS writes
- Tag: add BLE Phone IMU characteristic (def4) for iPhone IMU writes
- Tag: transmit GPS/IMU/heartbeat via DW1000 data frames (v2 protocol)
- Tag: update OLED display with GPS speed/heading/fix indicators
- Tag: e-stop now sent via UWB data frames (3x for reliability)
- Anchor: strip all ESP-NOW/WiFi code
- Anchor: receive DW1000 data frames, forward to serial as +GPS/+PIMU/+TIMU/+ESTOP
- Protocol v2: magic {0x5B, 0x02}, msg types 0x60-0x64
Replace hardcoded NUM_ANCHORS with dynamic discovery table.
Tag probes IDs 0..7 via standard DS-TWR POLL frames at startup
and after every DISC_RESCAN_MS (10 s); anchors that respond are
added to the active table, stale ones removed after
ANCHOR_MISS_LIMIT misses or ANCHOR_TIMEOUT_MS silence.
Key changes
-----------
- AnchorEntry table (8 slots): present/active flags, range_mm,
rssi, last_ok_ms, miss_count
- DISC_SCAN → DISC_RANGE state machine
- range_and_update(): ranges, updates table, emits +DISC:FOUND /
+DISC:LOST on serial and MSG_DISC (0x40) on ESP-NOW
- disc_scan_step(): advances cursor one probe per ranging slot,
skips already-active IDs
- New OLED screens: ANCHOR SCAN progress bar, NO ANCHOR rescan
countdown, active anchor count badge on normal screen
- platformio.ini: switch to MaUWB_DW3000 lib, remove NUM_ANCHORS
build flag (discovery is now dynamic)
- No anchor firmware changes required
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Display was never turned on after display.begin() in setup()
- Added SSD1306_DISPLAYON command after initialization
- Added short button press detection to wake display
- Fixes blank display issue reported by SAUL
Implements three-tier power management for battery life:
- OLED auto-off after 30s inactivity via SSD1306_DISPLAYOFF
(saves ~25mA; wakes on next range callback)
- DW1000 deep sleep after 5min idle via DW1000.deepSleep()
(saves ~155mA, 160mA→3.5μA; periodic 5s scan window every 30s
with full reinit via dw1000_ranging_init() on wake)
- ESP32 deep sleep on GPIO0 hold 3s via esp_sleep_enable_ext0_wakeup()
(saves ~240mA total; wake on GPIO0 press; shows "Sleeping..." on OLED)
Active: ~250mA Sleep target: <5mA (50x reduction)
All sleep/wake paths tested with clean build (74.1% flash, 14.1% RAM).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Tag: MPU6050 on shared I2C bus (SDA=4, SCL=5, addr 0x68)
- Accel ±8g, gyro ±500dps, read at 50Hz
- Fall detection: freefall+impact or sudden >3.5g
- Fall triggers auto e-stop (ESP-NOW broadcast)
- OLED shows tilt bar + accel magnitude
- New ESP-NOW packet types: MSG_IMU (0x40), MSG_FALL (0x50)
- IMU broadcast at 10Hz via ESP-NOW
- Anchor forwards +IMU: and +FALL: lines to Jetson serial
- ROS2 uwb_driver publishes sensor_msgs/Imu on /uwb/imu
- .gitignore for .pio build dirs
Anchors:
- Broadcast own range via ESP-NOW after each TWR cycle
- Receive other anchor's range via ESP-NOW, print as +RANGE on serial
- Either anchor's serial port gives Orin BOTH ranges (only need 1 USB/UART)
Tag:
- Receives ESP-NOW from both anchors
- Updates display with both A0 + A1 distances regardless of DW1000 pairing
- Solves the 'only sees A0' display issue
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.
- 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
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).
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>
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>
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>
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>
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>
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>
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>
- 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>