Compare commits

...

595 Commits

Author SHA1 Message Date
b353a2ba29 Merge pull request 'feat: systemd auto-start for ROS2 + dashboard on Orin boot (bd-1hyn)' (#732) from sl-perception/bd-1hyn-orin-autostart into main 2026-04-17 23:11:15 -04:00
329797d43c Merge pull request 'feat: ESP32-S3 OTA stack — partitions, Gitea checker, self-update, UART IO, display, Orin serial trigger (6 beads)' (#731) from sl-firmware/ota-esp32 into main 2026-04-17 23:11:04 -04:00
1ae600ead4 feat: Orin serial OTA_CHECK + OTA_UPDATE commands, version reporting (bd-1s1s)
Extends the bd-66hx serial protocol with two new Orin→ESP32 commands:
  CMD_OTA_CHECK  (0x10): triggers gitea_ota_check_now(), responds with
    TELEM_VERSION_INFO (0x84) for Balance and IO (current + available ver).
  CMD_OTA_UPDATE (0x11): uint8 target (0=balance, 1=io, 2=both) — triggers
    uart_ota_trigger() for IO or ota_self_trigger() for Balance.
    NACK with ERR_OTA_BUSY or ERR_OTA_NO_UPDATE on failure.
New telemetry: TELEM_OTA_STATUS (0x83, target+state+progress+err),
  TELEM_VERSION_INFO (0x84, target+current[16]+available[16]).
Wires OTA stack into app_main: ota_self_health_check on boot,
  gitea_ota_init + ota_display_init after peripherals ready.
CMakeLists updated with all OTA component dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:52 -04:00
e73674f161 feat: GC9A01 OTA notification badge + progress ring UI (bd-1yr8)
Adds ota_display_task (5 Hz) on GC9A01 240×240 round LCD:
- Idle: orange dot badge at top-right when update available, version text
- Progress: arc sweeping 0→360° around display perimeter with phase label
- States: Downloading/Verifying/Applying/Rebooting (Balance) and
  Downloading/Sending/Done (IO via UART)
- Error: red arc + "FAILED RETRY?" prompt
Display primitives (fill_rect, draw_string, draw_arc) are stubs called
from the GC9A01 SPI driver layer (separate driver bead).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:52 -04:00
972db16635 feat: UART OTA protocol Balance→IO board, 1 KB chunk + ACK (bd-21hv)
Balance side (uart_ota.c): downloads io-firmware.bin from Gitea to RAM,
computes SHA256, then streams to IO over UART1 (GPIO17/18, 460800 baud)
as OTA_BEGIN/OTA_DATA/OTA_END frames with CRC8 + per-chunk ACK/retry (×3).
IO side (uart_ota_recv.c): receives frames, writes to inactive OTA partition
via esp_ota_write, verifies SHA256 on OTA_END, sets boot partition, reboots.
IO board main.c + CMakeLists.txt scaffold included.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:52 -04:00
5250ce67ad feat: Balance self-OTA download, SHA256 verify, rollback (bd-18nb)
Downloads balance-firmware.bin from Gitea release URL to inactive OTA
partition, streams SHA256 verification via mbedTLS, sets boot partition
and reboots. Auto-rollback via CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE if
ota_self_health_check() not called within 30 s of boot. Progress 0-100%
in g_ota_self_progress for display task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:52 -04:00
d2175bf7d0 feat: Gitea release version checker with WiFi (bd-3hte)
Adds gitea_ota_check_task on Balance board: fetches Gitea releases API
every 30 min and on boot, filters by esp32-balance/ and esp32-io/ tag
prefixes, compares semver against embedded FW version, stores update info
(version string, download URL, SHA256) in g_balance_update / g_io_update.
WiFi credentials read from NVS namespace "wifi"; falls back to compile-time
DEFAULT_WIFI_SSID/PASS if NVS is empty.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:52 -04:00
2a13c3e18b feat: partition tables + OTA setup for Balance and IO boards (bd-3gwo)
Add dual OTA partitions (ota_0/ota_1 × 1.75 MB each) and otadata to
both esp32s3/balance/ and esp32s3/io/ on 4 MB flash layouts.
Enable CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE and OTA HTTP on Balance.
Create esp32s3/io/ project scaffold with config.h pin assignments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 23:10:51 -04:00
3f0508815d Merge pull request 'ci: OTA release pipeline — build + attach firmware binaries (bd-9kod)' (#730) from sl-jetson/bd-9kod-ota-ci into main 2026-04-17 23:10:26 -04:00
d9e7acfa0d Merge pull request 'feat: ESP32 Balance UART/USB protocol for Orin + VESC proxy (bd-66hx)' (#729) from sl-firmware/bd-66hx-esp32-uart-orin into main 2026-04-17 23:10:15 -04:00
c02faf3ac2 Merge pull request 'feat: Here4 GPS DroneCAN on Orin via CANable2 (bd-p47c)' (#728) from sl-perception/bd-p47c-here4-can-gps into main 2026-04-17 23:10:05 -04:00
61f241ae1d Merge pull request 'feat: Orin UART/USB serial comms with ESP32 Balance (bd-wim1)' (#727) from sl-perception/bd-wim1-orin-uart-esp32 into main 2026-04-17 23:09:53 -04:00
26e71d7a14 feat: systemd auto-start for ROS2 + dashboard on Orin boot (bd-1hyn)
Implements full boot-time auto-start for the SaltyBot ROS2 stack on
Jetson Orin. Everything comes up automatically after power-on with
correct dependency ordering and restart-on-failure for each service.

New systemd services:
  saltybot-ros2.service         full_stack.launch.py (perception + SLAM + Nav2)
  saltybot-esp32-serial.service ESP32-S3 BALANCE UART bridge (bd-wim1, PR #727)
  saltybot-here4.service        Here4 DroneCAN GPS bridge (bd-p47c, PR #728)
  saltybot-dashboard.service    Web dashboard on port 8080

Updated:
  saltybot.target               now Wants all four new services with
                                boot-order comments
  can-bringup.service           bitrate 500 kbps → 1 Mbps (DroneCAN for Here4)
  70-canable.rules              remove bitrate from udev RUN+=; let service
                                own the bitrate, add TAG+=systemd for device unit
  install_systemd.sh            installs all services + udev rules, colcon
                                build, enables mosquitto, usermod dialout
  full_stack.launch.py          resolve 8 merge conflict markers (ESP32-S3
                                rename) and fix missing indent on
                                enable_mission_logging_arg — file was
                                un-launchable with SyntaxError

New:
  scripts/ros2-launch.sh        sources ROS2 Humble + workspace overlay,
                                then exec ros2 launch — used by all
                                ROS2 service units via ExecStart=
  udev/80-esp32.rules           /dev/esp32-balance (CH343) and
                                /dev/esp32-io (ESP32-S3 native USB CDC)

Resolves bd-1hyn

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:20:40 -04:00
2fa097e3d6 ci: OTA release pipeline — build + attach firmware binaries (bd-9kod)
Adds .gitea/workflows/ota-release.yml: triggered on esp32-balance/vX.Y.Z
or esp32-io/vX.Y.Z tags, builds the corresponding ESP32-S3 project with
espressif/idf:v5.2.2, and attaches <app>_<version>.bin + .sha256 to the
Gitea release for OTA download.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:11:33 -04:00
b830420efc feat: add UART/USB serial protocol for Orin comms, proxy VESC CAN (bd-66hx)
Replaces Orin↔ESP32-S3 BALANCE CAN comms (0x300-0x303 / 0x400-0x401)
with binary serial framing over CH343 USB-CDC at 460800 baud.

Protocol matches bd-wim1 (sl-perception) exactly:
  Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8-SMBUS]
  CRC covers LEN+TYPE+PAYLOAD, big-endian multi-byte fields.

Commands (Orin→ESP32): HEARTBEAT/DRIVE/ESTOP/ARM/PID
Telemetry (ESP32→Orin): TELEM_STATUS, TELEM_VESC_LEFT (ID 56),
                         TELEM_VESC_RIGHT (ID 68), ACK/NACK

VESC CAN TWAI kept for motor control; drive commands from Orin
forwarded to VESCs via SET_RPM. Hardware note: SN65HVD230
rewired from IO43/44 to IO2/IO1 to free IO43/44 for CH343.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:09:12 -04:00
9d6c72bd24 feat: Here4 GPS DroneCAN integration via CANable2 (bd-p47c)
Implements saltybot_dronecan_gps ROS2 package — DroneCAN/UAVCAN v0
bridge that publishes Here4 GPS, IMU, magnetometer, and barometer data
to ROS2. CANable2 freed from ESP32 BALANCE comms (bd-wim1) now runs
Here4 at 1 Mbps DroneCAN.

Key features:
- dronecan_parser.py: pure-Python DSDL converters (Fix2, RawIMU, Mag,
  StaticPressure, StaticTemperature, NodeStatus, RTCMStream chunks),
  testable without dronecan library or CAN hardware
- here4_node.py: ROS2 node, auto-discovers Here4 node ID on first Fix2,
  publishes /gps/fix + /gps/velocity for navsat_transform EKF fusion,
  HDOP-based NavSatStatus upgrade (RTK/SBAS), RTCM injection via
  /rtcm (ByteMultiArray) or /rtcm_hex (String hex fallback)
- 39 unit tests, all passing
- bring_up_can:=true parameter to configure SocketCAN at launch

Resolves bd-p47c

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 21:49:00 -04:00
9b460e34db feat: Orin UART/USB serial interface for ESP32 Balance (bd-wim1)
New package saltybot_esp32_serial replaces saltybot_can_bridge
(CANable2/python-can) with direct USB-CDC serial to ESP32-S3 BALANCE
(CH343 chip, 1a86:55d3, /dev/esp32-balance @ 460800 baud).

Framing: [0xAA][LEN][TYPE][PAYLOAD][CRC8-SMBUS] matching bd-66hx spec.

esp32_balance_protocol.py — codec + streaming FrameParser (state-machine)
  - Commands: HEARTBEAT(0x01), DRIVE(0x02), ESTOP(0x03), ARM(0x04), PID(0x05)
  - Telemetry: STATUS(0x80), VESC_LEFT(0x81), VESC_RIGHT(0x82), ACK/NACK

esp32_balance_node.py — ROS2 node
  - Subs: /cmd_vel, /estop, /saltybot/arm, /saltybot/pid_update
  - Pubs: /saltybot/attitude, /saltybot/balance_state, /can/battery,
          /can/vesc/{left,right}/state, /can/connection_status
  - 500ms /cmd_vel watchdog → CMD_DRIVE(0,0)
  - 200ms CMD_HEARTBEAT keepalive timer
  - Auto-reconnect on serial disconnect

Proxied VESC telemetry: erpm, voltage, current, temp for IDs 56(L)/68(R).
Frees CANable2 for bd-p47c (Here4 GPS).
33 unit tests — all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 19:38:38 -04:00
c297d24a48 Merge pull request 'fix: Rename sultee-tracker → saul-tee-tracker' (#724) from sl-webui/fix-tracker-rename into main 2026-04-04 11:23:02 -04:00
885a66f24b Merge pull request 'feat: MQTT bridge for iOS GPS on /saltybot/ios/gps (Issue #681)' (#723) from sl-jetson/issue-681-ios-gps-bridge into main 2026-04-04 11:21:14 -04:00
fbc88f5c2a fix: correct rclpy logger calls to use f-strings (pre-existing bugs)
rclpy RcutilsLogger.info/warning/debug() do not accept printf-style
positional format args. Also fix p["use_phone_timestamp"] → p["use_phone_ts"]
key mismatch in __init__ log line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:20:58 -04:00
0122957b6b feat: Add iOS phone GPS MQTT-to-ROS2 bridge topic (Issue #681)
- Add _TOPIC_IOS_GPS = 'saltybot/ios/gps' constant
- Subscribe to saltybot/ios/gps in _on_mqtt_connect
- Dispatch to _handle_ios_gps() in _dispatch()
- _handle_ios_gps(): same logic as _handle_gps(), frame_id='ios_gps',
  publishes to /saltybot/ios/gps via self._ios_gps_pub
- Add rx/pub/err/last_rx_ts counters for the new topic
- Add /saltybot/ios/gps to rosbridge_params.yaml topics_glob

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 11:20:58 -04:00
759277a7e0 fix: Rename sultee-tracker to saul-tee-tracker (typo fix) 2026-04-04 11:19:35 -04:00
b1e8da4403 Merge pull request 'feat: iOS phone GPS via rosbridge topic /saltybot/ios/gps (Issue #681)' (#722) from sl-webui/issue-681-ios-gps-rosbridge into main 2026-04-04 11:15:13 -04:00
dd8afb480f Merge pull request 'fix: add phone bridge and GPS topics to rosbridge whitelist (Issue #681)' (#721) from sl-webui/issue-681-fix-gps-topics into main 2026-04-04 11:15:12 -04:00
43fb3f1147 feat: Route iOS phone GPS through rosbridge instead of raw WebSocket (Issue #681) 2026-04-04 11:11:15 -04:00
416a393134 fix: correct delay_between_messages type to float in rosbridge_params
rclpy expects DOUBLE for this param; integer 0 raises InvalidParameterTypeException.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:58:09 -04:00
60f500c206 fix: add phone bridge and GPS topics to rosbridge whitelist (Issue #681)
Add /saltybot/phone/gps, /saltybot/phone/imu, /saltybot/phone/battery,
/saltybot/phone/bridge/status, /gps/fix, /gps/vel to topics_glob so
the browser GPS dashboard can receive phone-bridged GPS data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:56:06 -04:00
b1cd15327f Merge pull request 'fix: GPS tracker subscribes to correct phone bridge topic (Issue #681)' (#720) from sl-webui/issue-681-fix-gps-topics into main 2026-04-04 10:07:10 -04:00
b72e435bf3 fix: Update tracker GPS topic to match phone bridge (Issue #681) 2026-04-04 10:01:57 -04:00
9cf98830c6 Merge pull request 'feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only' (#715) from sl-firmware/cleanup-legacy-hw into main 2026-04-04 09:00:55 -04:00
a1233dbd04 fix: scrub remaining Mamba references in can_bridge and e2e test protocol files
- balance_protocol.py: Mamba→Orin / Mamba→VESC comments → ESP32-S3 BALANCE
- can_bridge_node.py: docstring and inline comments
- __init__.py: package description
- protocol_defs.py: all Mamba references in docstring and comments
- test_fc_vesc_broadcast.py, test_drive_command.py: test comments

Zero Mamba/STM32F722/BlackPill/stm32_protocol/mamba_protocol references
now exist outside legacy/stm32/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:44 -04:00
fa75c442a7 feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only
Archive STM32 firmware to legacy/stm32/:
- src/, include/, lib/USB_CDC/, platformio.ini, test stubs, flash_firmware.py
- test/test_battery_adc.c, test_hw_button.c, test_pid_schedule.c, test_vesc_can.c, test_can_watchdog.c
- USB_CDC_BUG.md

Rename: stm32_protocol → esp32_protocol, mamba_protocol → balance_protocol,
  stm32_cmd_node → esp32_cmd_node, stm32_cmd_params → esp32_cmd_params,
  stm32_cmd.launch.py → esp32_cmd.launch.py,
  test_stm32_protocol → test_esp32_protocol, test_stm32_cmd_node → test_esp32_cmd_node

Content cleanup across all files:
- Mamba F722S → ESP32-S3 BALANCE
- BlackPill → ESP32-S3 IO
- STM32F722/F7xx → ESP32-S3
- stm32Mode/Version/Port → esp32Mode/Version/Port
- STM32 State/Mode labels → ESP32 State/Mode
- Jetson Nano → Jetson Orin Nano Super
- /dev/stm32 → /dev/esp32
- stm32_bridge → esp32_bridge
- STM32 HAL → ESP-IDF

docs/SALTYLAB.md:
- Update "Drone FC Details" to describe ESP32-S3 BALANCE board (Waveshare ESP32-S3 Touch LCD 1.28)
- Replace verbose "Self-Balancing Control" STM32 section with brief note pointing to SAUL-TEE-SYSTEM-REFERENCE.md

TEAM.md: Update Embedded Firmware Engineer role to ESP32-S3 / ESP-IDF

No new functionality — cleanup only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 09:00:38 -04:00
fe84ff6039 Merge pull request 'feat(arch): CAN/UART bridge update for SAUL-TEE ESP32 architecture' (#714) from sl-controls/esp32-can-bridge-update into main 2026-04-04 09:00:10 -04:00
fda6ab99ff feat(arch): align CAN/UART bridges with SAUL-TEE-SYSTEM-REFERENCE.md spec
Update CAN and serial bridge code to match authoritative protocol spec
from docs/SAUL-TEE-SYSTEM-REFERENCE.md §5-6 (hal, 2026-04-04).

mamba_protocol.py (CAN, Orin ↔ ESP32 BALANCE):
- 0x300 DRIVE: [speed:i16][steer:i16][mode:u8][flags:u8][_:u16] — combined frame
- 0x301 ARM:   [arm:u8]
- 0x302 PID:   [kp:f16][ki:f16][kd:f16][_:u16]  — half-float gains
- 0x303 ESTOP: [0xE5] — magic byte cut
- 0x400 ATTITUDE: [pitch:f16][speed:f16][yaw_rate:f16][state:u8][flags:u8]
- 0x401 BATTERY:  [vbat_mv:u16][fault_code:u8][rssi:i8]
- Add VESC STATUS1/4/5 decode helpers; VESC IDs 56 (left) / 68 (right)

can_bridge_node.py:
- /cmd_vel → encode_drive_cmd (speed/steer int16, MODE_DRIVE)
- /estop → encode_estop_cmd (magic 0xE5); clear → DISARM
- /saltybot/arm → encode_arm_cmd (new subscription)
- Watchdog sends DRIVE(0,0,MODE_IDLE) when /cmd_vel silent
- ATTITUDE (0x400) → /saltybot/attitude + /saltybot/balance_state JSON
- BATTERY  (0x401) → /can/battery BatteryState
- VESC STATUS1 frames → /can/vesc/left|right/state

stm32_cmd_node.py — rewritten for inter-board protocol API:
- Imports from updated stm32_protocol (BAUD_RATE=460800, new frame types)
- RX: RcChannels → /saltybot/rc_channels, SensorData → /saltybot/sensors
- TX: encode_led_cmd, encode_output_cmd from /saltybot/leds + /saltybot/outputs
- HEARTBEAT (0x20) timer replaces old SPEED_STEER/ARM logic

stm32_cmd_params.yaml: serial_port=/dev/esp32-io, baud=460800
stm32_cmd.launch.py: updated defaults and description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:45 -04:00
308be74330 feat(arch): implement SAUL-TEE ESP32 protocol specs from hal reference doc
Spec source: docs/SAUL-TEE-SYSTEM-REFERENCE.md (hal, 2026-04-04)

stm32_protocol.py — rewritten for inter-board UART protocol (ESP32 BALANCE ↔ IO):
- Frame: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud (was STX/ETX/CRC16)
- CRC-8 poly 0x07 over LEN+TYPE+PAYLOAD
- New message types: RC_CHANNELS(0x01), SENSORS(0x02), LED_CMD(0x10),
  OUTPUT_CMD(0x11), MOTOR_CMD(0x12), HEARTBEAT(0x20)

mamba_protocol.py — updated CAN IDs and frame formats:
- Orin→BALANCE: DRIVE(0x300) f32×2 LE, MODE(0x301), ESTOP(0x302), LED(0x303)
- BALANCE→Orin: FC_STATUS(0x400) pitch/vbat/state, FC_VESC(0x401) rpm/current
- VESC node IDs: Left=56, Right=68 (authoritative per §8)
- VESC extended frames: STATUS1(cmd=9), STATUS4(cmd=16), STATUS5(cmd=27)
- Replaced old MAMBA_CMD_*/MAMBA_TELEM_* constants

can_bridge_node.py — updated to use new IDs:
- ORIN_CMD_DRIVE/MODE/ESTOP replace MAMBA_CMD_VELOCITY/MODE/ESTOP
- FC_STATUS handler: publishes pitch→/can/imu, vbat_mv→/can/battery
- FC_VESC handler: publishes rpm/cur→/can/vesc/left|right/state
- VESC STATUS1 extended frames decoded per node ID (56/68)
- Removed PID CAN command (not in new spec)

CLAUDE.md — updated with ESP32-S3 BALANCE/IO hardware summary + key protocols

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:59:28 -04:00
19be6bbe11 Merge pull request 'docs: SAUL-TEE full ESP32-S3 system reference (arch migration)' (#712) from sl-firmware/arch-esp32-migration into main 2026-04-04 08:57:11 -04:00
5ef1f7e365 docs: full SAUL-TEE ESP32-S3 spec — pins, CAN, UART, RC mapping
Complete hardware reference from hal@Orin spec (2026-04-04):
- docs/SAUL-TEE-SYSTEM-REFERENCE.md: authoritative pin/protocol/CAN reference
  ESP32-S3 BALANCE: QMI8658 SPI(IO38-42), GC9A01 LCD, SN65HVD230 CAN(IO43/44),
  inter-board UART(IO17/18)
  ESP32-S3 IO: Crossfire UART0(IO43/44), ELRS UART2(IO16/17), BTS7960(IO1-8),
  I2C(IO11/12), WS2812(IO13), buzzer/headlight/fan, arming btn, kill-sw, UART(IO18/21)
- Inter-board binary protocol: [0xAA][LEN][TYPE][PAYLOAD][CRC8] @ 460800 baud
- CAN: VESC L=68, R=56; Orin cmds 0x300-0x303; telemetry 0x400-0x401 @ 10Hz
- RC: CH5=ARM, CH6=ESTOP, CH7=speed-limit; CRSF loss >100ms = motors cut
- CLAUDE.md, TEAM.md, docs/AGENTS.md, docs/SALTYLAB.md updated with full spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
bfca6d1d92 docs: Add SAUL-TEE system reference + update wiring diagram
- docs/SAUL-TEE-SYSTEM-REFERENCE.md: authoritative architecture doc for
  the new 4-wheel wagon. Covers ESP32-S3 BALANCE (Waveshare LCD 1.28,
  QMI8658, SN65HVD230 CAN), ESP32-S3 IO (TBS Crossfire, ELRS, BTS7960,
  NFC/baro/ToF, WS2812), inter-board UART protocol (460800 baud,
  [0xAA][len][type][payload][crc8]), CAN IDs (VESCs 68/56, Orin
  0x300-0x303 cmd / 0x400-0x401 telemetry), RC channel map, power
  architecture, safety systems, and firmware layout.

- docs/wiring-diagram.md: banner pointing to new reference doc;
  old Mamba F722S UART summary marked OBSOLETE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
f71dad5344 feat(arch): migrate all STM32/Mamba/BlackPill refs to ESP32 BALANCE/IO + fix roslib@1.4.0
Architecture change (2026-04-03): Mamba F722S (STM32F722) and BlackPill
replaced by ESP32 BALANCE (PID loop) and ESP32 IO (motors/sensors/comms).

- Update CLAUDE.md, docs, chassis BOM/ASSEMBLY, pinout, power-budget,
  wiring-diagram, TEAM.md, AUTONOMOUS_ARMING.md, docker-compose
- Update all ROS2 package comments, config labels, launch args
  (stm32_port→esp32_port, /dev/stm32-bridge→/dev/esp32-bridge)
- Update WebUI: stm32Mode→esp32Mode, stm32Version→esp32Version,
  "STM32 State/Mode" labels → "ESP32 State/Mode" (ControlMode, SettingsPanel)
- Add TODO(esp32-migration) markers on stm32_protocol.py and mamba_protocol.py
  binary frame layouts — pending ESP32 protocol spec from max
- Fix roslib CDN 1.3.0→1.4.0 in all 11 HTML panels (fixes ROS2 Humble
  rosbridge "Received a message without an op" incompatibility)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
5e97676703 docs: Update chassis docs for ESP32 architecture (retire Mamba F722S)
Replace Mamba F722S / STM32F722 references in BOM.md and ASSEMBLY.md
with ESP32 BALANCE + ESP32 IO. Board dimensions marked TBD pending
spec from max.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
30b0f245e1 docs: retire Mamba F722S/BlackPill, adopt ESP32 BALANCE + ESP32 IO architecture
Effective 2026-04-03: STM32F722 flight controller no longer used.
New architecture:
- ESP32 BALANCE: PID balance loop
- ESP32 IO: motors, sensors, comms

Updated: CLAUDE.md, TEAM.md, docs/AGENTS.md, docs/SALTYLAB.md
Legacy src/ STM32 firmware is archived — not extended.
Source code migration pending ESP32 hardware spec from max.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 08:25:24 -04:00
7db6158ada Merge pull request 'feat: Robot GPS live map panel (Issue #709 companion)' (#711) from sl-webui/robot-gps-map into main 2026-04-03 22:43:56 -04:00
f0d9fead74 Merge pull request 'feat: Sul-Tee GPS live tracking dashboard (Issue #709)' (#710) from sl-webui/issue-709-gps-tracker into main 2026-04-03 22:43:55 -04:00
811a2ccc5c fix(sultee-tracker): subscribe to proper ROS GPS topics for robot marker
Switch robot GPS subscription from custom saltybot/gps/* std_msgs/String
topics to the canonical /gps/fix (sensor_msgs/NavSatFix) and /gps/vel
(geometry_msgs/TwistStamped) published by the SIM7600X GPS driver node.

- /gps/fix: read msg.latitude/longitude/altitude/status.status directly
- /gps/vel: compute speed (sqrt(vx²+vy²) * 3.6 km/h) and heading
  (angular.z radians → degrees) from ENU velocity components

Closes #709

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:41:47 -04:00
bb354336c3 feat(sultee-tracker): add dual device map — phone (blue) + robot (orange)
Previously showed only phone GPS. Now also subscribes via ROSLIB to
saltybot/gps/fix + saltybot/gps/vel on the same rosbridge URL for
robot (SAUL-TEE) position. Blue marker+trail for phone (raw WS
{type:gps}), orange marker+trail for robot (ROS topics). Sidebar shows
phone speed/alt/heading/accuracy + robot lat/lon/speed + distance
between the two. FIT ALL button auto-zooms to show both. Status bar
badges for phone staleness and robot fix/vel freshness.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:38:28 -04:00
6d047ca50c feat(gps-map): add phone/user GPS as second marker on robot GPS map
Subscribes to saltybot/phone/gps (JSON: {ts, lat, lon, alt_m,
accuracy_m, speed_ms, bearing_deg, provider}) and renders a blue
Leaflet marker + blue breadcrumb trail alongside the robot's
orange/cyan marker. Status bar now shows PHONE badge with stale
detection. Sidebar adds phone lat/lon/speed/accuracy/provider section.
Clear button resets both trails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:34:44 -04:00
f384cc4810 feat: Robot GPS live map panel (Issue #709 companion)
Adds gps_map_panel.html/css/js — standalone dashboard panel:

- Leaflet.js + OpenStreetMap with dark CSS filter (matches dashboard theme)
- Heading-aware SVG robot marker (orange arrow shows direction of travel)
- Orange breadcrumb trail polyline (up to 2000 pts, CLEAR button)
- FOLLOW mode auto-pan; drag map to switch to FREE mode
- Sidebar: speed (km/h, color-coded), altitude, heading compass rose,
  fix status (0=NO FIX…4=RTK), fix count, lat/lon, trail log
- Exponential backoff auto-reconnect (2s→30s cap)
- Stale detection at 5s for fix + velocity badges

Subscribes via rosbridge to:
  saltybot/gps/fix  std_msgs/String JSON — {lat, lon, alt, stat, t}
  saltybot/gps/vel  std_msgs/String JSON — {spd, hdg, t}

index.html: new GPS MAP card (🛰️, #709) before CAN MONITOR
dashboard.js: gpsWatch subscription + 'gps' panel entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 22:28:44 -04:00
2560718b39 feat: Sul-Tee GPS live tracking dashboard (Issue #709)
Single-file vanilla JS dashboard at ui/sultee-tracker.html:

- Connects to ws://100.64.0.2:9090 (configurable, saved in localStorage)
- Parses {"type":"gps","data":{...},"timestamp":...} JSON frames from iPhone
- Leaflet.js + OpenStreetMap tiles with dark CSS filter
- Live position marker (cyan pulsing dot SVG icon)
- Orange polyline trail (up to 2000 points)
- Auto-centers on first GPS fix; FOLLOW/FREE toggle; drag disables follow
- Sidebar: speed (km/h, color-coded), altitude, heading, compass rose canvas,
  h-accuracy bar (green/amber/red), coordinate display, fix count
- Scrollable trail log with timestamp + coords + speed per fix
- Exponential backoff auto-reconnect (2s→30s cap)
- CLEAR button resets trail, marker, log, fix count

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:12:37 -04:00
e220797c07 Merge pull request 'feat: CAN bus watchdog and error recovery (Issue #694)' (#708) from sl-firmware/issue-694-can-watchdog into main 2026-03-20 17:59:02 -04:00
b5354e1ac0 Merge pull request 'feat: PID tuning interface via CAN/ROS2 (Issue #693)' (#707) from sl-controls/issue-693-pid-tuning into main 2026-03-20 17:58:34 -04:00
f59bc9931e feat: CAN bus watchdog and error recovery (Issue #694)
- CAN1_SCE_IRQHandler: detects bus-off/error-passive/error-warning from ESR
- can_driver_watchdog_tick(): polls ESR each cycle, auto-restarts after CAN_WDOG_RESTART_MS (200ms)
- can_wdog_t: tracks restart_count, busoff_count, errpassive_count, errwarn_count, tec, rec
- JLink TLM code 0x8F (JLINK_TLM_CAN_WDOG) with jlink_send_can_wdog_tlm()
- main.c: calls watchdog_tick() each loop, sends CAN wdog TLM at 1 Hz
- TEST_HOST: inject_esr() stub + busoff_pending flag fixes t=0 sentinel ambiguity
- test/test_can_watchdog.c: 23 unit tests, all pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:39:01 -04:00
de4d1bbe3a feat: PID tuning interface via CAN/ROS2 (Issue #693)
- Mamba (STM32): add ORIN_CAN_ID_PID_SET (0x305) handler in orin_can.c.
  Receives kp/ki/kd as uint16*100 (BE), applies to running balance loop,
  replies with FC_PID_ACK (0x405) echoing clamped gains. Gains persist in
  RAM until reboot; not saved to flash.
- Jetson: expose pid/kp, pid/ki, pid/kd as ROS2 parameters in
  can_bridge_node. Parameter changes trigger encode_pid_set_cmd() and
  send CAN frame 0x305 immediately. ACK frame 0x405 logged at DEBUG.
- mamba_protocol.py: add ORIN_CAN_ID_PID_SET / FC_PID_ACK IDs,
  PidGains dataclass, encode_pid_set_cmd(), decode_pid_ack().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:39:00 -04:00
d235c414e0 Merge pull request 'feat: SLAM map persistence for AMCL (Issue #696)' (#705) from sl-perception/issue-696-slam-map-persistence into main 2026-03-20 17:38:29 -04:00
62d7525df7 Merge pull request 'feat: VESC dual ESC mount bracket for T-slot (Issue #699)' (#704) from sl-mechanical/issue-699-vesc-mount into main 2026-03-20 17:38:27 -04:00
2b3f3584a9 Merge pull request 'feat: End-to-end CAN integration tests (Issue #695)' (#703) from sl-jetson/issue-695-can-e2e-test into main 2026-03-20 17:38:25 -04:00
7a100b2d14 Merge pull request 'feat: WebSocket bridge for CAN monitor dashboard (Issue #697)' (#702) from sl-webui/issue-697-websocket-bridge into main 2026-03-20 17:38:23 -04:00
37b646780d Merge pull request 'feat: Android BLE pairing UI for UWB tag (Issue #700)' (#701) from sl-android/issue-700-ble-pairing-ui into main 2026-03-20 17:38:22 -04:00
2d60aab79c feat: SLAM map persistence for AMCL (Issue #696)
- New map_persistence.launch.py: launches map_saver_server lifecycle node
  (nav2_map_server) + saltybot_map_saver helper node + lifecycle_manager.
  Configurable map_dir (default /mnt/nvme/saltybot/maps) and map_name.

- New map_saver_node.py: ROS2 node providing /saltybot/save_map (Trigger
  service) that calls nav2_map_server map_saver_cli. On startup logs whether
  a saved map is present. Auto-saves map on shutdown (auto_save_on_shutdown).

- New config/map_saver_params.yaml: map_saver_server params
  (save_map_timeout=5s, free/occupied thresholds, transient-local QoS).

- nav2_slam_bringup.launch.py: adds map_dir + map_name args; includes
  map_persistence.launch.py so map_saver_server runs during SLAM sessions.

- nav2_amcl_bringup.launch.py: adds map_dir arg; auto-detects saved map at
  /mnt/nvme/saltybot/maps/saltybot_map.yaml at launch time and uses it as
  the AMCL map; falls back to placeholder if not found.

- setup.py: registers map_persistence.launch.py, map_saver_params.yaml,
  map_saver_node console_scripts entry point.

- test_nav2_amcl.py: 21 new tests covering params, launch syntax,
  node service/shutdown behaviour, SLAM bringup inclusion, AMCL auto-detect.

Workflow:
  1. ros2 launch saltybot_nav2_slam nav2_slam_bringup.launch.py   (build map)
  2. ros2 service call /saltybot/save_map std_srvs/srv/Trigger {}  (save)
  3. ros2 launch saltybot_nav2_slam nav2_amcl_bringup.launch.py   (auto-loads)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:27:52 -04:00
af982bb575 feat: VESC dual ESC mount bracket (Issue #699)
3D-printable PETG cradle for FSESC 6.7 Pro Mini Dual on 2020 T-slot rail.
4x M5 T-nut mounting, open-top heatsink exposure, XT60/XT30/CAN cutouts,
floor grille and side louvre ventilation, M3 heat-set insert posts for
board retention. No supports required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:26:07 -04:00
6d59baa30e feat: End-to-end CAN integration tests (Issue #695)
Add saltybot_can_e2e_test package with 64 tests covering the full
Orin↔Mamba↔VESC CAN pipeline: drive commands, heartbeat timeout,
e-stop escalation, mode switching, and FC_VESC status broadcasts.
Tests run with plain pytest — no ROS2 or real CAN hardware required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:25:23 -04:00
1ec4d3fc58 feat: WebSocket bridge for CAN monitor dashboard (Issue #697)
rosbridge config:
- rosbridge_params.yaml: add /saltybot/barometer, /vesc/left/state,
  /vesc/right/state to topics_glob whitelist (were missing, blocked
  the CAN monitor panel from receiving data)
- can_monitor.launch.py: new lightweight launch — rosbridge only,
  whitelist scoped to the 5 CAN monitor topics, port overridable via
  launch arg (ros2 launch saltybot_bringup can_monitor.launch.py port:=9091)

can_monitor_panel.js auto-reconnect:
- Exponential backoff: 2s → 3s → 4.5s → ... → 30s cap (×1.5 factor)
- Countdown displayed in conn-label ("Retry in Xs…") during wait
- Backoff resets to 2s on successful connection
- Manual CONNECT / Enter resets backoff and cancels pending timer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:23:27 -04:00
sl-android
c6cf64217d feat: Android BLE pairing UI for UWB tag (Issue #700)
- UwbTagBleActivity: BLE scan filtered to 'UWB_TAG_XXXX' device names
- Connects to GATT service 12345678-1234-5678-1234-56789abcdef0
- Read/write JSON config char: sleep_timeout_s, display_brightness,
  tag_name, uwb_channel, ranging_interval_ms, battery_report
- Subscribes to status + battery notification characteristics
- Material Design UI with scan list, config form, and live status
- Runtime BLE permission handling for API 26+ / API 31+

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 16:21:45 -04:00
5906af542b Merge pull request 'feat: BME280 baro pressure & ambient temp (Issue #672)' (#673) from sl-firmware/issue-672-bme280-baro-temp into main 2026-03-18 08:27:39 -04:00
4318589496 feat: BME280 baro pressure & ambient temp driver (Issue #672)
- New baro module (include/baro.h, src/baro.c): reads BME280/BMP280
  at 1 Hz on I2C1, computes pressure altitude (ISA formula), publishes
  JLINK_TLM_BARO (0x8D) telemetry to Orin. Runs entirely on Mamba F722S
  with no Orin dependency. baro_get_alt_cm() exposes altitude to balance
  PID slope compensation.
- New JLink telemetry frame 0x8D (jlink_tlm_baro_t, 12 bytes packed):
  pressure_pa (int32), temp_x10 (int16), alt_cm (int32),
  humidity_pct_x10 (int16; -1 = BMP280/absent).
- Wire into main.c: baro_init() after bmp280_init(), baro_tick(now)
  each ms (self-rate-limits to 1 Hz).
- Unit tests (test/test_baro.c): 31 tests, all pass. Build:
    gcc -I include -I test/stubs -DTEST_HOST -lm -o /tmp/test_baro test/test_baro.c

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:25:07 -04:00
441c56b1d9 Merge pull request 'feat: Hardware button park/disarm/re-arm (Issue #682)' (#688) from sl-firmware/issue-682-hw-button into main 2026-03-18 08:21:55 -04:00
334ab9249c Merge pull request 'feat: CAN sensor remote monitor panel (Issue #681)' (#687) from sl-webui/issue-681-can-monitor into main 2026-03-18 08:10:41 -04:00
4affd6d0cb feat: Hardware button park/disarm/re-arm (Issue #682)
Add hw_button driver (PC2 active-low, 20ms debounce) with gesture detection:
- Single short press + 500ms quiet -> BTN_EVENT_PARK
- SHORT+SHORT+LONG combo (within 3s) -> BTN_EVENT_REARM_COMBO

New BALANCE_PARKED state: PID frozen, motors off, quick re-arm via button
combo without the 3-second arm interlock required from DISARMED.

FC_BTN (0x404) CAN frame sent to Orin on each event:
  event_id 1=PARKED, 2=UNPARKED, 3=UNPARK_FAILED (pitch > 20 deg)

Includes 11 unit tests (1016 assertions) exercising debounce, bounce
rejection, short/long classification, sequence detection, and timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:10:10 -04:00
fe979fdd1f feat: CAN sensor remote monitor panel (Issue #681)
Adds standalone vanilla JS/HTML/CSS panel for live CAN sensor monitoring:
- can_monitor_panel.html: 5-card dashboard grid with VESC L/R, Balance,
  IMU Attitude (span-2), and Barometer cards
- can_monitor_panel.css: dark-theme styles matching existing panel suite;
  bidirectional bars, live-dot flash, stat-grid, responsive layout
- can_monitor_panel.js: rosbridge subscriptions to
    /vesc/left/state + /vesc/right/state (RPM bidir bar, current gauge,
    voltage/duty/temp stats, fault badge, stale detection)
    /saltybot/imu (quaternion→Euler, angular vel, lin accel, cal badge
    from orientation_covariance[0], canvas artificial horizon + compass)
    /saltybot/balance_state (state badge, motor_cmd bidir bar, PID grid)
    /saltybot/barometer (altitude, pressure, temp)
  Auto-connect from localStorage, 1 Hz stale checker, msg rate display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:09:03 -04:00
9e8ea3c411 Merge pull request 'feat: VESC CAN health monitor (Issue #651)' (#666) from sl-jetson/issue-651-vesc-health into main 2026-03-18 08:03:32 -04:00
d57c0bd51d feat: VESC CAN health monitor (Issue #651)
New package: saltybot_vesc_health

- recovery_fsm.py: pure state machine (no ROS2/CAN deps; fully unit-tested)
  - VescHealthState: HEALTHY → DEGRADED (>500 ms) → ESTOP (>2 s) / BUS_OFF
  - VescMonitor.tick(): drives recovery sequence per VESC; startup-safe
  - VescMonitor.on_frame(): resets state on CAN frame arrival
  - VescMonitor.on_bus_off/on_bus_ok(): bus-off override + recovery
  - HealthFsm: dual-VESC wrapper aggregating both monitors

- health_monitor_node.py: ROS2 node
  - Subscribes /vesc/left/state + /vesc/right/state (JSON from vesc_telemetry)
  - Sends GET_VALUES alive frames via SocketCAN on DEGRADED state
  - Publishes /vesc/health (JSON, 10 Hz) — state, elapsed, recent faults
  - Publishes /diagnostics (DiagnosticArray, OK/WARN/ERROR per VESC)
  - Publishes /estop (JSON event) + zero /cmd_vel on e-stop trigger/clear
  - Polls ip link for bus-off state (1 Hz)
  - 200-entry fault event log included in /vesc/health

- test/test_vesc_health.py: 39 unit tests, all passing, no hardware needed
- config/vesc_health_params.yaml, launch/vesc_health.launch.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 08:03:19 -04:00
fdda6fe5ee Merge pull request 'feat: Nav2 AMCL integration (Issue #655)' (#664) from sl-perception/issue-655-nav2-integration into main 2026-03-18 07:57:02 -04:00
3457919c7a Merge pull request 'feat: UWB geofence speed limiting (Issue #657)' (#663) from sl-uwb/issue-657-geofence-speed-limit into main 2026-03-18 07:56:53 -04:00
cfdd74a9dc Merge pull request 'feat: VESC motor dashboard panel (Issue #653)' (#662) from sl-webui/issue-653-vesc-panel into main 2026-03-18 07:56:43 -04:00
4f3a30d871 Merge pull request 'feat: Smooth velocity controller (Issue #652)' (#661) from sl-controls/issue-652-smooth-velocity into main 2026-03-18 07:56:26 -04:00
7eb3f187e2 feat: Smooth velocity controller (Issue #652)
Adds velocity_smoother_node.py with configurable accel/decel ramps,
e-stop bypass, and optional jerk limiting. VESC driver updated to
subscribe /cmd_vel_smoothed instead of /cmd_vel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:56:16 -04:00
sl-android
a50dbe7e56 feat: VESC CAN telemetry MQTT relay (Issue #656)
Add vesc_mqtt_relay_node.py to saltybot_phone: subscribes to
/vesc/left/state, /vesc/right/state, /vesc/combined ROS2 topics and
publishes JSON telemetry to saltybot/phone/vesc_{left,right,combined}
MQTT topics at 5 Hz per motor.  32 unit tests, no ROS2/paho required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:56:02 -04:00
6561e35fd6 Merge pull request 'feat: VESC MQTT telemetry relay (Issue #656)' (#660) from sl-android/issue-656-vesc-mqtt-relay into main 2026-03-18 07:55:42 -04:00
4dc75c8a70 Merge pull request 'feat: CANable 2.0 mount (Issue #654)' (#659) from sl-mechanical/issue-654-canable-mount into main 2026-03-18 07:55:31 -04:00
4d0a377cee Merge pull request 'feat: VESC CAN odometry (Issue #646)' (#649) from sl-perception/issue-646-vesc-odometry into main 2026-03-18 07:55:17 -04:00
06101371ff fix: Use correct VESC topic names /vesc/left|right/state (Issue #670)
- VESCCANOdometryNode subscriptions now use left_state_topic/right_state_topic
  params (defaulting to /vesc/left/state and /vesc/right/state) instead of
  building /vesc/can_<id>/state from CAN IDs — those topics never existed
- Update right_can_id default: 79 → 68 (Mamba F722S architecture update)
- Update vesc_odometry_params.yaml: CAN IDs 61/79 → 56/68; add explicit
  left_state_topic and right_state_topic entries; remove stale can_N comments
- All IDs remain fully configurable via ROS2 params

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:55:04 -04:00
cf0ceb4641 Merge pull request 'fix: Configurable VESC CAN IDs, default 56/68 (Issue #667)' (#668) from sl-controls/issue-667-configurable-can-ids into main 2026-03-18 07:50:33 -04:00
ee16bae9fb fix: Make VESC CAN IDs configurable, default 56/68 (Issue #667)
FSESC 6.7 Pro Mini Dual uses CAN IDs 56/68, not 61/79. Updates all
driver, telemetry, and odometry bridge files to use correct defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 07:50:20 -04:00
70fa404437 Merge pull request 'fix: Standardize VESC topic naming (Issue #669)' (#671) from sl-jetson/issue-669-vesc-topic-fix into main 2026-03-18 07:49:20 -04:00
c11cbaf3e6 Merge pull request 'feat: IMU mount cal, CAN telemetry, LED CAN override (Issues #680, #672, #685)' (#686) from sl-jetson/issue-681-vesc-telemetry-publish into main 2026-03-18 07:49:12 -04:00
d132b74df0 Merge pull request 'fix: Move lines=[] above lock in _read_cb() (Issue #683)' (#684) from sl-jetson/issue-683-read-cb-fix into main 2026-03-18 07:49:02 -04:00
8985934f29 Merge pull request 'fix: Bump arm pitch threshold to 20° (Issue #678)' (#679) from sl-firmware/issue-678-pitch-threshold into main 2026-03-18 07:48:49 -04:00
9ed678ca35 feat: IMU mount angle cal, CAN telemetry, LED override (Issues #680, #672, #685)
Issue #680 — IMU mount angle calibration:
- imu_cal_flash.h/.c: store pitch/roll offsets in flash sector 7
  (0x0807FF00, 64 bytes; preserves PID records across sector erase)
- mpu6000_set_mount_offset(): subtracts offsets from pitch/roll output
- mpu6000_has_mount_offset(): reports cal_status=2 to Orin
- 'O' CDC command: capture current pitch/roll → save to flash → ACK JSON
- Load offsets on boot; report in printf log

CAN telemetry correction (Tee: production has no USB to Orin):
- FC_IMU (0x402): pitch/roll/yaw/cal_status/balance_state at 50 Hz
- orin_can_broadcast_imu() rate-limited to ORIN_IMU_TLM_HZ (50 Hz)
- FC_BARO (0x403): pressure_pa/temp_x10/alt_cm at 1 Hz (Issue #672)
- orin_can_broadcast_baro() rate-limited to ORIN_BARO_TLM_HZ (1 Hz)

Issue #685 — LED CAN override:
- ORIN_CAN_ID_LED_CMD (0x304): pattern/brightness/duration_ms from Orin
- orin_can_led_override volatile state + orin_can_led_updated flag
- main.c: apply pattern to LED state machine on each LED_CMD received

Orin side:
- saltybot_can_node.py: production SocketCAN bridge — reads 0x400-0x403,
  publishes /saltybot/imu, /saltybot/balance_state, /saltybot/barometer;
  subscribes /cmd_vel → 0x301 DRIVE; /saltybot/leds → 0x304 LED_CMD;
  sends 0x300 HEARTBEAT at 5 Hz; sends 0x303 ESTOP on shutdown
- setup.py: register saltybot_can_node entry point + uart_bridge launch

Fix: re-apply --defsym __stack_end=_estack-0x1000 linker fix to branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:49:21 -04:00
06db56103f feat: Enable VESC driver telemetry publishing (Issue #681)
vesc_driver_node.py:
- Add VescState dataclass with to_dict() serialization
- Add CAN_PACKET_STATUS/STATUS_4/STATUS_5 (9/16/27) RX constants
- Add FAULT_NAMES lookup (11 VESC FW 6.6 fault codes)
- Add background CAN RX thread (_rx_loop / _dispatch_frame) that
  parses STATUS broadcast frames using struct.unpack
- Add publishers for /saltybot/vesc/left and /saltybot/vesc/right
  (std_msgs/String JSON) at configurable telemetry_rate_hz (default 10 Hz)
- Combine watchdog + publish into single timer callback
- Proper thread cleanup in destroy_node()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:49:06 -04:00
05ba557dca fix: Move lines=[] above lock in _read_cb() (Issue #683)
UnboundLocalError when _ser is None — lines was only assigned inside
the else branch. Move initialisation to function scope so the for-loop
outside the lock always has a valid list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 22:36:20 -04:00
0a2f336eb8 Merge pull request 'feat: Orin CAN bus bridge — CANable 2.0 (Issue #674)' (#675) from sl-jetson/issue-674-can-bus-orin into main 2026-03-17 21:41:29 -04:00
5e82878083 feat: bxCAN integration for VESC motor control and Orin comms (Issue #674)
- can_driver: add filter bank 15 (all ext IDs → FIFO1) and widen bank 14
  to accept all standard IDs; add can_driver_send_ext/std and ext/std
  frame callbacks (can_driver_set_ext_cb / can_driver_set_std_cb)
- vesc_can: VESC 29-bit extended CAN protocol driver — send RPM to IDs 56
  and 68 (FSESC 6.7 Pro Mini Dual), parse STATUS/STATUS_4/STATUS_5
  big-endian payloads, alive timeout, JLINK_TLM_VESC_STATE at 1 Hz
- orin_can: Orin↔FC standard CAN protocol — HEARTBEAT/DRIVE/MODE/ESTOP
  commands in, FC_STATUS + FC_VESC broadcast at 10 Hz
- jlink: add JLINK_TLM_VESC_STATE (0x8E), jlink_tlm_vesc_state_t (22 bytes),
  jlink_send_vesc_state_tlm()
- main: wire vesc_can_init/orin_can_init; replace can_driver_send_cmd with
  vesc_can_send_rpm; inject Orin CAN speed/steer into balance PID; add
  Orin CAN estop/clear handling; add orin_can_broadcast at 10 Hz
- test: 56-test host-side suite for vesc_can; test/stubs/stm32f7xx_hal.h
  minimal HAL stub for all future host-side tests

Safety: balance PID runs independently on Mamba — if Orin CAN link drops
(orin_can_is_alive() == false) the robot continues balancing in-place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:41:19 -04:00
92c0628c62 feat: Orin CANable 2.0 bridge for Mamba and VESC CAN bus (Issue #674)
Adds slcan setup script and saltybot_can_bridge ROS2 package implementing
full CAN bus integration between the Orin and the Mamba motor controller /
VESC motor controllers via a CANable 2.0 USB dongle (slcan interface).

- jetson/scripts/setup_can.sh: slcand-based bring-up/tear-down for slcan0
  at 500 kbps with error handling (already up, device missing, retry)
- saltybot_can_bridge/mamba_protocol.py: CAN message ID constants and
  encode/decode helpers for velocity, mode, e-stop, IMU, battery, VESC state
- saltybot_can_bridge/can_bridge_node.py: ROS2 node subscribing to /cmd_vel
  and /estop, publishing /can/imu, /can/battery, /can/vesc/{left,right}/state
  and /can/connection_status; background reader thread, watchdog zero-vel,
  auto-reconnect every 5 s on CAN error
- config/can_bridge_params.yaml: default params (slcan0, VESC IDs 56/68,
  Mamba ID 1, 0.5 s command timeout)
- test/test_can_bridge.py: 30 unit tests covering encode/decode round-trips
  and edge cases — all pass without ROS2 or CAN hardware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:40:07 -04:00
56c59f60fe fix: add __stack_end defsym for fault_handler MPU guard (Issue #678)
STM32Cube ld script provides _estack but not __stack_end. Define
__stack_end = _estack - 0x1000 (_Min_Stack_Size) via --defsym so
fault_mpu_guard_init() and fault_mem_c() can locate the stack bottom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:39:44 -04:00
7f67fc6abe Merge pull request 'fix: remap CAN from CAN2/PB12-13 to CAN1/PB8-9 (Issue #676)' (#677) from sl-firmware/issue-597-can-driver into main 2026-03-17 21:39:29 -04:00
ea5203b67d fix: bump arm pitch threshold 10°→20° (Issue #678)
Mamba is mounted at ~12° on the frame, causing all three arm-interlock
checks to block arming. Raise fabsf(bal.pitch_deg) < 10.0f to 20.0f
at lines 375, 512, 532 (JLink arm, RC arm rising-edge, CDC arm).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:38:02 -04:00
14c80dc33c fix: remap CAN from CAN2/PB12-13 to CAN1/PB8-9 (Issue #676)
Mamba F722S MK2 does not expose PB12/PB13 externally. Waveshare CAN
module is wired to the SCL (PB8) and SDA (PB9) header pads.

Changes in can_driver_init():
- Drop __HAL_RCC_CAN2_CLK_ENABLE() — CAN1 needs no slave clock
- GPIO: GPIO_PIN_12/13 → GPIO_PIN_8/9, GPIO_AF9_CAN2 → GPIO_AF9_CAN1
- Instance: CAN2 → CAN1
- Filter bank: 14 → 0 (CAN1 master banks start at 0; bank 14 is the
  CAN2 slave-start boundary, unused here)

I2C1 is free: BME280 has been moved to I2C2 (PB10/PB11), so PB8/PB9
are available for CAN1 without any peripheral conflict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 20:31:59 -04:00
7d2d41ba9f fix: Standardize VESC topic naming to /vesc/left|right/state (Issue #669)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 15:18:43 -04:00
b74307c58a feat: Nav2 AMCL integration with VESC odometry + LiDAR (Issue #655)
AMCL-based autonomous navigation on pre-built static maps, wired to
VESC CAN differential-drive odometry (/odom, Issue #646) and RPLiDAR
(/scan) as the primary sensor sources.

New files (saltybot_nav2_slam):
- config/amcl_nav2_params.yaml — complete Nav2 + AMCL parameter file
  with inline global/local costmap configs (required by nav2_bringup):
  · AMCL: DifferentialMotionModel, 500–3000 particles, z-weights=1.0,
    odom_frame=/odom, scan_topic=/scan
  · Global costmap: static_layer + obstacle_layer (LiDAR) +
    inflation_layer (0.55m radius)
  · Local costmap: 4m rolling window, obstacle_layer (LiDAR) +
    inflation_layer, global_frame=odom
  · DWB controller: 1.0 m/s max, diff-drive constrained (vy=0)
  · NavFn A* planner
  · Recovery: spin + backup + wait
  · Lifecycle managers for localization and navigation
- launch/nav2_amcl_bringup.launch.py — orchestrates:
  1. sensors.launch.py (RealSense + RPLIDAR, conditional)
  2. odometry_bridge.launch.py (VESC CAN → /odom)
  3. nav2_bringup localization_launch.py (map_server + AMCL)
  4. nav2_bringup navigation_launch.py (full nav stack)
  Exposes: map, use_sim_time, autostart, params_file, include_sensors
- maps/saltybot_map.yaml — placeholder map descriptor (0.05m/cell)
- maps/saltybot_map.pgm — 200×200 P5 PGM, all free space (10m×10m)
- test/test_nav2_amcl.py — 38 unit tests (no ROS2 required):
  params structure, z-weight sum, costmap layers, DWB/NavFn validity,
  recovery behaviors, PGM format, launch file syntax checks

Updated:
- saltybot_bringup/launch/nav2.launch.py — adds nav_mode argument:
  nav_mode:=slam (default, existing RTAB-Map behaviour unchanged)
  nav_mode:=amcl (new, delegates to nav2_amcl_bringup.launch.py)
- saltybot_nav2_slam/setup.py — installs new launch, config, maps
- saltybot_nav2_slam/package.xml — adds nav2_amcl, nav2_map_server,
  nav2_behaviors, dwb_core, nav2_navfn_planner exec_depends

All 58 tests pass (38 new + 20 from Issue #646).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:39:08 -04:00
sl-uwb
9d2b19104f feat: UWB geofence speed limiting (Issue #657)
Add saltybot_uwb_geofence ROS2 package — Jetson-side node that subscribes
to /saltybot/pose/authoritative (UWB+IMU fused PoseWithCovarianceStamped),
enforces configurable polygon speed-limit zones (YAML), and publishes
speed-limited /cmd_vel_limited with smooth ramp transitions.

Emergency boundary: if robot exits outer polygon, cmd_vel is zeroed and
/saltybot/geofence_violation (Bool) is latched True, triggering the
existing e-stop cascade.  Publishes /saltybot/geofence/status (JSON).

Pure-geometry helpers (zone_checker.py) have no ROS2 dependency;
35 unit tests pass (pytest).  ESP32 UWB firmware untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:36:37 -04:00
89f892e5ef feat: VESC motor dashboard panel (Issue #653)
Standalone panel ui/vesc_panel.{html,js,css} with live CAN telemetry
via rosbridge. Subscribes to /vesc/left/state, /vesc/right/state
(std_msgs/String JSON) and /vesc/combined for battery voltage.

Features:
- Canvas arc gauge per motor showing RPM + direction (FWD/REV/STOP)
- Current draw bar (motor + input), duty cycle bar, temperature bars
- FET and motor temperature boxes with warn/crit colour coding
- Sparkline charts for RPM and current (last 60 s, 120 samples)
- Battery card: voltage, total draw, both RPMs, SOC progress bar
- Colour-coded health: green/amber/red at configurable thresholds
- E-stop button: publishes zero /cmd_vel + /saltybot/emergency event
- Stale detection (2 s timeout → OFFLINE state)
- Hz counter + last-stamp display in header
- Mobile-responsive layout (single-column below 640 px)
- WS URL persisted in localStorage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:35:35 -04:00
sl-android
289185e6cf feat: VESC CAN telemetry MQTT relay (Issue #656)
Add vesc_mqtt_relay_node.py to saltybot_phone: subscribes to
/vesc/left/state, /vesc/right/state, /vesc/combined ROS2 topics and
publishes JSON telemetry to saltybot/phone/vesc_{left,right,combined}
MQTT topics at 5 Hz per motor.  32 unit tests, no ROS2/paho required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:33:59 -04:00
4f81571dd3 feat: CANable 2.0 mount for T-slot rail (Issue #654)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:33:06 -04:00
Sebastien Vayrette
d9b4b10b90 Merge remote-tracking branch 'origin/sl-perception/issue-646-vesc-odometry' 2026-03-17 11:27:45 -04:00
Sebastien Vayrette
a96fd91ed7 Merge remote-tracking branch 'origin/sl-firmware/issue-645-vesc-telemetry' 2026-03-17 11:27:36 -04:00
Sebastien Vayrette
bf8df6af8f Merge remote-tracking branch 'origin/sl-controls/issue-644-vesc-can-driver' 2026-03-17 11:27:26 -04:00
d8b25bad77 feat: VESC CAN odometry for nav2 (Issue #646)
Replace single-motor vesc_odometry_bridge with dual-CAN differential
drive odometry for left (CAN 61) and right (CAN 79) VESC motors.

New files:
- diff_drive_odom.py: pure-Python kinematics (eRPM→wheel vel, exact arc
  integration, heading wrap), no ROS deps, fully unit-tested
- test/test_vesc_odometry.py: 20 unit tests (straight, arc, spin,
  invert_right, guard conditions) — all pass
- config/vesc_odometry_params.yaml: configurable wheel_radius,
  wheel_separation, motor_poles, invert_right, covariance tuning

Updated:
- vesc_odometry_bridge.py: rewritten as VESCCANOdometryNode; subscribes
  to /vesc/can_61/state and /vesc/can_79/state (std_msgs/String JSON);
  publishes /odom and /saltybot/wheel_odom (nav_msgs/Odometry) + TF
  odom→base_link with proper 6×6 covariance matrices
- odometry_bridge.launch.py: updated to launch vesc_can_odometry with
  vesc_odometry_params.yaml
- setup.py: added vesc_can_odometry entry point + config install
- pose_fusion_node.py: added optional wheel_odom_topic subscriber that
  feeds DiffDriveOdometry velocities into EKF via update_vo_velocity
- pose_fusion_params.yaml: added use_wheel_odom, wheel_odom_topic,
  sigma_wheel_vel_m_s, sigma_wheel_omega_r_s parameters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:54:19 -04:00
b2c9f368f6 feat: VESC CAN telemetry for dual motors (Issue #645)
New saltybot_vesc_telemetry ROS2 package — SocketCAN (python-can, can0)
telemetry for dual FSESC 6.7 Pro (FW 6.6) on CAN IDs 61 (left) and 79 (right).

- vesc_can_protocol.py: STATUS/STATUS_4/STATUS_5 frame parsers, VescState
  dataclass, GET_VALUES request builder (CAN_PACKET_PROCESS_SHORT_BUFFER)
- vesc_telemetry_node.py: ROS2 node; background CAN RX thread; publishes
  /vesc/left/state, /vesc/right/state, /vesc/combined (JSON String msgs),
  /diagnostics (DiagnosticArray); overcurrent/overtemp/fault alerting;
  configurable poll rate 10-50 Hz (default 20 Hz)
- test_vesc_telemetry.py: 31 unit tests, all passing (no ROS/CAN required)
- config/vesc_telemetry_params.yaml, launch file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:53:09 -04:00
a506989af6 feat: CANable 2.0 bringup with udev rule and systemd service (Issue #643)
- udev: 70-canable.rules — gs_usb VID/PID 1d50:606f, names iface can0 and brings it up at 500 kbps on plug-in
- systemd: can-bringup.service — oneshot service bound to sys-subsystem-net-devices-can0.device
- scripts: can_setup.sh — manual up/down/verify helper; candump verify for VESC IDs 61 (0x3D) and 79 (0x4F)
- install_systemd.sh updated to install can-bringup.service and all udev rules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:49:21 -04:00
1d87899270 feat: VESC SocketCAN dual-motor driver IDs 61/79 (Issue #644)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:47:57 -04:00
0fcad75cb4 Merge pull request 'feat: Encoder odometry and wheel speed feedback (Issue #632)' (#642) from sl-controls/issue-632-encoder-odom into main 2026-03-15 17:29:54 -04:00
5aadf4b5c8 Merge pull request 'feat: Jetson Orin system monitor ROS2 node (Issue #631)' (#640) from sl-jetson/issue-631-system-monitor into main 2026-03-15 17:29:50 -04:00
5f0affcd79 feat: Jetson Orin system monitor ROS2 node (Issue #631)
New package saltybot_system_monitor:
- jetson_stats.py: pure-Python data layer (JetsonStats, CpuCore,
  TegrastatsParser, JtopReader, TegrastatsReader, MockReader,
  AlertChecker, AlertThresholds) — no ROS2 dependency
- system_monitor_node.py: ROS2 node publishing /saltybot/system/stats
  (JSON) and /saltybot/diagnostics (DiagnosticArray) at 1 Hz
- Alerts: CPU/GPU >85% WARN (+10% ERROR), temp >80°C, disk/RAM >90%,
  power >30 W; each alert produces a DiagnosticStatus entry
- Stats source priority: jtop > tegrastats > mock (auto-detected)
- config/system_monitor.yaml: all thresholds and rate tunable via params
- launch/system_monitor.launch.py: single-node launch with config arg
- test/test_system_monitor.py: 50+ pytest tests, ROS2-free

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:36:04 -04:00
779f9d00e2 feat: Encoder odometry and wheel speed feedback (Issue #632)
- TIM2 (32-bit) left encoder, TIM3 (16-bit) right encoder in mode 3
- RPM calculation with int16 clamp; 16-bit wrap handled via signed delta
- Differential-drive odometry: x/y/theta Euler-forward integration
- Flash config (sector 7, 0x0807FF00) for ticks_per_rev/wheel_diam/base
- JLINK_TLM_ODOM (0x8C) at 50 Hz: rpm_l/r, x_mm, y_mm, theta_cdeg, speed_mmps
- 75/75 unit tests passing (TEST_HOST build)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 16:34:38 -04:00
4c7fa938a5 Merge pull request 'feat: UWB accuracy analyzer (Issue #634)' (#641) from sl-uwb/issue-634-uwb-logger into main 2026-03-15 16:30:16 -04:00
45332f1a8b Merge pull request 'feat: UART command protocol for Jetson-STM32 (Issue #629)' (#639) from sl-firmware/issue-629-uart-protocol into main 2026-03-15 16:30:09 -04:00
af46b15391 Merge pull request 'feat: ArUco docking detection (Issue #627)' (#638) from sl-perception/issue-627-aruco-docking into main 2026-03-15 16:30:04 -04:00
e1d605dba7 Merge pull request 'feat: WebUI main dashboard (Issue #630)' (#637) from sl-webui/issue-630-main-dashboard into main 2026-03-15 16:30:00 -04:00
c8c8794daa Merge pull request 'feat: Termux voice command interface (Issue #633)' (#636) from sl-android/issue-633-voice-commands into main 2026-03-15 16:29:56 -04:00
b5862ef529 Merge pull request 'feat: Cable management tray (Issue #628)' (#635) from sl-mechanical/issue-628-cable-tray into main 2026-03-15 16:29:52 -04:00
sl-uwb
343e53081a feat: UWB position logger and accuracy analyzer (Issue #634)
saltybot_uwb_logger_msgs (new package):
- AccuracyReport.msg: n_samples, mean/bias/std (x,y,2D), CEP50, CEP95,
  RMSE, max_error, per-anchor range stats, test_id, duration_s
- StartAccuracyTest.srv: request (truth_x/y_m, n_samples, timeout_s,
  test_id) → response (success, message, test_id)

saltybot_uwb_logger (new package):
- accuracy_stats.py: compute_stats() + RangeAccum — pure numpy, no ROS2
  CEP50/CEP95 = 50th/95th percentile of 2-D error; bias, std, RMSE, max
- logger_node.py: /uwb_logger ROS2 node
  Subscribes:
    /saltybot/pose/fused  → fused_pose_<DATE>.csv (ts, x, y, heading)
    /saltybot/uwb/pose    → uwb_pose_<DATE>.csv   (ts, x, y)
    /uwb/ranges           → uwb_ranges_<DATE>.csv (ts, anchor_id, range_m,
                                                   raw_mm, rssi, tag_id)
  Service /saltybot/uwb/start_accuracy_test:
    Collects N fused-pose samples at known (truth_x, truth_y) in background
    thread. On completion or timeout: publishes AccuracyReport on
    /saltybot/uwb/accuracy_report + writes accuracy_<test_id>.json.
    Per-anchor range stats included. CSV flushed every 5 s.

Tests: 16/16 passing (test/test_accuracy_stats.py, no ROS2/hardware)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:44:21 -04:00
602fbc6ab3 feat: UART command protocol for Jetson-STM32 (Issue #629)
Implements binary command protocol on UART5 (PC12/PD2) at 115200 baud
for Jetson→STM32 communication. Frame: STX+LEN+CMD+PAYLOAD+CRC8+ETX.

Commands: SET_VELOCITY (RPM direct to CAN), GET_STATUS, SET_PID, ESTOP,
CLEAR_ESTOP. DMA1_Stream0_Channel4 circular 256-byte RX ring. ACK/NACK
inline; STATUS pushed at 10 Hz. Heartbeat timeout 500 ms (UART_PROT_HB_TIMEOUT_MS).

NOTE: Spec requested USART1 @ 115200; USART1 occupied by JLink @ 921600.
Implemented on UART5 instead; note in code comments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:41:00 -04:00
1fd935b87e feat: ArUco marker detection for docking (Issue #627)
New package saltybot_aruco_detect — DICT_4X4_50 ArUco detection from
RealSense D435i RGB, pose estimation, PoseArray + dock target output.

aruco_math.py (pure Python, no ROS2): rot_mat_to_quat (Shepperd),
  rvec_to_quat (Rodrigues + cv2 fallback), tvec_distance, tvec_yaw_rad,
  MarkerPose dataclass with lazy-cached distance_m/yaw_rad/lateral_m/quat.

aruco_detect_node.py (ROS2 node 'aruco_detect'):
  Subscribes: /camera/color/image_raw (30Hz BGR8) + /camera/color/camera_info.
  Converts to greyscale, cv2.aruco.ArucoDetector.detectMarkers().
  estimatePoseSingleMarkers (legacy API) with solvePnP(IPPE_SQUARE) fallback.
  Dock target: closest marker in dock_marker_ids (default=[42], empty=any),
    filtered to max_dock_range_m (3.0m).
  Publishes: /saltybot/aruco/markers (PoseArray — all detected, camera frame),
    /saltybot/aruco/dock_target (PoseStamped — closest dock candidate,
    position.z=forward, position.x=lateral), /saltybot/aruco/viz (MarkerArray
    — SPHERE + TEXT per marker, dock in red), /saltybot/aruco/status (JSON
    10Hz — detected_count, dock_distance_m, dock_yaw_deg, dock_lateral_m).
  Optional debug image with drawDetectedMarkers + drawFrameAxes.
  corner_refinement=CORNER_REFINE_SUBPIX.

config/aruco_detect_params.yaml, launch/aruco_detect.launch.py.
test/test_aruco_math.py: 22 unit tests (rotation/quat math, distance,
  yaw sign/magnitude, MarkerPose accessors + caching).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:37:22 -04:00
b6c6dbd838 feat: WebUI main dashboard with panel launcher (Issue #630)
Replaces ui/index.html (old USB-serial HUD) with a full rosbridge
dashboard. Adds ui/dashboard.{css,js}.

Top bar:
- Robot name +  SALTYBOT logo
- Live battery % + voltage with fill bar (4S LiPo: 12.0V–16.8V)
- Safety state from /saltybot/safety_zone/status (GREEN/AMBER/RED)
- E-stop state display
- Drive mode display
- ROS uptime counter
- rosbridge WS input + CONNECT button

Panel grid (auto-fill responsive):
- MAP VIEW (#587) — /saltybot/pose/fused liveness dot
- GAMEPAD TELEOP (#598) — /cmd_vel activity indicator
- DIAGNOSTICS (#562) — /diagnostics liveness dot
- EVENT LOG (#576) — /rosout liveness dot
- SETTINGS (#614) — param service (config state, no topic)
- GIMBAL (#551) — /gimbal/state liveness dot

Each card shows: icon, title, issue #, description, topic chips,
and a LIVE/IDLE/OFFLINE status badge updated every second. Cards
open the linked standalone panel in the same tab.

Auto-detect rosbridge:
- Probes: page hostname:9090, localhost:9090, saltybot.local:9090
- Progress dots per candidate (trying/ok/fail)
- Falls back to manual URL entry
- Saves last successful URL to localStorage

Bottom bar:
-  E-STOP button (latches, publishes zero Twist to /cmd_vel)
  Space bar shortcut from dashboard
- RESUME button
- Drive mode switcher: MANUAL / AUTO / FOLLOW / DOCK
  (publishes to /saltybot/drive_mode std_msgs/String)
- Session timer (HH:MM:SS since page load)

Info strip: rosbridge URL · msg rate · latency (5s ping via
/rosapi/get_time) · robot IP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:35:56 -04:00
sl-android
26bf4ab8d3 feat: Add Termux voice command interface (Issue #633)
phone/voice_cmd.py — listens via termux-speech-to-text, parses commands
(go forward/back, turn left/right, stop, e-stop, go to waypoint, speed
up/down, status) and publishes structured JSON to saltybot/phone/voice_cmd.
TTS confirmation via termux-tts-speak. Manual text fallback via --text flag.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:35:27 -04:00
cb802ee76f feat: Cable management tray (Issue #628) 2026-03-15 14:33:49 -04:00
0e8758e9e1 Merge pull request 'feat: Battery voltage telemetry + LVC (Issue #613)' (#626) from sl-firmware/issue-613-battery-voltage into main 2026-03-15 13:29:32 -04:00
7785a16bff feat: Battery voltage telemetry and LVC (Issue #613)
- Add include/lvc.h + src/lvc.c: 3-stage low voltage cutoff state machine
  WARNING  21.0V: MELODY_LOW_BATTERY buzzer, full motor power
  CRITICAL 19.8V: double-beep every 10s, 50% motor power scaling
  CUTOFF   18.6V: MELODY_ERROR one-shot, motors disabled + latched
  200mV hysteresis on recovery; CUTOFF latched until reboot
- Add JLINK_TLM_LVC (0x8B, 4 bytes): voltage_mv, percent, protection_state
  jlink_send_lvc_tlm() frame encoder in jlink.c
- Wire into main.c:
  lvc_init() at startup; lvc_tick() each 1kHz loop tick
  lvc_is_cutoff() triggers safety_arm_cancel + balance_disarm + motor_driver_estop
  lvc_get_power_scale() applied to ESC speed command (100/50/0%)
  1Hz JLINK_TLM_LVC telemetry with fuel-gauge percent field
- Add LVC thresholds to config.h (LVC_WARNING/CRITICAL/CUTOFF/HYSTERESIS_MV)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:04:38 -04:00
68568b2b66 Merge pull request 'feat: WebUI settings panel (Issue #614)' (#622) from sl-webui/issue-614-settings-panel into main 2026-03-15 11:03:04 -04:00
38df5b4ebb Merge pull request 'feat: GPS waypoint logger (Issue #617)' (#620) from sl-android/issue-617-waypoint-logger into main 2026-03-15 11:02:58 -04:00
fea550c851 Merge pull request 'feat: ROS2 bag recording manager (Issue #615)' (#625) from sl-jetson/issue-615-bag-recorder into main 2026-03-15 11:02:37 -04:00
13b17a11e1 Merge pull request 'feat: Steering PID controller (Issue #616)' (#624) from sl-controls/issue-616-steering-pid into main 2026-03-15 11:02:33 -04:00
96d13052b4 Merge pull request 'feat: RealSense obstacle detection (Issue #611)' (#623) from sl-perception/issue-611-obstacle-detect into main 2026-03-15 11:02:29 -04:00
a01fa091d4 Merge pull request 'feat: ESP-NOW to ROS2 serial relay node (Issue #618)' (#621) from sl-uwb/issue-618-espnow-relay into main 2026-03-15 11:02:21 -04:00
62aab7164e Merge pull request 'feat: Jetson Orin Nano mount bracket (Issue #612)' (#619) from sl-mechanical/issue-612-jetson-mount into main 2026-03-15 11:02:14 -04:00
7e12dab4ae feat: ROS2 bag recording manager (Issue #615)
Upgrades saltybot_bag_recorder (Issue #488) with:

- Motion-triggered auto-record: subscribes /cmd_vel, starts on non-zero
  velocity, stops after 30s idle timeout (configurable)
- Auto-split at 1 GB or 10 min via subprocess restart
- USB/NVMe storage selection: ordered priority list, picks first path
  with >= 2 GB free (/media/usb0 -> /media/usb1 -> /mnt/nvme -> ~/bags)
- Disk monitoring: warns at 70%, triggers cleanup of bags >7 days at 80%
- JSON status on /saltybot/bag_recorder/status at 1 Hz
- Services: /saltybot/bag_recorder/{start,stop,split}
  (legacy /saltybot/{start,stop}_recording kept for compatibility)
- bag_policy.py: pure-Python MotionState, DiskInfo, StorageSelector,
  BagPolicy — ROS2-free, fully unit-testable
- 76 unit tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:12:40 -04:00
1e69337ffd feat: Steering PID for differential drive (Issue #616)
Closed-loop yaw-rate controller that converts Jetson Twist.angular.z
to a differential wheel speed offset using IMU gyro Z as feedback.

- include/steering_pid.h + src/steering_pid.c: PID with anti-windup
  (integral clamped to ±200 counts) and rate limiter (10 counts/ms
  max output change) to protect balance PID from sudden steering steps.
  JLINK_TLM_STEERING (0x8A) telemetry at 10 Hz.
- include/mpu6000.h + src/mpu6000.c: expose yaw_rate (board_gz) in
  IMUData so callers have direct bias-corrected gyro Z feedback.
- include/jlink.h + src/jlink.c: add JLINK_TLM_STEERING (0x8A),
  jlink_tlm_steering_t (8 bytes), jlink_send_steering_tlm().
- test/test_steering_pid.c: 78 unit tests (host build with gcc),
  all passing.

Usage (main loop):
  steering_pid_set_target(&s, jlink_state.steer * STEER_OMEGA_SCALE);
  int16_t steer_out = steering_pid_update(&s, imu.yaw_rate, dt);
  motor_driver_update(&motor, balance_cmd, steer_out, now_ms);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:11:05 -04:00
82ad626a94 feat: RealSense depth obstacle detection (Issue #611)
New package saltybot_obstacle_detect — RANSAC ground plane fitting on
D435i depth images with 2D grid BFS obstacle clustering.

ground_plane.py (pure Python + numpy):
  fit_ground_plane(pts, n_iter=50, inlier_thresh_m=0.06): RANSAC over 3D
  point cloud in camera optical frame (+Z forward). Samples 3 points, fits
  plane via cross-product, counts inliers, refines via SVD on best inlier
  set. Orients normal toward -Y (upward in world). Returns (normal, d).
  height_above_plane(pts, plane): signed h = d - n·p (h>0 = above ground).
  obstacle_mask(pts, plane, min_h, max_h): min_obstacle_h_m < h < max_h.
  ground_mask(pts, plane, thresh): inlier classification.

obstacle_clusterer.py (pure Python + numpy):
  cluster_obstacles(pts, heights, cell_m=0.30, min_pts=5): projects
  obstacle 3D points onto (X,Z) bird's-eye plane, discretises into grid
  cells, runs 4-connected BFS flood-fill, returns ObstacleCluster list
  sorted by forward distance. ObstacleCluster: centroid(3), radius_m,
  height_m, n_pts + distance_m/lateral_m properties.

obstacle_detect_node.py (ROS2 node 'obstacle_detect'):
  - Subscribes: /camera/depth/camera_info (latched, once),
    /camera/depth/image_rect_raw (BEST_EFFORT, 30Hz float32 depth).
  - Pipeline: stride downsample (default 8x → 80x60) → back-project to
    3D → RANSAC ground plane (temporally blended α=0.3) → obstacle mask
    (min_h=0.05m, max_h=0.80m) → BFS clustering → alert classification.
  - Publishes:
      /saltybot/obstacles (MarkerArray): SPHERE markers colour-coded
        DANGER(red)/WARN(yellow)/CLEAR(green) + distance TEXT labels.
      /saltybot/obstacles/cloud (PointCloud2): xyz float32 non-ground pts.
      /saltybot/obstacles/alert (String JSON): alert_level, closest_m,
        obstacle_count, per-obstacle {x,y,z,radius_m,height_m,level}.
  - Safety zone integration (depth_estop_enabled=false by default):
      DANGER → zero Twist to depth_estop_topic (/cmd_vel_input) feeds
      into safety_zone's cmd_vel chain for independent depth e-stop.

config/obstacle_detect_params.yaml: all tuneable parameters with comments.
launch/obstacle_detect.launch.py: single node with params_file arg.
test/test_ground_plane.py: 10 unit tests (RANSAC correctness, normal
  orientation, height computation, inlier/obstacle classification).
test/test_obstacle_clusterer.py: 8 unit tests (single/dual cluster,
  distance sort, empty, min_pts filter, centroid accuracy, range clip).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:09:23 -04:00
921eaba8b3 feat: WebUI settings and configuration panel (Issue #614)
Standalone ui/settings_panel.{html,js,css} — no build step.

Sections / tabs:
- PID: balance_controller (Kp/Ki/Kd/i_clamp/rate),
  adaptive_pid (kp/ki/kd per load profile, output bounds)
- Speed: tank_driver (max_linear_vel, max_angular_vel, slip_factor),
  smooth_velocity_controller (accel/decel limits),
  battery_speed_limiter (speed factors)
- Safety: safety_zone (danger_range_m, warn_range_m, forward_arc_deg,
  debounce, min_valid_range, publish_rate),
  power_supervisor_node (battery % thresholds, speed factors),
  lidar_avoidance (e-stop distance, safety zone sizes)
- Sensors: boolean toggles (estop_all_arcs, lidar_enabled, uwb_enabled),
  uwb_imu_fusion weights and publish rate
- System: live /diagnostics subscriber (CPU/GPU/board/motor temps,
  RAM/GPU/disk usage, WiFi RSSI+latency, MQTT status, last-update),
  /rosapi/nodes node list

ROS2 parameter services (rcl_interfaces/srv/GetParameters +
SetParameters) via rosbridge WebSocket. Each section has independent
↓ LOAD (get_parameters) and ↑ APPLY (set_parameters) buttons with
success/error status feedback.

Presets: save/load/delete named snapshots of all values to
localStorage. Reset-to-defaults button restores built-in defaults.
Changed fields highlighted in amber (slider thumb + input border).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:08:47 -04:00
sl-uwb
65e0009118 feat: ESP-NOW to ROS2 serial relay (Issue #618)
New ROS2 package saltybot_uwb_espnow_relay:
- packet.py: EspNowPacket dataclass + FrameReader stateful decoder
  - Parses 20-byte ESP-NOW packets: MAGIC, tag_id, msg_type, anchor_id,
    range_mm (int32 LE), rssi_dbm (float32), timestamp_ms, battery_pct,
    flags (bit0=estop), seq_num
  - Serial framing: STX(0x02) + LEN(0x14) + DATA[20] + XOR-CRC(1)
  - Sync recovery: re-hunts STX after bad LEN or CRC; byte-by-byte capable
- relay_node.py: /espnow_relay ROS2 node
  - Reads from USB serial in background thread (auto-reconnects on error)
  - MSG_RANGE (0x10): publishes UwbRange on /uwb/espnow/ranges
  - MSG_ESTOP (0x20): publishes std_msgs/Bool on /uwb/espnow/estop
    and /saltybot/estop (latched True for estop_latch_s after last packet)
  - MSG_HEARTBEAT (0x30): publishes EspNowHeartbeat on /uwb/espnow/heartbeat
  - Range validity gating: min_range_m / max_range_m params
- 16/16 unit tests passing (test/test_packet.py, no ROS2/hardware needed)

saltybot_uwb_msgs: add EspNowHeartbeat.msg
  (tag_id, battery_pct, seq_num, timestamp_ms + std_msgs/Header)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:08:19 -04:00
sl-android
9b1f3ddaf0 feat: GPS waypoint logger and route planner (Issue #617)
Add phone/waypoint_logger.py — interactive Termux CLI for recording,
managing, and publishing GPS waypoints:

GPS acquisition
  - termux-location with gps/network/passive provider selection
  - Falls back to network provider on GPS timeout
  - Optional --live-gps flag: subscribes to saltybot/phone/gps MQTT
    topic (sensor_dashboard.py stream) to avoid redundant GPS calls

Waypoint operations
  - Record: acquires GPS fix, prompts for name + tags, appends to route
  - List: table with lat/lon/alt/accuracy/tags + inter-waypoint
    distance (haversine) and bearing (8-point compass)
  - Delete: by index with confirmation prompt
  - Clear: entire route with confirmation
  - Rename: route name

Persistence
  - Routes saved as JSON to ~/saltybot_route.json (configurable)
  - Auto-loads on startup; survives session restarts

MQTT publish (saltybot/phone/route, QoS 1, retained)
  - Full waypoint list with metadata
  - nav2_poses array: flat-earth x/y (metres from origin),
    quaternion yaw facing next waypoint (last faces prev)
  - Compatible with Nav2 FollowWaypoints action input

Geo maths
  - haversine_m(): great-circle distance
  - bearing_deg(): initial bearing with 8-point compass label
  - flat_earth_xy(): ENU metres for Nav2 pose export (<1% error <100km)

Flags: --broker, --port, --file, --route, --provider, --live-gps,
       --no-mqtt, --debug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 10:05:57 -04:00
837c42a00d feat: Jetson Orin Nano mount bracket (Issue #612) 2026-03-15 10:04:37 -04:00
c0bb4f6276 Merge pull request 'feat: CAN bus driver for BLDC motor controllers (Issue #597)' (#610) from sl-firmware/issue-597-can-driver into main 2026-03-14 16:27:36 -04:00
2996d18ace feat: CAN bus driver for BLDC motor controllers (Issue #597)
- Add can_driver.h / can_driver.c: CAN2 on PB12/PB13 (AF9) at 500 kbps
  APB1=54 MHz, PSC=6, BS1=13tq, BS2=4tq, SJW=1tq → 18tq/bit, SP 77.8%
  Filter bank 14 (SlaveStartFilterBank=14); 32-bit mask; FIFO0
  Accept std IDs 0x200–0x21F (left/right feedback frames)
  TX: velocity+torque cmd (0x100+nid, DLC=4) at 100 Hz via main loop
  RX: velocity/current/position/temp/fault feedback (0x200+nid, DLC=8)
  AutoBusOff=ENABLE for HW recovery; can_driver_process() drains FIFO0
- Add JLINK_TLM_CAN_STATS (0x89, 16 bytes) + JLINK_CMD_CAN_STATS_GET (0x10)
  Also add JLINK_TLM_SLOPE (0x88) + jlink_tlm_slope_t missing from Issue #600
- Wire into main.c: init after jlink_init; 100Hz TX loop (differential drive
  speed_rpm ± steer_rpm/2); CAN enable follows arm state; 1Hz stats telemetry
- Add CAN_RPM_SCALE=10 and CAN_TLM_HZ=1 to config.h

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:58:13 -04:00
bb5eff1382 Merge pull request 'feat: MQTT-to-ROS2 phone sensor bridge (Issue #601)' (#605) from sl-android/issue-601-mqtt-ros2-bridge into main 2026-03-14 15:55:22 -04:00
8b1d6483cf Merge pull request 'feat: Slope tilt compensation (Issue #600)' (#609) from sl-controls/issue-600-slope-compensation into main 2026-03-14 15:55:01 -04:00
6c00d6a321 Merge pull request 'feat: UWB anchor auto-calibration via inter-anchor ranging + MDS (Issue #602)' (#608) from sl-uwb/issue-602-anchor-calibration into main 2026-03-14 15:54:56 -04:00
2460ba27c7 Merge pull request 'feat: Nav2 with UWB localization (Issue #599)' (#607) from sl-jetson/issue-599-nav2-uwb into main 2026-03-14 15:54:52 -04:00
2367e08140 Merge pull request 'feat: Multi-sensor pose fusion (Issue #595)' (#606) from sl-perception/issue-595-pose-fusion into main 2026-03-14 15:54:48 -04:00
f188997192 Merge pull request 'feat: RPLIDAR A1 mount bracket (Issue #596)' (#604) from sl-mechanical/issue-596-rplidar-mount into main 2026-03-14 15:54:40 -04:00
7e5f673f7d Merge pull request 'feat: WebUI gamepad teleop panel (Issue #598)' (#603) from sl-webui/issue-598-gamepad-teleop into main 2026-03-14 15:54:36 -04:00
be4966b01d feat: Tilt compensation for slopes (Issue #600)
Adds a slow-adapting terrain slope estimator (IIR tau=5s) that decouples
the robot's balance offset from genuine ground incline.  The balance
controller subtracts the slope estimate from measured pitch so the PID
balances around the slope surface rather than absolute vertical.

- include/slope_estimator.h + src/slope_estimator.c: first-order IIR
  filter clamped to ±15°; JLINK_TLM_SLOPE (0x88) telemetry at 1 Hz
- include/jlink.h + src/jlink.c: add JLINK_TLM_SLOPE (0x88),
  jlink_tlm_slope_t (4 bytes), jlink_send_slope_tlm()
- include/balance.h + src/balance.c: integrate slope_estimator into
  balance_t; update, reset on tilt-fault and disarm
- test/test_slope_estimator.c: 35 unit tests, all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:07:05 -04:00
sl-uwb
82cc223fb8 feat: Add AT+PEER_RANGE= command for inter-anchor calibration (Issue #602)
- peer_range_once(): DS-TWR initiator role toward a peer anchor
  (POLL → RESP → FINAL, one-sided range estimate Ra - Da/2)
- AT+PEER_RANGE=<id>: returns +PEER_RANGE:<my>,<peer>,<mm>,<rssi>
  or +PEER_RANGE:ERR,<peer>,TIMEOUT

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:06:29 -04:00
5f03e4cbef feat: Tilt compensation for slopes (Issue #600)
Adds a slow-adapting terrain slope estimator (IIR tau=5s) that decouples
the robot's balance offset from genuine ground incline.  The balance
controller subtracts the slope estimate from measured pitch so the PID
balances around the slope surface rather than absolute vertical.

- include/slope_estimator.h + src/slope_estimator.c: first-order IIR
  filter clamped to ±15°; JLINK_TLM_SLOPE (0x88) telemetry at 1 Hz
- include/jlink.h + src/jlink.c: add JLINK_TLM_SLOPE (0x88),
  jlink_tlm_slope_t (4 bytes), jlink_send_slope_tlm()
- include/balance.h + src/balance.c: integrate slope_estimator into
  balance_t; update, reset on tilt-fault and disarm
- test/test_slope_estimator.c: 35 unit tests, all passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:04:58 -04:00
sl-uwb
587ca8a98e feat: UWB anchor auto-calibration via inter-anchor ranging + MDS (Issue #602)
Anchor firmware (esp32/uwb_anchor/src/main.cpp):
- Add peer_range_once(peer_id) — DS-TWR initiator role toward a peer anchor
- Add AT+PEER_RANGE=<id> command: triggers inter-anchor ranging and returns
  +PEER_RANGE:<my_id>,<peer_id>,<range_mm>,<rssi_dbm> (or ERR,TIMEOUT)

ROS2 package saltybot_uwb_calibration_msgs:
- CalibrateAnchors.srv: request (anchor_ids[], n_samples) →
  response (positions_x/y/z[], residual_rms_m, anchor_positions_json)

ROS2 package saltybot_uwb_calibration:
- mds_math.py: classical MDS (double-centred D², eigendecomposition),
  anchor_frame_align() to fix anchor-0 at origin / anchor-1 on +X
- calibration_node.py: /saltybot/uwb/calibrate_anchors service —
  opens anchor serial ports, rounds-robin AT+PEER_RANGE= for all pairs,
  builds N×N distance matrix, runs MDS, returns JSON anchor positions
- 12/12 unit tests passing (test/test_mds_math.py)
- Supports ≥ 4 anchors; 5× averaged ranging per pair by default

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:03:53 -04:00
40b0917c33 feat: Nav2 integration with UWB localization (Issue #599)
New package saltybot_nav2_uwb replacing AMCL-based localization with
UWB-IMU EKF fused pose. Key components:

- uwb_pose_bridge_node: subscribes /saltybot/pose/fused_cov (from EKF),
  computes map→odom TF via T_map_odom = T_map_base × inv(T_odom_base),
  broadcasts at 20 Hz. Publishes /initialpose on first valid pose.
- waypoint_sequencer.py: pure-Python state machine (IDLE→RUNNING→
  SUCCEEDED/ABORTED/CANCELED) for sequential waypoint execution.
- waypoint_follower_node: action server on /saltybot/nav/follow_waypoints
  (nav2_msgs/FollowWaypoints), sends each goal to Nav2 NavigateToPose
  in sequence; JSON topic /saltybot/nav/waypoints for operator use.
- nav2_uwb_params.yaml: DWB controller capped at 1.0 m/s, global+local
  costmap with /scan (RPLIDAR), rolling-window global costmap (no static
  map needed), AMCL removed from lifecycle manager.
- nav2_uwb.launch.py: bridge (t=0) → Nav2 (t=2s) → waypoint follower
  (t=4s) with LogInfo markers.
- 65 unit tests passing (waypoint dataclass, sequencer state machine,
  2-D TF maths, progress tracking).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:02:26 -04:00
c76d5b0dd7 feat: Multi-sensor pose fusion node (Issue #595)
New package saltybot_pose_fusion — EKF fusing UWB+IMU absolute pose,
visual odometry velocity, and raw IMU into a single authoritative pose.

pose_fusion_ekf.py (pure Python, no ROS2 deps):
  PoseFusionEKF — state [x, y, θ, vx, vy, ω], 6-state EKF.
  - predict_imu(ax_body, ay_body, omega, dt): body-frame IMU predict step
    with Jacobian F, bias-compensated accel, process noise Q.
  - update_uwb_position(x, y, sigma_m): absolute position measurement
    (H=[1,0,0,0,0,0; 0,1,0,0,0,0]) from UWB+IMU fused stream.
  - update_uwb_heading(heading_rad, sigma_rad): heading measurement.
  - update_vo_velocity(vx_body, omega, ...): VO velocity measurement —
    body-frame vx rotated to world via cos/sin(θ), updates [vx,vy,ω] state.
  - Joseph-form covariance update for numerical stability.
  - Dual dropout clocks: uwb_dropout_s, vo_dropout_s (reset on each update).
  - Velocity damping when uwb_dropout_s > 2s.
  - Sensor weight parameters: sigma_uwb_pos_m, sigma_uwb_head_rad,
    sigma_vo_vel_m_s, sigma_vo_omega_r_s, sigma_imu_accel/gyro,
    sigma_vel_drift, dropout_vel_damp.

pose_fusion_node.py (ROS2 node 'pose_fusion'):
  - Subscribes: /imu/data (Imu, 200Hz → predict), /saltybot/pose/fused_cov
    (PoseWithCovarianceStamped, 10Hz → position+heading update, σ extracted
    from message covariance when use_uwb_covariance=true), /saltybot/visual_odom
    (Odometry, 30Hz → velocity update, σ from twist covariance).
  - Publishes: /saltybot/pose/authoritative (PoseWithCovarianceStamped),
    /saltybot/pose/status (String JSON, 10Hz).
  - TF2: map→base_link broadcast at IMU rate.
  - Suppresses output when uwb_dropout_s > uwb_dropout_max_s (10s).
  - Warns (throttled) on UWB/VO dropout.

config/pose_fusion_params.yaml: sensor weights + dropout thresholds.
launch/pose_fusion.launch.py: single node launch with params_file arg.
test/test_pose_fusion_ekf.py: 13 unit tests — init, predict, UWB/VO
  updates, dropout reset, covariance shape/convergence, sigma override.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 15:00:54 -04:00
sl-android
c62444cc0e chore: Register mqtt_ros2_bridge entry point and paho-mqtt dep (Issue #601)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:59:41 -04:00
sl-android
dd13569413 feat: MQTT-to-ROS2 phone sensor bridge (Issue #601)
Add saltybot_phone/mqtt_ros2_bridge_node.py — ROS2 node bridging the three
MQTT topics published by phone/sensor_dashboard.py into typed ROS2 messages:

  saltybot/phone/imu     → /saltybot/phone/imu     sensor_msgs/Imu
  saltybot/phone/gps     → /saltybot/phone/gps     sensor_msgs/NavSatFix
  saltybot/phone/battery → /saltybot/phone/battery sensor_msgs/BatteryState
  (status)               → /saltybot/phone/bridge/status std_msgs/String

Key design:
- paho-mqtt loop_start() runs in dedicated network thread; on_message
  enqueues (topic, payload) pairs into a thread-safe queue
- ROS2 timer drains queue at 50 Hz — all publishing stays on executor
  thread, avoiding any rclpy threading concerns
- Timestamp alignment: uses ROS2 wall clock by default; opt-in
  use_phone_timestamp param uses phone epoch ts when drift < warn_drift_s
- IMU: populates accel + gyro with diagonal covariance; orientation_cov[0]=-1
  (unknown per REP-145)
- GPS: NavSatStatus.STATUS_FIX for gps/fused/network providers; full 3×3
  position covariance from accuracy_m; COVARIANCE_TYPE_APPROXIMATED
- Battery: pct→percentage [0-1], temp Kelvin, health/status mapped from
  Android health strings, voltage/current=NaN (unavailable on Android)
- Input validation: finite value checks on IMU, lat/lon range on GPS,
  pct [0-100] on battery; bad messages logged at DEBUG and counted
- Status topic at 0.2 Hz: JSON {mqtt_connected, rx/pub/err counts,
  age_s per sensor, queue_depth}
- Auto-reconnect via paho reconnect_delay_set (5 s → 20 s max)

Add launch/mqtt_bridge.launch.py with args: mqtt_host, mqtt_port,
reconnect_delay_s, use_phone_timestamp, warn_drift_s, imu_accel_cov,
imu_gyro_cov.

Register mqtt_ros2_bridge console script in setup.py.
Add python3-paho-mqtt exec_depend to package.xml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:59:02 -04:00
816d165db4 feat: RPLIDAR A1 mount bracket (Issue #596) 2026-03-14 14:58:41 -04:00
cbcae34b79 feat: WebUI gamepad teleoperation panel (Issue #598)
- Standalone ui/gamepad_panel.{html,js,css} — no build step
- Web Gamepad API integration: L-stick=linear, R-stick=angular
  - LT trigger scales speed down (fine control)
  - B/Circle button toggles E-stop; Start button resumes
  - Live raw axis bars and button state in sidebar
- Virtual dual joystick (left=drive, right=steer) via Pointer Capture API
  - Deadzone ring drawn on canvas; configurable 0–40%
  - Touch and mouse support
- WASD/Arrow keyboard input (W/S=forward/reverse, A/D=turn, Space=E-stop)
- Speed limiter sliders: linear (0–1.0 m/s), angular (0–2.0 rad/s)
- Configurable deadzone slider (0–40%)
- E-stop: latches zero-velocity command, blinking overlay, resume button
- Publishes geometry_msgs/Twist to /cmd_vel at 20 Hz via rosbridge WebSocket
- Input priority: gamepad > keyboard > virtual sticks
- Live command display (m/s, rad/s) with color feedback
- Pub rate display (Hz) in sidebar
- localStorage WS URL persistence, auto-reconnect on load
- Mobile-responsive: sidebar hidden ≤800px, right stick hidden ≤560px

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:58:41 -04:00
061189670a Merge pull request 'feat: STM32 watchdog and fault recovery handler (Issue #565)' (#583) from sl-firmware/issue-565-fault-handler into main 2026-03-14 13:54:22 -04:00
8fbe7c0033 feat: STM32 watchdog and fault recovery handler (Issue #565)
- New src/fault_handler.c + include/fault_handler.h:
  - HardFault/MemManage/BusFault/UsageFault naked ISR stubs with
    Cortex-M7 stack-frame capture (R0-R3, LR, PC, xPSR, CFSR, HFSR,
    MMFAR, BFAR, SP) and NVIC_SystemReset()
  - .noinit SRAM capture ring survives soft reset; persisted to flash
    sector 7 (0x08060000, 8x64-byte slots) on subsequent boot
  - MPU Region 0 stack guard (32 B at __stack_end, no-access) ->
    MemManage fault detected as FAULT_STACK_OVF
  - Brownout detect via RCC_CSR_BORRSTF on boot -> FAULT_BROWNOUT
  - Watchdog reset detection delegates to existing watchdog.c
  - LED blink codes on LED2 (PC14, active-low) for 10 s post-recovery:
    HARDFAULT=3, WATCHDOG=2, BROWNOUT=1, STACK_OVF=4 fast blinks
  - fault_led_tick(), fault_log_read(), fault_log_get_count(),
    fault_get_last_type(), fault_log_clear(), FAULT_ASSERT() macro
- jlink.h: add JLINK_CMD_FAULT_LOG_GET (0x0F), JLINK_TLM_FAULT_LOG
  (0x86), jlink_tlm_fault_log_t (20 bytes), fault_log_req in JLinkState,
  jlink_send_fault_log() declaration
- jlink.c: dispatch JLINK_CMD_FAULT_LOG_GET; implement
  jlink_send_fault_log() (26-byte CRC16-XModem framed response)
- main.c: call fault_handler_init() first in main(); send fault log
  TLM on boot if prior fault recorded; fault_led_tick() in main loop;
  handle fault_log_req flag to respond to Jetson queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 13:37:14 -04:00
15ff5acca7 Merge pull request 'feat: Visual odometry from RealSense stereo ORB (Issue #586)' (#593) from sl-perception/issue-586-visual-odom into main 2026-03-14 13:32:56 -04:00
f2743198e5 Merge pull request 'feat: WebUI map view (Issue #587)' (#591) from sl-webui/issue-587-map-view into main 2026-03-14 13:32:50 -04:00
6512c805be Merge pull request 'feat: Motor current monitoring (Issue #584)' (#594) from sl-controls/issue-584-motor-current into main 2026-03-14 13:32:30 -04:00
1da1d50171 Merge pull request 'feat: Phone video bridge (Issue #585)' (#592) from sl-android/issue-585-video-bridge into main 2026-03-14 13:32:24 -04:00
6a8b6a679e Merge pull request 'feat: Integrate UWB tag display + ESP-NOW + e-stop (salty/uwb-tag-display-wireless)' (#590) from sl-uwb/issue-merge-uwb-tag-display into main 2026-03-14 13:32:19 -04:00
ddf8332cd7 Merge pull request 'feat: Battery holder bracket (Issue #588)' (#589) from sl-mechanical/issue-588-battery-holder into main 2026-03-14 13:32:16 -04:00
e9429e6177 Merge pull request 'feat: ROS2 launch orchestrator for full SaltyBot bringup (Issue #577)' (#582) from sl-jetson/issue-577-bringup-launch into main 2026-03-14 13:32:10 -04:00
2b06161cb4 feat: Motor current monitoring and overload protection (Issue #584)
Adds ADC-based motor current sensing with configurable overload threshold,
soft PWM limiting, hard cutoff on sustained overload, and auto-recovery.

Changes:
- include/motor_current.h: MotorCurrentState enum (NORMAL/SOFT_LIMIT/COOLDOWN),
  thresholds (5A hard, 4A soft, 2s overload, 10s cooldown), full API
- src/motor_current.c: reads battery_adc_get_current_ma() each tick (reuses
  existing ADC3 IN13/PC3 DMA sampling); linear PWM scale in soft-limit zone
  (scale256 fixed-point); fault counter + one-tick fault_pending flag for
  main-loop fault log integration; telemetry at MOTOR_CURR_TLM_HZ (5 Hz)
- include/pid_flash.h: add pid_sched_entry_t (16 bytes), pid_sched_flash_t
  (128 bytes at 0x0807FF40), PID_SCHED_MAX_BANDS=6, pid_flash_load_schedule(),
  pid_flash_save_all() — fixes missing types needed by jlink.h (Issue #550)
- src/pid_flash.c: implement flash_write_words() helper, pid_flash_load_schedule(),
  pid_flash_save_all() — single sector-7 erase covers both schedule and PID records
- include/jlink.h: add JLINK_TLM_MOTOR_CURRENT (0x86), jlink_tlm_motor_current_t
  (8 bytes: current_ma, limit_pct, state, fault_count), jlink_send_motor_current_tlm()
- src/jlink.c: implement jlink_send_motor_current_tlm() (14-byte frame)

Motor overload state machine:
  MC_NORMAL     : current_ma < 4000 mA — full PWM authority
  MC_SOFT_LIMIT : 4000-5000 mA — linear reduction (0% at 4A → 100% at 5A)
  MC_COOLDOWN   : >5A sustained 2s → zero output for 10s then NORMAL

Main-loop integration:
  motor_current_tick(now_ms);
  if (motor_current_fault_pending()) fault_log_append(FAULT_MOTOR_OVERCURRENT);
  cmd = motor_current_apply_limit(balance_pid_output());
  motor_current_send_tlm(now_ms);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:25:29 -04:00
c1b82608d5 feat: Visual odometry from RealSense stereo ORB (Issue #586)
Adds stereo ORB-based visual odometry to saltybot_visual_odom package.

New modules:
- orb_stereo_matcher.py: ORB feature detection (cv2.ORB_create) with BFMatcher
  NORM_HAMMING + Lowe ratio test for temporal matching (infra1 prev→curr).
  Stereo scale method matches infra1↔infra2 under epipolar row constraint
  (|Δrow|≤2px), computes depth = baseline_m * fx / disparity.
- stereo_orb_node.py: StereoOrbNode subscribes to infra1+infra2+depth
  (ApproximateTimeSynchronizer 3-topic), detects/matches ORB temporally,
  estimates SE(3) via Essential matrix (5-point RANSAC) using StereoVO,
  recovers metric scale from D435i aligned depth (primary) or stereo
  baseline disparity (fallback). Publishes nav_msgs/Odometry on
  /saltybot/visual_odom and broadcasts TF2 odom→camera_link. Baseline
  auto-updated from infra2 camera_info Tx (overrides parameter).
- config/stereo_orb_params.yaml, launch/stereo_orb.launch.py
- setup.py: adds stereo_orb entrypoint, installs launch+config dirs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:21:58 -04:00
sl-android
08bc23f6df feat: Phone video streaming bridge (Issue #585)
Phone side — phone/video_bridge.py:
- MJPEG streaming server for Android/Termux phone camera
- Dual camera backends: OpenCV VideoCapture (V4L2) with automatic
  fallback to termux-camera-photo for unmodified Android
- WebSocket server (ws://0.0.0.0:8765) — binary JPEG frames + JSON
  info/error control messages; supports multiple concurrent clients
- HTTP server (http://0.0.0.0:8766):
    /stream    — multipart/x-mixed-replace MJPEG
    /snapshot  — single JPEG
    /health    — JSON stats (frame count, dropped, resolution, fps)
- Thread-safe single-slot FrameBuffer; CaptureThread rate-limited with
  wall-clock accounting for capture latency
- Flags: --ws-port, --http-port, --width, --height, --fps, --quality,
  --device, --camera-id, --no-http, --debug

Jetson side — saltybot_phone/phone_camera_node.py:
- ROS2 node: receives JPEG frames, publishes:
    /saltybot/phone/camera            sensor_msgs/Image (bgr8)
    /saltybot/phone/camera/compressed sensor_msgs/CompressedImage
    /saltybot/phone/camera/info       std_msgs/String (stream metadata)
- WebSocket client (primary); HTTP MJPEG polling fallback on WS failure
- Auto-reconnect loop (default 3 s) for both transports
- Latency warning when frame age > latency_warn_ms (default 200 ms)
- 10 s diagnostics log: received/published counts + last frame age
- Registered as phone_camera_node console script in setup.py
- Added to phone_bringup.py launch with phone_host / phone_cam_enabled args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:20:28 -04:00
5dac6337e6 feat: WebUI map view (Issue #587)
Standalone 3-file 2D map panel (ui/map_panel.{html,js,css}).
No build step. Open directly or serve ui/ directory.

Canvas layers (drawn every animation frame):
  - Grid lines (1m spacing) + world-origin axis cross + 1m scale bar
  - RPLIDAR scan dots (/scan, green, cbor-compressed, 100ms throttle)
  - Safety zone rings: danger 0.30m (red dashed) + warn 1.00m (amber dashed)
  - 100-position breadcrumb trail with fading cyan polyline + dots every 5 pts
  - UWB anchor markers (amber diamond + label, user-configured)
  - Robot marker: circle + forward arrow, red when e-stopped

Interactions:
  - Mouse wheel zoom (zooms around cursor)
  - Click+drag pan
  - Pinch-to-zoom (touch, two-finger)
  - Auto-center toggle (robot stays centered when on)
  - Zoom +/- buttons, Reset view button
  - Clear trail button
  - Mouse hover shows world coords (m) in bottom-left HUD

ROS topics:
  SUB /saltybot/pose/fused        geometry_msgs/PoseStamped   50ms throttle
  SUB /scan                       sensor_msgs/LaserScan       100ms + cbor
  SUB /saltybot/safety_zone/status std_msgs/String (JSON)     200ms throttle

Sidebar:
  - Robot position (x, y m) + heading (°)
  - Safety zone: forward zone (CLEAR/WARN/DANGER), closest obstacle (m), e-stop
  - UWB anchor manager: add/remove anchors with x/y/label, persisted localStorage
  - Topic reference

E-stop banner: pulsing red overlay when /saltybot/safety_zone/status estop_active=true

Mobile-responsive: sidebar hidden on <700px, canvas fills viewport.
WS URL persisted in localStorage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:20:04 -04:00
sl-uwb
4b8d1b2ff7 feat: Integrate UWB tag display + ESP-NOW + e-stop (salty/uwb-tag-display-wireless)
Integrates Tee's additions to the DS-TWR tag firmware (esp32/uwb_tag/).
Base is our DS-TWR initiator from Issue #545; extensions added:

OLED display (SSD1306 128×64, I2C SDA=4 SCL=5):
- Big distance readout (nearest anchor, auto m/mm)
- Per-anchor range rows with link-age indicator
- Signal strength bars (RSSI)
- Uptime + sequence counter
- Full-screen E-STOP warning when button held

ESP-NOW wireless (peer-to-peer, no AP required):
- 20-byte broadcast packet: magic, tag_id, msg_type, anchor_id,
  range_mm, rssi_dbm, timestamp_ms, battery_pct, flags, seq_num
- MSG_RANGE (0x10) on every successful TWR
- MSG_ESTOP (0x20) at 10 Hz while button held; 3× clear on release
- MSG_HEARTBEAT (0x30) at 1 Hz

Emergency stop (GPIO 0 / BOOT button, active LOW):
- Blocks ranging while active
- 10 Hz ESP-NOW e-stop TX, serial +ESTOP:ACTIVE / +ESTOP:CLEAR
- 3× clear packets on release

Build: adds Adafruit SSD1306 + GFX libraries to platformio.ini.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 12:19:31 -04:00
5556c06153 feat: Battery holder bracket (Issue #588) 2026-03-14 12:18:37 -04:00
5a1290a8f9 Merge pull request 'feat: UWB anchor mount bracket (Issue #564)' (#569) from sl-mechanical/issue-564-uwb-anchor-mount into main 2026-03-14 12:15:43 -04:00
7b75cdad1a feat: UWB anchor mount bracket (Issue #564) 2026-03-14 12:15:12 -04:00
b09017c949 Merge pull request 'feat: UWB-IMU EKF fusion for robust indoor localization (Issue #573)' (#581) from sl-uwb/issue-573-uwb-imu-fusion into main 2026-03-14 12:14:05 -04:00
1726558a7a Merge pull request 'feat: RPLIDAR safety zone detector (Issue #575)' (#580) from sl-perception/issue-575-safety-zone into main 2026-03-14 12:14:01 -04:00
5a3f4d1df6 Merge pull request 'feat: WebUI event log panel (Issue #576)' (#579) from sl-webui/issue-576-event-log into main 2026-03-14 12:13:56 -04:00
b2f01b42f3 Merge pull request 'feat: Termux sensor dashboard (Issue #574)' (#578) from sl-android/issue-574-sensor-dashboard into main 2026-03-14 12:13:51 -04:00
a7eb2ba3e5 Merge pull request 'feat: PID gain scheduling for speed-dependent balance (Issue #550)' (#560) from sl-controls/issue-550-pid-scheduling into main 2026-03-14 12:13:44 -04:00
4035b4cfc3 feat: ROS2 launch orchestrator for full SaltyBot bringup (Issue #577)
Adds saltybot_bringup.launch.py with ordered startup groups (drivers→
perception→navigation→UI), timer-based health gates, configurable
profiles (minimal/full/debug), and estop on Ctrl-C shutdown.

Also adds launch_profiles.py dataclass module and 53-test coverage for
profile hierarchy, timing gates, safety bounds, and to_dict serialization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:57:57 -04:00
sl-uwb
7708c63698 feat: UWB-IMU EKF fusion for robust indoor localization (Issue #573)
EKF fusing UWB position (10Hz) with IMU accel+gyro (200Hz) for
SaltyBot indoor localization with UWB dropout resilience.

Package: saltybot_uwb_imu_fusion

- ekf_math.py: 6-state EKF [x,y,θ,vx,vy,ω], IMU predict + UWB update
  - IMU as process input: body-frame accel rotated to world via heading
  - Jacobian F for nonlinear rotation effect
  - Process noise Q from continuous white-noise model
  - UWB 2D position update, heading update from quaternion
  - Accel bias estimation (low-pass)
  - Velocity damping during UWB dropout (>2s threshold)
- ekf_node.py: ROS2 node subscribing to /imu/data (200Hz) + /saltybot/uwb/pose
  or /uwb/bearing (10Hz)
  - Publishes /saltybot/pose/fused (PoseStamped)
  - Publishes /saltybot/pose/fused_cov (PoseWithCovarianceStamped)
  - Broadcasts base_link → map TF2 at IMU rate
  - Suppresses output after max_dead_reckoning_s without UWB
- 14/14 unit tests passing (predict, update, dropout, PD covariance)
- Launch: ros2 launch saltybot_uwb_imu_fusion uwb_imu_fusion.launch.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:55:43 -04:00
131d85a0d3 feat: RPLIDAR safety zone detector (Issue #575)
Add saltybot_safety_zone — ROS2 Python node that processes the RPLIDAR
A1M8 /scan into three concentric 360° safety zones, latches an e-stop
when DANGER is detected in the forward arc, and overrides /cmd_vel to
zero while the latch is active.

Zone thresholds (default):
  DANGER  < 0.30 m — latching e-stop in forward arc
  WARN    < 1.00 m — advisory (published in sector data)
  CLEAR   otherwise

Sector grid:
  36 sectors of 10° each (sector 0 = robot forward, CCW positive).
  Per-sector: angle_deg, zone, min_range_m, in_forward_arc flag.

E-stop behaviour:
  - Latches after estop_debounce_frames (2) consecutive DANGER scans
    in the forward arc (configurable ±30°, or all-arcs mode).
  - While latched: zero Twist published to /cmd_vel every scan + every
    incoming /cmd_vel_input message is blocked.
  - Clear only via service (obstacle must be gone):
    /saltybot/safety_zone/clear_estop  (std_srvs/Trigger)

Published topics:
  /saltybot/safety_zone          String/JSON  every scan
    — per-sector {sector, angle_deg, zone, min_range_m, forward}
    — estop_active, estop_reason, danger_sectors[], warn_sectors[]
  /saltybot/safety_zone/status   String/JSON  10 Hz
    — forward_zone, closest_obstacle_m, danger/warn counts
  /cmd_vel                       Twist        zero when e-stopped

Subscribed topics:
  /scan           LaserScan  — RPLIDAR A1M8
  /cmd_vel_input  Twist      — upstream velocity (pass-through / block)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:54:52 -04:00
44691742c8 feat: WebUI event log panel (Issue #576)
Standalone 3-file filterable real-time event log (no build step).

Files:
  ui/event_log_panel.html  — layout, toolbar, empty state
  ui/event_log_panel.js    — rosbridge subscriptions, ring buffer, render
  ui/event_log_panel.css   — dark-theme, responsive grid layout

Features:
- 1000-entry ring buffer (oldest dropped when full, FIFO)
- Subscribes /rosout (rcl_interfaces/msg/Log) + /saltybot/events (std_msgs/String JSON)
- Severity filter buttons: DEBUG / INFO / WARN / ERROR / FATAL / EVENT (toggle on/off)
- Node name filter: select dropdown populated from seen nodes
- Live text search with <mark> highlight, Ctrl+F shortcut, Esc to clear
- Auto-scroll to latest entry; pauses on mouse hover (messages still buffered)
- Manual pause/resume button; detects user scroll-up and stops auto-scroll
- CSV export of current filtered view with timestamp (filename includes ISO date)
- Clear all entries button
- Color-coded by severity: left border stripe + text color per level
- Entry columns: timestamp (ms precision) | severity | node | message
- [system] entries for connect/disconnect events
- WS URL persisted in localStorage
- Responsive: node column hidden on narrow screens

ROS topics:
  SUB /rosout               rcl_interfaces/msg/Log  (all nodes)
  SUB /saltybot/events      std_msgs/String (JSON: {level,node,msg})

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:54:13 -04:00
sl-android
814624045a feat: Termux sensor dashboard (Issue #574)
Add phone/sensor_dashboard.py — publishes phone sensors to SaltyBot MQTT:

- IMU  → saltybot/phone/imu     @ 5 Hz  (accelerometer + gyroscope via
  termux-sensor -s <name> -n 1)
- GPS  → saltybot/phone/gps     @ 1 Hz  (lat/lon/alt/accuracy/speed/bearing
  via termux-location; GPS→network fallback on cold start)
- Battery → saltybot/phone/battery @ 1 Hz (pct/charging/temp/health/plugged
  via termux-battery-status)
- paho-mqtt with loop_start() + on_connect/on_disconnect callbacks for
  automatic reconnect (exponential back-off, max 60 s)
- Each sensor runs in its own daemon thread (SensorPoller); rate enforced
  by wall-clock sleep accounting for read latency
- 30 s status log: per-poller publish/error counts + MQTT state
- Flags: --broker, --port, --imu-hz, --gps-hz, --bat-hz, --qos,
  --no-imu, --no-gps, --no-battery, --debug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:53:04 -04:00
929c9ecd74 feat: UWB anchor mount bracket (Issue #564) 2026-03-14 11:51:50 -04:00
8592361095 feat: PID gain scheduling for speed-dependent balance (Issue #550)
Implements a speed-dependent PID gain scheduler that interpolates Kp/Ki/Kd
across a configurable table of velocity breakpoints, replacing the fixed
single-gain PID used previously.

Changes:
- include/pid_flash.h: add pid_sched_entry_t (16-byte entry), pid_sched_flash_t
  (128-byte record at 0x0807FF40), pid_flash_load_schedule(), pid_flash_save_all()
  (atomic single-sector erase for both schedule and single-PID records)
- src/pid_flash.c: implement load_schedule and save_all; single erase covers
  both records at 0x0807FF40 (schedule) and 0x0807FFC0 (single PID)
- include/pid_schedule.h: API header -- init, get_gains, apply, set/get table,
  flash_save, active_band_idx, get_default_table
- src/pid_schedule.c: linear interpolation between sorted speed-band entries;
  integrator reset on band transition; default 3-band table (0/0.3/0.8 m/s)
- include/jlink.h: add SCHED_GET (0x0C), SCHED_SET (0x0D), SCHED_SAVE (0x0E)
  commands; TLM_SCHED (0x85); jlink_tlm_sched_t; JLinkSchedSetBuf;
  sched_get_req, sched_save_req fields in JLinkState; include pid_flash.h
- src/jlink.c: dispatch SCHED_GET/SET/SAVE; implement jlink_send_sched_telemetry,
  jlink_get_sched_set; add JLinkSchedSetBuf static buffer
- test/test_pid_schedule.c: 48 unit tests -- all passing (gcc host build)

Flash layout (sector 7):
  0x0807FF40  pid_sched_flash_t (128 bytes) -- schedule
  0x0807FFC0  pid_flash_t       ( 64 bytes) -- single PID (existing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 11:51:11 -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
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
7141e12320 feat: Integration test suite expanded (Issue #504) - resolve conflicts 2026-03-06 23:10:42 -05:00
sl-android
d5246fe3a8 chore: Guard ESC debug output behind compile flag (Issue #521)
- esc_hoverboard.c: huart2 static in production; non-static only under
  #ifdef DEBUG_MOTOR_TEST (needed by R command in jetson_uart.c)
- esc_hoverboard.c: UART5 diagnostic in hoverboard_backend_init() and
  per-packet printf in hoverboard_backend_send() guarded by same flag
- esc_hoverboard.c: #include <stdio.h> also guarded (not needed in production)
- jetson_uart.c: R (baud sweep) and X (GPIO test) commands guarded by
  #ifdef DEBUG_MOTOR_TEST — not compiled into production firmware

Production build: no debug output, static huart2, no R/X commands.
Debug build: define DEBUG_MOTOR_TEST to re-enable all diagnostics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:07:36 -05:00
7ad8c82da6 feat: Orin motor control daemon (Issue #523)
Add saltybot_motor_daemon ROS2 package — Python daemon that subscribes
to /cmd_vel and drives the FC via W<speed>,<steer>\n over /dev/ttyTHS1
at 921600 baud.

- motor_daemon_node.py: 50 Hz fixed-rate TX, 200ms safety watchdog,
  Twist→ESC conversion (±1000 range), FC ack parsing (W:<s>,<st>),
  periodic ? status query, /diagnostics publisher, auto-reconnect
- config/motor_daemon_params.yaml: all tunable params with comments
- launch/motor_daemon.launch.py: parameterised launch file
- test/test_motor_daemon.py: 25 unit tests (all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:04:07 -05:00
b128e164e7 fix: USART6 TX mutex to prevent truncated output (Issue #522)
USART1 IDLE interrupt (DMA circular RX) was calling HAL_UART_IRQHandler
mid-frame during polling HAL_UART_Transmit, resetting gState and causing
leading nulls / truncated frames on the Jetson telemetry link at 921600 baud.

Fix: introduce jlink_tx_locked() which disables USART1_IRQn around every
blocking HAL_UART_Transmit call, preventing IRQHandler from corrupting
gState while the TX loop is running.  A s_tx_busy flag drops any
re-entrant caller (ESC debug, future USART6/VESC paths).

Both jlink_send_telemetry (50 Hz) and jlink_send_power_telemetry (1 Hz)
now use jlink_tx_locked().  Also correct the stale config.h comment that
misidentified the Jetson link as USART6 (it moved to USART1 in Issue #120).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:02:57 -05:00
e28f1549cb feat: Orin motor control daemon (Issue #523)
Add saltybot_motor_daemon ROS2 package — Python daemon that subscribes
to /cmd_vel and drives the FC via W<speed>,<steer>\n over /dev/ttyTHS1
at 921600 baud.

- motor_daemon_node.py: 50 Hz fixed-rate TX, 200ms safety watchdog,
  Twist→ESC conversion (±1000 range), FC ack parsing (W:<s>,<st>),
  periodic ? status query, /diagnostics publisher, auto-reconnect
- config/motor_daemon_params.yaml: all tunable params with comments
- launch/motor_daemon.launch.py: parameterised launch file
- test/test_motor_daemon.py: 25 unit tests (all passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:02:57 -05:00
dd4eabf218 fix: Investigate USB CDC TX failure (Issue #524)
Root causes confirmed from code audit:
1. DCache coherency: USB OTG FS reads physical SRAM while CPU writes through
   DCache. Fix: MPU Region 0 marks 512B aligned USB buffer struct non-cacheable
   (TEX=1, C=0, B=0) before HAL_PCD_Init(). DCache stays enabled globally.
2. IWDG ordering: safety_init() (IWDG start) deferred after all peripheral inits
   to avoid watchdog reset during mpu6000_calibrate() (~510ms blocking).

DMA conflicts, GPIO conflicts, clock tree, and interrupt priorities all ruled out
with evidence. Full findings documented in USB_CDC_BUG.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:01:00 -05:00
Salty Bead
3bb4b71cea feat: motor bench test working — UART5 ESC + USART6 Jetson + W command
- Fix UART5 ESC: PC12=TX, PD2=RX @ 115200 baud (was USART2/38400)
- Add jetson_uart.c: USART6 command interface (A/D/E/Z/H/C/W/R/X/? commands)
- Add W command: persistent direct motor test (sets globals, main loop sends at 50Hz)
- Fix power_mgmt: keep UART5 clock active, PC7 EXTI wake source
- Fix main loop: direct_test_speed/steer override disarmed zero-send
- Add boot banner on USART6

Tested: Orin -> FC USART6 -> FC UART5 -> EFeru ESC -> both motors spin
2026-03-06 22:35:24 -05:00
f14ce5c3ba Merge remote-tracking branch 'origin/sl-perception/issue-469-terrain-classification' 2026-03-06 17:37:27 -05:00
2e2ed2d0a7 Merge remote-tracking branch 'origin/sl-controls/issue-506-launch-profiles' 2026-03-06 17:37:27 -05:00
5b9e9dd412 Merge pull request 'feat: Headscale VPN auto-connect (Issue #502)' (#517) from sl-jetson/issue-502-headscale-vpn into main 2026-03-06 17:37:07 -05:00
706a67c0b7 Merge pull request 'feat: Charging dock hardware design (Issue #505)' (#516) from sl-webui/issue-504-integration-tests into main 2026-03-06 17:37:06 -05:00
8d58d5e34c feat: Terrain classification for speed adaptation (Issue #469)
Implement multi-sensor terrain classification using RealSense D435i depth and RPLIDAR A1M8:

- saltybot_terrain_classification: New ROS2 package for terrain classification
- TerrainClassifier: Rule-based classifier matching depth variance + reflectance to terrain type
  (smooth/carpet/grass/gravel) with hysteresis + confidence scoring
- DepthExtractor: Extracts roughness from depth discontinuities and surface gradients
- LidarExtractor: Extracts reflectance from RPLIDAR scan intensities
- terrain_classification_node: 10Hz node fusing both sensors, publishes:
  - /saltybot/terrain_type (JSON with type, confidence, speed_scale)
  - /saltybot/terrain_type_string (human-readable type)
  - /saltybot/terrain_speed_scale (0.0-1.0 speed multiplier for smooth/carpet/grass/gravel)

Speed scales: smooth=1.0, carpet=0.9, grass=0.75, gravel=0.6

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:43:21 -05:00
d3eca7bebc feat: Integration test suite (Issue #504)
Add comprehensive integration testing for complete ROS2 system stack:

Integration Tests (test_integration_full_stack.py):
  - Verifies all ROS2 nodes launch successfully
  - Checks critical topics are published (sensors, nav, control)
  - Validates system component health and stability
  - Tests launch file validity and configuration
  - Covers indoor/outdoor/follow modes

Launch Testing (test_launch_full_stack.py):
  - Validates launch file syntax and configuration
  - Verifies all required packages are installed
  - Checks launch sequence timing
  - Validates conditional logic for optional components

Test Coverage:
  ✓ SLAM/RTAB-Map (indoor mode)
  ✓ Nav2 navigation stack
  ✓ Perception (YOLOv8n person detection)
  ✓ Control (cmd_vel bridge, STM32 bridge)
  ✓ Audio pipeline and monitoring
  ✓ Sensors (LIDAR, RealSense, UWB, CSI cameras)
  ✓ Battery and temperature monitoring
  ✓ Autonomous docking behavior
  ✓ TF2 tree and odometry

Usage:
  pytest test/test_integration_full_stack.py -v
  pytest test/test_launch_full_stack.py -v

Documentation:
  See test/README_INTEGRATION_TESTS.md for detailed information.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:38 -05:00
8d67d06857 feat: Integration test suite (Issue #504)
Add comprehensive integration testing for complete ROS2 system stack:

Integration Tests (test_integration_full_stack.py):
  - Verifies all ROS2 nodes launch successfully
  - Checks critical topics are published (sensors, nav, control)
  - Validates system component health and stability
  - Tests launch file validity and configuration
  - Covers indoor/outdoor/follow modes

Launch Testing (test_launch_full_stack.py):
  - Validates launch file syntax and configuration
  - Verifies all required packages are installed
  - Checks launch sequence timing
  - Validates conditional logic for optional components

Test Coverage:
  ✓ SLAM/RTAB-Map (indoor mode)
  ✓ Nav2 navigation stack
  ✓ Perception (YOLOv8n person detection)
  ✓ Control (cmd_vel bridge, STM32 bridge)
  ✓ Audio pipeline and monitoring
  ✓ Sensors (LIDAR, RealSense, UWB, CSI cameras)
  ✓ Battery and temperature monitoring
  ✓ Autonomous docking behavior
  ✓ TF2 tree and odometry

Usage:
  pytest test/test_integration_full_stack.py -v
  pytest test/test_launch_full_stack.py -v

Documentation:
  See test/README_INTEGRATION_TESTS.md for detailed information.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
e5329391bc feat: Add parameter profile YAML files for Nav2 (Issue #506)
- profile_indoor.yaml: Conservative settings (0.4 m/s, 0.35m inflation)
- profile_outdoor.yaml: Moderate settings (0.8 m/s, 0.3m inflation)
- profile_demo.yaml: Agile settings (0.6 m/s, 0.32m inflation)

Each profile customizes velocity limits, costmap inflation, and obstacle detection.
2026-03-06 16:42:31 -05:00
5d17b6c501 feat: Issue #506 — Update nav2.launch.py for profile support
Add profile argument to nav2.launch.py to accept launch profile parameter
and log profile selection for debugging/monitoring.

Changes:
- Add profile_arg declaration with choices (indoor/outdoor/demo)
- Add profile substitution and log output
- Update docstring with profile documentation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
b5acb32ee6 feat: Issue #506 — Update full_stack.launch.py for profile support
Add profile argument and documentation to full_stack.launch.py for
Issue #506 launch parameter profiles. Updated to support:
- profile:=indoor (conservative)
- profile:=outdoor (moderate)
- profile:=demo (agile with tricks/social features)

Changes:
- Add profile_arg declaration
- Add profile substitution handle
- Update docstring with profile examples
- Ready for profile-based Nav2 parameter overrides

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
bbfcd2a9d1 feat: Issue #506 — Launch parameter profiles (indoor/outdoor/demo)
Implement profile-based parameter overrides for Nav2, costmap, and behavior
server configurations. Profiles predefine parameter sets for different
deployment scenarios.

New files:
- config/profiles/indoor.yaml: Conservative (0.2 m/s, tight geofence, no GPS)
- config/profiles/outdoor.yaml: Moderate (0.5 m/s, wide geofence, GPS-enabled)
- config/profiles/demo.yaml: Agile (0.3 m/s, tricks/social features enabled)
- saltybot_bringup/profile_loader.py: YAML loader and parameter merger utility

Supports: ros2 launch saltybot_bringup full_stack.launch.py profile:=<profile>

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 16:42:31 -05:00
5add2cab51 Merge remote-tracking branch 'origin/sl-mechanical/issue-505-charging-dock'
# Conflicts:
#	phone/INSTALL_MOTOR_TEST.md
#	phone/MOTOR_TEST_JOYSTICK.md
#	phone/motor_test_joystick.py
2026-03-06 14:59:59 -05:00
892d0a2089 Merge remote-tracking branch 'origin/sl-android/issue-513-phone-joystick'
# Conflicts:
#	phone/MOTOR_TEST_JOYSTICK.md
#	phone/motor_test_joystick.py
2026-03-06 14:00:21 -05:00
678fd221f5 Merge pull request 'feat: Remove ELRS arm requirement (Issue #512)' (#514) from sl-firmware/issue-512-autonomous-arming into main 2026-03-06 12:52:05 -05:00
sl-android
f49e84b8bb feat: Phone-based motor test joystick app (Issue #513)
Implements terminal-based curses UI for interactive bench testing of SaltyBot motors via Termux.

Features:
- Interactive keyboard-based joystick (W/A/S/D or arrow keys)
- Conservative velocity defaults: 0.1 m/s linear, 0.3 rad/s angular
- Real-time velocity feedback with bar graphs
- Spacebar e-stop (instantly zeros velocity)
- 500ms timeout safety (sends zero velocity if idle)
- Dual backend: ROS2 (/cmd_vel) or WebSocket
- Graceful fallback if ROS2 unavailable

Safety Features:
- Conservative defaults (0.1/0.3 m/s)
- E-stop button (spacebar)
- 500ms timeout (sends zero if idle)
- Input clamping and exponential decay
- Status/warning displays

Files:
- motor_test_joystick.py: Main application
- MOTOR_TEST_JOYSTICK.md: User documentation
- INSTALL_MOTOR_TEST.md: Installation guide

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 11:47:15 -05:00
48e9af60a9 feat: Remove ELRS arm requirement for autonomous operation (Issue #512)
Enable Jetson autonomous arming while keeping RC as optional override.

Key Changes:
=============

1. RC Kill Switch (CH5 OFF) → Emergency Stop (not disarm)
   - Motors cutoff immediately on kill switch
   - Robot remains armed (allows re-arm without re-initializing)
   - Maintains kill switch safety for emergency situations

2. RC Disarm Only on Explicit CH5 Falling Edge (while RC alive)
   - RC disconnect doesn't disarm Jetson-controlled missions
   - RC failsafe timer (500ms) still handles signal loss

3. Jetson Autonomous Arming (via CDC 'A' command)
   - Works independently of RC state
   - Requires: calibrated IMU, robot level (±10°), no estop active
   - Uses same 500ms arm-hold safety as RC

4. All Safety Preserved
   - Arming hold timer: 500ms
   - Tilt limit: ±10° level
   - IMU calibration required
   - Remote E-stop override
   - RC failsafe: 500ms signal loss = disarm
   - Jetson timeout: 500ms heartbeat = zero motors

Command Protocol (CDC):
   A = arm (Jetson)
   D = disarm (Jetson)
   E/Z = estop / clear estop
   H = heartbeat (keep-alive)
   C<spd>,<str> = drive command

Behavior Matrix:
   RC disconnected      → Jetson-armed stays armed, normal operation
   RC connected + armed → Both Jetson and RC can arm, blended control
   RC kill switch (CH5) → Emergency stop + can re-arm via Jetson 'A'
   RC signal lost       → Disarm after 500ms (failsafe)

See AUTONOMOUS_ARMING.md for complete protocol and testing checklist.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 11:46:03 -05:00
e4116dffc0 feat: Remove ELRS arm requirement for autonomous operation (Issue #512)
Enable Jetson autonomous arming while keeping RC as optional override.

Changes:
- RC kill switch (CH5 OFF) now triggers emergency stop instead of disarm
  → Allows Jetson-armed robots to remain armed when RC disconnects
  → Maintains kill switch safety for emergency situations

- RC disarm only triggers on explicit CH5 falling edge (RC still alive)
  → RC disconnect doesn't disarm Jetson-controlled missions
  → RC failsafe timer (500ms) handles signal loss separately

- Jetson arming via CDC 'A' command works independently of RC state
  → Robots can operate fully autonomous without RC transmitter
  → Heartbeat timeout (500ms) prevents runaway if Jetson crashes

Safety maintained:
- Arming hold timer: 500ms (prevents accidental arm)
- Tilt limit: ±10° level required
- IMU calibration: Required before any arm attempt
- Remote E-stop: Blocks all arming
- RC failsafe: 500ms signal loss = disarm
- Jetson timeout: 500ms heartbeat = zero motors

Command protocol (unchanged):
- Jetson: A=arm, D=disarm, E=estop, Z=clear estop
- RC: CH5 switch (optional override)
- Heartbeat: H command every ≤500ms
- Drive: C<speed>,<steer> every ≤200ms

See AUTONOMOUS_ARMING.md for complete protocol and testing checklist.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 11:45:12 -05:00
b0c2b5564d feat: Add Issue #505 CAD - 24V Charging Dock OpenSCAD Models
CAD implementation files for Issue #505 (24V charging dock upgrade):

- charging_dock_505.scad: Main dock assembly
  * Base plate: 340×320×12 mm (enlarged for 240W PSU)
  * Back wall: 250×85×10 mm (pogo pin housing, LED bezel recess)
  * V-guide rails: 100mm deep, self-centering funnel (print 2×)
  * ArUco marker frame: ID 42 (DICT_4X4_250), 15cm mast
  * PSU bracket: Sized for Mean Well IRM-240-24 (210×108×56 mm)
  * LED bezel: 4× status indicators (SEARCHING/ALIGNED/CHARGING/FULL)

- charging_dock_receiver_505.scad: Robot-side receiver variants
  * Lab receiver: Stem collar mount (SaltyLab)
  * Rover receiver: Deck flange mount (SaltyRover)
  * Tank receiver: Skid plate mount + extended nose (SaltyTank)
  * Common contact geometry: 20mm CL-to-CL brass pads, V-nose guide
  * Wire bore: 3mm (supports 12 AWG charging wires)

Key changes from Issue #159 (5V):
- PSU dimensions: 63×45×28 mm → 210×108×56 mm
- Base/bracket enlarged accordingly
- ArUco ID: 0 → 42
- Contact geometry unchanged (compatible with Issue #159 receivers)
- Pogo pins, V-guides, LED circuit identical

Files ready for:
- STL export via OpenSCAD render commands
- 3D printing (PETG recommended)
- Assembly integration with docking node (#489)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 11:44:28 -05:00
1f4929b68c Merge remote-tracking branch 'origin/sl-jetson/issue-477-urdf'
# Conflicts:
#	jetson/config/RECOVERY_BEHAVIORS.md
2026-03-06 11:43:31 -05:00
d97fa5fab0 Merge remote-tracking branch 'origin/sl-webui/issue-482-behavior-tree'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_bringup/behavior_trees/autonomous_coordinator.xml
#	jetson/ros2_ws/src/saltybot_bringup/launch/autonomous_mode.launch.py
2026-03-06 11:43:26 -05:00
a3d3ea1471 Merge remote-tracking branch 'origin/sl-perception/issue-478-costmaps'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_bringup/config/nav2_params.yaml
2026-03-06 11:43:11 -05:00
e1248f92b9 Merge pull request 'feat: Face display animations on STM32 LCD (Issue #507)' (#508) from sl-android/issue-507-face-animations into main 2026-03-06 10:57:28 -05:00
6f3dd46285 feat: Add Issue #503 - Audio pipeline with Jabra SPEAK 810
Implement full audio pipeline with:
- Jabra SPEAK 810 USB audio I/O (mic + speaker)
- openwakeword 'Hey Salty' wake word detection
- whisper.cpp GPU-accelerated STT (small/base/medium/large models)
- piper TTS synthesis and playback
- Audio state machine: listening → processing → speaking
- MQTT status and state reporting
- Real-time latency metrics tracking

ROS2 Topics Published:
- /saltybot/speech/transcribed_text: STT output for voice router
- /saltybot/audio/state: Current audio state
- /saltybot/audio/status: JSON metrics with latencies

MQTT Topics:
- saltybot/audio/state: Current state (listening/processing/speaking)
- saltybot/audio/status: Complete status JSON

Configuration parameters in yaml:
- device_name: Jabra device pattern
- wake_word_threshold: 0.5 (tunable)
- whisper_model: small/base/medium/large
- mqtt_enabled: true/false with broker config

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:30:58 -05:00
5cea0812d5 feat: Add Issue #505 - 24V Charging Dock Hardware Design
- Design specification: 24V DC power delivery (upgraded from 5V Issue #159)
- ArUco marker ID 42 (15cm frame) for precision alignment
- Spring-loaded contact pads with V-channel guide rails
- Comprehensive BOM for 24V PSU, wiring, LED status circuit
- Compatible with docking node #489 (ROS2 integration)
- 3D-printable PETG frame (base, back wall, guide rails, brackets)
- Electrical: 240W Mean Well IRM-240-24 PSU, 20A current capacity
- Safety: Fused output, varistor protection, soft-start capable
- Integration: MQTT status reporting, GPIO LED control (Jetson Orin NX)

Files:
- ISSUE_505_CHARGING_DOCK_24V_DESIGN.md: Complete design spec (mechanical, electrical, assembly)
- charging_dock_505_BOM.csv: Procurement list with sourcing info

Next: CAD implementation (charging_dock_505.scad, receiver variant)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:30:44 -05:00
sl-android
49628bcc61 feat: Add Issue #507 - Face display animations on STM32 LCD
Implements expressive face animations with 5 core emotions (happy/sad/curious/angry/sleeping) and smooth transitions on small LCD displays.

Features:
- State machine with smooth 0.5s emotion transitions (ease-in-out cubic easing)
- Automatic idle blinking (4-6s intervals, 100-150ms duration per blink)
- UART command interface via USART3 @ 115200 (text-based protocol)
- 30Hz target refresh rate via systick integration
- Low-level LCD abstraction supporting monochrome and RGB565
- Rendering primitives: pixel, line (Bresenham), circle (midpoint), filled rect

Architecture:
- face_lcd.h/c: Hardware-agnostic framebuffer & display driver
- face_animation.h/c: Emotion state machine & parameterized face rendering
- face_uart.h/c: UART command parser (HAPPY/SAD/CURIOUS/ANGRY/SLEEP/NEUTRAL/BLINK/STATUS)
- Unit tests (14 test cases): emotion transitions, blinking, rendering, all emotions

Integration:
- main.c: Added includes, initialization (servo_init), systick tick, main loop processing
- Pending: LCD hardware initialization (SPI/I2C config, display controller setup)

Files: 9 new (headers, source, tests, docs), 1 modified (main.c)
Lines: ~1450 total (345 headers, 650 source, 350 tests, 900 docs)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:27:36 -05:00
062c05cac0 feat: Add Issue #502 - Headscale VPN auto-connect on Orin
Configure Jetson Orin with Tailscale client connecting to Headscale
coordination server at tailscale.vayrette.com:8180. Device registers
as 'saltylab-orin' with persistent auth key for unattended login.

Features:
- systemd auto-start and restart on WiFi drops
- Persistent auth key storage at /opt/saltybot/tailscale-auth.key
- SSH + HTTP access over Tailscale tailnet (encrypted WireGuard)
- IP forwarding enabled for relay/exit node capability
- WiFi resilience with aggressive restart policy
- MQTT reporting of VPN status, IP, and connection type

Components added:
- jetson/scripts/setup-tailscale.sh: Tailscale package installation
- jetson/scripts/headscale-auth-helper.sh: Auth key management utility
- jetson/systemd/tailscale-vpn.service: systemd service unit
- jetson/docs/headscale-vpn-setup.md: Comprehensive setup documentation
- saltybot_cellular/vpn_status_node.py: ROS2 node for MQTT reporting

Updated:
- jetson/systemd/install_systemd.sh: Include tailscale-vpn.service
- jetson/scripts/setup-jetson.sh: Add Tailscale setup steps

Access patterns:
- SSH: ssh user@saltylab-orin.tail12345.ts.net
- HTTP: http://saltylab-orin.tail12345.ts.net:port
- Direct IP: 100.x.x.x (Tailscale allocated address)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:25:04 -05:00
767f377120 feat: Add Issue #504 - Integration test suite with launch_testing
Create saltybot_tests package with comprehensive automated testing:

Test Coverage:
- Node startup verification (all critical nodes within 30s)
- Topic publishing verification
- TF tree completeness (all transforms present)
- Sensor health checks (RPLIDAR, RealSense, IMU)
- Perception pipeline (person detection availability)
- Navigation stack (odometry, transforms)
- System stability (30-second no-crash test)
- Graceful shutdown verification

Features:
- launch_testing framework for automated startup tests
- NodeChecker: wait for nodes in ROS graph
- TFChecker: verify TF tree completeness
- TopicMonitor: track message rates and counts
- Follow mode tests (minimal hardware deps)
- Subsystem-specific tests for sensor health
- Comprehensive README with troubleshooting

Usage:
  pytest src/saltybot_tests/test/test_launch.py -v -s
  or
  colcon test --packages-select saltybot_tests

Performance Targets:
- Node startup: <30s (follow mode)
- RPLIDAR: 10 Hz scan rate
- RealSense: 30 Hz RGB + depth
- Person detection: 5 Hz
- System stability: 30s no-crash validation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-06 10:22:38 -05:00
seb
80ee9ece87 Merge pull request 'feat: Voice command router (Issue #491)' (#499) from sl-webui/issue-491-voice-router into main 2026-03-05 19:25:32 -05:00
Sebastien Vayrette
868b453777 fix: resolve merge conflicts for voice router PR #499 (keep both docking + mission logging) 2026-03-05 19:25:23 -05:00
83e3033abe Merge pull request 'feat: OTA firmware update (Issue #492)' (#500) from sl-jetson/issue-492-ota-update into main 2026-03-05 17:24:58 -05:00
5f6a13ccca Merge pull request 'feat: Multi-sensor fusion (Issue #490)' (#498) from sl-perception/issue-490-sensor-fusion into main 2026-03-05 17:24:03 -05:00
4dc18201aa Merge pull request 'feat: Docking station behavior (Issue #489)' (#497) from sl-controls/issue-489-docking into main 2026-03-05 17:16:37 -05:00
2dd03a245d Merge pull request 'feat: ROS2 bag recording for mission logging (Issue #488)' (#496) from sl-firmware/issue-488-bag-recording into main 2026-03-05 17:16:22 -05:00
340248a0d2 feat: Add docking state publisher and update configuration (Issue #489)
- Add /saltybot/docking_state publisher (std_msgs/String) for monitoring
- Update docking_params.yaml battery_low_pct: 15% → 20%
- Add Issue #475 references for conservative servo speeds

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:08:48 -05:00
7a4930e8d2 feat: ROS2 bag recording for mission logging (Issue #488)
Implement automatic mission logging with bag recorder:
- Auto-records to ~/.saltybot-data/bags/ with 30min rotation
- Records mission-critical topics: /scan, /cmd_vel, /odom, /tf, /camera/color/image_raw/compressed, /saltybot/diagnostics
- MCAP format (preferred) with fallback to sqlite3 with zstd compression
- Services: /saltybot/save_bag, /saltybot/start_recording, /saltybot/stop_recording
- FIFO 20GB disk limit with automatic cleanup of oldest bags
- Auto-starts on launch, auto-saves on graceful shutdown

Changes:
- Updated bag_recorder_node.py with new parameters and services
- Changed default bag_dir to ~/.saltybot-data/bags/
- Set max_storage_gb to 20 (FIFO limit)
- Changed storage_format to MCAP by default
- Added start/stop recording service callbacks
- Updated package.xml description for mission logging

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:08:21 -05:00
b986702aed feat: Docking station behavior for auto-charging (Issue #489)
- Integrate saltybot_docking package into full_stack.launch.py
- Auto-trigger docking when battery drops to 20% (configurable via battery_low_pct)
- Launch docking at t=7s (after sensors, before Nav2)
- Add /saltybot/docking_state publisher (std_msgs/String) for state monitoring
- Update docking_params.yaml:
  - battery_low_pct: 15% → 20% per Issue #489
  - Add references to Issue #475 for conservative FC+hoverboard speeds
- Docking behavior includes:
  - ArUco marker or IR beacon detection for dock location
  - Nav2-based approach to pre-dock pose (~1m away)
  - Visual servoing final alignment with contact detection
  - Auto-undocking on full charge (80%) or command
  - Integration with power management for mission interruption/resumption

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:08:21 -05:00
a4285b5ecd feat: Docking station behavior for auto-charging (Issue #489)
- Integrate saltybot_docking package into full_stack.launch.py
- Auto-trigger docking when battery drops to 20% (configurable via battery_low_pct)
- Launch docking at t=7s (after sensors, before Nav2)
- Add /saltybot/docking_state publisher (std_msgs/String) for state monitoring
- Update docking_params.yaml:
  - battery_low_pct: 15% → 20% per Issue #489
  - Add references to Issue #475 for conservative FC+hoverboard speeds
- Docking behavior includes:
  - ArUco marker or IR beacon detection for dock location
  - Nav2-based approach to pre-dock pose (~1m away)
  - Visual servoing final alignment with contact detection
  - Auto-undocking on full charge (80%) or command
  - Integration with power management for mission interruption/resumption

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:08:15 -05:00
d770cb99a3 feat: Multi-sensor fusion for obstacle avoidance (Issue #490)
- saltybot_sensor_fusion: ROS2 node for LIDAR + depth sensor fusion
- Fuses RPLIDAR A1M8 (360° 2D) + RealSense D435i (front 87° 3D)
- Message filters for time-synchronized sensor inputs
- Smart blind spot handling: rear/sides LIDAR-only, front uses both
- Publishes /scan_fused (unified LaserScan) + PointCloud2 for voxel layer
- Configurable front sector angle (±45°), range multiplier, max range limit
- Parameters: depth_range_multiplier=0.9 (safety margin), max_range=5m

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:05:25 -05:00
fabfd5e974 feat: TTS personality engine (Issue #494)
Implement context-aware text-to-speech with emotion-driven expression for SaltyBot.

Features:
  ✓ Context-aware greetings (time of day, person names, emotion)
  ✓ Priority queue management (safety > social > idle)
  ✓ Emotion-based rate/pitch modulation (happy: faster+higher, sad: slower+lower)
  ✓ Integration with emotion engine (Issue #429) and TTS service (Issue #421)
  ✓ Configurable personality parameters
  ✓ Person recognition for personalized responses
  ✓ Queue management with 16-item buffer

Architecture:
  Node: tts_personality_node
    - Subscribes: /saltybot/tts_request, /saltybot/emotion_state, /saltybot/person_detected
    - Publishes: /saltybot/tts_command (formatted for TTS service), /saltybot/personality_state
    - Runs worker thread for asynchronous queue processing

Personality Parameters:
  - Name: "Luna" (default, configurable)
  - Speed modulation: happy=1.1x, sad=0.9x, neutral=1.0x
  - Pitch modulation: happy=1.15x, sad=0.85x, neutral=1.0x
  - Time-based greetings for 4 periods (morning, afternoon, evening, night)
  - Known people mapping for personalization

Queue Priority Levels:
  - SAFETY (3): Emergency/safety messages
  - SOCIAL (2): Greetings and interactions
  - IDLE (1): Commentary and chatter
  - NORMAL (0): Default messages

Files Created:
  - saltybot_tts_personality package with main personality node
  - config/tts_personality_params.yaml with configurable parameters
  - launch/tts_personality.launch.py for easy startup
  - Unit tests for personality context and emotion handling
  - Comprehensive README with usage examples

Integration Points:
  - Emotion engine (Issue #429): Listens to emotion updates
  - TTS service (Issue #421): Publishes formatted commands
  - Jabra SPEAK 810: Output audio device
  - Person tracking: Uses detected person names

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:05:11 -05:00
6d6909d9d9 feat: Voice command router (Issue #491)
Natural language voice command routing with fuzzy matching for speech variations.

Supported Commands:
- Follow me / Come with me
- Stop / Halt / Freeze
- Go home / Return to dock / Charge
- Patrol / Autonomous mode
- Come here / Approach
- Sit / Sit down
- Spin / Rotate / Turn around
- Dance / Groove
- Take photo / Picture / Smile
- What's that / Identify / Recognize
- Battery status / Battery level

Features:
- Fuzzy matching (rapidfuzz token_set_ratio) with 75% threshold
- Multiple pattern support per command for natural variations
- Three routing types: velocity (/cmd_vel), actions (/saltybot/action_command), services
- Command monitoring via /saltybot/voice_command
- Graceful handling of unrecognized speech

Architecture:
- Input: /saltybot/speech/transcribed_text (lowercase text)
- Fuzzy match against 11 command groups with 40+ patterns
- Route to: /cmd_vel (velocity), /saltybot/action_command (actions), or services

Files:
- saltybot_voice_router_node.py: Main router with fuzzy matching
- launch/voice_router.launch.py: Launch configuration
- VOICE_ROUTER_README.md: Usage documentation

Dependencies:
- rapidfuzz: Fuzzy string matching for natural speech handling
- rclpy, std_msgs, geometry_msgs: ROS2 core

Performance: <100ms per command (fuzzy matching + routing)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 17:05:02 -05:00
d79e38eb5b feat: OTA firmware update (Issue #492)
Complete over-the-air (OTA) firmware update system with:

Features:
- Downloads releases from Gitea (seb/saltylab-firmware)
- Automatic colcon build in staging directory
- Symlink-based atomic deployment
- ROS2 service restart via systemd
- Automatic rollback on build failure
- Version tracking in ~/.saltybot-data/versions.json
- Update history with timestamps

Safety:
- Blocks updates if robot velocity > 0.05 m/s
- Velocity monitoring via odometry subscription
- Backup before update for recovery

Triggers:
- MQTT /saltybot/ota_command: 'check', 'update:<version>', 'rollback'
- /saltybot/ota_status: JSON status updates
- Dashboard integration ready

Configuration:
- Gitea API base, repo info, directories
- Build timeout: 3600s (1 hour)
- Service restart automation
- Backup retention policy

ROS2 package structure complete with launch files and config.
2026-03-05 17:04:35 -05:00
7b22141142 feat: OTA firmware update (Issue #492)
Complete over-the-air (OTA) firmware update system with:

Features:
- Downloads releases from Gitea (seb/saltylab-firmware)
- Automatic colcon build in staging directory
- Symlink-based atomic deployment
- ROS2 service restart via systemd
- Automatic rollback on build failure
- Version tracking in ~/.saltybot-data/versions.json
- Update history with timestamps

Safety:
- Blocks updates if robot velocity > 0.05 m/s
- Velocity monitoring via odometry subscription
- Backup before update for recovery

Triggers:
- MQTT /saltybot/ota_command: 'check', 'update:<version>', 'rollback'
- /saltybot/ota_status: JSON status updates
- Dashboard integration ready

Configuration:
- Gitea API base, repo info, directories
- Build timeout: 3600s (1 hour)
- Service restart automation
- Backup retention policy

ROS2 package structure complete with launch files and config.
2026-03-05 17:04:27 -05:00
285178b2f9 feat: Configure Nav2 navigation stack conservative speeds (Issue #475)
- Update max_vel_x to 0.3 m/s (conservative for FC + hoverboard ESC)
- Update max_vel_theta to 0.5 rad/s (conservative for FC + hoverboard ESC)
- Set robot_radius to 0.22 m for 0.4m x 0.4m footprint
- Configure velocity smoother with conservative limits
- Both DWB local planner and velocity smoother updated for consistency
- RPLIDAR (/scan) + depth_to_laserscan (/depth_scan) costmap layers enabled
- NavFn global planner, DWB local planner configured

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:50:41 -05:00
56a48b4e25 Merge PR #486: Issue #480 - Map save/load 2026-03-05 14:48:59 -05:00
8f0215d461 Merge PR #485: Issue #483 - Monitoring dashboard 2026-03-05 14:48:46 -05:00
f5877756d5 feat: Map save/load service for SLAM Toolbox persistence (Issue #480)
Implement automatic map serialization and persistence for slam_toolbox:
- New SlamToolboxPersistenceNode with auto-save every 5 minutes
- Auto-load most recent map on startup
- Services: /saltybot/save_map, /saltybot/load_map, /saltybot/list_maps
- Export to Nav2-compatible YAML + PGM format
- Stores maps in ~/.saltybot-data/maps/ with .posegraph format
- Integrates with slam_toolbox serialize/deserialize services

Changes:
- Created saltybot_mapping/slam_toolbox_persistence.py
- Added slam_toolbox_persistence.launch.py
- Updated slam.launch.py to include persistence service
- Updated CMakeLists.txt to install new executable
- Added slam_toolbox dependency to package.xml

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:46:33 -05:00
33036e2967 feat: Add URDF robot description (Issue #477) 2026-03-05 14:43:01 -05:00
50d1ebbdcc feat: Behavior tree coordinator for autonomous mode (Issue #482)
State machine: idle → patrol → investigate → interact → return
- IDLE: Waiting for activation or battery recovery
- PATROL: Autonomous patrolling with curiosity-driven exploration (#470)
- INVESTIGATE: Approach and track detected persons
- INTERACT: Social interaction with face recognition and gestures
- RETURN: Navigate back to home/dock with recovery behaviors

Integrations:
- Patrol mode (#446): Waypoint routes with geofence (#441)
- Curiosity behavior (#470): Autonomous exploration
- Person following: Approach detected persons
- Emergency stop cascade (#459): Highest priority safety
- Geofence constraint (#441): Keep patrol within boundaries

Blackboard variables:
- battery_level: Current battery percentage
- person_detected: Person detection state
- current_mode: Current behavior tree state
- home_pose: Home/dock location

Files:
- behavior_trees/autonomous_coordinator.xml: BehaviorTree definition
- launch/autonomous_mode.launch.py: Full launch setup
- saltybot_bringup/bt_nodes.py: Custom BT node plugins
- BEHAVIOR_TREE_README.md: Documentation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:42:57 -05:00
60e16dc6ca feat: Add URDF robot description (Issue #477) 2026-03-05 14:42:49 -05:00
e66bcc2ab0 feat: Configure Nav2 costmaps (Issue #478)
- Create nav2_params.yaml in saltybot_bringup/config/
- Global costmap: static layer + obstacle layer with /scan + /depth_scan
- Local costmap: rolling window 3m×3m, voxel layer + obstacle layer
- Inflation radius: 0.3m (robot_radius 0.15m + 0.15m padding)
- Robot footprint: 0.4m × 0.4m square [-0.2, +0.2] in x, y
- RPLIDAR A1M8 at /scan (360° LaserScan)
- RealSense D435i via depth_to_laserscan at /depth_scan
- Surround vision support for dynamic obstacles

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:41:41 -05:00
7379aa459c feat: Configure Nav2 recovery behaviors (Issue #479)
Implement conservative recovery behaviors for autonomous navigation on FC + Hoverboard ESC drivetrain.

Recovery Sequence (round-robin, 6 retries):
  1. Clear costmaps (local + global)
  2. Spin 90° @ 0.5 rad/s max (conservative for self-balancer)
  3. Wait 5 seconds (allow dynamic obstacles to move)
  4. Backup 0.3m @ 0.1 m/s (deadlock escape, very conservative)

Configuration Details:
  - backup: 0.3m reverse, 0.1 m/s speed, 0.15 m/s max, 5s timeout
  - spin: 90° rotation, 0.5 rad/s max angular velocity, 1.6 rad/s² accel
  - wait: 5-second pause for obstacle clearing
  - progress_checker: 20cm minimum movement threshold in 10s window

Safety:
  - E-stop (Issue #459) takes priority over recovery behaviors
  - Emergency stop system runs independently on STM32 firmware
  - Conservative speeds for FC + Hoverboard ESC stability

Files Modified:
  - jetson/config/nav2_params.yaml: behavior_server parameters
  - jetson/ros2_ws/src/saltybot_bringup/behavior_trees/navigate_to_pose_with_recovery.xml: BT updates
  - jetson/config/RECOVERY_BEHAVIORS.md: Configuration documentation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:41:19 -05:00
dd459c0df7 feat: Configure Nav2 recovery behaviors (Issue #479)
Implement conservative recovery behaviors for autonomous navigation on FC + Hoverboard ESC drivetrain.

Recovery Sequence (round-robin, 6 retries):
  1. Clear costmaps (local + global)
  2. Spin 90° @ 0.5 rad/s max (conservative for self-balancer)
  3. Wait 5 seconds (allow dynamic obstacles to move)
  4. Backup 0.3m @ 0.1 m/s (deadlock escape, very conservative)

Configuration Details:
  - backup: 0.3m reverse, 0.1 m/s speed, 0.15 m/s max, 5s timeout
  - spin: 90° rotation, 0.5 rad/s max angular velocity, 1.6 rad/s² accel
  - wait: 5-second pause for obstacle clearing
  - progress_checker: 20cm minimum movement threshold in 10s window

Safety:
  - E-stop (Issue #459) takes priority over recovery behaviors
  - Emergency stop system runs independently on STM32 firmware
  - Conservative speeds for FC + Hoverboard ESC stability

Files Modified:
  - jetson/config/nav2_params.yaml: behavior_server parameters
  - jetson/ros2_ws/src/saltybot_bringup/behavior_trees/navigate_to_pose_with_recovery.xml: BT updates
  - jetson/config/RECOVERY_BEHAVIORS.md: Configuration documentation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:41:11 -05:00
fd742f6890 feat: Configure Nav2 recovery behaviors (Issue #479)
Implement conservative recovery behaviors for autonomous navigation on FC + Hoverboard ESC drivetrain.

Recovery Sequence (round-robin, 6 retries):
  1. Clear costmaps (local + global)
  2. Spin 90° @ 0.5 rad/s max (conservative for self-balancer)
  3. Wait 5 seconds (allow dynamic obstacles to move)
  4. Backup 0.3m @ 0.1 m/s (deadlock escape, very conservative)

Configuration:
  - backup: 0.3m reverse, 0.1 m/s speed, 5s timeout
  - spin: 90° rotation, 0.5 rad/s max angular velocity
  - wait: 5-second pause for obstacle clearing
  - progress_checker: 20cm minimum movement threshold in 10s window

Safety:
  - E-stop (Issue #459) takes priority over recovery behaviors
  - Emergency stop system runs independently on STM32 firmware
  - Conservative speeds for FC + Hoverboard ESC stability

Files Modified:
  - jetson/config/nav2_params.yaml: behavior_server parameters
  - jetson/ros2_ws/src/saltybot_bringup/behavior_trees/navigate_to_pose_with_recovery.xml: BT updates
  - jetson/config/RECOVERY_BEHAVIORS.md: Configuration documentation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:40:45 -05:00
d7051fe854 feat: Add Issue #467 - Power management supervisor with battery protection
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 11s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
- New ROS2 node: power_supervisor_node for battery state monitoring
- Battery thresholds: 30% warning, 20% dock search, 10% graceful shutdown, 5% force kill
- Charge cycle tracking and battery health estimation
- CSV logging to battery_log.csv for external analysis
- Publishes /saltybot/power_state for MQTT relay
- Graceful shutdown cascade: save state, stop motors, disarm on critical low battery
- Replaces/extends Issue #125 battery_node with supervisor-level power management

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 14:34:37 -05:00
6293d2ec60 Merge Issue #473: event logger - structured JSON activity logging 2026-03-05 14:22:45 -05:00
b178614e6e Merge Issue #468: object detection - perception system enhancements 2026-03-05 14:22:32 -05:00
e26301c7ca Merge Issue #472: dance choreography - behavioral sequences 2026-03-05 14:22:06 -05:00
c96c68a7c4 feat: YOLOv8n object detection with RealSense depth integration (Issue #468)
- saltybot_object_detection_msgs: DetectedObject, DetectedObjectArray, QueryObjects.srv
- saltybot_object_detection: YOLOv8n TensorRT FP16 node with depth projection
- Message filters for RGB-depth sync, TF2 transform to base_link
- Configurable confidence and class filtering (COCO 80 classes)
- Query service for voice integration ("whats in front of you")
- TensorRT build script with ONNX fallback
- Launch file with parameter configuration
- Full stack integration at t=6s (30 FPS target alongside person tracker)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:14:50 -05:00
sl-android
2e9fd6fa4c feat: curiosity behavior — autonomous exploration when idle (Issue #470)
- Frontier exploration toward unexplored areas
- Activates when idle >60s + no people detected
- Turns toward detected sounds via audio_direction node
- Approaches colorful/moving objects
- Self-narrates findings via TTS
- Respects geofence and obstacle boundaries
- 10-minute max duration with auto-return
- Configurable curiosity level (0-1.0)
- Publishes /saltybot/curiosity_state

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:10:33 -05:00
0c03060016 feat: centralized parameter server + dynamic reconfiguration (Issue #471)
ROS2 node and WebUI component for centralized dynamic parameter configuration.
- Parameter server node with validation, range checks, and persistence
- Organized into groups: hardware/perception/controls/social/safety/debug
- Service-based API for dynamic reconfiguration (/saltybot/set_param)
- Named presets: indoor/outdoor/demo/debug
- WebUI component with parameter browsing, editing, and preset loading
- Safety parameter confirmation dialogs
- Real-time parameter metadata display (type, range, description)

Files:
- jetson/ros2_ws/src/saltybot_param_server/ - ROS2 parameter server node
- ui/social-bot/src/components/ParameterServer.jsx - WebUI component
- App.jsx - Integrated into CONFIG tab group as 'Parameters'

Build status:  PASSING (127 modules)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:10:04 -05:00
00e632ecbf feat: centralized parameter server + dynamic reconfiguration (Issue #471)
ROS2 node and WebUI component for centralized dynamic parameter configuration.
- Parameter server node with validation, range checks, and persistence
- Organized into groups: hardware/perception/controls/social/safety/debug
- Service-based API for dynamic reconfiguration (/saltybot/set_param)
- Named presets: indoor/outdoor/demo/debug
- WebUI component with parameter browsing, editing, and preset loading
- Safety parameter confirmation dialogs
- Real-time parameter metadata display (type, range, description)

Files:
- jetson/ros2_ws/src/saltybot_param_server/ - ROS2 parameter server node
- ui/social-bot/src/components/ParameterServer.jsx - WebUI component
- App.jsx - Integrated into CONFIG tab group as 'Parameters'

Build status:  PASSING (127 modules)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:09:45 -05:00
d421d63c6f feat: Add Issue #473 - Event Logger with structured JSON logging
Comprehensive event logging system for SaltyBot with:

Features:
- MQTT subscription to /saltybot/* state topics (9 event types)
- Structured JSON event logging to ~/.saltybot-data/events/YYYY-MM-DD.jsonl
- Event types: encounter, voice_command, trick, e-stop, geofence, dock, error, boot, shutdown
- Query service (time range + type filter)
- Stats publisher (/saltybot/event_stats) with daily summaries
- Dashboard live feed (/saltybot/event_feed)
- Automatic log rotation: compress >7 days, delete >90 days
- CSV export functionality
- Thread-safe event processing

Components:
- EventLogger ROS2 node with background timers
- Event dataclass with timestamp/type/source/data
- File-based storage (JSONL + compressed archives)
- Query API for time/type filtering
- CSV export utility
- Comprehensive unit tests (15 test cases)

Configuration:
- Data directory: ~/.saltybot-data/events
- Rotation: 7 days compress, 90 days delete
- Stats interval: 60 seconds
- Rotation check: hourly

Launch:
- event_logger.launch.py - Standalone launch
- Config: event_logger.yaml with topic mappings

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 12:09:07 -05:00
02e9b15e6f Merge remote-tracking branch 'origin/sl-webui/issue-453-obstacle-map' 2026-03-05 11:09:46 -05:00
c25d63772a Merge remote-tracking branch 'origin/sl-jetson/issue-456-photo-capture'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_obstacle_memory/package.xml
#	jetson/ros2_ws/src/saltybot_photo_capture/package.xml
#	jetson/ros2_ws/src/saltybot_photo_capture/setup.py
2026-03-05 11:09:41 -05:00
b38f948844 Merge remote-tracking branch 'origin/sl-controls/issue-455-smooth-velocity'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_smooth_velocity/config/smooth_velocity_config.yaml
#	jetson/ros2_ws/src/saltybot_smooth_velocity/launch/smooth_velocity.launch.py
#	jetson/ros2_ws/src/saltybot_smooth_velocity/package.xml
#	jetson/ros2_ws/src/saltybot_smooth_velocity/saltybot_smooth_velocity/smooth_velocity_node.py
#	jetson/ros2_ws/src/saltybot_smooth_velocity/setup.cfg
#	jetson/ros2_ws/src/saltybot_smooth_velocity/setup.py
2026-03-05 11:09:32 -05:00
3ecf334642 Merge pull request 'feat: emergency stop cascade (Issue #459)' (#466) from sl-firmware/issue-459-estop into main 2026-03-05 11:08:01 -05:00
5a4150a2d0 Merge pull request 'feat: WiFi mesh handoff (Issue #458)' (#462) from sl-android/issue-458-wifi-handoff into main 2026-03-05 11:07:26 -05:00
270507ad49 Merge pull request 'feat: Add gesture recognition system (Issue #454)' (#461) from sl-webui/sl-perception/issue-454-gestures into main 2026-03-05 11:07:13 -05:00
f47b01eff6 Merge pull request 'feat: sound effects (Issue #457)' (#460) from sl-mechanical/issue-457-sound-effects into main 2026-03-05 11:07:11 -05:00
2ba8df17fd feat: Add Issue #453 - Obstacle memory map with persistent hazard zones
- 2D occupancy grid (100x100 cells @ 10cm resolution, configurable)
- LIDAR integration: subscribes to /scan and /odom for real-time obstacle detection
- Ray-casting: marks hit points as obstacles, intermediate points as free space
- Cell states: unknown/free/obstacle/hazard with confidence tracking (0.0–1.0)
- Hazard classification: 3+ detections = permanent hazard (stays in memory)
- Temporal decay: 95%/day for hazards (30-day half-life), 85%/day for obstacles (~21-day)
- Decay interval: applied hourly, cells revert to free when confidence < 20%
- Persistence: auto-saves to /home/seb/saltybot-data/obstacle_map.yaml every 5 minutes
- YAML format: grid metadata + cell array with state/confidence/detection_count/timestamp
- OccupancyGrid publisher: /saltybot/obstacle_map for Nav2 integration at 5 Hz
- Thread-safe: all grid operations protected with locks for concurrent callbacks
- Statistics: hazard/obstacle/free cell counts and coverage percentage
- Dashboard overlay ready: color-coded cells (red=hazard, orange=obstacle, gray=free)
- Configurable via obstacle_memory.yaml: grid size/resolution, range limits, decay rates

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:21:34 -05:00
fb93acee0a feat: Add Issue #453 - Obstacle memory map with persistent hazard zones
- 2D occupancy grid (100x100 cells @ 10cm resolution, configurable)
- LIDAR integration: subscribes to /scan and /odom for real-time obstacle detection
- Ray-casting: marks hit points as obstacles, intermediate points as free space
- Cell states: unknown/free/obstacle/hazard with confidence tracking (0.0–1.0)
- Hazard classification: 3+ detections = permanent hazard (stays in memory)
- Temporal decay: 95%/day for hazards (30-day half-life), 85%/day for obstacles (~21-day)
- Decay interval: applied hourly, cells revert to free when confidence < 20%
- Persistence: auto-saves to /home/seb/saltybot-data/obstacle_map.yaml every 5 minutes
- YAML format: grid metadata + cell array with state/confidence/detection_count/timestamp
- OccupancyGrid publisher: /saltybot/obstacle_map for Nav2 integration at 5 Hz
- Thread-safe: all grid operations protected with locks for concurrent callbacks
- Statistics: hazard/obstacle/free cell counts and coverage percentage
- Dashboard overlay ready: color-coded cells (red=hazard, orange=obstacle, gray=free)
- Configurable via obstacle_memory.yaml: grid size/resolution, range limits, decay rates

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:21:30 -05:00
9027fa9e12 feat: add photo capture package files (Issue #456) 2026-03-05 09:21:26 -05:00
5e62e81a97 feat: photo capture service (Issue #456) 2026-03-05 09:20:57 -05:00
sl-android
f4e4f184ef feat: WiFi mesh handoff - seamless AP roaming (Issue #458)
Add WiFi mesh handoff infrastructure:

WiFiState.msg interface:
- SSID, signal strength (dBm), connection status
- AP address, frequency, link quality
- TX/RX rates, roaming flag
- Available APs for handoff decision

WiFi Monitor Node:
- Signal monitoring via iwconfig/nmcli
- Auto-roam at -70dBm threshold
- Seamless wpa_supplicant roaming
- Gateway ping (8.8.8.8) every 5s
- USB tethering fallback if offline >30s
- TTS warnings for connectivity issues
- Coverage logging with AP transitions

Features:
- Configurable roaming threshold (-70dBm default)
- Gateway connectivity verification
- Offline detection with configurable timeout
- USB tethering auto-activation/deactivation
- Signal strength change logging (>5dBm)
- AP transition logging
- Manual rescan/tether control commands

Topics:
- /saltybot/wifi_state (String)
- /saltybot/speech_text (String warnings)
- /saltybot/wifi_cmd (String commands)

Configuration:
- interface: wlan0
- roam_threshold_dbm: -70
- offline_warning_timeout: 30.0
- target_ssid: SaltyLab
- fallback_tether: true
- coverage_log_file: /tmp/wifi_coverage.log

Roaming Behavior:
- Monitors signal continuously
- When signal < -70dBm, scans for stronger AP
- wpa_supplicant performs seamless handoff
- Logs all AP transitions to coverage file

Fallback Behavior:
- Pings gateway every 5 seconds
- If unreachable for >30s, activates USB tether
- TTS alert: 'Warning: Lost WiFi connectivity...'
- Auto-deactivates when WiFi restored

Coverage Mapping:
- Logs timestamp, SSID, signal, connected status
- Tracks roaming events
- Useful for mesh network optimization

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:20:42 -05:00
dc525d652c feat: Add gesture recognition system (Issue #454)
Implements hand and body gesture recognition via MediaPipe on Jetson Orin GPU.
- MediaPipe Hands (21-point hand landmarks) + Pose (33-point body landmarks)
- Recognizes: wave, point, stop_palm, thumbs_up, come_here, arms_up, arms_spread
- GestureArray publishing at 10–15 fps on Jetson Orin
- Confidence threshold: 0.7 (configurable)
- Range: 2–5 meters optimal
- GPU acceleration via Jetson Tensor RT
- Integrates with voice command router for multimodal interaction
- Temporal smoothing: history-based motion detection (wave, beckon)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:43 -05:00
569ac3fb35 feat: Add gesture recognition system (Issue #454)
Implements hand and body gesture recognition via MediaPipe on Jetson Orin GPU.
- MediaPipe Hands (21-point hand landmarks) + Pose (33-point body landmarks)
- Recognizes: wave, point, stop_palm, thumbs_up, come_here, arms_up, arms_spread
- GestureArray publishing at 10–15 fps on Jetson Orin
- Confidence threshold: 0.7 (configurable)
- Range: 2–5 meters optimal
- GPU acceleration via Jetson Tensor RT
- Integrates with voice command router for multimodal interaction
- Temporal smoothing: history-based motion detection (wave, beckon)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:40 -05:00
8538fa2f9d feat: Add Issue #455 - Smooth velocity controller with S-curve jerk reduction
Implement acceleration-limited velocity controller:
- Subscribe to /cmd_vel_raw, publish smoothed /cmd_vel
- Max linear acceleration: 0.5 m/s², angular: 1.0 rad/s²
- Deceleration: 0.8 m/s² (linear), 1.0 rad/s² (angular)
- S-curve jerk limiting with 0.2s ramp time
- E-stop immediate stop capability
- Command priority system (e-stop > teleop > geofence > follow-me > nav2 > patrol)
- Publish /saltybot/velocity_profile for monitoring
- 50Hz update rate (configurable)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:33 -05:00
381549b33f feat: Emergency stop cascade system (Issue #459)
Critical safety system with <50ms latency emergency response.

ROS2 action server /saltybot/estop with unified trigger sources:
- Voice commands: 'stop', 'emergency', 'e-stop'
- Gamepad emergency button
- IMU tilt detection (>45 degrees)
- LiDAR obstacle detection (<0.3m)
- Geofence boundary violation
- Watchdog timeout trigger
- MQTT remote kill signal

Cascade response (within 500ms):
1. Zero cmd_vel in <50ms (priority critical)
2. Disable autonomous mode
3. Face alert animation (emergency_stop)
4. LED red indicator
5. TTS alert announcement
6. Event logging + sensor snapshot

Safety properties:
- Cannot be overridden once triggered
- Manual resume only (gamepad Start or voice 'resume')
- Non-blockable execution (separate thread)
- Redundant trigger sources for reliability

Published topics:
- /saltybot/estop_active (Bool) - System state
- /saltybot/estop_event (String) - Event log (JSON)

Subscribed triggers:
- /camera/imu - Tilt detection
- /scan - Obstacle detection <0.3m
- /voice/command - Voice e-stop/resume
- /gamepad/emergency, /gamepad/start - Gamepad controls
- /saltybot/geofence_violation - Geofence boundary
- /saltybot/watchdog_timeout - Watchdog signal
- /saltybot/mqtt_kill - Remote kill signal

Package structure:
- estop_server: Main emergency stop node
- estop_config.yaml: Timing, triggers, cascade config
- estop.launch.py: Launch with safety parameters
- Unit tests for trigger detection and cascade timing

Logging: /home/seb/saltybot-data/estop/ (JSON format)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:31 -05:00
5c05874ed3 feat: sound effects library (Issue #457)
Event-driven audio feedback system with 9 sound events:
- boot_complete: Startup chime (ascending notes)
- person_detected: Subtle detection sound (800Hz sine)
- wake_word: Activation beep (1000Hz)
- low_battery: Warning tone (880Hz double beep)
- obstacle_close: Proximity beeps (rapid 1200Hz pulses)
- trick_complete: Success jingle (ascending arpeggio)
- error: Descending warning tone (800→400Hz)
- charging_start: Power-up tone (rising 400→1200Hz)
- geofence_warning: Boundary alert (950Hz)

Features:
- Priority queue for event handling
- Dynamic sound synthesis if WAV/OGG files missing
- Volume control with quiet/night mode support
- Fade in/out for smooth playback
- Configurable per-event duration and priority
- Caching of loaded audio files
- Background playback thread
- Real-time state publishing

Configuration:
- WAV/OGG file loading from /home/seb/saltybot-data/sounds/
- Programmatic sound generation (sine, chime, descending, power-up, beep, jingle, proximity, alert)
- Numpy/scipy-based synthesis
- YAML configuration for all events
- Volume and fade timing controls
- Sample rate: 16kHz, 16-bit audio

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:21 -05:00
35e35a2997 feat: sound effects library (Issue #457)
Event-driven audio feedback system with 9 sound events:
- boot_complete: Startup chime (ascending notes)
- person_detected: Subtle detection sound (800Hz sine)
- wake_word: Activation beep (1000Hz)
- low_battery: Warning tone (880Hz double beep)
- obstacle_close: Proximity beeps (rapid 1200Hz pulses)
- trick_complete: Success jingle (ascending arpeggio)
- error: Descending warning tone (800→400Hz)
- charging_start: Power-up tone (rising 400→1200Hz)
- geofence_warning: Boundary alert (950Hz)

Features:
- Priority queue for event handling
- Dynamic sound synthesis if WAV/OGG files missing
- Volume control with quiet/night mode support
- Fade in/out for smooth playback
- Configurable per-event duration and priority
- Caching of loaded audio files
- Background playback thread
- Real-time state publishing

Configuration:
- WAV/OGG file loading from /home/seb/saltybot-data/sounds/
- Programmatic sound generation (sine, chime, descending, power-up, beep, jingle, proximity, alert)
- Numpy/scipy-based synthesis
- YAML configuration for all events
- Volume and fade timing controls
- Sample rate: 16kHz, 16-bit audio

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:19:19 -05:00
9d36e1007d feat: Add Issue #455 - Smooth velocity controller with jerk reduction
Implement acceleration-limited velocity controller with S-curve jerk limiting:
- Subscribe to /cmd_vel_raw, publish smoothed /cmd_vel
- Max linear acceleration: 0.5 m/s²
- Max angular acceleration: 1.0 rad/s²
- Deceleration: 0.8 m/s² (linear), 1.0 rad/s² (angular)
- S-curve jerk limiting for smooth acceleration profiles (0.2s ramp)
- E-stop immediate stop capability
- Command priority system (e-stop > teleop > geofence > follow-me > nav2 > patrol)
- Publish /saltybot/velocity_profile for monitoring
- Configurable via smooth_velocity_config.yaml
- 50Hz update rate (configurable)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:18:58 -05:00
3ea19fbb99 Merge remote-tracking branch 'origin/sl-jetson/issue-447-full-launch'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_geofence/saltybot_geofence/geofence_node.py
2026-03-05 09:10:33 -05:00
b9a6eaa3fa Merge remote-tracking branch 'origin/sl-firmware/issue-445-diagnostics'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_diagnostics/README.md
#	jetson/ros2_ws/src/saltybot_diagnostics/config/diagnostic_checks.yaml
#	jetson/ros2_ws/src/saltybot_diagnostics/launch/diagnostics.launch.py
#	jetson/ros2_ws/src/saltybot_diagnostics/package.xml
#	jetson/ros2_ws/src/saltybot_diagnostics/saltybot_diagnostics/diagnostics_node.py
#	jetson/ros2_ws/src/saltybot_diagnostics/setup.py
#	jetson/ros2_ws/src/saltybot_diagnostics/test/test_diagnostics.py
2026-03-05 09:10:24 -05:00
3d008ddbb7 Merge remote-tracking branch 'origin/sl-controls/issue-441-geofence'
# Conflicts:
#	jetson/ros2_ws/src/saltybot_geofence/saltybot_geofence/geofence_node.py
2026-03-05 09:10:17 -05:00
fac873c261 Merge pull request 'feat: night mode (Issue #444)' (#451) from sl-mechanical/issue-444-night-mode into main 2026-03-05 09:09:14 -05:00
27a95dce0a Merge pull request 'feat: social memory DB (Issue #443)' (#450) from sl-webui/issue-443-social-memory into main 2026-03-05 09:09:09 -05:00
1530104e22 Merge pull request 'feat: weather awareness (Issue #442)' (#448) from sl-android/issue-446-patrol into main 2026-03-05 09:09:00 -05:00
ab5dd97fcb feat: add consolidated params and stack state monitor (Issue #447) 2026-03-05 09:07:17 -05:00
41dc80ea7d feat: Add diagnostic self-test system (Issue #445)
Comprehensive hardware diagnostics and health monitoring for SaltyBot.

Startup hardware checks:
- RPLIDAR, RealSense, VESC, Jabra microphone, STM32 bridge
- Servo controller, WiFi, GPS module
- Disk space, system RAM, CPU temperature
- Boot result via TTS + face animation

Runtime monitoring:
- Temperature thresholds (Orin GPU >80C, VESC >60C)
- Network latency to 8.8.8.8
- System resource tracking (CPU, RAM, disk)

Features:
- Publishes /saltybot/diagnostics (DiagnosticArray)
- JSON logging to /home/seb/saltybot-data/diagnostics/
- Configuration-driven via YAML
- Boot animations and TTS announcements
- Configurable startup and runtime checks

Package structure:
- saltybot_diagnostics: Main diagnostics node
- diagnostic_checks.yaml: Hardware config + thresholds
- diagnostics.launch.py: Launch file with parameters
- Unit tests for check aggregation and logging

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:06:18 -05:00
292c73fb60 feat: Add Issue #441 - Geofence safety with configurable boundary
Implement GPS-based geofence safety system:
- Subscribe to /phone/gps and /odom for position tracking
- Support circle (default 50m radius) or polygon geofence
- Three zones: SAFE, WARNING (2m buffer), VIOLATION
- WARNING zone: slow + concern emotion + TTS warning + amber LED
- VIOLATION zone: stop + auto-return + red LED + TTS alert
- Publish /saltybot/geofence_state (UInt8: 0=SAFE, 1=WARNING, 2=VIOLATION)
- LED feedback colors: off (safe), amber (warning), red (violation)
- Auto-return to safe zone when breaching boundary
- Configurable via geofence_config.yaml (type, radius, warning distance, etc)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:05:55 -05:00
c1076b8230 feat: full stack master launch with profiles (Issue #447)
Master saltybot_bringup.launch.py with:
- Profile support (full/minimal/headless) for flexible deployments
- Boot order: hardware → perception → control → social → monitoring
- Dependency-ordered launch with 30-second boot timeout
- Consolidated saltybot_params.yaml with all stack parameters
- Stack state publisher (/saltybot/stack_state)
- Post-launch diagnostics self-test
- Graceful shutdown support (motors first)

Boot sequence (Issue #447):
- t=0s:    hardware (robot description, STM32 bridge)
- t=2-6s:  perception (sensors, cameras, detection, SLAM)
- t=2-14s: control (cmd_vel bridge, follower, Nav2)
- t=17-19s: social & monitoring (rosbridge, stack state)

Features:
- full profile: complete stack (SLAM, Nav2, detection, follower)
- minimal profile: hardware + control only
- headless profile: sensors + control (no CSI cameras)
- Configurable modes: indoor (SLAM+Nav2), outdoor (GPS nav), follow

Parameters consolidated in config/saltybot_params.yaml:
- Hardware (bridge, motors)
- Perception (sensors, detection, SLAM)
- Control (follower, Nav2)
- Social (TTS, gestures, face tracking)
- Monitoring (rosbridge, health checks)

New: stack_state_monitor_node.py (publishes stack boot state)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:05:25 -05:00
2d144f5bcc feat: full stack master launch with profiles (Issue #447)
Master saltybot_bringup.launch.py with:
- Profile support (full/minimal/headless) for flexible deployments
- Boot order: hardware → perception → control → social → monitoring
- Dependency-ordered launch with 30-second boot timeout
- Consolidated saltybot_params.yaml with all stack parameters
- Stack state publisher (/saltybot/stack_state)
- Post-launch diagnostics self-test
- Graceful shutdown support (motors first)

Boot sequence (Issue #447):
- t=0s:    hardware (robot description, STM32 bridge)
- t=2-6s:  perception (sensors, cameras, detection, SLAM)
- t=2-14s: control (cmd_vel bridge, follower, Nav2)
- t=17-19s: social & monitoring (rosbridge, stack state)

Features:
- full profile: complete stack (SLAM, Nav2, detection, follower)
- minimal profile: hardware + control only
- headless profile: sensors + control (no CSI cameras)
- Configurable modes: indoor (SLAM+Nav2), outdoor (GPS nav), follow

Parameters consolidated in config/saltybot_params.yaml:
- Hardware (bridge, motors)
- Perception (sensors, detection, SLAM)
- Control (follower, Nav2)
- Social (TTS, gestures, face tracking)
- Monitoring (rosbridge, health checks)

New: stack_state_monitor_node.py (publishes stack boot state)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:05:18 -05:00
3830db8225 feat: night mode controller (Issue #444)
Automatic night mode detection and stealth operation:
- Ambient light detection: auto-switch at 50 lux threshold
- Motor speed reduction: 50% speed in stealth mode
- LED control: dim to 5% minimum brightness with slow blue fade
- Face-only mode: disable TTS speaker, show text on face
- IR-based tracking: use IR cameras only (RGB disabled)
- Face brightness: reduce to 30% for low-light visibility
- Manual override: voice commands and gamepad toggle (Y button)
- Smooth transitions: 1-second fade between modes with ramps

Features:
- Hysteresis: 5 lux band prevents mode flickering
- Light sensor smoothing: 5-sample averaging for stability
- Transition manager: smooth motor ramp (2s), LED fade (0.5s)
- Multiple sensor support: RealSense IR, phone ambient sensor
- Stealth LED pattern: slow breathing dim blue (0.3 Hz)

Configuration:
- YAML-based threshold and behavior settings
- Per-subsystem transition timing
- Tracking parameter tuning for IR mode
- Face control with contrast boost

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:05:16 -05:00
7cad97d0db feat: Add Issue #443 - Social memory database with persistent person knowledge
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 29s
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
- SQLite database at /home/seb/saltybot-data/social_memory.db
- Tables: persons (name, embeddings, relationship_tier, notes), encounters (person_id, timestamp, transcript, mood)
- ROS2 services: /saltybot/social_memory/lookup, /update, /encounter, /stats
- Automatic tier promotion by encounter count (5→regular, 20→favorite)
- Quality-based promotion: 80%+ positive interactions required
- Custom greetings per relationship tier (stranger/regular/favorite)
- Encounter tracking: transcript, mood, engagement_score, positive_interaction flag
- Face embedding storage support for face recognition integration
- Relationship score computation from interaction history
- Thread-safe concurrent service calls
- Periodic stats publishing on /saltybot/social_memory/stats_update
- Backup/restore functionality with gzip compression
- Full database statistics: person counts by tier, total encounters, database size
- Configurable via social_memory.yaml: thresholds, backup dir, publish interval

Two packages:
- saltybot_social_memory: Service definitions (CMake, ROS2 services)
- saltybot_social_memory_node: Python service server with SQLite backend

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:04:49 -05:00
sl-android
bd46aa5bc3 feat: Patrol mode - autonomous waypoint circuit (Issue #446)
Add autonomous patrol mode with idle behaviors and social integration:

Patrol Node:
- Nav2 /navigate_to_pose integration for autonomous navigation
- Idle behaviors at each waypoint (10-30s pan-tilt sweep + greetings)
- Person detection → pause patrol → social mode → resume
- Battery monitoring: automatic return to dock at <25%
- Teleop override: can be interrupted by joystick control
- Voice commands: 'patrol start/stop' via speech
- Randomized waypoint order for variety
- State publishing to /saltybot/patrol_state

Features:
- Waypoint loading from patrol_params.yaml (x, y, yaw coordinates)
- Random idle durations (configurable 10-30s)
- Pan-tilt sweep and greeting at each waypoint
- Person detection pause with timeout
- Automatic dock return when battery low
- Polling loop for state management
- Voice control integration

State Machine:
  IDLE ↔ NAVIGATE → IDLE_BEHAVIOR → (next waypoint)
           ↓
    [Person Detected] → PAUSE_PERSON
           ↓
    [Battery Low] → RETURN_TO_DOCK
           ↓
    [Teleop/Voice] → IDLE

Configuration:
- Waypoints with name, x, y, yaw
- Idle time range (min/max)
- Battery dock threshold (default 25%)
- Person detection pause timeout

Topics:
- /saltybot/patrol_state (String)
- /saltybot/speech_text (String)
- /saltybot/pan_tilt_cmd (String)
- /saltybot/person_detections (Detection2DArray)
- /saltybot/teleop_cmd (String)
- /saltybot/voice_cmd (String)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 09:04:10 -05:00
b950528079 Merge pull request 'feat: trick routines (Issue #431)' (#440) from sl-jetson/issue-431-tricks into main 2026-03-05 09:00:40 -05:00
d657696840 Merge pull request 'feat: system health monitor (Issue #408)' (#439) from sl-firmware/issue-408-health-monitor into main 2026-03-05 09:00:34 -05:00
27bf0efd94 Merge pull request 'feat: gamepad teleop (Issue #433)' (#438) from sl-controls/issue-433-gamepad-teleop into main 2026-03-05 08:59:29 -05:00
3b10d77208 Merge pull request 'feat: Android/Termux OpenClaw node (Issue #420)' (#437) from sl-android/issue-420-termux-openclaw into main 2026-03-05 08:59:24 -05:00
7c287e6f37 Merge pull request 'feat: LED strip controller (Issue #432)' (#436) from sl-mechanical/issue-432-led-controller into main 2026-03-05 08:59:19 -05:00
2203773377 Merge pull request 'feat: emotion engine (Issue #429)' (#435) from sl-webui/issue-429-emotion-engine into main 2026-03-05 08:59:14 -05:00
df0329d4f1 Merge pull request 'feat: audio direction estimator (Issue #430)' (#434) from sl-perception/issue-430-audio-direction into main 2026-03-05 08:59:09 -05:00
sl-android
eb37f160ce feat: Android/Termux OpenClaw node (Issue #420)
Add complete phone node infrastructure:

ROS2 Package (saltybot_phone):
- camera_node: /phone/camera/image_raw (320x240, 15 FPS)
- gps_node: /phone/gps (NavSatFix via termux-api)
- imu_node: /phone/imu (accelerometer data)
- openclaw_chat_node: Local LLM inference
  Subscribes /saltybot/speech_text
  Publishes /saltybot/chat_response
- ws_bridge: WebSocket bridge to Jetson Orin (:9090)

Phone Scripts:
- termux-bootstrap.sh: Complete Termux setup
- power-management.sh: Battery/thermal monitoring
- README.md: Setup documentation

Architecture:
- Termux on Android phone
- WiFi/USB tether to Jetson Orin
- ROS2 topic bridge for sensors
- Local LLM for on-device chat (Phi-2)
- Termux:Boot auto-start
- Power management for extended runtime

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:55:55 -05:00
sl-android
df49eec21f feat: Android/Termux OpenClaw node (Issue #420)
Add complete phone node infrastructure:

ROS2 Package (saltybot_phone):
- camera_node: /phone/camera/image_raw (320x240, 15 FPS)
- gps_node: /phone/gps (NavSatFix via termux-api)
- imu_node: /phone/imu (accelerometer data)
- openclaw_chat_node: Local LLM inference
  Subscribes /saltybot/speech_text
  Publishes /saltybot/chat_response
- ws_bridge: WebSocket bridge to Jetson Orin (:9090)

Phone Scripts:
- termux-bootstrap.sh: Complete Termux setup
- power-management.sh: Battery/thermal monitoring
- README.md: Setup documentation

Architecture:
- Termux on Android phone
- WiFi/USB tether to Jetson Orin
- ROS2 topic bridge for sensors
- Local LLM for on-device chat (Phi-2)
- Termux:Boot auto-start
- Power management for extended runtime

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:55:47 -05:00
54d4bd97f5 feat: LED strip controller (Issue #432)
NeoPixel status indicators with animated patterns:
- Breathing blue (idle state)
- Green pulse (follow mode)
- Red flash (error state)
- Rainbow animation (celebrate)
- Amber pulse (low battery <20%)
- White chase (search mode)

Features:
- Smooth transitions between states
- Configurable LED count (default 30) and GPIO pin (default GPIO18)
- Auto-dim brightness control
- Subscribes to battery, balance, social, emotion, health topics
- Publishes LED state JSON to /saltybot/led_state
- 30Hz update frequency with multiple animation patterns

Configuration:
- YAML-based hardware and pattern settings
- Per-pattern speed and color customization
- State priority system for concurrent status indicators

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:55:41 -05:00
c00967f33d feat: trick routines action server (Issue #431)
Implement ROS2 node for fun behavior routines:
- spin: 360° rotation with audio
- dance: sway side-to-side + bob
- nod: head yes/no patterns
- celebrate: spin + look up + smile
- shy: back away + head down + bashful

Features:
- Voice command integration (/saltybot/voice_command)
- Timed sequences: /cmd_vel + pan_tilt controls
- Obstacle safety abort on /scan near-field detection
- 10s cooldown between tricks to prevent repetition
- Trick state publishing (/saltybot/trick_state)
- Background execution thread for non-blocking operation

Package structure:
- saltybot_tricks/trick_routines_node.py (main node)
- launch/tricks.launch.py (configurable launch)
- config/tricks_params.yaml (tuning parameters)
- test/test_tricks.py (module structure tests)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:54:06 -05:00
7000cbfbd3 feat: Add Issue #429 - Emotion engine with context-aware face expression selection
- ROS2 node subscribing to orchestrator state, battery, balance, person tracker, voice commands, health
- State-to-emotion mapping: navigation → excited, social → happy/curious, low battery → concerned, etc.
- Smooth emotion transitions (0.3–1.2s) with confidence tracking
- Idle behaviors: blink (~3s), look-around (~8s), breathing (sine wave)
- Social memory: familiarity-based warmth modifier (0.3–1.0) for known people
- Personality-aware responses: extroversion, playfulness, responsiveness, anxiety (0.0–1.0 configurable)
- Publishes /saltybot/emotion_state (JSON): emotion, intensity, confidence, expression name, context, idle_flags
- Configurable via emotion_engine.yaml: personality traits, battery thresholds, update rate
- Launch file: emotion_engine.launch.py

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:56 -05:00
b8b29125d6 feat: trick routines action server (Issue #431)
Implement ROS2 node for fun behavior routines:
- spin: 360° rotation with audio
- dance: sway side-to-side + bob
- nod: head yes/no patterns
- celebrate: spin + look up + smile
- shy: back away + head down + bashful

Features:
- Voice command integration (/saltybot/voice_command)
- Timed sequences: /cmd_vel + pan_tilt controls
- Obstacle safety abort on /scan near-field detection
- 10s cooldown between tricks to prevent repetition
- Trick state publishing (/saltybot/trick_state)
- Background execution thread for non-blocking operation

Package structure:
- saltybot_tricks/trick_routines_node.py (main node)
- launch/tricks.launch.py (configurable launch)
- config/tricks_params.yaml (tuning parameters)
- test/test_tricks.py (module structure tests)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:55 -05:00
d7b1366d6c feat: Add audio direction estimator (Issue #430)
Implements GCC-PHAT beamforming for sound source localization via Jabra mic.
- GCC-PHAT cross-correlation for direction of arrival (DoA) estimation
- Voice activity detection (VAD) using RMS energy + smoothing
- Stereo/quadrophonic channel support (left/right/front/rear estimation)
- ROS2 publishers: /saltybot/audio_direction (Float32 bearing), /saltybot/audio_activity (Bool VAD)
- Configurable parameters: sample_rate, chunk_size, publish_hz, vad_threshold, gcc_phat_max_lag
- Integration-ready for multi-person tracker speaker tracking

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:43 -05:00
d10c385bdc feat: Add Issue #429 - Emotion engine with context-aware face expression selection
- ROS2 node subscribing to orchestrator state, battery, balance, person tracker, voice commands, health
- State-to-emotion mapping: navigation → excited, social → happy/curious, low battery → concerned, etc.
- Smooth emotion transitions (0.3–1.2s) with confidence tracking
- Idle behaviors: blink (~3s), look-around (~8s), breathing (sine wave)
- Social memory: familiarity-based warmth modifier (0.3–1.0) for known people
- Personality-aware responses: extroversion, playfulness, responsiveness, anxiety (0.0–1.0 configurable)
- Publishes /saltybot/emotion_state (JSON): emotion, intensity, confidence, expression name, context, idle_flags
- Configurable via emotion_engine.yaml: personality traits, battery thresholds, update rate
- Launch file: emotion_engine.launch.py

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:42 -05:00
1878b09eee feat: Add audio direction estimator (Issue #430)
Implements GCC-PHAT beamforming for sound source localization via Jabra mic.
- GCC-PHAT cross-correlation for direction of arrival (DoA) estimation
- Voice activity detection (VAD) using RMS energy + smoothing
- Stereo/quadrophonic channel support (left/right/front/rear estimation)
- ROS2 publishers: /saltybot/audio_direction (Float32 bearing), /saltybot/audio_activity (Bool VAD)
- Configurable parameters: sample_rate, chunk_size, publish_hz, vad_threshold, gcc_phat_max_lag
- Integration-ready for multi-person tracker speaker tracking

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:25 -05:00
a9afcffc1d feat: LED strip controller (Issue #432)
NeoPixel status indicators with animated patterns:
- Breathing blue (idle state)
- Green pulse (follow mode)
- Red flash (error state)
- Rainbow animation (celebrate)
- Amber pulse (low battery <20%)
- White chase (search mode)

Features:
- Smooth transitions between states
- Configurable LED count (default 30) and GPIO pin (default GPIO18)
- Auto-dim brightness control
- Subscribes to battery, balance, social, emotion, health topics
- Publishes LED state JSON to /saltybot/led_state
- 30Hz update frequency with multiple animation patterns

Configuration:
- YAML-based hardware and pattern settings
- Per-pattern speed and color customization
- State priority system for concurrent status indicators

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:53:12 -05:00
9683fd3685 feat: Add ROS2 system health monitor (Issue #408)
Implement centralized health monitoring node that:
- Subscribes to /saltybot/<node>/heartbeat from all tracked nodes
- Tracks expected nodes from YAML configuration
- Marks nodes DEAD if silent >5 seconds
- Triggers auto-restart via ros2 launch when nodes fail
- Publishes /saltybot/system_health JSON with full status
- Alerts face display on critical node failures

Features:
- Configurable heartbeat timeout (default 5s)
- Automatic dead node detection and restart
- System health JSON publishing (timestamp, uptime, node status, critical alerts)
- Face alert system for critical failures
- Rate-limited alerting to avoid spam
- Comprehensive monitoring config with critical/important node tiers

Package structure:
- saltybot_health_monitor: Main health monitoring node
- health_config.yaml: Configurable list of monitored nodes
- health_monitor.launch.py: Launch file with parameters
- Unit tests for heartbeat parsing and health status generation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-05 08:52:52 -05:00
c1b3a4368d Merge pull request 'feat: Nav2 SLAM integration (Issue #422)' (#428) from sl-controls/issue-422-nav2-slam into main 2026-03-04 23:59:32 -05:00
73263c1faa Merge pull request 'feat: remote monitoring relay (Issue #424)' (#427) from sl-jetson/issue-424-remote-monitor into main 2026-03-04 23:59:25 -05:00
524d2545ed Merge pull request 'feat: multi-person tracker (Issue #423)' (#426) from sl-perception/issue-423-multi-person into main 2026-03-04 23:59:17 -05:00
eda5154650 Merge pull request 'feat: Piper TTS service (Issue #421)' (#425) from sl-mechanical/issue-421-tts-service into main 2026-03-04 23:59:11 -05:00
ca95489b1d feat: Nav2 SLAM integration with RPLIDAR + RealSense (Issue #422)
Complete autonomous navigation stack for SaltyBot:
- SLAM Toolbox: online_async 2D LIDAR SLAM from RPLIDAR A1M8
- RealSense D435i: depth → pointcloud → costmap obstacle layer
- Nav2 stack: controllers, planners, behavior server, lifecycle management
- DWB planner: tuned for 20km/h (5.5 m/s) max velocity operation
- VESC odometry bridge: motor telemetry → nav_msgs/Odometry
- Costmap integration: LIDAR + depth for global + local costmaps
- TF tree: complete setup with base_link→laser, camera_link, odom
- Goal interface: /navigate_to_pose action for autonomous goals

Configuration:
- slam_toolbox_params: loop closure, scan matching, fine/coarse search
- nav2_params: AMCL, controllers, planners, behavior trees, lifecycle
- Global costmap: static layer + LIDAR obstacle layer + inflation
- Local costmap: rolling window + LIDAR + RealSense depth + inflation
- DWB planner: 20 vx samples, 40 theta samples, 1.7s horizon

Nodes and launch files:
- vesc_odometry_bridge: integrates motor RPM to wheel odometry
- nav2_slam_bringup: main integrated launch entry point
- depth_to_costmap: RealSense depth processing pipeline
- odometry_bridge: VESC telemetry bridge

Hardware support:
- RPLIDAR A1M8: 5.5 Hz, 12m range, 360° omnidirectional
- RealSense D435i: 15 Hz RGB-D, 200 Hz IMU, depth range 5m
- VESC Flipsky FSESC 4.20: dual motor control via UART
- SaltyBot 2-wheel balancer: 0.35m radius, hoverboard motors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 23:35:15 -05:00
16068aa3e4 feat: remote monitoring WebSocket relay (Issue #424)
Implement WebSocket telemetry relay with:
- 2Hz JSON aggregation (battery, motors, IMU, GPS, health, social)
- Port 9091 with token authentication
- msgpack compression option
- 5-minute circular history buffer
- Mobile-friendly responsive HTML UI
- Auto-reconnect WebSocket with fallback
- Critical alerts: low battery (< 15%), high temps, node crash
- Real-time dashboard with telemetry gauges

Features:
- Battery monitoring with SOC/voltage/current
- Motor command visualization (L/R duty)
- IMU attitude display (roll/pitch/yaw)
- CPU/GPU temperature with thresholds
- RAM/Disk usage progress bars
- GPS coordinates (lat/lon/alt)
- Social state (speaking, face tracking)
- Alert history with severity levels

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 23:33:55 -05:00
33fcc9d935 feat: remote monitoring WebSocket relay (Issue #424)
Implement WebSocket telemetry relay with:
- 2Hz JSON aggregation (battery, motors, IMU, GPS, health, social)
- Port 9091 with token authentication
- msgpack compression option
- 5-minute circular history buffer
- Mobile-friendly responsive HTML UI
- Auto-reconnect WebSocket with fallback
- Critical alerts: low battery (< 15%), high temps, node crash
- Real-time dashboard with telemetry gauges

Features:
- Battery monitoring with SOC/voltage/current
- Motor command visualization (L/R duty)
- IMU attitude display (roll/pitch/yaw)
- CPU/GPU temperature with thresholds
- RAM/Disk usage progress bars
- GPS coordinates (lat/lon/alt)
- Social state (speaking, face tracking)
- Alert history with severity levels

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 23:33:31 -05:00
31cfb9dcb9 feat: Add Issue #423 - Multi-person tracker with group handling + target priority
Implement multi-person tracking with:
- Track up to 10 people with persistent unique IDs
- Target priority: wake-word speaker > closest known > largest bbox
- Occlusion handoff with 3-second grace period
- Re-ID via face embedding (cosine similarity) + HSV color histogram
- Group detection and centroid calculation
- Lost target behavior: stop + rotate + SEARCHING state
- 15+ fps on Jetson Orin Nano Super
- PersonArray message publishing with active target tracking
- Configurable similarity thresholds and grace periods
- Unit tests for tracking, matching, priority, and re-ID

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 23:33:18 -05:00
9e3e586fca feat: Issue #421 - Piper TTS service with queue + priority + Jabra output
- Central ROS2 TTS node using Piper (offline ONNX speech synthesis)
- Subscribe to /saltybot/tts_request (String messages) for TTS requests
- Priority queue management with interrupt capability
- Audio output to Jabra device via ALSA/PulseAudio
- Configurable voice, speed, pitch, and volume parameters
- Publish /saltybot/tts_state (idle/synthesizing/playing) for status tracking
- Preload Piper model on startup for faster synthesis
- Queue management with configurable max size (default 16)
- Non-blocking async playback via worker thread
- Complete ROS2 package with launch file and tests
2026-03-04 23:32:21 -05:00
a06821a8c8 Merge pull request 'feat: sensor feeds in HUD (Issue #413)' (#419) from sl-webui/issue-413-sensor-hud into main 2026-03-04 23:21:34 -05:00
eff69b2037 feat(hud): add sensor feeds (GPS, LIDAR, RealSense) (Issue #413)
Add tabbed sensor feed interface to the HUD center viewport:

GPS Map Panel:
- Fetches location data from /gps HTTP endpoint on port 8888
- Renders OpenStreetMap with real-time location marker
- Displays: coordinates, altitude, accuracy

LIDAR Point Cloud Visualization:
- Subscribes to /scan topic via rosbridge WebSocket
- 2D polar plot with grid, cardinal directions, forward indicator
- Real-time point cloud rendering with range statistics
- Displays: point count, max range (0-30m)

RealSense Dual Stream:
- Subscribes to /camera/color/image_raw/compressed (RGB)
- Subscribes to /camera/depth/image_rect_raw/compressed (Depth)
- Side-by-side canvas rendering with independent scaling
- FPS counter and resolution display

Tab System:
- 4-way view switching: 3D Model ↔ GPS ↔ LIDAR ↔ RealSense
- Persistent tab state, lazy initialization on demand
- Dark theme with cyan/orange accent colors
- Status indicators for each sensor (loading/error/ready)

Architecture:
- Browser native canvas for LIDAR visualization
- WebSocket rosbridge integration for sensor subscriptions
- Fetch API for HTTP GPS data (localhost:8888)
- Leaflet.js for OSM map rendering (CDN)
- 2s polling interval for GPS updates

Rosbridge Endpoints (assumes localhost:9090):
- /scan (sensor_msgs/LaserScan) — 1Hz LIDAR
- /camera/color/image_raw/compressed — RGB stream
- /camera/depth/image_rect_raw/compressed — Depth stream

HTTP Endpoints (assumes localhost:8888):
- GET /gps → { lat, lon, alt, accuracy, timestamp }

Integration:
- Preserves existing 3D HUD viewport and controls
- Left/right sidebars remain unchanged
- Bottom PID control bar operational
- Tab switching preserves center panel size/aspect ratio

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:57:10 -05:00
4cb3ac7fe7 Merge remote-tracking branch 'origin/sl-webui/issue-412-ops-dashboard'
# Conflicts:
#	ui/social-bot/src/components/OpsDashboard.jsx
2026-03-04 22:47:57 -05:00
6aca138af1 Merge pull request 'feat: auto-dock (Issue #410)' (#417) from sl-mechanical/issue-410-auto-dock into main 2026-03-04 22:46:50 -05:00
8981b0b87a Merge pull request 'feat: voice commands (Issue #409)' (#416) from sl-perception/issue-409-voice-commands into main 2026-03-04 22:46:45 -05:00
8070dcd4b1 Merge pull request 'feat: VESC balance PID controller with tilt safety (Issue #407)' (#415) from sl-controls/issue-407-vesc-balance into main 2026-03-04 22:46:40 -05:00
2fad597976 Merge pull request 'feat: ROS2 bag recording service (Issue #411)' (#414) from sl-jetson/issue-411-bag-recording into main 2026-03-04 22:46:36 -05:00
3746a5b92d feat(webui): live operations dashboard (Issue #412)
Comprehensive real-time telemetry dashboard consolidating:
- Battery state & power (10Hz): voltage, current, SOC
- Motor PWM control (10Hz): left/right duty cycle display
- IMU attitude gauges (10Hz): roll, pitch, yaw visualization
- LIDAR polar map (1Hz): 360° obstacle visualization
- Social state (1Hz): speech detection, face ID
- System health (1Hz): CPU/GPU temps, RAM/disk usage
- 2D odometry trail (10Hz): position history map

Responsive 3-column grid layout (1-3 cols based on viewport).
Dark theme with mobile optimization. Canvas-based visualizations.
Real-time ROS subscriptions via rosbridge. Auto-scaling meters.
Quaternion to Euler angle conversion. 500-point trail history.

ROS Topics (10Hz critical / 1Hz system):
- /saltybot/imu → sensor_msgs/Imu (attitude)
- /diagnostics → diagnostic_msgs/DiagnosticArray (power, temps)
- /saltybot/balance_state → std_msgs/String (motor commands)
- /scan → sensor_msgs/LaserScan (LIDAR)
- /odom → nav_msgs/Odometry (position trail)
- /social/speech/is_speaking → std_msgs/Bool (voice activity)
- /social/face/active → std_msgs/String (face ID)

Build: ✓ 126 modules, 281.64 KB main bundle

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:45:24 -05:00
fdc44f7025 feat: Issue #410 - Auto-dock charging with 20% SoC trigger and charging animation
- Update battery_low_pct from 15% to 20% for auto-dock trigger (Issue #410 requirement)
- Add social mood publisher for charging animation display
- Trigger face charging animation (happy mood) when entering CHARGING state
- ArUco marker ID 42 detection via RealSense D435i (already configured)
- Approach sequence: detect → align → slow → contact → verify charging
- 360° search then expand capability (DETECTING state handles search)
- Safety abort timeouts already implemented in FSM state machine
- IR beacon fallback detection also available
2026-03-04 22:44:15 -05:00
4e6ecacd37 feat: Issue #410 - Auto-dock charging with 20% SoC trigger and charging animation
- Update battery_low_pct from 15% to 20% for auto-dock trigger (Issue #410)
- Add social mood publisher for charging animation display
- Trigger face charging animation (happy mood) when entering CHARGING state
- ArUco marker ID 42 detection via RealSense D435i (already configured)
- Approach sequence: detect → align → slow → contact → verify charging
- 360° search then expand capability (DETECTING state handles search)
- Safety abort timeouts already implemented in FSM state machine
2026-03-04 22:44:04 -05:00
250c71d930 feat: Add Issue #409 - Voice command interpreter
Implement lightweight voice command system with:
- Simple keyword + fuzzy matching parser for 7 basic commands
- Voice command node subscribing to /saltybot/speech_text
- VoiceCommand message publishing to /saltybot/voice_command
- Command router executing intents with TTS confirmation via Piper
- Support for: follow me, stop, come here, go home, battery, spin, quiet mode
- Configuration parameters for confidence thresholding
- Launch file for integrated voice command pipeline
- Unit tests for parser (keyword + fuzzy matching + normalization)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:43:04 -05:00
c86abdd1b8 feat: Add VESC balance PID controller with tilt safety (Issue #407)
- ROS2 node for balance mode PID parameter management via pyvesc UART
- Tilt safety kill switch: ±45° pitch > 500ms triggers motor cutoff
- Startup ramp: gradual acceleration from 0 to full output over configurable duration
- IMU integration: subscribe to /imu/data for pitch/roll angle computation
- State publishing: /saltybot/balance_state with tilt angles, PID values, motor telemetry
- Data logging: /saltybot/balance_log publishes CSV-formatted IMU + motor data
- Configurable parameters: PID gains, tilt thresholds, ramp duration, control frequency
- Test suite: quaternion to Euler conversion, tilt safety checks, startup ramp

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:40:40 -05:00
f69c02880e feat: Add VESC balance PID controller with tilt safety (Issue #407)
- ROS2 node for balance mode PID parameter management via pyvesc UART
- Tilt safety kill switch: ±45° pitch > 500ms triggers motor cutoff
- Startup ramp: gradual acceleration from 0 to full output over configurable duration
- IMU integration: subscribe to /imu/data for pitch/roll angle computation
- State publishing: /saltybot/balance_state with tilt angles, PID values, motor telemetry
- Data logging: /saltybot/balance_log publishes CSV-formatted IMU + motor data
- Configurable parameters: PID gains, tilt thresholds, ramp duration, control frequency
- Test suite: quaternion to Euler conversion, tilt safety checks, startup ramp

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:40:26 -05:00
88deacb337 feat: ROS2 bag recording service (Issue #411)
Implement circular buffer bag recorder with:
- Configurable topics recording
- Last N minutes circular buffer (default 30min)
- Manual save trigger via /saltybot/save_bag service
- Auto-save on crash with signal handlers
- Storage management (7-day TTL, 50GB quota)
- Compression via zstd
- Optional rsync to NAS for backup
- Periodic maintenance (cleanup expired, enforce quota)

Saves to /home/seb/rosbags/ by default.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 22:40:19 -05:00
9257f4c7de Merge branch 'origin/sl-firmware/issue-400-encounter-enrollment' into main
Resolved conflicts in social_enrollment_node.py by keeping comprehensive
docstring and imports from both versions.
2026-03-04 13:32:20 -05:00
1222532971 Merge pull request 'feat: encounter offline queue sync service (Issue #400)' (#406) from sl-mechanical/issue-400-encounter-queue into main 2026-03-04 13:31:03 -05:00
a9d66ddcc1 Merge pull request 'feat: first encounter orchestrator (Issue #400)' (#402) from sl-jetson/issue-400-encounter-launch into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 9s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-04 13:30:15 -05:00
136ca1fb9c feat: Add Issue #400 - Encounter offline queue sync service
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 4s
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
Implement EncounterSyncService ROS2 node for managing offline-first
encounter data syncing. Features:
- Monitors /home/seb/encounter-queue/ for JSON files
- Uploads to configurable cloud API when connectivity detected
- Exponential backoff retry with max 5 attempts
- Moves synced files to /home/seb/encounter-queue/synced/
- Publishes status on /social/encounter_sync_status topic
- Connectivity check via HTTP ping (configurable URL)
- Handles offline operation gracefully

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 13:16:00 -05:00
020adf1b94 feat: Add Issue #400 - Encounter offline queue sync service
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 4s
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
Implement EncounterSyncService ROS2 node for managing offline-first
encounter data syncing. Features:
- Monitors /home/seb/encounter-queue/ for JSON files
- Uploads to configurable cloud API when connectivity detected
- Exponential backoff retry with max 5 attempts
- Moves synced files to /home/seb/encounter-queue/synced/
- Publishes status on /social/encounter_sync_status topic
- Connectivity check via HTTP ping (configurable URL)
- Handles offline operation gracefully

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 13:14:44 -05:00
86c60f48e6 feat: First Encounter social interaction launch (Issue #400)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 19s
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 17s
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 encounter.launch.py orchestrating all First Encounter nodes:
- encounter_sync_service (offline queue backend)
- social_enrollment_node (face/voice enrollment)
- first_encounter_node (interaction orchestrator)
- wake_word_node (speech detection)
- face_display_bridge_node (UI frontend)

Include in full_stack.launch.py at t=9s with enable_encounter flag.
Add encounter_params.yaml with configurable greeting, TTS voice,
enrollment thresholds, database paths, and cloud sync settings.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 13:13:22 -05:00
c85619b8da feat: first encounter orchestrator state machine (Issue #400)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 13s
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
Implement state machine for detecting and enrolling unknown persons.
Manages workflow: DETECT → GREET → ASK_NAME → SMALL_TALK → ENROLL → FAREWELL

Features:
- Subscribes to /saltybot/person_tracker for unknown face detection
- Unknown person threshold configurable (default: 30% confidence)
- State machine with Piper TTS triggers for each state
- Captures STT responses for name and conversation context
- Publishes /social/orchestrator/state for coordination with other nodes
- Handles person interruptions gracefully (walks away)
- Auto-enrolls person to face gallery (configurable)
- Stores encounter data as JSON in /home/seb/encounter-queue/
- Tracks duration, responses, interests, and enrollment success

Encounter data structure:
{
  person_id, timestamp, state, name, context, greeting_response,
  interests[], enrollment_success, duration_sec, notes
}

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 13:12:47 -05:00
b2c8f76114 Merge pull request 'feat: hey salty wake word model (Issue #393)' (#401) from sl-perception/issue-393-wake-word into main 2026-03-04 13:11:30 -05:00
65ec1151f8 Merge pull request 'feat: face display bridge (Issue #394)' (#399) from sl-controls/issue-394-face-bridge into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 4s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-04 13:11:01 -05:00
7bc2b64c1d Merge pull request 'fix: MeshPeer namespace reserved keyword (Issue #392)' (#398) from sl-webui/issue-392-meshpeer-fix into main
Some checks failed
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been cancelled
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
social-bot integration tests / Lint (flake8 + pep257) (push) Has been cancelled
2026-03-04 13:10:50 -05:00
39258f465b feat: hey salty wake word template (Issue #393)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 24s
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
Creates log-mel spectrogram template for 'hey salty' wake word detection
using synthetic speech generation. Template generated from 5 synthetic
audio samples with varying pitch to improve robustness.

- generate_wake_word_template.py: Script to synthesize and generate template
- hey_salty.npy: 40-band log-mel template (40, 61) shape
- wake_word_params.yaml: Updated template_path
- README.md: Documentation for template usage and retraining procedures

The template is used by wake_word_node.py via cosine similarity matching
against incoming audio. Configurable sensitivity via match_threshold.

Future work: Collect real training recordings to improve accuracy.
2026-03-04 12:44:36 -05:00
82cb2bde79 feat: face display bridge node (Issue #394)
Implement ROS2 node bridging orchestrator state to face display expressions.
Maps /social/orchestrator/state and /saltybot/wake_word_detected to HTTP API.

Features:
- Subscribes to /social/orchestrator/state (JSON: IDLE, LISTENING, THINKING, SPEAKING, THROTTLED)
- Subscribes to /saltybot/wake_word_detected for immediate Alert response
- HTTP GET requests to face display server (configurable localhost:3000/face/{id})
- State to expression mapping:
  * IDLE → 0 (Tracking)
  * LISTENING → 1 (Alert)
  * THINKING → 3 (Searching)
  * SPEAKING → 4 (Social)
  * Wake word → 1 (Alert, immediate override)
- Publishes /face/state with JSON: {face_id, orchestrator_state, timestamp}
- Configurable face_server_url parameter
- Fallback to urllib if requests library unavailable

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:44:33 -05:00
fc6151ae16 fix: rename MeshPeer namespace field to ros_namespace (Issue #392)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 3s
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
2026-03-04 12:43:00 -05:00
7a17b1bf16 fix: rename MeshPeer namespace field to ros_namespace (Issue #392)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 10s
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
2026-03-04 12:42:35 -05:00
858ae1e7b9 Merge pull request 'feat: 360 obstacle avoidance with RPLIDAR (Issue #364)' (#396) from sl-firmware/issue-364-obstacle-avoidance into main 2026-03-04 12:39:46 -05:00
a4351d0da3 Merge pull request 'feat: accessibility communication UI (Issue #371)' (#391) from sl-webui/issue-371-accessibility into main 2026-03-04 12:39:20 -05:00
19017eca37 Merge pull request 'feat: pan/tilt camera head (Issue #384)' (#390) from sl-controls/issue-384-pan-tilt into main 2026-03-04 12:39:07 -05:00
55c8f387ab Merge pull request 'feat: STM32 watchdog timer driver (Issue #300)' (#386) from sl-mechanical/issue-300-watchdog into main 2026-03-04 12:39:05 -05:00
e35bd949c0 feat: Integrate 360° LIDAR obstacle avoidance into full_stack (Issue #364)
Adds saltybot_lidar_avoidance node to the launch sequence at t=3s.
The RPLIDAR A1M8 node was already fully implemented with:
- Emergency stop if obstacle detected within 0.5m
- Speed-dependent safety zones (0.6m @ 0 m/s → 3.0m @ 5.56 m/s)
- Forward scanning window (±30° cone)
- Debounced obstacle detection (2-frame debounce)
- Publishes /cmd_vel_safe with filtered velocity commands

Integrates seamlessly with Nav2 stack and cmd_vel_mux multiplexer.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:23:43 -05:00
9fc3e9894e feat: deaf/accessibility communication UI (Issue #371) 2026-03-04 12:19:28 -05:00
170c64eec1 feat: Add watchdog reset detection and status reporting (Issue #300)
- Detect if MCU was reset by IWDG watchdog timeout at startup
- Log watchdog reset events to debug terminal (USB CDC)
- Store watchdog reset flag for status reporting to Jetson
- Watchdog timer configured with 2-second timeout in safety_init()
- Main loop calls safety_refresh() to kick the watchdog every iteration

The IWDG (Independent Watchdog) resets the MCU if the main loop
hangs and fails to call safety_refresh() within the timeout window.
This provides hardware-enforced detection of software failures.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:17:56 -05:00
93756f5248 feat: Add FC↔Orin UART verification (Issue #362)
Implements UART bridge verification between Flight Controller (STM32F722)
and Jetson Orin.

Changes:
1. jetson/scripts/uart_test.py (12.7 KB)
   - Opens /dev/ttyTHS1 at 921600 baud
   - Sends jlink binary test frames (PING, VERSION, ECHO)
   - Verifies CRC16-CCITT frame integrity
   - Logs transactions with timestamps
   - JSON result export and optional MQTT publishing

2. jetson/ros2_ws/src/saltybot_bridge/launch/uart_bridge.launch.py
   - ROS2 launch file for serial_bridge_node on UART port
   - Configurable port (default /dev/ttyTHS1), baud rate (921600)
   - Bridges FC telemetry to /saltybot/imu, /saltybot/balance_state
   - Publishes diagnostics to /diagnostics

Usage:
  Test: sudo python3 jetson/scripts/uart_test.py
  Launch: ros2 launch saltybot_bridge uart_bridge.launch.py

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 12:17:56 -05:00
3c93a72d01 Merge pull request 'refactor: ESC abstraction layer with pluggable backends (Issue #388)' (#389) from sl-firmware/issue-388-esc-abstraction into main 2026-03-04 11:36:24 -05:00
844504e92e refactor: ESC abstraction layer with pluggable backends (Issue #388)
BREAKING CHANGE: Hoverboard implementation moved to pluggable vtable architecture.

## Implementation

### New Files
- include/esc_backend.h: Abstract interface (vtable) with:
  - esc_telemetry_t struct (voltage, current, temp, speed, steer, fault)
  - esc_backend_t vtable (init, send, estop, resume, get_telemetry)
  - Runtime registration (esc_backend_register/get)
  - Convenience wrappers (esc_init, esc_send, esc_estop, etc)

- src/esc_backend.c: Backend registry and wrapper implementations

- src/esc_hoverboard.c: Hoverboard backend implementing vtable
  - USART2 @ 115200 baud configuration
  - EFeru FOC packet encoding (0xABCD start, XOR checksum)
  - Backward-compatible hoverboard_init/send wrappers
  - Telemetry stub (future: add RX feedback parsing)

- src/esc_vesc.c: VESC backend stub (filled by Issue #383)
  - Placeholder functions for FSESC 4.20 Plus integration
  - Public vesc_backend_register_impl() for runtime registration
  - Ready for pyvesc protocol implementation

### Modified Files
- src/motor_driver.c: Changed from direct hoverboard_send() calls to esc_send()
  - No logic changes, ESC-agnostic via vtable

- include/config.h: Added ESC_BACKEND define
  - Compile-time selection (default: HOVERBOARD)
  - Comments document architecture for future VESC support

### Removed Files
- src/hoverboard.c: Original implementation merged into esc_hoverboard.c

## Architecture Benefits
1. **Backend Pluggability**: Support multiple ESC types without code duplication
2. **Zero Direct Dependencies**: motor_driver.c never calls hoverboard functions directly
3. **Clean Testing**: Each backend can be tested/stubbed independently
4. **Future-Ready**: VESC integration (Issue #383) just implements the vtable
5. **Backward Compatible**: Existing code calling hoverboard_init/send still works

## Testing
- pio run:  PASS (55.4KB Flash, 16.9KB RAM)
- Hoverboard backend tested via existing balance tests (unchanged logic)
- VESC backend stub compiles and links (no-op until #383 fills implementation)

## Blocks
- Issue #383 (VESC integration) — ready to implement vtable functions
- Issue #384 (pan/tilt servo) — may use independent PWM (not blocked)

## Dependencies
- None — this is pure refactoring, no API changes for callers

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 10:36:35 -05:00
985e03a26d Merge pull request 'fix: add missing bno055.h include in main.c' (#387) from sl-firmware/fix-bno055-include into main 2026-03-04 09:54:56 -05:00
d52e7af554 fix: Add missing bno055.h include to resolve implicit declaration warnings
Adds #include "bno055.h" to src/main.c to resolve implicit declaration
warnings for bno055_read(), bno055_calib_status(), and bno055_temperature().
Functions were properly implemented but header was missing from includes.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 08:45:51 -05:00
7cfb98aec1 feat: Add pan/tilt camera head servo control (Issue #384)
Implement SCS/STS serial protocol driver for Waveshare ST3215 servos
at 1Mbps daisy-chain configuration. Pan and tilt servos on single UART.

Features:
- SCSServoBus class: Low-level protocol handler with packet construction
- Position write commands with configurable speed (0-1000)
- Position and temperature readback from servos
- PanTiltNode: ROS2 node with target tracking control loop
- Subscribes to /saltybot/target_track for centroid position
- Proportional control to keep target centered in D435i FOV
- Publishes /pan_tilt/state with angles and temperatures
- Publishes /pan_tilt/command for servo position monitoring
- 30 Hz control loop, 1 Hz telemetry loop
- Configurable servo limits and speeds

Servos: 0.24° resolution, 0-4095 position range
Camera: 87° × 58° field of view

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 08:44:51 -05:00
ce1a5e5fee Merge pull request 'feat: VESC UART driver node with pyvesc (Issue #383)' (#385) from sl-controls/issue-383-vesc into main 2026-03-04 08:40:15 -05:00
a11722e872 feat: Implement VESC UART driver node (Issue #383)
ROS2 driver for Flipsky FSESC 4.20 Plus (VESC dual ESC) motor control.
Replaces hoverboard ESC communication with pyvesc library.

Features:
- UART serial communication (configurable port/baud)
- Dual command modes: duty_cycle (-100 to 100) and RPM setpoint
- Telemetry publishing: voltage, current, RPM, temperature, fault codes
- Command timeout: auto-zero throttle if no cmd_vel received
- Heartbeat-based connection management
- Comprehensive error handling and logging

Topics:
- Subscribe: /cmd_vel (geometry_msgs/Twist)
- Publish: /vesc/state (JSON telemetry)
- Publish: /vesc/raw_telemetry (debug)

Launch: ros2 launch saltybot_vesc_driver vesc_driver.launch.py
Config: config/vesc_params.yaml

Next phase: Integrate with cmd_vel_mux + safety layer.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-04 07:05:46 -05:00
bc3ed1a0c7 Merge pull request 'fix: resolve all compile errors across 6 files (Issue #337)' (#382) from sl-controls/issue-337-build-fix into main 2026-03-03 19:58:54 -05:00
f4e71777ec fix: Resolve all compile and linker errors (Issue #337)
Fixed 7 compile errors across 6 files:

1. servo.c: Removed duplicate ServoState typedef, updated struct definition in header
2. watchdog.c: Fixed IWDG handle usage - moved to global scope for IRQHandler access
3. ultrasonic.c: Fixed timer handle type mismatches - use TIM_HandleTypeDef instead of TIM_TypeDef, replaced HAL_TIM_IC_Init_Compat with proper HAL functions
4. main.c: Replaced undefined functions - imu_calibrated() → mpu6000_is_calibrated(), crsf_is_active() → manual state check
5. ina219.c: Stubbed I2C functions pending HAL implementation

Build now passes with ZERO errors.
- RAM: 6.5% (16964 bytes / 262144)
- Flash: 10.6% (55368 bytes / 524288)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 19:00:12 -05:00
6df453e8d0 Merge pull request 'feat: deaf/accessibility mode (Issue #371)' (#381) from sl-controls/issue-371-accessibility into main 2026-03-03 18:51:30 -05:00
5604670646 feat: Implement deaf/accessibility mode with STT, touch keyboard, TTS (Issue #371)
Accessibility mode for hearing-impaired users:
- Speech-to-text display: Integrates with saltybot_social speech_pipeline_node
- Touch keyboard overlay: 1024x600 optimized for MageDok 7in display
- TTS output: Routes to MageDok speakers via PulseAudio
- Web UI server: Responsive keyboard interface with real-time display updates
- Auto-confirm: Optional TTS feedback for spoken input
- Physical keyboard support: Both touch and physical input methods

Features:
- Keyboard buffer with backspace/clear/send controls
- Transcript history display (max 10 entries)
- Status indicators for STT/TTS ready state
- Number/symbol support (1-5, punctuation)
- HTML/CSS responsive design optimized for touch
- ROS2 integration via /social/speech/transcript and /social/conversation/request

Launch: ros2 launch saltybot_accessibility_mode accessibility_mode.launch.py
UI Port: 8080 (MageDok display access)
Config: config/accessibility_params.yaml

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 18:17:41 -05:00
b942bb549a Merge pull request 'feat: 360° LIDAR obstacle avoidance (Issue #364)' (#380) from sl-controls/issue-364-lidar-avoidance into main 2026-03-03 18:15:28 -05:00
3a639507c7 Merge pull request 'feat: Salty Face animated expression UI (Issue #370)' (#379) from sl-webui/issue-370-salty-face into main 2026-03-03 18:15:17 -05:00
8aa4072a63 feat(webui): Salty Face animated expression UI — contextual emotions (Issue #370)
Add animated facial expression interface for MageDok 7" display:

Core Features:
✓ 8 emotional states:
  - Happy (default idle)
  - Alert (obstacles detected)
  - Confused (searching, target lost)
  - Sleeping (prolonged inactivity)
  - Excited (target reacquired)
  - Emergency (e-stop triggered)
  - Listening (microphone active)
  - Talking (TTS output)

Visual Design:
✓ Minimalist Cozmo/Vector-inspired eyes + optional mouth
✓ Canvas-based GPU-accelerated rendering
✓ 30fps target on Jetson Orin Nano
✓ Emotion-specific eye characteristics:
  - Scale changes (alert widened eyes)
  - Color coding per emotion
  - Pupil position tracking
  - Blinking rates vary by state
  - Eye wandering (confused searching)
  - Bouncing animation (excited)
  - Flash effect (emergency)

Mouth Animation:
✓ Synchronized with text-to-speech output
✓ Shape frames: closed, smile, oh, ah, ee sounds
✓ ~10fps lip sync animation

ROS2 Integration:
✓ Subscribe to /saltybot/state (emotion triggers)
✓ Subscribe to /saltybot/target_track (tracking state)
✓ Subscribe to /saltybot/obstacles (alert state)
✓ Subscribe to /social/speech/is_speaking (talking mode)
✓ Subscribe to /social/speech/is_listening (listening mode)
✓ Subscribe to /saltybot/battery (status tracking)
✓ Subscribe to /saltybot/audio_level (audio feedback)

HUD Overlay:
✓ Tap-to-toggle status display
✓ Battery percentage indicator
✓ Robot state label
✓ Distance to target (meters)
✓ Movement speed (m/s)
✓ System health percentage
✓ Color-coded health indicator (green/yellow/red)

Integration:
✓ New DISPLAY tab group (rose color)
✓ Full-screen rendering on 1024×600 MageDok display
✓ Responsive to robot state machine
✓ Supports kiosk mode deployment

Build Status:  PASSING
- 126 modules (+1 for SaltyFace)
- 281.57 KB main bundle (+11 KB)
- 0 errors

Depends on: Issue #369 (MageDok display setup)
Foundation for: Issue #371 (Accessibility mode)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 18:14:49 -05:00
cfa8ee111d Merge pull request 'feat: Replace GNOME with Cage+Chromium kiosk (Issue #374)' (#377) from sl-webui/issue-374-cage-kiosk into main 2026-03-03 17:46:14 -05:00
34c7af38b2 Merge pull request 'feat: battery coulomb counter (Issue #325)' (#378) from sl-perception/issue-325-battery-coulomb into main 2026-03-03 17:46:03 -05:00
410ace3540 feat: battery coulomb counter (Issue #325)
Add coulomb counter for accurate SoC estimation independent of load:

- New coulomb_counter module: integrate current over time to track Ah consumed
  * coulomb_counter_init(capacity_mah) initializes with battery capacity
  * coulomb_counter_accumulate(current_ma) integrates current at 100 Hz
  * coulomb_counter_get_soc_pct() returns SoC 0-100% (255 = invalid)
  * coulomb_counter_reset() for charge-complete reset

- Battery module integration:
  * battery_accumulate_coulombs() reads motor INA219 currents and accumulates
  * battery_get_soc_coulomb() returns coulomb-based SoC with fallback to voltage
  * Initialize coulomb counter at startup with DEFAULT_BATTERY_CAPACITY_MAH

- Telemetry updates:
  * JLink STATUS: use coulomb SoC if available, fallback to voltage-based
  * CRSF battery frame: now includes remaining capacity in mAh (from coulomb counter)
  * CRSF capacity field was always 0; now reflects actual remaining mAh

- Mainloop integration:
  * Call battery_accumulate_coulombs() every tick for continuous integration
  * INA219 motor currents + 200 mA subsystem baseline = total battery draw

Motor current sources (INA219 addresses 0x40/0x41) provide most power draw;
Jetson ROS2 battery_node already prioritizes coulomb-based soc_pct from STATUS frame.

Default capacity: 2200 mAh (typical lab 3S LiPo); configurable via firmware parameter.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 17:35:34 -05:00
5cec6779e5 feat: Integrate IWDG watchdog timer driver (Issue #300)
- Replace safety.c's direct IWDG initialization with watchdog module API
- Use watchdog_init(2000) for ~2s timeout in safety_init()
- Use watchdog_kick() in safety_refresh() to feed the watchdog
- Remove unused watchdog_get_divider() helper function
- Watchdog now configured with automatic prescaler selection

The watchdog module provides a clean, flexible IWDG interface that:
- Automatically calculates prescaler and reload values
- Detects watchdog-triggered resets via watchdog_was_reset_by_watchdog()
- Supports timeout range of ~1ms to ~32 seconds
- Integrates seamlessly with existing safety system

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 17:29:59 -05:00
aeb90efa61 feat: Implement 360° LIDAR obstacle avoidance (Issue #364)
Implements ROS2 node for RPLIDAR A1M8 obstacle detection with:
- Emergency stop at 0.5m
- Speed-dependent safety zone (3m @ 20km/h, scales linearly)
- Forward-facing 60° obstacle cone scanning
- Publishes /saltybot/obstacle_alert and /cmd_vel_safe
- Debounced obstacle detection (2 frames)
- JSON status reporting

Launch: ros2 launch saltybot_lidar_avoidance lidar_avoidance.launch.py
Config: config/lidar_avoidance_params.yaml

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 17:29:14 -05:00
b04fd916ff Merge pull request 'feat: MageDok 7in display setup for Orin (Issue #369)' (#373) from sl-webui/issue-369-display-setup into main 2026-03-03 17:20:15 -05:00
a8a9771ec7 Merge pull request 'feat: adaptive camera power modes (Issue #375)' (#376) from sl-perception/issue-375-camera-power-modes into main 2026-03-03 17:20:04 -05:00
042c0529a1 feat: Add Issue #375 — adaptive camera power mode manager
Implements a 5-mode FSM for dynamic sensor activation based on speed,
scenario, and battery level — avoids running all 4 CSI cameras + full
sensor suite when unnecessary, saving ~1 GB RAM and significant compute.

Five modes (sensor sets):
  SLEEP  — no sensors       (~150 MB RAM)
  SOCIAL — webcam only      (~400 MB RAM, parked/socialising)
  AWARE  — front CSI + RealSense + LIDAR   (~850 MB RAM, indoor/<5km/h)
  ACTIVE — front+rear CSI + RealSense + LIDAR + UWB  (~1.15 GB, 5-15km/h)
  FULL   — all 4 CSI + RealSense + LIDAR + UWB       (~1.55 GB, >15km/h)

Core library — _camera_power_manager.py (pure Python, no ROS2 deps)
- CameraPowerFSM.update(speed_mps, scenario, battery_pct) → ModeDecision
- Speed-driven upgrades: instant (safety-first)
- Speed-driven downgrades: held for downgrade_hold_s (default 5s, anti-flap)
- Scenario overrides (instant, bypass hysteresis):
  · CROSSING / EMERGENCY → FULL always
  · PARKED → SOCIAL immediately
  · INDOOR → cap at AWARE (never ACTIVE/FULL indoors)
- Battery low cap: battery_pct < threshold → cap at AWARE
- Idle timer: near-zero speed holds at AWARE for idle_to_social_s (30s)
  before dropping to SOCIAL (avoids cycling at traffic lights)

ROS2 node — camera_power_node.py
- Subscribes: /saltybot/speed, /saltybot/scenario, /saltybot/battery_pct
- Publishes: /saltybot/camera_mode (CameraPowerMode, latched, 2 Hz)
- Publishes: /saltybot/camera_cmd/{front,rear,left,right,realsense,lidar,uwb,webcam}
  (std_msgs/Bool, TRANSIENT_LOCAL so late subscribers get last state)
- Logs mode transitions with speed/scenario/battery context

Tests — test/test_camera_power_manager.py: 64/64 passing
- Sensor configs: counts, correct flags per mode, safety invariants
- Speed upgrades: instantaneous at all thresholds, no hold required
- Downgrade hysteresis: hold timer, cancellation on speed spike, hold=0 instant
- Scenario overrides: CROSSING/EMERGENCY/PARKED/INDOOR, all CSIs on crossing
- Battery low: cap at AWARE, threshold boundary
- Idle timer: delay AWARE→SOCIAL, motion resets timer
- Reset, labels, ModeDecision fields
- Integration: full ride scenario (walk→jog→sprint→crossing→indoor→park→low bat)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:48:17 -05:00
e2587b60fb feat: SaltyFace web app UI for Chromium kiosk (Issue #370)
Animated robot expression interface as lightweight web application:

**Architecture:**
- HTML5 Canvas rendering engine
- Node.js HTTP server (localhost:3000)
- ROSLIB WebSocket bridge for ROS2 topics
- Fullscreen responsive design (1024×600)

**Features:**
- 8 emotional states (happy, alert, confused, sleeping, excited, emergency, listening, talking)
- Real-time ROS2 subscriptions:
  - /saltybot/state (emotion triggers)
  - /saltybot/battery (status display)
  - /saltybot/target_track (EXCITED emotion)
  - /saltybot/obstacles (ALERT emotion)
  - /social/speech/is_speaking (TALKING emotion)
  - /social/speech/is_listening (LISTENING emotion)
- Tap-to-toggle status overlay
- 60fps Canvas animation on Wayland
- ~80MB total memory (Node.js + browser)

**Files:**
- public/index.html — Main page (1024×600 fullscreen)
- public/salty-face.js — Canvas rendering + ROS2 integration
- server.js — Node.js HTTP server with CORS support
- systemd/salty-face-server.service — Auto-start systemd service
- docs/SALTY_FACE_WEB_APP.md — Complete setup & API documentation

**Integration:**
- Runs in Chromium kiosk (Issue #374)
- Depends on rosbridge_server for WebSocket bridge
- Serves on localhost:3000 (configurable)

**Next:** Issue #371 (Accessibility enhancements)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 16:42:41 -05:00
82b8f40b39 feat: Replace GNOME with Cage + Chromium kiosk (Issue #374)
Lightweight fullscreen kiosk for MageDok 7" display:

**Architecture:**
- Cage: Minimal Wayland compositor (replaces GNOME)
- Chromium: Fullscreen kiosk browser for SaltyFace web UI
- PulseAudio: HDMI audio routing (from Issue #369)
- Touch: HID input from MageDok USB device

**Memory Savings:**
- GNOME desktop: ~650MB RAM
- Cage + Chromium: ~200MB RAM
- Net gain: ~450MB for ROS2 workloads

**Files:**
- config/cage-magedok.ini — Cage display settings (1024×600@60Hz)
- config/wayland-magedok.conf — Wayland output configuration
- scripts/chromium_kiosk.sh — Cage + Chromium launcher
- systemd/chromium-kiosk.service — Auto-start systemd service
- launch/cage_display.launch.py — ROS2 launch configuration
- docs/CAGE_CHROMIUM_KIOSK.md — Complete setup & troubleshooting guide

**Next:** Issue #370 (Salty Face as web app in Chromium kiosk)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 16:41:00 -05:00
46fc2db8e6 Merge pull request 'feat: smooth velocity ramp controller (Issue #350)' (#372) from sl-perception/issue-350-velocity-ramp into main 2026-03-03 16:17:55 -05:00
6592b58f65 feat: Add Issue #350 — smooth velocity ramp controller
Adds a rate-limiting shim between raw /cmd_vel and the drive stack to
prevent wheel slip, tipping, and jerky motion from step velocity inputs.

Core library — _velocity_ramp.py (pure Python, no ROS2 deps)
- VelocityRamp: applies independent accel/decel limits to linear-x and
  angular-z with configurable max_lin_accel, max_lin_decel,
  max_ang_accel, max_ang_decel
- _ramp_axis(): per-axis rate limiter with correct accel/decel selection
  (decel when |target| < |current| or sign reversal; accel otherwise)
- Emergency stop: step(0.0, 0.0) bypasses ramp → immediate zero output
- Asymmetric limits supported (e.g. faster decel than accel)

ROS2 node — velocity_ramp_node.py
- Subscribes /cmd_vel, publishes /cmd_vel_smooth at configurable rate_hz
- Parameters: max_lin_accel (0.5 m/s²), max_lin_decel (0.5 m/s²),
  max_ang_accel (1.0 rad/s²), max_ang_decel (1.0 rad/s²), rate_hz (50)

Tests — test/test_velocity_ramp.py: 50/50 passing
- _ramp_axis: accel/decel selection, sign reversal, overshoot prevention
- Construction: invalid params raise ValueError, defaults verified
- Linear/angular ramp-up: step size, target reached, no overshoot
- Deceleration: asymmetric limits, partial decel (non-zero target)
- Emergency stop: immediate zero, state cleared, resume from zero
- Sign reversal: passes through zero without jumping
- Reset: state cleared, next ramp starts from zero
- Monotonicity: linear and angular outputs are monotone toward target
- Rate accuracy: 50Hz/10Hz step sizes, 100-step convergence verified

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:45:05 -05:00
45d456049a feat: MageDok 7in display setup for Jetson Orin (Issue #369)
Add complete display integration for MageDok 7" IPS touchscreen:

Configuration Files:
- X11 display config (xorg-magedok.conf) — 1024×600 @ 60Hz
- PulseAudio routing (pulseaudio-magedok.conf) — HDMI audio to speakers
- Udev rules (90-magedok-touch.rules) — USB touch device permissions
- Systemd service (magedok-display.service) — auto-start on boot

ROS2 Launch:
- magedok_display.launch.py — coordinate display/touch/audio setup

Helper Scripts:
- verify_display.py — validate 1024×600 resolution via xrandr
- touch_monitor.py — detect MageDok USB touch, publish status
- audio_router.py — configure PulseAudio HDMI sink routing

Documentation:
- MAGEDOK_DISPLAY_SETUP.md — complete installation and troubleshooting guide

Features:
✓ DisplayPort → HDMI video from Orin DP connector
✓ USB touch input as HID device (driver-free)
✓ HDMI audio routing to built-in speakers
✓ 1024×600 native resolution verification
✓ Systemd auto-launch on boot (no login prompt)
✓ Headless fallback when display disconnected
✓ ROS2 status monitoring (touch/audio/resolution)

Supports Salty Face UI (Issue #370) and accessibility features (Issue #371)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 15:44:03 -05:00
631282b95f Merge pull request 'feat: Issue #365 — UWB DW3000 anchor/tag tracking (bearing + distance)' (#368) from sl-perception/issue-365-uwb-tracking into main 2026-03-03 15:41:49 -05:00
0ecf341c57 feat: Add Issue #365 — UWB DW3000 anchor/tag tracking (bearing + distance)
Software-complete implementation of the two-anchor UWB ranging stack.
All ROS2 / serial code written against an abstract interface so tests run
without physical hardware (anchors on order).

New message
- UwbTarget.msg: valid, bearing_deg, distance_m, confidence,
  anchor0/1_dist_m, baseline_m, fix_quality (0=none 1=single 2=dual)

Core library — _uwb_tracker.py (pure Python, no ROS2/runtime deps)
- parse_frame(): ASCII RANGE,<id>,<tag>,<mm> protocol decoder
- bearing_from_ranges(): law-of-cosines 2-anchor bearing with confidence
  (penalises extreme angles + close-range geometry)
- bearing_single_anchor(): fallback bearing=0, conf≤0.3
- BearingKalman: 1-D constant-velocity Kalman filter [bearing, rate]
- UwbRangingState: thread-safe per-anchor state + stale timeout + Kalman
- AnchorSerialReader: background thread, readline() interface (real or mock)

ROS2 node — uwb_node.py
- Opens /dev/ttyUSB0 + /dev/ttyUSB1 (configurable)
- Non-fatal serial open failure (will publish FIX_NONE until plugged in)
- Publishes /saltybot/uwb_target at 10 Hz (configurable)
- Graceful shutdown: stops reader threads

Tests — test/test_uwb_tracker.py: 64/64 passing
- Frame parsing: valid, malformed, STATUS, CR/LF, mm→m conversion
- Bearing geometry: straight-ahead, ±45°, ±30°, symmetry, confidence
- Kalman: seeding, smoothing, convergence, rate tracking
- UwbRangingState: single/dual fix, stale timeout, thread safety
- AnchorSerialReader: mock serial, bytes decode, stop()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:25:23 -05:00
94d12159b4 Merge pull request 'feat(webui): ROS parameter editor in Settings panel (Issue #354)' (#360) from sl-webui/issue-354-settings into main 2026-03-03 15:20:48 -05:00
eac203ecf4 Merge pull request 'feat: Issue #363 — P0 person tracking for follow-me mode' (#367) from sl-perception/issue-363-person-tracking into main 2026-03-03 15:20:26 -05:00
c620dc51a7 feat: Add Issue #363 — P0 person tracking for follow-me mode
Implements real-time person detection + tracking pipeline for the
follow-me motion controller on Jetson Orin Nano Super (D435i).

Core components
- TargetTrack.msg: bearing_deg, distance_m, confidence, bbox, vel_bearing_dps,
  vel_dist_mps, depth_quality (0-3)
- _person_tracker.py (pure-Python, no ROS2/runtime deps):
  · 8-state constant-velocity Kalman filter [cx,cy,w,h,vcx,vcy,vw,vh]
  · Greedy IoU data association
  · HSV torso colour histogram re-ID (16H×8S, Bhattacharyya similarity)
    with fixed saturation clamping (s = (cmax−cmin)/cmax, clipped to [0,1])
  · FollowTargetSelector: nearest person auto-lock, hold_frames hysteresis
  · TENTATIVE→ACTIVE after min_hits; LOST track removal after max_lost_frames
    with per-frame lost_age increment across all LOST tracks
  · bearing_from_pixel, depth_at_bbox (median, quality flags)
- person_tracking_node.py:
  · YOLOv8n via ultralytics (TRT FP16 on first run) → HOG+SVM fallback
  · Subscribes colour + depth + camera_info + follow_start/stop
  · Publishes /saltybot/target_track at ≤30 fps
- test/test_person_tracker.py: 59/59 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 15:19:02 -05:00
bcf848109b Merge pull request 'feat(perception): geometric face emotion classifier (Issue #359)' (#361) from sl-perception/issue-359-face-emotion into main 2026-03-03 15:07:17 -05:00
672120bb50 feat(perception): geometric face emotion classifier (Issue #359)
Classifies facial expressions into neutral/happy/surprised/angry/sad
using geometric rules over MediaPipe Face Mesh landmarks — no ML model
required at runtime.

Rules
-----
  surprised: brow_raise > 0.12 AND eye_open > 0.07 AND mouth_open > 0.07
  happy:     smile > 0.025  (lip corners above lip midpoint)
  angry:     brow_furl > 0.02 AND smile < 0.01
  sad:       smile < -0.025 AND brow_furl < 0.015
  neutral:   default

Changes
-------
- saltybot_scene_msgs/msg/FaceEmotion.msg       — per-face emotion + features
- saltybot_scene_msgs/msg/FaceEmotionArray.msg
- saltybot_scene_msgs/CMakeLists.txt            — register new msgs
- _face_emotion.py   — pure-Python: FaceLandmarks, compute_features,
                        classify_emotion, detect_emotion, from_mediapipe
- face_emotion_node.py  — subscribes /camera/color/image_raw,
                           publishes /saltybot/face_emotions (≤15 fps)
- test/test_face_emotion.py  — 48 tests, all passing
- setup.py  — add face_emotion entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:39:49 -05:00
f7f89403d5 Merge pull request 'feat(social): system resource monitor for Jetson Orin (Issue #355)' (#357) from sl-jetson/issue-355-sysmon into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 4s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-03 14:32:49 -05:00
ae76697a1c Merge pull request 'feat(perception): MFCC nearest-centroid audio scene classifier (Issue #353)' (#358) from sl-perception/issue-353-audio-scene into main 2026-03-03 14:32:36 -05:00
677e6eb75e feat(perception): MFCC nearest-centroid audio scene classifier (Issue #353)
Classifies ambient audio into indoor/outdoor/traffic/park at 1 Hz using
a 16-d feature vector (13 MFCC + spectral centroid + rolloff + ZCR) with
a normalised nearest-centroid classifier. Centroids are computed at import
time from seeded synthetic prototypes, ensuring deterministic behaviour.

Changes
-------
- saltybot_scene_msgs/msg/AudioScene.msg  — label + confidence + features[16]
- saltybot_scene_msgs/CMakeLists.txt      — register AudioScene.msg
- _audio_scene.py  — pure-numpy feature extraction + NearestCentroidClassifier
- audio_scene_node.py  — subscribes /audio/audio, publishes /saltybot/audio_scene
- test/test_audio_scene.py  — 53 tests (all passing) with synthetic audio
- setup.py  — add audio_scene entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:03:11 -05:00
0af4441120 feat(social): system resource monitor for Jetson Orin (Issue #355)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 9s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
Polls /proc/stat (CPU delta), /proc/meminfo (RAM), os.statvfs (disk),
/sys/devices/gpu.0/load (GPU), and thermal zone sysfs paths; publishes
JSON payload on /saltybot/system_resources at 1 Hz.

Pure helpers (parse_proc_stat, cpu_percent_from_stats, parse_meminfo,
compute_ram_stats, read_disk_usage, read_gpu_load, read_thermal_zones)
are all unit-tested offline.  Injectable I/O on SysmonNode allows full
node tick tests without /proc or /sys.  67/67 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:54:31 -05:00
ddb93bec20 Issue #354: Add ROS parameter editor to Settings
Add Parameters tab for live ROS parameter editing with:
- get_parameters service integration
- set_parameter service support
- Type-specific input controls
- Node-based grouping
- Search filtering

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 13:49:51 -05:00
358c1ab6f9 Merge pull request 'feat(webui): dedicated CAMERAS tab group with live MJPEG viewer (Issue #349)' (#352) from sl-webui/issue-349-camera-viewer into main 2026-03-03 13:44:48 -05:00
7966eb5187 Merge pull request 'feat(perception): depth-based obstacle size estimator (Issue #348)' (#351) from sl-perception/issue-348-obstacle-size into main 2026-03-03 13:44:32 -05:00
2a9b03dd76 feat(perception): depth-based obstacle size estimator (Issue #348)
Projects LIDAR clusters into the D435i depth image to estimate 3-D
obstacle width and height in metres.

- saltybot_scene_msgs/msg/ObstacleSize.msg      — new message
- saltybot_scene_msgs/msg/ObstacleSizeArray.msg — array wrapper
- saltybot_scene_msgs/CMakeLists.txt            — register new msgs
- saltybot_bringup/_obstacle_size.py            — pure-Python helper:
    CameraParams (intrinsics + LIDAR→camera extrinsics)
    ObstacleSizeEstimate (NamedTuple)
    lidar_to_camera()         LIDAR frame → camera frame transform
    project_to_pixel()        pinhole projection + bounds check
    sample_depth_median()     uint16 depth image window → median metres
    estimate_height()         vertical strip scan for row extent → height_m
    estimate_cluster_size()   full pipeline: cluster → size estimate
- saltybot_bringup/obstacle_size_node.py        — ROS2 node
    sub: /scan, /camera/depth/image_rect_raw, /camera/depth/camera_info
    pub: /saltybot/obstacle_sizes (ObstacleSizeArray)
    width from LIDAR bbox; height from depth strip back-projection;
    graceful fallback (LIDAR-only) when depth image unavailable;
    intrinsics latched from CameraInfo on first arrival
- test/test_obstacle_size.py                    — 33 tests, 33 passing
- setup.py                                      — add obstacle_size entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:32:41 -05:00
93028dc847 feat(webui): add dedicated CAMERAS tab group for camera viewer (Issue #349)
Move camera viewer from TELEMETRY to new CAMERAS tab group (rose color).
Reorganizes tab structure to separate media capture from system telemetry.
CameraViewer.jsx already provides comprehensive MJPEG stream support:
- Multi-camera switching (7 total: front/left/rear/right CSI, D435i RGB/depth, panoramic)
- FPS counter per camera with quality badge (FULL/GOOD/LOW/NO SIGNAL)
- Resolution and camera info display
- Detection overlays (faces, gestures, scene objects)
- Picture-in-picture support (up to 3 pinned cameras)
- Video recording (MP4/WebM) and snapshot capture
- 360° panoramic viewer with mouse drag pan
- Color-coded quality indicators based on FPS

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 13:27:09 -05:00
3bee8f3cb4 Merge pull request 'feat(social): trigger-based ROS2 bag recorder (Issue #332)' (#335) from sl-jetson/issue-332-rosbag-recorder into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 11s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-03 13:25:55 -05:00
813d6f2529 feat(social): trigger-based ROS2 bag recorder (Issue #332)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 12s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
BagRecorderNode: subscribes /saltybot/record_trigger (Bool), spawns
ros2 bag record subprocess, drives idle/recording/stopping/error
state machine; auto-stop timeout, SIGINT graceful shutdown with
SIGKILL fallback. Publishes /saltybot/recording_status (String).
Configurable topics (csv), bag_dir, prefix, compression, size limit.
Subprocess injectable for offline testing. 101/101 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:19:49 -05:00
9b538395c0 Merge pull request 'feat(webui): hand tracking skeleton visualization (Issue #344)' (#346) from sl-webui/issue-344-hand-viz into main 2026-03-03 13:19:46 -05:00
a0f3677732 Merge pull request 'feat: Add pure pursuit path follower for Nav2 (Issue #333)' (#334) from sl-controls/issue-333-pure-pursuit into main 2026-03-03 13:19:29 -05:00
3fce9bf577 Merge pull request 'feat(perception): MediaPipe hand tracking — Leap Motion pivot (Issue #342)' (#345) from sl-perception/issue-342-hand-tracking into main 2026-03-03 13:19:28 -05:00
1729e43964 feat(perception): MediaPipe hand tracking — Leap Motion pivot (Issue #342)
PART 1 AUDIT: Zero Leap Motion / UltraLeap references found in any
saltybot_* package. Existing gesture_node.py (saltybot_social) already
uses MediaPipe — no cleanup required.

PART 2 NEW PACKAGES:

saltybot_hand_tracking_msgs (ament_cmake)
  - HandLandmarks.msg   — 21 landmarks (float32[63]), handedness,
                          gesture label + direction, wrist position
  - HandLandmarksArray.msg

saltybot_hand_tracking (ament_python)
  - _hand_gestures.py   — pure-Python gesture classifier (no ROS2/MP deps)
                          Vocabulary: stop (open palm) → pause/stop,
                          point (index up) → direction command + 8-compass,
                          disarm (fist) → emergency-off,
                          confirm (thumbs-up) → confirm action,
                          follow_me (peace sign) → follow mode,
                          greeting (wrist oscillation) → greeting response
                          WaveDetector: sliding-window lateral wrist tracking
  - hand_tracking_node.py — ROS2 node
                          sub: /camera/color/image_raw (BEST_EFFORT)
                          pub: /saltybot/hands (HandLandmarksArray)
                               /saltybot/hand_gesture (std_msgs/String)
                          MediaPipe model_complexity=0 (lite) for 20+ FPS
                          on Orin Nano Super; background MP init thread;
                          per-hand WaveDetector instances
  - test/test_hand_gestures.py — 35 tests, 35 passing
    Covers: Landmark, HandGestureResult, WaveDetector, all 6 gesture
    classifiers, priority ordering, direction vectors, confidence bounds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 12:47:22 -05:00
347449ed95 feat(webui): hand pose tracking and gesture visualization (Issue #344)
Features:
- Subscribes to /saltybot/hands (21 landmarks per hand - MediaPipe format)
- Subscribes to /saltybot/hand_gesture (String gesture label)
- Canvas-based hand skeleton rendering with bone connections
- Support for dual hand tracking (left and right)
- Handedness indicators with color coding
  * Left hand: green
  * Right hand: yellow
- Real-time gesture display with confidence indicator
- Per-landmark confidence visualization
- Bone connections between all 21 joints

Hand Skeleton Features:
- 21 MediaPipe landmarks per hand
  * Wrist (1)
  * Thumb (4)
  * Index finger (4)
  * Middle finger (4)
  * Ring finger (4)
  * Pinky finger (4)
- 20 bone connections between joints
- Confidence-based rendering (only show high-confidence points)
- Scaling and normalization for viewport
- Joint type indicators (tips with ring outline)
- Glow effects around landmarks

Gesture Recognition:
- Real-time gesture label display
- Confidence percentage (0-100%)
- Color-coded confidence:
  * Green: >80% (high confidence)
  * Yellow: 50-80% (medium confidence)
  * Blue: <50% (detecting)

Hand Status Display:
- Live detection status for both hands
- Visual indicators (✓ detected / ◯ not detected)
- Dual-hand canvas rendering
- Gesture info panel with confidence bar

Integration:
- Added to SOCIAL tab group as "Hands" tab
- Positioned after "Faces" tab
- Uses subscribe hook for real-time updates
- Dark theme with color-coded hands
- Canvas-based rendering for smooth visualization

Build: 125 modules, no errors
Main bundle: 270.08 KB

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 12:43:19 -05:00
5156100197 Merge pull request 'feat(webui): diagnostics panel — system health overview with alerts (Issue #340)' (#343) from sl-webui/issue-340-diagnostics into main 2026-03-03 12:41:48 -05:00
a7d9531537 Merge pull request 'feat(perception): lane/path edge detector (Issue #339)' (#341) from sl-perception/issue-339-path-edges into main 2026-03-03 12:41:23 -05:00
bd9cb6da35 feat(perception): lane/path edge detector (Issue #339)
Adds Canny+Hough+bird-eye perspective pipeline for detecting left/right
path edges from the forward camera.  Pure-Python helper (_path_edges.py)
is fully tested; ROS2 node publishes PathEdges on /saltybot/path_edges.

- saltybot_scene_msgs/msg/PathEdges.msg — new message
- saltybot_scene_msgs/CMakeLists.txt    — register PathEdges.msg
- saltybot_bringup/_path_edges.py       — PathEdgeConfig, PathEdgesResult,
                                          build/apply_homography, canny_edges,
                                          hough_lines, classify_lines,
                                          average_line, warp_segments,
                                          process_frame
- saltybot_bringup/path_edges_node.py  — ROS2 node (sensor_msgs/Image →
                                          PathEdges, parameters for all
                                          tunable Canny/Hough/birdseye params)
- test/test_path_edges.py              — 38 tests, 38 passing
- setup.py                             — add path_edges console_script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 11:33:22 -05:00
c50899f000 feat(webui): system diagnostics panel with hardware status monitoring (Issue #340)
Features:
- Subscribes to /diagnostics (diagnostic_msgs/DiagnosticArray)
- Hardware status cards per subsystem with color-coded health
- Real-time error and warning counts
- Expandable diagnostic cards with detailed key-value pairs
- Diagnostic status timeline with timestamps
- Aggregated system health summary
- Status indicators: OK (green), WARNING (yellow), ERROR (red), STALE (gray)

- Hardware Status Display
  * Per-subsystem diagnostic cards
  * Status level with color coding
  * Expandable details with key values
  * Hardware ID tracking
  * Name and message display

- Health Summary Card
  * Total diagnostic count
  * OK/WARNING/ERROR/STALE breakdowns
  * Overall system health status
  * Visual status indicator

- Timeline and History
  * Recent status timeline (10 latest events)
  * Timestamp tracking
  * Status transitions
  * Scrollable history

- Status Legend
  * Color-coded reference guide
  * Status descriptions
  * Quick status lookup

Integration:
- Added to MONITORING tab group as first tab (highest priority)
- Uses subscribe hook for real-time updates
- Dark theme with comprehensive status visualization
- Max 100 diagnostic events in history

Build: 124 modules, no errors
Main bundle: 264.31 KB

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 11:23:51 -05:00
79505579b1 Merge pull request 'feat(webui): gamepad teleoperation panel with WASD + e-stop (Issue #319)' (#331) from sl-webui/issue-319-teleop into main 2026-03-03 11:22:28 -05:00
a310c8afc9 Merge pull request 'fix(webui): add 3 missing imports to App.jsx (Issue #329) P0' (#338) from sl-webui/issue-329-import-fix into main 2026-03-03 11:22:26 -05:00
2f76d1d0d5 Merge pull request 'feat(perception): dynamic obstacle velocity estimator (Issue #326)' (#336) from sl-perception/issue-326-obstacle-velocity into main 2026-03-03 11:22:21 -05:00
eb61207532 feat(perception): dynamic obstacle velocity estimator (Issue #326)
Adds ObstacleVelocity/ObstacleVelocityArray msgs and an
ObstacleVelocityNode that clusters /scan points, tracks each centroid
with a constant-velocity Kalman filter, and publishes velocity vectors
on /saltybot/obstacle_velocities.

New messages (saltybot_scene_msgs):
  msg/ObstacleVelocity.msg      — obstacle_id, centroid, velocity,
                                  speed_mps, width_m, depth_m,
                                  point_count, confidence, is_static
  msg/ObstacleVelocityArray.msg — array wrapper with header

New files (saltybot_bringup):
  saltybot_bringup/_obstacle_velocity.py   — pure helpers (no ROS2 deps)
    KalmanTrack   constant-velocity 2-D KF: predict(dt) / update(centroid)
                  coasting counter → alive flag; confidence = age/n_init
    associate()   greedy nearest-centroid matching (O(N·M), strict <)
    ObstacleTracker  predict-all → associate → update/spawn → prune cycle
  saltybot_bringup/obstacle_velocity_node.py
    Subscribes /scan (BEST_EFFORT); reuses _lidar_clustering helpers;
    publishes ObstacleVelocityArray on /saltybot/obstacle_velocities
    Parameters: distance_threshold_m=0.20, min_points=3, range 0.05–12m,
                max_association_dist_m=0.50, max_coasting_frames=5,
                n_init_frames=3, q_pos=0.05, q_vel=0.50, r_pos=0.10,
                static_speed_threshold=0.10
  test/test_obstacle_velocity.py — 48 tests, all passing

Modified:
  saltybot_scene_msgs/CMakeLists.txt  — register new msgs
  saltybot_bringup/setup.py           — add obstacle_velocity console_script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 06:53:04 -05:00
1c8430e68a feat: Add pure pursuit path follower for Nav2 (Issue #333)
Implements saltybot_pure_pursuit package:
- Pure pursuit algorithm for path following with configurable parameters
- Lookahead distance (0.5m default) for target point on path
- Goal tolerance (0.1m) for goal detection
- Heading error correction to reduce speed when misaligned with path
- Publishes Twist commands on /cmd_vel_tracked for Nav2 integration
- Subscribes to /odom (odometry) and /path (Path trajectory)
- Tracks and publishes cross-track error for monitoring

Pure pursuit geometry:
- Finds closest point on path to robot current position
- Looks ahead specified distance along path from closest point
- Computes steering angle to follow circular arc to lookahead point
- Reduces linear velocity when heading error is large (with correction enabled)
- Clamps velocities to configurable maximums

Configuration parameters:
- lookahead_distance: 0.5m (typical range: 0.1-1.0m)
- goal_tolerance: 0.1m (distance to goal before stopping)
- heading_tolerance: 0.1 rad (unused but can support in future)
- max_linear_velocity: 1.0 m/s
- max_angular_velocity: 1.57 rad/s
- use_heading_correction: true (reduces speed on large heading errors)

Comprehensive test suite: 20+ tests covering:
- Geometric calculations (distance, quaternion conversions)
- Path following logic (empty path, straight/curved/spiral paths)
- Steering calculations (heading errors, velocity limits)
- Edge cases and realistic scenarios
- Control loop integration
- Parameter variations

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 06:47:45 -05:00
ba39e9ba26 fix(webui): add missing imports to App.jsx (Issue #329) — P0 BUG FIX
Critical bug fix:
- Added missing StatusHeader import (used in JSX line 215)
- Added missing LogViewer import (used in JSX line 291)
- Added missing MotorCurrentGraph import (used in JSX line 264)

These imports were referenced in JSX but not imported, causing dashboard crashes on load.

Build verification:
- 122 modules, all compiled successfully
- No errors or warnings
- Bundle: 255.38 KB main

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 06:46:19 -05:00
4fd7306d01 Merge pull request 'feat(perception): appearance-based person re-identification (Issue #322)' (#330) from sl-perception/issue-322-person-reid into main 2026-03-03 06:46:07 -05:00
4dbb4c6f0d feat(perception): appearance-based person re-identification (Issue #322)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 12s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Adds PersonTrack/PersonTrackArray msgs and a PersonReidNode that matches
individuals across camera views using HSV colour histogram appearance
features and cosine similarity, with EMA gallery update and 30s stale timeout.

New messages (saltybot_scene_msgs):
  msg/PersonTrack.msg        — track_id, camera_id, bbox, confidence,
                               first_seen, last_seen, is_stale
  msg/PersonTrackArray.msg   — array wrapper with header

New files (saltybot_bringup):
  saltybot_bringup/_person_reid.py    — pure kinematics (no ROS2 deps)
    extract_hsv_histogram()  2-D HS histogram (H=16, S=8 → 128-dim, L2-norm)
    cosine_similarity()      handles zero/non-unit vectors
    match_track()            best gallery match above threshold (strict >)
    TrackGallery             add/update/match/mark_stale/prune_stale
    TrackEntry               mutable dataclass; EMA feature blend (α=0.3)
  saltybot_bringup/person_reid_node.py
    Subscribes /camera/color/image_raw + /saltybot/scene/objects (BEST_EFFORT)
    Crops COCO person (class_id=0) ROIs; extracts features; matches gallery
    Publishes PersonTrackArray on /saltybot/person_tracks at 5 Hz
    Parameters: camera_id, similarity_threshold=0.75, stale_timeout_s=30,
                max_tracks=20, publish_hz=5.0
  test/test_person_reid.py   — 50 tests, all passing

Modified:
  saltybot_scene_msgs/CMakeLists.txt  — register PersonTrack/Array msgs
  saltybot_bringup/setup.py           — add person_reid console_script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 06:45:43 -05:00
f506c89960 Merge pull request 'feat(social): USB camera hot-plug monitor (Issue #320)' (#328) from sl-jetson/issue-320-camera-hotplug into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 4s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-03 06:45:23 -05:00
00c94cfe0d Merge pull request 'feat: Add battery-aware speed limiter ROS2 node (Issue #321)' (#327) from sl-controls/issue-321-battery-speed into main 2026-03-03 06:45:18 -05:00
a3386e1694 feat(social): USB camera hot-plug monitor (Issue #320)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 5s
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
Polls /dev/video* at 2 Hz, drives a three-state machine
(connected/disconnected/restarting) and publishes to
/saltybot/camera_status (std_msgs/String).  Reconnects within
restart_grace_s (5 s) → 'restarting' held for restart_hold_s (2 s)
to signal downstream capture pipelines to restart.  Scan function
is injected for offline testing. 82/82 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:47:27 -05:00
45828b1cc2 feat: Add battery-aware speed limiter ROS2 node (Issue #321)
Implements saltybot_battery_speed_limiter package:
- Subscribes to /saltybot/battery_state, publishes speed limit factor
- Battery percentage thresholds: 100-50%=1.0, 50-25%=0.7, 25-15%=0.4, <15%=0.0
- Reduces speed to preserve battery when running low
- Automatically stops movement below 15% to prevent deep discharge
- Configurable thresholds and speed factors
- Comprehensive test suite: 30+ tests covering all threshold transitions
- Handles both direct percentage and voltage-based fallback calculation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 00:46:12 -05:00
23e05a634f feat(webui): gamepad teleoperation panel with dual-stick control (Issue #319)
Features:
- Virtual dual-stick gamepad interface
  * Left stick: linear velocity (forward/backward)
  * Right stick: angular velocity (turn left/right)
- Canvas-based rendering with real-time visual feedback
- Velocity vector visualization on sticks
- 10% deadzone on both axes for dead-stick detection

- WASD keyboard fallback for manual driving
  * W/S: forward/backward
  * A/D: turn left/right
  * Smooth blending between gamepad and keyboard input

- Speed limiter slider (0-100%)
  * Real-time control of max velocity
  * Disabled during e-stop state

- Emergency stop button (e-stop)
  * Immediate velocity zeroing
  * Visual status indicator (red when active)
  * Prevents accidental motion during e-stop

- ROS Integration
  * Publishes geometry_msgs/Twist to /cmd_vel
  * Max linear velocity: 0.5 m/s
  * Max angular velocity: 1.0 rad/s
  * 50ms update rate

- UI Features
  * Real-time velocity display (m/s and rad/s)
  * Status bar with control mode indicator
  * Speed limit percentage display
  * Control info with keyboard fallback guide

Integration:
- Replaces previous split-view control layout
- Enhanced TELEMETRY/control tab
- Comprehensive gamepad + keyboard control system

Build: 120 modules, no errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 00:43:47 -05:00
ffc69a05c0 Merge pull request 'feat(perception): wheel encoder differential drive odometry (Issue #184)' (#318) from sl-perception/issue-184-wheel-odom into main 2026-03-03 00:42:27 -05:00
067a871103 feat(perception): wheel encoder differential drive odometry (Issue #184)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 7s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Adds saltybot_bridge_msgs package with WheelTicks.msg (int32 left/right
encoder counts) and a WheelOdomNode that subscribes to
/saltybot/wheel_ticks, integrates midpoint-Euler differential drive
kinematics (handling int32 counter rollover), and publishes
nav_msgs/Odometry on /odom_wheel at 50 Hz with optional TF broadcast.

New files:
  jetson/ros2_ws/src/saltybot_bridge_msgs/
    msg/WheelTicks.msg
    CMakeLists.txt, package.xml

  jetson/ros2_ws/src/saltybot_bringup/
    saltybot_bringup/_wheel_odom.py     — pure kinematics (no ROS2 deps)
    saltybot_bringup/wheel_odom_node.py — 50 Hz timer node + TF broadcast
    test/test_wheel_odom.py             — 42 tests, all passing

Modified:
  saltybot_bringup/package.xml  — add saltybot_bridge_msgs, nav_msgs deps
  saltybot_bringup/setup.py     — add wheel_odom console_script entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:41:39 -05:00
b96c6b96d0 Merge pull request 'feat(social): audio wake-word detector 'hey salty' (Issue #320)' (#317) from sl-jetson/wake-word-detect into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 10s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 10s
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
2026-03-03 00:41:22 -05:00
d5e0c58594 Merge pull request 'feat: Add velocity smoothing filter ROS2 node' (#316) from sl-controls/velocity-smooth-filter into main 2026-03-03 00:41:16 -05:00
d6553ce3d6 feat(social): audio wake-word detector 'hey salty' (Issue #320)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 10s
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
Energy-gated log-mel + cosine-similarity wake-word node. Subscribes to
/social/speech/audio_raw (PCM-16 UInt8MultiArray), maintains a 1.5 s
sliding ring buffer, runs detection every 100 ms; fires Bool(True) on
/saltybot/wake_word_detected with 2 s cooldown. Template loaded from
.npy file; passive (no detections) when template_path is empty.
91/91 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:26:59 -05:00
b23b208d46 Merge pull request 'feat(webui): ROS node list viewer' (#315) from sl-webui/node-list-viewer into main 2026-03-03 00:24:03 -05:00
2919e629e9 feat: Add velocity smoothing filter with Butterworth low-pass filtering
Implements saltybot_velocity_smoother package:
- Subscribes to /odom, applies digital Butterworth low-pass filter
- Filters linear (x,y,z) and angular (x,y,z) velocity components independently
- Publishes smoothed odometry on /odom_smooth
- Reduces encoder jitter and improves state estimation stability
- Configurable filter order (1-4), cutoff frequency (Hz), publish rate
- Can be enabled/disabled via enable_smoothing parameter
- Comprehensive test suite: 18+ tests covering filter behavior, edge cases, scenarios

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 00:23:53 -05:00
3f558046c5 feat(webui): ROS node list viewer with heartbeat status
Features:
- Enumerate active ROS nodes from /rosout activity
- Real-time node status tracking (alive/dead based on heartbeat)
- Heartbeat timeout: 5 seconds without updates = dead
- Display node name, status, uptime, and last seen timestamp
- Color-coded status indicators (green=alive, gray=dead)
- Sortable table with node statistics
- Summary card showing alive/dead node counts
- Periodic status polling every 2 seconds

Integration:
- Added to MONITORING tab group as 'Nodes' tab
- Subscribes to /rosout (rcl_interfaces/Log) to detect active nodes
- Real-time updates with smooth transitions

Build: 119 modules, no errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-03 00:21:35 -05:00
d468cb515e Merge pull request 'feat(social): personal space respector node (Issue #310)' (#314) from sl-jetson/issue-310-personal-space into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 5s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 15s
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
2026-03-03 00:20:10 -05:00
0b35a61217 Merge pull request 'feat(perception): sky detector for outdoor navigation (Issue #307)' (#313) from sl-perception/issue-307-sky-detect into main 2026-03-03 00:20:03 -05:00
0342caecdb Merge pull request 'feat(webui): thermal status gauge with CPU/GPU temperature display (Issue #308)' (#312) from sl-webui/issue-308-temp-gauge into main 2026-03-03 00:19:58 -05:00
6d80ca35af Merge pull request 'Adaptive PID gain scheduler (Issue #309)' (#311) from sl-controls/issue-309-gain-schedule into main 2026-03-03 00:19:46 -05:00
b8a14e2bfc feat(social): personal space respector node (Issue #310)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 5s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
Subscribes /social/faces/detected + /social/person_states; backs robot
away (cmd_vel linear.x = -backup_speed) when closest person <= 0.8 m,
latches /saltybot/too_close Bool; hysteresis band prevents chatter.
92/92 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:42:33 -05:00
e24c0b2e26 feat(perception): sky detector for outdoor navigation — Issue #307
- Add _sky_detector.py: SkyResult NamedTuple; detect_sky() with dual HSV
  band masking (blue sky H∈[90,130]/S∈[40,255]/V∈[80,255] OR overcast
  S∈[0,50]/V∈[185,255]), cv2.bitwise_or combined mask; sky_fraction over
  configurable top scan_frac region; horizon_y = bottommost row where
  per-row sky fraction ≥ row_threshold (−1 when no sky detected)
- Add sky_detect_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
  publishes Float32 /saltybot/sky_fraction and Int32 /saltybot/horizon_y
  per frame; scan_frac (default 0.60) and row_threshold (default 0.30) params
- Register sky_detector console script in setup.py
- 33/33 unit tests pass (no ROS2 required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:39:28 -05:00
20801c4a0e feat(webui): thermal status gauge with CPU/GPU temperature display (Issue #308)
Features:
- Circular gauge visualization for CPU and GPU temperatures
- Color-coded temperature zones: green <60°C, yellow 60-75°C, red >75°C
- Real-time needle pointer animation
- Fan speed percentage display for each sensor
- Peak temperature tracking and max reached indicator
- Thermal alert status (Normal/Caution/Critical)
- ROS subscription to /saltybot/thermal_status
- Integrated into TELEMETRY tab group

Build: 118 modules, no errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 21:37:59 -05:00
fc87862603 feat(controls): Adaptive PID gain scheduler (Issue #309)
Implements ROS2 adaptive PID gain scheduler for SaltyBot with:
- Subscribes to /saltybot/speed_scale for speed conditions
- Subscribes to /saltybot/terrain_roughness for surface conditions
- Adjusts PID gains dynamically:
  * P gain increases with terrain roughness (better response on rough)
  * D gain decreases at low speed (prevent oscillation when slow)
  * I gain scales with both conditions for stability
- Publishes Float32MultiArray [Kp, Ki, Kd] on /saltybot/pid_gains
- Configurable scaling factors for each gain modulation
- Includes 18+ unit tests for gain scheduling logic

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 21:37:08 -05:00
b8f9d3eca6 feat(mechanical): RPLIDAR A1 parametric dust/splash cover (Issue #301) 2026-03-02 21:36:58 -05:00
4c751e576b Merge pull request 'feat(webui): motor current live graph (#297)' (#306) from sl-webui/issue-297-motor-graph into main 2026-03-02 21:35:56 -05:00
becd0bc717 Merge pull request 'feat(perception): terrain roughness estimator via Gabor + LBP (Issue #296)' (#305) from sl-perception/issue-296-terrain-rough into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
2026-03-02 21:35:40 -05:00
a8838cfbbd Merge pull request 'feat(social): conversation topic memory (Issue #299)' (#304) from sl-jetson/issue-299-topic-memory into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 13s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 21:35:35 -05:00
86d798afe7 Merge pull request 'Geofence boundary enforcer (Issue #298)' (#302) from sl-controls/issue-298-geofence into main 2026-03-02 21:35:26 -05:00
3bf603f685 feat(perception): terrain roughness estimator via Gabor + LBP — Issue #296
- Add _terrain_roughness.py: RoughnessResult NamedTuple; gabor_energy() with
  4-orientation × 2-wavelength (5px, 10px) quadrature Gabor bank, DC removal
  via image mean subtraction (prevents false high energy on uniform surfaces);
  lbp_variance() using 8-point radius-1 LBP in vectorised numpy slice
  comparisons (no sklearn); estimate_roughness() with bottom roi_frac crop,
  normalised blend roughness = 0.5*(gabor/500) + 0.5*(lbp/5000) clipped [0,1]
- Add terrain_rough_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
  publishes Float32 /saltybot/terrain_roughness at 2Hz (configurable via
  publish_hz param); roi_frac param default 0.40 (bottom 40% = floor region)
- Register terrain_roughness console script in setup.py
- 37/37 unit tests pass (no ROS2 required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 21:12:51 -05:00
797ed711b9 feat(social): conversation topic memory — Issue #299
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 9s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
Adds topic_memory_node to saltybot_social:
- Subscribes /social/conversation_text (String, JSON {person_id, text})
- Extracts keywords via stop-word filtered pipeline:
    lowercase -> strip punctuation -> filter length/alpha -> remove
    stop words -> deduplicate -> cap at max_keywords_per_msg
- Maintains per-person rolling topic history (PersonTopicMemory):
    ordered list, dedup with recency promotion, capped at
    max_topics_per_person, evicts oldest when full
- Prunes persons idle for > prune_after_s (default 30 min; 0=disabled)
- Publishes JSON on /saltybot/conversation_topics on each utterance:
    {person_id, recent_topics (most-recent first), new_topics, ts}
- Enables recall like 'last time you mentioned X'
- 71/71 tests passing

Closes #299
2026-03-02 21:11:14 -05:00
f12f0bdc2b feat(webui): motor current live graph (Issue #297)
Real-time motor current visualization with:
- Subscribes to /saltybot/motor_currents for dual-motor current data
- Rolling 60-second history window with automatic data culling
- Dual-axis line chart for left (cyan) and right (amber) motor amps
- Canvas-based rendering for performance
- Thermal warning threshold line (25A, configurable)
- Real-time statistics:
  * Current draw for left and right motors
  * Peak current tracking over 60-second window
  * Average current calculation
  * Thermal status indicator with warning badge
- Color-coded thermal alerts:
  * Red background when threshold exceeded
  * Warning indicator and message
- Grid overlay, axis labels, time labels, legend
- Takes absolute value of currents (handles reverse direction)

Integrated into TELEMETRY tab group as 'Motor Current' tab.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 21:09:17 -05:00
aa0c3da022 feat(mechanical): RPLIDAR A1 parametric dust/splash cover (Issue #301) 2026-03-02 21:08:12 -05:00
71e49e1e9d feat(webui): motor current live graph (Issue #297)
Real-time motor current visualization with:
- Subscribes to /saltybot/motor_currents for dual-motor current data
- Rolling 60-second history window with automatic data culling
- Dual-axis line chart:
  * Left motor current (cyan) in Amps
  * Right motor current (amber) in Amps
- Canvas-based rendering for performance
- Thermal warning threshold line (25A, configurable):
  * Dashed red line overlay on chart
  * Configurable threshold constant for motor specs
  * Auto-detects thermal warnings when threshold exceeded
- Real-time statistics display:
  * Current draw for left and right motors (real-time)
  * Peak current tracking over 60-second window
  * Average current calculation
  * Thermal status indicator with warning badge
- Color-coded thermal alerts:
  * Red background when threshold exceeded
  * Warning indicator with icon
  * Left/right motor temperature monitoring
- Grid overlay, axis labels, time labels, legend
- Takes absolute value of currents (handles reverse direction)

Integrated into TELEMETRY tab group as 'Motor Current' tab.
Follows established canvas rendering patterns from BatteryChart.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 21:07:26 -05:00
bfd291cbdd feat(controls): Geofence boundary enforcer (Issue #298)
Implements ROS2 geofence enforcer for SaltyBot with:
- Loads polygon geofence from params (list of x/y vertices)
- Subscribes to /odom for real-time robot position
- Point-in-polygon ray casting algorithm for boundary checking
- Publishes Bool on /saltybot/geofence_breach on boundary violation
- Optional enforcement flag for cmd_vel zeroing
- Configurable safety margin
- Includes 20+ unit tests for geometry and breach detection

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 21:06:58 -05:00
accda32c7a Merge pull request 'feat(webui): topic bandwidth monitor (#287)' (#295) from sl-webui/issue-287-bandwidth into main 2026-03-02 21:05:45 -05:00
b3edabc9c5 Merge pull request 'feat: Add RGB status LED state machine (Issue #290)' (#294) from sl-firmware/issue-290-rgb-fsm into main 2026-03-02 21:05:34 -05:00
b087af4b94 Merge pull request 'feat(social): speech volume auto-adjuster (Issue #289)' (#293) from sl-jetson/issue-289-volume-adjust into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 10s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 21:05:28 -05:00
1a47abf5ae Merge pull request 'feat(perception): motion blur detector via Laplacian variance (Issue #286)' (#292) from sl-perception/issue-286-blur-detect into main 2026-03-02 21:05:23 -05:00
6518171d42 Merge pull request 'Cliff and drop-off detection safety node (Issue #288)' (#291) from sl-controls/issue-288-cliff-stop into main 2026-03-02 21:05:18 -05:00
a4d29376b8 Merge pull request 'feat(mechanical): Cable management clips (Issue #264)' (#283) from sl-mechanical/issue-264-cable-clips into main 2026-03-02 21:05:14 -05:00
70e94dc100 feat: Add RGB status LED state machine (Issue #290)
Implements STM32F722 driver for WS2812 NeoPixel 8-LED ring with finite state machine.

Features:
- 8 operational states with animations:
  * BOOT: Blue pulse (0.5 Hz)
  * IDLE: Green breathe (0.5 Hz)
  * ARMED: Solid green
  * NAV: Cyan spin (1 Hz)
  * ERROR: Red flash (2 Hz)
  * LOW_BATT: Orange blink (1 Hz)
  * CHARGING: Green fill (1 Hz)
  * ESTOP: Red solid
- Non-blocking tick-based animation system
- State transitions via API
- PWM control on PB4 (TIM3_CH1) at 800 kHz
- Color interpolation for smooth effects

All 25 unit tests passing covering state transitions, animations, timing, and edge cases.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 20:49:26 -05:00
e8b787a987 feat(social): speech volume auto-adjuster — Issue #289
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 9s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 8s
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
Adds volume_adjust_node to saltybot_social:
- Subscribes /social/speech/energy (Float32 linear RMS [0..1] from vad_node)
- Maps energy to TTS volume via:
    1. Normalise into [noise_floor, noise_ceil]
    2. Power-curve shaping (curve_gamma; <1 concave, 1 linear, >1 convex)
    3. Lerp to [min_volume, max_volume]
    4. Exponential moving-average smoothing (smoothing_alpha)
- Publishes Float32 on /saltybot/tts_volume at publish_rate Hz (default 5 Hz)
- Holds last value when energy is stale (> stale_timeout_s)
- All params exposed: min/max_volume, noise_floor/ceil, curve_gamma,
  smoothing_alpha, publish_rate, stale_timeout_s
- 74/74 tests passing

Closes #289
2026-03-02 20:47:53 -05:00
c5f3a5b2ce feat(perception): motion blur detector via Laplacian variance — Issue #286
- Add _blur_detector.py: BlurResult NamedTuple, laplacian_variance() (ksize=3
  Laplacian on greyscale, with optional ROI crop), detect_blur() returning
  variance + is_blurred flag + threshold; handles greyscale and BGR inputs,
  empty ROI returns 0.0
- Add blur_detect_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
  publishes Bool /saltybot/image_blurred and Float32 /saltybot/blur_score per
  frame; threshold and roi_frac ROS params
- Register blur_detector console script in setup.py
- 25/25 unit tests pass (no ROS2 required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 20:46:37 -05:00
793ba31ada feat(webui): ROS topic bandwidth monitor (Issue #287)
Topic bandwidth tracking and visualization with:
- Tracks message rates for all subscribed ROS topics
- Estimates bandwidth based on message size and frequency
- Message size estimated from JSON serialization
- Updates every 1 second with rolling 30-second history window
- Sortable table display:
  * Topic name with truncation for long names
  * Message rate (messages per second)
  * Average message size (bytes)
  * Bandwidth estimate (B/s, KB/s, or MB/s)
  * Sparkline mini-chart showing bandwidth trend
- Total bandwidth summary at top
- Click column headers to sort (ascending/descending toggle)
- Visual indicators with color-coded columns

Integrated into MONITORING tab group as 'Bandwidth' tab.
Component provides window.__trackRosMessage() hook for optional
bandwidth tracking integration with ROS bridge.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 20:45:41 -05:00
dab0f96399 feat(controls): Cliff and drop-off detection safety node (Issue #288)
Implements ROS2 cliff detector for SaltyBot with:
- Subscribes to /saltybot/cliff_sensors (IR range array)
- Threshold-based detection (default 0.5m)
- Debouncing (3 consecutive frames) for robustness
- Majority voting (min 2 sensors) for safety
- Publishes Bool on /saltybot/cliff_detected
- Emergency stop trigger on cliff/drop-off detection
- Includes 15+ unit tests for detection logic

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 20:45:21 -05:00
479a33a6fa feat(mechanical): parametric cable management clips (Issue #264)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
2026-03-02 20:44:27 -05:00
d3806094ee Merge pull request 'feat(webui): battery history chart (#280)' (#285) from sl-webui/issue-280-battery-chart into main 2026-03-02 20:44:16 -05:00
d1f0e95fa2 Merge pull request 'feat(social): face-tracking head servo controller (Issue #279)' (#284) from sl-jetson/issue-279-face-track-servo into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 14s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 20:44:00 -05:00
5f3b5caef7 Merge pull request 'IMU calibration routine (Issue #278)' (#282) from sl-controls/issue-278-imu-cal into main 2026-03-02 20:43:52 -05:00
cb8f6c82a4 Merge pull request 'feat(perception): HSV color object segmenter (Issue #274)' (#281) from sl-perception/issue-274-color-segment into main 2026-03-02 20:43:46 -05:00
de1166058c Merge pull request 'feat: Add cooling fan PWM speed controller (Issue #263)' (#276) from sl-firmware/issue-263-fan-pwm into main 2026-03-02 20:43:38 -05:00
c3d36e9943 feat(social): face-tracking head servo controller — Issue #279
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 8s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 8s
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
Adds face_track_servo_node to saltybot_social:
- Subscribes /social/faces/detected (FaceDetectionArray)
- Picks closest face by largest bbox area (proximity proxy)
- Computes pan/tilt error from bbox centre vs image centre using
  configurable FOV (fov_h_deg=60°, fov_v_deg=45°)
- Independent PID controllers for pan and tilt (velocity/incremental
  output with anti-windup); servo position integrates velocity*dt
- Clamps commands to ±pan_limit_deg / ±tilt_limit_deg
- Returns to centre at return_rate_deg_s when face lost >lost_timeout_s
- Dead zone suppresses jitter for small errors
- Publishes Float32 on /saltybot/head_pan and /saltybot/head_tilt
- 81/81 tests passing

Closes #279
2026-03-02 17:38:02 -05:00
fc26226368 feat(webui): battery voltage and percentage history chart (Issue #280)
Real-time battery history visualization with:
- Subscribes to /saltybot/battery_state for continuous battery data
- Rolling 30-minute history window with automatic data culling
- Dual-axis line chart: voltage (left, cyan) and percentage (right, amber)
- Canvas-based rendering for performance
- Charge/discharge rate calculation (last 5-minute average):
  * Voltage rate in mV/min with up/down/stable indicator
  * Percentage rate in %/min with up/down/stable indicator
- Grid overlay, axis labels, time labels, and legend
- Current stats display: voltage, percentage, rates
- Responsive canvas sizing

Integrated into TELEMETRY tab group as 'Battery History' tab.
Follows established canvas rendering and data subscription patterns.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 17:36:18 -05:00
dd033b9827 feat(controls): IMU calibration routine (Issue #278)
Implements ROS2 IMU gyro + accel calibration node with:
- Service-triggered calibration via /saltybot/calibrate_imu
- Optional auto-calibration on startup (configurable)
- Collects N stationary samples (default 100)
- Computes mean bias offsets for gyro and accel
- Publishes bias-corrected IMU on /imu/calibrated
- Includes 10+ unit tests for calibration logic

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 17:35:30 -05:00
f5093ecd34 feat(perception): HSV color object segmenter — Issue #274
- Add ColorDetection.msg + ColorDetectionArray.msg to saltybot_scene_msgs
- Add _color_segmenter.py: HsvRange/ColorBlob types, COLOR_RANGES defaults,
  mask_for_color() (dual-band red wrap), find_color_blobs() with morph open,
  contour extraction, area filter and max-blob-per-color limit
- Add color_segment_node.py: subscribes /camera/color/image_raw (BEST_EFFORT),
  publishes /saltybot/color_objects (ColorDetectionArray) per frame;
  active_colors, min_area_px, max_blobs_per_color params
- Add saltybot_scene_msgs exec_depend to saltybot_bringup/package.xml
- Register color_segmenter console_script in setup.py
- 34/34 unit tests pass (no ROS2 required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 17:32:41 -05:00
54bc37926b Merge pull request 'feat(webui): system log tail viewer (#275)' (#277) from sl-webui/issue-275-log-viewer into main 2026-03-02 17:31:01 -05:00
30ad71e7d8 Merge pull request 'feat(social): proximity-based greeting trigger (Issue #270)' (#272) from sl-jetson/issue-270-greeting-trigger into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 10s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 17:30:54 -05:00
b6104763c5 Merge pull request 'feat(controls): Wheel slip detector (Issue #262)' (#266) from sl-controls/issue-262-wheel-slip into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Has been cancelled
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been cancelled
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 17:30:52 -05:00
5108fa8fa1 feat(controls): Wheel slip detector (Issue #262)
Detect wheel slip by comparing commanded velocity vs actual encoder velocity.
Publishes Bool flag on /saltybot/wheel_slip_detected when slip detected >0.5s.

Features:
- Subscribe to /cmd_vel (commanded) and /odom (actual velocity)
- Compare velocity magnitudes with 0.1 m/s threshold
- Persistence: slip must persist >0.5s to trigger (debounces transients)
- Publish Bool on /saltybot/wheel_slip_detected with detection status
- 10Hz monitoring frequency, configurable parameters

Algorithm:
- Compute linear speed from x,y components
- Calculate velocity difference
- If exceeds threshold: increment slip duration
- If duration > timeout: declare slip detected

Benefits:
- Detects environmental slip (ice, mud, wet surfaces)
- Triggers speed reduction to maintain traction
- Prevents wheel spinning/rut digging
- Safety response for loss of grip

Topics:
- Subscribed: /cmd_vel (Twist), /odom (Odometry)
- Published: /saltybot/wheel_slip_detected (Bool)

Config: frequency=10Hz, slip_threshold=0.1 m/s, slip_timeout=0.5s

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 17:29:03 -05:00
5362536fb1 feat(webui): waypoint editor with click-to-navigate (Issue #261) 2026-03-02 17:29:03 -05:00
305ce6c971 feat(webui): system log tail viewer (Issue #275)
Real-time ROS log stream viewer with:
- Subscribes to /rosout (rcl_interfaces/Log)
- Severity-based color coding:
  DEBUG=grey | INFO=white | WARN=yellow | ERROR=red | FATAL=magenta
- Filter by severity level (multi-select toggle)
- Filter by node name (text input)
- Auto-scroll to latest logs
- Max 500 logs in history (configurable)
- Scrolling log output in monospace font
- Proper timestamp formatting (HH:MM:SS)

Integrated into MONITORING tab group as 'Logs' tab alongside 'Events'.
Follows established React/Tailwind patterns from other dashboard components.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 17:28:28 -05:00
c7dd07f9ed feat(social): proximity-based greeting trigger — Issue #270
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 12s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (pull_request) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Adds greeting_trigger_node to saltybot_social:
- Subscribes to /social/faces/detected (FaceDetectionArray) for face arrivals
- Subscribes to /social/person_states (PersonStateArray) to cache face_id→distance
- Fires greeting when face_id is within proximity_m (default 2m) and
  not in per-face_id cooldown window (default 300s)
- Publishes JSON on /saltybot/greeting_trigger:
  {face_id, person_name, distance_m, ts}
- unknown_distance param controls assumed distance for faces with no PersonState yet
- Thread-safe distance cache and greeted map
- 50/50 tests passing

Closes #270
2026-03-02 17:26:40 -05:00
0776003dd3 Merge pull request 'feat(webui): robot status dashboard header (#269)' (#273) from sl-webui/issue-269-status-header into main 2026-03-02 17:26:40 -05:00
01ee02f837 Merge pull request 'feat(bringup): D435i depth hole filler via bilateral interpolation (Issue #268)' (#271) from sl-perception/issue-268-depth-holes into main 2026-03-02 17:26:22 -05:00
f0e11fe7ca feat(bringup): depth image hole filler via bilateral interpolation (Issue #268)
Adds multi-pass spatial-Gaussian hole filler for D435i depth images.
Each pass replaces zero/NaN pixels with the Gaussian-weighted mean of valid
neighbours in a growing kernel (×1, ×2.5, ×6 default); original valid
pixels are never modified.  Handles uint16 mm → float32 m conversion,
border pixels via BORDER_REFLECT, and above-d_max pixels as holes.
Publishes filled float32 depth on /camera/depth/filled at camera rate.
37/37 pure-Python tests pass (no ROS2 required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 14:19:27 -05:00
201dea4c01 feat(webui): robot status dashboard header bar (Issue #269)
Persistent top bar showing real-time robot health indicators:
- Battery percentage and voltage (color-coded: green >60%, amber 30-60%, red <30%)
- WiFi signal strength (RSSI dBm with quality assessment)
- Motor status and current draw in Amperes
- Emergency state indicator (red highlight when active)
- System uptime in hours and minutes
- Current operational mode (idle/nav/social/docking)
- Connection status indicator

Component subscribes to relevant ROS topics and displays in compact
flex layout matching dashboard dark theme.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 14:19:03 -05:00
477258f321 Merge pull request 'feat(webui): waypoint editor with click-to-navigate (#261)' (#267) from sl-webui/issue-261-waypoint-editor-fix into main 2026-03-02 14:13:54 -05:00
94a6f0787e Merge pull request 'feat(bringup): visual odometry drift detector (Issue #260)' (#265) from sl-perception/issue-260-vo-drift into main 2026-03-02 14:13:34 -05:00
50636de5a9 Merge pull request 'feat(social): ambient sound classifier via mel-spectrogram (Issue #252)' (#258) from sl-jetson/issue-252-ambient-sound into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 11s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 14:13:33 -05:00
c348e093ef feat: Add cooling fan PWM speed controller (Issue #263)
Implements STM32F722 driver for brushless cooling fan on PA9 using TIM1_CH2 PWM.
Features:
- Temperature-based speed curve: off <40°C, 30% at 50°C, 100% at 70°C
- Smooth speed ramp transitions with configurable rate (default 0.05%/ms)
- Linear interpolation between curve points
- PWM duty cycle control (0-100%)
- State transitions and edge case handling

All 51 unit tests passing:
- Temperature curve verification (6 test zones)
- Speed boundaries and transitions
- Ramp timing and rate control
- PWM duty cycle calculation
- Temperature extremes and boundary conditions

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 13:29:18 -05:00
c865e84e16 feat(webui): waypoint editor with click-to-navigate (Issue #261)
Interactive waypoint editor for Nav2 goal-based navigation:
- Click on map display to place waypoints
- Drag waypoints in list to reorder navigation sequence
- Right-click waypoints to delete them
- Visual waypoint overlay on map with numbering
- Robot position indicator at center
- Waypoint list sidebar with selection and ordering
- Send Nav2 goal to individual selected waypoint
- Execute all waypoints in sequence with automatic progression
- Clear all waypoints button
- Real-time coordinate display and robot pose tracking
- Integrated into new NAVIGATION tab group
- Uses /navigate_to_pose service for goal publishing

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 13:28:01 -05:00
9d12805843 feat(bringup): visual odometry drift detector (Issue #260)
Adds sliding-window drift detector that compares cumulative path lengths
of visual odom and wheel odom over a configurable window (default 10 s).
Drift = |vo_path − wheel_path|; flagged when ≥ 0.5 m (configurable).
OdomBuffer handles per-source rolling storage with automatic age eviction.
Publishes Bool on /saltybot/vo_drift_detected and Float32 on
/saltybot/vo_drift_magnitude at 2 Hz.  27/27 pure-Python tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 13:26:07 -05:00
03999e7f91 Merge pull request 'feat(webui): SLAM map viewer with occupancy grid (#250)' (#259) from sl-webui/issue-250-map-viewer into main 2026-03-02 13:22:40 -05:00
3cd9faeed9 feat(social): ambient sound classifier via mel-spectrogram — Issue #252
Some checks failed
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
Adds ambient_sound_node to saltybot_social:
- Accumulates 1 s of PCM-16 audio from /social/speech/audio_raw
- Extracts mel-spectrogram feature vector (energy_db, zcr, mel_centroid,
  mel_flatness, low_ratio, high_ratio) using pure numpy (no torch/onnx)
- Priority-cascade classifier: silence → music → speech → crowd → outdoor → alarm
- Publishes label as std_msgs/String on /saltybot/ambient_sound on each buffer fill
- All 11 thresholds exposed as ROS parameters (yaml + launch file)
- numpy-free energy-only fallback for edge environments
- 77/77 tests passing

Closes #252
2026-03-02 13:22:38 -05:00
5e40504297 Merge pull request 'feat: Piezo buzzer melody driver (Issue #253)' (#257) from sl-firmware/issue-253-buzzer into main 2026-03-02 13:22:22 -05:00
a55cd9c97f Merge pull request 'feat(bringup): floor surface type classifier on D435i RGB (Issue #249)' (#256) from sl-perception/issue-249-floor-classifier into main 2026-03-02 13:22:17 -05:00
a16cc06d79 Merge pull request 'feat(controls): Battery-aware speed scaling (Issue #251)' (#255) from sl-controls/issue-251-battery-speed into main 2026-03-02 13:22:12 -05:00
8f51390e43 feat: Add piezo buzzer melody driver (Issue #253)
Implements STM32F7 non-blocking driver for piezo buzzer on PA8 using TIM1 PWM.
Plays predefined melodies and custom sequences with melody queue.

Features:
- PA8 TIM1_CH1 PWM output with dynamic frequency control
- Predefined melodies: startup jingle, battery warning, error alert, docking chime
- Non-blocking melody queue with FIFO scheduling (4-slot capacity)
- Custom melody and simple tone APIs
- 15 musical notes (C4-C6) with duration presets
- Rest (silence) notes for composition
- 50% duty cycle for optimal piezo buzzer drive

API Functions:
- buzzer_init(): Configure PA8 PWM and TIM1
- buzzer_play_melody(type): Queue predefined melody
- buzzer_play_custom(notes): Queue custom note sequence
- buzzer_play_tone(freq, duration): Queue simple tone
- buzzer_stop(): Stop playback and clear queue
- buzzer_is_playing(): Query playback status
- buzzer_tick(now_ms): Periodic timing update (10ms recommended)

Test Suite:
- 52 passing unit tests covering:
  * Melody structure and termination
  * Simple and multi-note playback
  * Frequency transitions
  * Queue management
  * Timing accuracy
  * Rest notes in sequences
  * Musical frequency ranges

Integration:
- Called at startup and ticked every 10ms in main loop
- Used for startup jingle, battery warnings, error alerts, success feedback

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:51:42 -05:00
32857435a1 feat(bringup): floor surface type classifier on D435i RGB (Issue #249)
Adds multi-feature nearest-centroid classifier for 6 surface types:
carpet, tile, wood, concrete, grass, gravel.  Features: circular hue mean,
saturation mean/std, brightness, Laplacian texture variance, Sobel edge
density — all extracted from the bottom 40% of each frame (floor ROI).
Majority-vote temporal smoother (window=5) suppresses single-frame noise.
Publishes std_msgs/String on /saltybot/floor_type at 2 Hz.
34/34 pure-Python tests pass (no ROS2 required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:51:14 -05:00
161cba56d4 feat(webui): map viewer with SLAM occupancy grid (Issue #250) 2026-03-02 12:49:48 -05:00
7d7f1c0e5b feat(controls): Battery-aware speed scaling (Issue #251)
Implement dynamic speed scaling based on battery charge level to extend operational range.
Reduces maximum velocity when battery is low to optimize power consumption.

Battery Scaling Strategy:
- 100-50% charge: 1.0 scale (full speed - normal operation)
- 50-20% charge:  0.7 scale (70% speed - warning zone)
- <20% charge:    0.4 scale (40% speed - critical zone)

Features:
- Subscribe to /saltybot/battery_state (sensor_msgs/BatteryState)
- Publish /saltybot/speed_scale (std_msgs/Float32) with scaling factor
- Configurable thresholds and scaling factors via YAML
- 1Hz monitoring frequency (sufficient for battery state changes)
- Graceful defaults when battery state unavailable

Benefits:
- Extends operational range by 30-40% when running at reduced speed
- Prevents over-discharge that damages battery
- Smooth degradation: no sudden stops, gradual speed reduction
- Allows mission completion even with battery warnings

Algorithm:
- Monitor battery percentage from BatteryState message
- Apply threshold-based scaling:
  if percentage >= 50%: scale = 1.0
  elif percentage >= 20%: scale = 0.7
  else: scale = 0.4
- Publish scaling factor for downstream speed limiter to apply

Configuration:
- critical_threshold: 0.20 (20%)
- warning_threshold: 0.50 (50%)
- full_scale: 1.0
- warning_scale: 0.7
- critical_scale: 0.4

Test Coverage:
- 20+ unit tests covering:
  - Node initialization and parameters
  - Battery state subscription
  - All scaling thresholds (100%, 75%, 50%, 30%, 20%, 10%, 1%)
  - Boundary conditions at exact thresholds
  - Default behavior without battery state
  - Scaling factor hierarchy validation
  - Threshold ordering validation
  - Realistic scenarios: gradual discharge, sudden drops, recovery,
    mission planning, critical mode, oscillating levels, deep discharge

Topics:
- Subscribed: /saltybot/battery_state (sensor_msgs/BatteryState)
- Published: /saltybot/speed_scale (std_msgs/Float32)

Use Case:
Pair with saltybot_cmd_vel_mux and accel_limiter:
cmd_vel → speed_scaler (battery) → accel_limiter (smooth) → cmd_vel_smooth

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:48:16 -05:00
c7a33bace8 Merge pull request 'feat(webui): conversation history panel (#240)' (#248) from sl-webui/issue-240-conversation-history into main 2026-03-02 12:46:32 -05:00
82e836ec3f Merge pull request 'feat(social): energy+ZCR voice activity detection node (Issue #242)' (#247) from sl-jetson/issue-242-vad into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 10s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 12:46:26 -05:00
3c276b0bce Merge pull request 'feat(controls): Smooth acceleration rate limiter (Issue #241)' (#246) from sl-controls/issue-241-smooth-accel into main 2026-03-02 12:46:22 -05:00
81786f5e4e Merge pull request 'feat: HC-SR04 ultrasonic distance sensor driver (Issue #243)' (#245) from sl-firmware/issue-243-ultrasonic into main 2026-03-02 12:46:18 -05:00
fcc492ade0 Merge pull request 'feat(bringup): LIDAR Euclidean object clustering + RViz visualisation (Issue #239)' (#244) from sl-perception/issue-239-lidar-clustering into main 2026-03-02 12:46:11 -05:00
4919dc0bc6 feat(social): energy+ZCR voice activity detection node (Issue #242)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
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) (push) Has been cancelled
social-bot integration tests / Latency profiling (GPU, Orin) (pull_request) Has been cancelled
Add vad_node to saltybot_social: subscribes to /social/speech/audio_raw
(UInt8MultiArray PCM-16), computes RMS energy (dBFS) and zero-crossing
rate per chunk, applies onset/offset hysteresis (VadStateMachine), and
publishes /social/speech/is_speaking (Bool) and /social/speech/energy
(Float32 linear RMS). All thresholds configurable via ROS params:
rms_threshold_db=-35.0, zcr_min=0.01, zcr_max=0.40, onset_frames=2,
offset_frames=8, audio_topic. 69/69 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:26:01 -05:00
60d8d19342 feat(controls): Smooth acceleration rate limiter (Issue #241)
Implement rate-limiting for velocity commands to prevent jerky motion.
Applies independent acceleration/deceleration limits to linear and angular velocity.

Features:
- Smooth acceleration/deceleration rate limiting
  Default: 0.5 m/s² linear, 1.0 rad/s² angular (configurable)
- Independent limits for each velocity component (x, y, z linear; x, y, z angular)
- Calculates maximum change per control cycle: limit * dt
- Clamps velocity changes to stay within acceleration envelope
- 50Hz control frequency with configurable parameters

Algorithm:
- Subscribe to /cmd_vel (target velocity)
- For each component: change = target - current
- Clamp change: |change| ≤ accel_limit * period
- Apply clamped change to current velocity
- Publish smoothed /cmd_vel_smooth

Benefits:
- Prevents jerky motion from sudden velocity jumps
- Protects mechanical systems from shock loads
- Enables gradual speed/direction changes
- Smooth tracking of dynamic targets

Test Coverage:
- 30+ unit tests covering:
  - Node initialization and parameter configuration
  - Individual component rate limiting (linear, angular)
  - Acceleration and deceleration scenarios
  - Multi-component simultaneous limiting
  - Reaching target velocity after multiple cycles
  - Emergency stops and rapid direction changes
  - Independent linear vs angular limits
  - Realistic scenarios: gradual acceleration, smooth stops, turns while moving,
    obstacle avoidance, continuous motion tracking, oscillating targets

Topics:
- Subscribed: /cmd_vel (geometry_msgs/Twist)
- Published: /cmd_vel_smooth (geometry_msgs/Twist)

Config: frequency=50Hz, linear_accel_limit=0.5 m/s², angular_accel_limit=1.0 rad/s²

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:22:58 -05:00
34a003d0b1 feat: Add HC-SR04 ultrasonic distance sensor driver (Issue #243)
Implements STM32F7 non-blocking driver for HC-SR04 ultrasonic ranger with TIM1 input capture.
Supports distance measurement via echo pulse width analysis.

Features:
- Trigger: PA0 GPIO output (10µs pulse)
- Echo: PA1 TIM1_CH2 input capture (both edges)
- TIM1 configured for 1MHz clock (1µs per count)
- Distance range: 20-5000mm (±3mm accuracy)
- Distance = (pulse_width_us / 2) / 29.1mm
- Non-blocking API with optional callback
- Timeout detection (30ms max echo wait)
- State machine: IDLE → TRIGGERED → MEASURING → COMPLETE/ERROR

API Functions:
- ultrasonic_init(): Configure GPIO and TIM1
- ultrasonic_trigger(): Start measurement
- ultrasonic_set_callback(): Register completion callback
- ultrasonic_get_state(): Query current state
- ultrasonic_get_result(): Retrieve measurement result
- ultrasonic_tick(): Periodic timeout handler

Test Suite:
- 26 passing unit tests
- Distance conversion accuracy (100mm-2000mm)
- State machine transitions
- Range validation (20-5000mm boundaries)
- Timeout detection
- Multiple sequential measurements

Integration:
- ultrasonic_init() called in main() startup after servo_init()
- Non-blocking operation suitable for autonomous navigation/obstacle avoidance

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:22:09 -05:00
04652c73f8 feat(webui): conversation history panel (Issue #240)
Implements a chat-style conversation viewer that subscribes to
/social/conversation_text and displays user speech (STT) and robot
responses (TTS) with timestamps and speaker labels. Includes auto-scroll
to latest message, manual scroll detection, and message history limiting.

- New component: ConversationHistory.jsx (chat-style message bubbles)
  - User messages in blue, robot responses in green
  - Auto-scrolling with manual scroll detection toggle
  - Timestamp formatting (HH:MM:SS)
  - Message history limiting (max 100 messages)
  - Clear history button
- Integrated into SOCIAL tab group as "History" tab
- Subscribes to /social/conversation_text topic
  (saltybot_social_msgs/ConversationMessage)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:21:38 -05:00
ff34f5ac43 feat(bringup): LIDAR Euclidean object clustering + RViz visualisation (Issue #239)
Adds gap-based Euclidean distance clustering of /scan LaserScan points.
Each cluster is published as a labelled semi-transparent CUBE + TEXT marker
in /saltybot/lidar_clusters (MarkerArray), sorted nearest-first.  Stale
markers from shrinking cluster counts are explicitly deleted each cycle.
22/22 pure-Python tests pass (no ROS2 required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:21:35 -05:00
3901e27683 Merge pull request 'feat(webui): audio level meter (Issue #234)' (#238) from sl-webui/issue-234-audio-meter into main 2026-03-02 12:15:31 -05:00
80a049fb0e Merge pull request 'feat(perception): QR code reader on CSI surround frames (Issue #233)' (#237) from sl-perception/issue-233-qr-reader into main 2026-03-02 12:15:08 -05:00
718c68e74d Merge pull request 'feat(controls): Tilt-compensated compass heading node (Issue #235)' (#236) from sl-controls/issue-235-compass into main 2026-03-02 12:15:01 -05:00
8d72b85b07 feat(webui): audio level meter with speech activity (Issue #234)
Add real-time audio level visualization with VU-style meter:
- Responsive VU bar with color gradient (silent to clipping)
- Peak hold indicator with exponential decay
- Speech activity detection from /social/speech/is_speaking
- Color-coded audio levels with visual feedback
- Grid markers for level reference (25%, 50%, 75%)
- Comprehensive audio statistics (average, max, peak count)

Features:
- Dynamic color coding: Gray (silent) → Red (clipping)
- Level thresholds: Silent, Low, Moderate, Good, Loud, Clipping
- Peak hold with 1-second hold time + 5% decay per 50ms
- Speech activity indicator with pulsing animation
- 100-sample rolling average for statistics
- Real-time metric updates

Visual Elements:
- Main VU bar with smooth fill animation
- Separate peak hold display with glow effect
- Color reference legend (all 6 levels)
- Statistics panel (average, max, peak holds)
- Grid-based scale (0-100%)
- Speech status badge (SPEAKING/SILENT)

Integration:
- Added to SOCIAL tab as new "Audio" tab
- Subscribes to /saltybot/audio_level and /social/speech/is_speaking
- Properly formatted topic info footer
- Responsive design matching dashboard theme

Build: ✓ Passing (113 modules, 202.67 KB main bundle)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:14:00 -05:00
b2fdc3a500 feat(perception): QR code reader on CSI surround frames (Issue #233)
Adds cv2.QRCodeDetector-based QR reader that subscribes to all four IMX219
CSI camera streams, deduplicates detections with a 2 s per-payload cooldown,
and publishes /saltybot/qr_codes (QRDetectionArray) at 10 Hz.  New
QRDetection / QRDetectionArray messages added to saltybot_scene_msgs.
16/16 pure-Python tests pass (no ROS2 required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:12:57 -05:00
570ebd3d22 feat(controls): Tilt-compensated compass heading node (Issue #235)
Implement magnetometer-based heading calculation with tilt compensation
and magnetic declination correction for France (1.5° east).

Features:
- Tilt-compensated heading using quaternion-based orientation
  Roll and pitch compensation for uneven terrain
- Magnetic declination correction: 1.5° for France
- Heading normalization to 0-360 degree range
- Publishes both Float64 (degrees) and quaternion representations
- 10Hz publishing frequency with configurable parameters

Algorithm:
- Subscribe to IMU (quaternion orientation) and magnetometer data
- Convert quaternion to roll/pitch/yaw for tilt compensation
- Project magnetometer vector onto horizontal plane using trig functions
- Apply declination correction and normalize heading
- Publish heading as Float64 degrees and quaternion (Z-axis rotation only)

Test Coverage:
- 30+ unit tests covering:
  - Node initialization and parameters
  - Quaternion to Euler conversion (identity, 90° rotations)
  - Heading quaternion creation (0°, 90°, 180°, custom angles)
  - Tilt-compensated heading with roll, pitch, combined tilts
  - Declination correction application
  - Sensor subscription handlers
  - Heading angle normalization and wrapping
  - Realistic scenarios (level, tilted uphill/sideways, 3D tilt, weak signal, continuous rotation)

Topics:
- Subscribed: /saltybot/imu/data (Imu), /saltybot/mag (MagneticField)
- Published: /saltybot/heading (Float64), /saltybot/heading_quaternion (QuaternionStamped)

Config: frequency=10Hz, declination_deg=1.5

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:11:37 -05:00
ff3eb340d8 feat(webui): audio level meter with speech activity (Issue #234)
Add real-time audio level visualization with VU-style meter:
- Responsive VU bar with color gradient (silent to clipping)
- Peak hold indicator with exponential decay
- Speech activity detection from /social/speech/is_speaking
- Color-coded audio levels with visual feedback
- Grid markers for level reference (25%, 50%, 75%)
- Comprehensive audio statistics (average, max, peak count)

Features:
- Dynamic color coding: Gray (silent) → Red (clipping)
- Level thresholds: Silent, Low, Moderate, Good, Loud, Clipping
- Peak hold with 1-second hold time + 5% decay per 50ms
- Speech activity indicator with pulsing animation
- 100-sample rolling average for statistics
- Real-time metric updates

Visual Elements:
- Main VU bar with smooth fill animation
- Separate peak hold display with glow effect
- Color reference legend (all 6 levels)
- Statistics panel (average, max, peak holds)
- Grid-based scale (0-100%)
- Speech status badge (SPEAKING/SILENT)

Integration:
- Added to SOCIAL tab as new "Audio" tab
- Subscribes to /saltybot/audio_level and /social/speech/is_speaking
- Properly formatted topic info footer
- Responsive design matching dashboard theme

Build: ✓ Passing (113 modules, 202.67 KB main bundle)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:11:12 -05:00
e477284c2f Merge pull request 'feat(webui): 3D robot pose viewer (Issue #229)' (#232) from sl-webui/issue-229-pose-viewer into main 2026-03-02 12:09:09 -05:00
3b352ad2c5 Merge pull request 'feat(social): 68-point Kalman landmark smoother (Issue #227)' (#231) from sl-perception/issue-227-landmark-smooth into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 10s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 12:08:53 -05:00
a566ab61cd Merge pull request 'feat(controls): Priority-based cmd_vel multiplexer (Issue #228)' (#230) from sl-controls/issue-228-cmd-vel-mux into main 2026-03-02 12:08:48 -05:00
24340dea9b feat(social): add 68-point Kalman landmark smoother (Issue #227)
Some checks failed
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
Adds per-person constant-velocity Kalman filter that smooths raw 68-point
facial landmarks and republishes on /social/faces/landmarks_smooth at input
rate.  New FaceLandmarks / FaceLandmarksArray messages added to
saltybot_social_msgs.  21/21 pure-Python tests pass (no ROS2 required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 12:05:24 -05:00
173b538f44 feat(controls): Priority-based cmd_vel multiplexer (Issue #228)
Implement priority-based velocity command routing with timeout-based fallback:
- Teleop (priority 1) > Nav2 (priority 2) > Docking (priority 3)
- 0.5s timeout per source: inactive sources are skipped in priority selection
- When no source is active, publish zero command for safety

Features:
- 50Hz multiplexing frequency with configurable parameters
- JSON status publishing (/saltybot/cmd_vel_mux_status) for telemetry
- Automatic priority escalation: teleop preempts nav2, nav2 preempts docking
- Fallback chain: if teleop times out, nav2 takes over; if nav2 times out, docking active
- Zero command safety: all sources timeout = immediate motor stop

Test Coverage:
- 20+ unit tests covering enum, initialization, subscriptions
- Priority selection logic (teleop preemption, nav2 preemption)
- Timeout detection and source fallback
- Realistic scenario tests (teleop release, priority escalation chains)
- JSON status format validation

Config: frequency=50Hz, source_timeout=0.5s

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 12:01:00 -05:00
5e05622628 feat(webui): 3D robot pose viewer with IMU visualization (Issue #229)
Replace basic IMU panel with interactive Three.js 3D pose viewer:
- Real-time robot box model visualization
- Rotation controlled by /saltybot/imu quaternion data
- 30-second position trail from /odom navigation
- Trail visualization with point history
- Reset button to clear trail history
- Grid ground plane and directional indicator arrow
- Interactive 3D scene with proper lighting and shadows

Features:
- Three.js scene with dynamic box model (0.3x0.3x0.5m)
- Forward direction indicator (amber cone)
- Ambient and directional lighting with shadows
- Grid helper for spatial reference
- Trail line geometry with dynamic point updates
- Automatic window resize handling
- Proper cleanup of WebGL resources and subscriptions

Integration:
- Replaces ImuPanel in TELEMETRY tab (IMU view)
- Added to flex layout for proper 3D viewport sizing
- Subscribes to /saltybot/imu and /odom topics
- Updates trail points every 100ms (max 30-second history)

Build: ✓ Passing (112 modules, 196.69 KB main bundle)
Three.js vendor: 473.57 KB

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:59:42 -05:00
3a7cd3649a Merge pull request 'feat(controls): motor current protection (Issue #223)' (#224) from sl-controls/issue-223-motor-protection into main 2026-03-02 11:57:54 -05:00
213d7fe13d Merge pull request 'feat(social): hand gesture pointing direction node (Issue #221)' (#226) from sl-perception/issue-221-pointing into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 11:57:27 -05:00
b29d65883f Merge pull request 'feat(webui): network diagnostics panel (Issue #222)' (#225) from sl-webui/issue-222-network into main 2026-03-02 11:57:20 -05:00
4730ea46d1 feat: Add Issue #223 - Motor current protection (I^2R thermal derating)
Monitors motor current, detects overcurrent (soft 5A, hard 8A), models thermal
state using I^2R heating model, and applies speed derating for safe operation.

Overcurrent Protection:
  - Soft limit: 5A (warning, trigger speed derating)
  - Hard limit: 8A (fault, immediate shutdown)
  - Monitors both left and right motor currents
  - Triggers emergency stop on hard fault

Thermal Derating (I^2R Model):
  Heat generation: P_loss = I^2 * R (both motors combined)
  Heat dissipation: P_cool = cooling_constant * ΔT
  Temperature dynamics: dT/dt = (P_loss - P_cool) / thermal_mass

  Temperature-based speed derating:
  - Full speed at ambient temperature
  - Linear derating from 25°C to 80°C limit
  - Aggressive derating near 80°C limit
  - Zero speed at or above 80°C

Combined Protection:
  - Speed derate = min(current_derate, thermal_derate)
  - Hard fault → 0% speed (immediate stop)
  - Soft fault → gradual derating based on current
  - High temperature → gradual derating approaching zero
  - Provides protective action before damage

Published Topics:
  - /saltybot/motor_protection (std_msgs/String) - JSON status
    * state (NORMAL/SOFT_FAULT/HARD_FAULT)
    * current_left, current_right, current_max (A)
    * motor_temp (°C)
    * soft/hard limits
  - /saltybot/speed_limit (std_msgs/Float32) - Thermal derate [0, 1]

Subscribed Topics:
  - /saltybot/motor_current_left (std_msgs/Float32) - Left motor (A)
  - /saltybot/motor_current_right (std_msgs/Float32) - Right motor (A)

Package: saltybot_motor_protection
Entry point: motor_protection_node
Frequency: 50Hz (20ms cycle)

Thermal Model Parameters:
  - Motor resistance: 1.5 Ω
  - Thermal mass: 100 J/°C
  - Ambient temperature: 25°C
  - Max safe temperature: 80°C
  - Cooling constant: 0.05 1/s

Features:
  ✓ Multi-motor current monitoring (worst-case approach)
  ✓ I^2R Joule heating with passive cooling
  ✓ Exponential temperature dynamics
  ✓ Two-level overcurrent protection
  ✓ Combined current+thermal derating
  ✓ Soft fault duration tracking
  ✓ Automatic recovery on current drop
  ✓ JSON telemetry with state and metrics

Tests: 25+ unit tests covering:
  - ThermalModel initialization and parameters
  - Current subscription and clamping
  - Overcurrent detection (soft, hard)
  - Fault recovery and state transitions
  - Joule heating calculation (I^2R)
  - Temperature rise and cooling
  - Speed derating (normal, soft fault, thermal, hard fault)
  - Derate clipping and bounds
  - JSON status format
  - Realistic scenarios (thermal rise, overcurrent spike, asymmetric loading)
  - Combined current+thermal derating

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:57:17 -05:00
0cdbe4b43e Merge pull request 'feat: INA219 dual motor current monitor driver (Issue #214)' (#218) from sl-firmware/issue-214-ina219 into main 2026-03-02 11:57:02 -05:00
bd4a9f094d feat(social): hand gesture pointing direction node (Issue #221)
Some checks failed
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
saltybot_social_msgs:
- Add PointingTarget.msg: origin (INDEX_MCP), direction (unit vec), target,
  range_m, person_id, confidence, coarse_direction, is_active
- Register in CMakeLists.txt

saltybot_social:
- _pointing_ray.py (pure Python, no rclpy): unproject(), sample_depth()
  (median with outlier rejection), compute_pointing_ray() — reprojects
  INDEX_MCP and INDEX_TIP into 3-D using D435i depth; falls back to image-
  plane direction when both depths are equal; gracefully handles one-sided
  missing depth
- pointing_node.py: subscribes /social/gestures + synced D435i colour+depth;
  re-runs MediaPipe Hands when a 'point' gesture is cached (within
  gesture_timeout_s); picks closest hand to gesture anchor; publishes
  PointingTarget on /saltybot/pointing_target at 5 Hz
- setup.py: adds pointing_node entry point
- 18/18 unit tests pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:56:54 -05:00
3c0c781c3b feat(webui): network diagnostics panel (Issue #222)
Add NetworkPanel component to CONFIG tab for real-time network monitoring:
- WiFi RSSI signal strength with color-coded signal bars
- Rosbridge latency measurement with quality indicators
- WebSocket message rate and throughput tracking
- Connection status monitoring
- Network health summary with overall status assessment

Features:
- 5-level signal strength indicator (Excellent to Poor)
- 5-level latency quality assessment (Excellent to Critical)
- Real-time message rate counter
- Cumulative stats (messages received/sent, bytes transferred)
- Responsive design with Tailwind CSS styling

Integration:
- Added to CONFIG tab group alongside Settings
- Uses rosbridge connection status and WebSocket URL
- Simulates realistic metrics with configurable intervals
- Proper cleanup of monitoring intervals on unmount

Build: ✓ Passing (111 modules, 192.59 KB main bundle)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:53:38 -05:00
3a34ec84e0 Merge pull request 'feat(bringup): obstacle height filter with IMU pitch compensation (Issue #211)' (#219) from sl-perception/issue-211-height-filter into main 2026-03-02 11:51:37 -05:00
f6ed0bd4ec Merge pull request 'feat(webui): joystick teleop widget (Issue #212)' (#220) from sl-webui/issue-212-joystick into main 2026-03-02 11:51:30 -05:00
eceda99bb5 feat: Add INA219 dual motor current monitor driver (Issue #214)
Implements complete I2C1 driver for TI INA219 power monitoring IC supporting:
- Dual sensors on I2C1 (left motor @ 0x40, right motor @ 0x41)
- Auto-calibration for 5A max current, 0.1Ω shunt resistance
- Current LSB: 153µA, Power LSB: 3060µW (20× current LSB)
- Bus voltage: 0-26V @ 4mV/LSB (13-bit, 4mV resolution)
- Shunt voltage: ±327mV @ 10µV/LSB (signed 16-bit)
- Calibration register computation for arbitrary max current/shunt values
- Efficient single/batch read functions (voltage, current, power)
- Alert threshold configuration for overcurrent protection
- Full test suite: 12 passing unit tests covering calibration, conversions, edge cases

Integration:
- ina219_init() called after i2c1_init() in main startup sequence
- Ready for motor power monitoring and thermal protection logic

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:51:26 -05:00
0940af56ef Merge pull request 'feat(controls): odometry fusion (Issue #216)' (#217) from sl-controls/issue-216-odom-fusion into main 2026-03-02 11:51:12 -05:00
b722279739 feat(bringup): scan height filter with IMU pitch compensation (Issue #211)
Two files added to saltybot_bringup:
- _scan_height_filter.py: pure-Python helpers (no rclpy) —
  filter_scan_by_height() projects each LIDAR ray to world-frame height
  using pitch/roll from the IMU and filters ground/ceiling returns;
  pitch_roll_from_accel() uses convention-agnostic atan2 formula;
  AttitudeEstimator low-pass filters the accelerometer attitude.
- scan_height_filter_node.py: subscribes /scan + /camera/imu, publishes
  /scan_filtered (LaserScan) for Nav2 at source rate (up to 20 Hz).

setup.py: adds scan_height_filter entry point.
18/18 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:50:56 -05:00
938c2a33d4 feat: Add Issue #216 - Odometry fusion node (complementary filter)
Fuses wheel, visual, and IMU odometry using complementary filtering. Publishes
fused /odom (nav_msgs/Odometry) and broadcasts odom→base_link TF at 50Hz.

Sensor Fusion Strategy:
  - Wheel odometry: High-frequency accurate linear displacement (weight: 0.6)
  - Visual odometry: Loop closure and long-term drift correction (weight: 0.3)
  - IMU: High-frequency attitude and angular velocity (weight: 0.1)

Complementary Filter Architecture:
  - Fast loop (IMU): High-frequency attitude updates, angular velocity
  - Slow loop (Vision): Low-frequency position/orientation correction
  - Integration: Velocity-based position updates with covariance weighting
  - Dropout handling: Continues with available sources if sensors drop

Fusion Algorithm:
  1. Extract velocities from wheel odometry (most reliable linear)
  2. Apply IMU angular velocity (highest frequency rotation)
  3. Update orientation from IMU with blending
  4. Integrate velocities to position (wheel odometry frame)
  5. Apply visual odometry drift correction (low-frequency)
  6. Update covariances based on available measurements
  7. Publish fused odometry with full covariance matrices

Published Topics:
  - /odom (nav_msgs/Odometry) - Fused pose/twist with covariance
  - /saltybot/odom_fusion_info (std_msgs/String) - JSON debug info

TF Broadcasts:
  - odom→base_link - Position (x, y) and orientation (yaw)

Subscribed Topics:
  - /saltybot/wheel_odom (nav_msgs/Odometry) - Wheel encoder odometry
  - /rtab_map/odom (nav_msgs/Odometry) - Visual/SLAM odometry
  - /imu/data (sensor_msgs/Imu) - IMU data

Package: saltybot_odom_fusion
Entry point: odom_fusion_node
Frequency: 50Hz (20ms cycle)

Features:
  ✓ Multi-source odometry fusion
  ✓ Complementary filtering with configurable weights
  ✓ Full covariance matrices for uncertainty tracking
  ✓ TF2 transform broadcasting
  ✓ Sensor dropout handling
  ✓ JSON telemetry with fusion status
  ✓ Configurable normalization of weights

Tests: 20+ unit tests covering:
  - OdomState initialization and covariances
  - Subscription handling for all three sensors
  - Position integration from velocity
  - Angular velocity updates
  - Velocity blending from multiple sources
  - Drift correction from visual odometry
  - Covariance updates based on measurement availability
  - Quaternion to Euler angle conversion
  - Realistic fusion scenarios (straight line, circles, drift correction)
  - Sensor dropout and recovery

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:50:13 -05:00
a05c93669e Merge pull request 'feat(controls): PID auto-tuner (Issue #213)' (#215) from sl-controls/issue-213-autotune into main 2026-03-02 11:48:02 -05:00
fd7eddd44d feat: Add Issue #213 - PID auto-tuner (Astrom-Hagglund relay oscillation)
Implements PID auto-tuning ROS2 node using relay feedback (Astrom-Hagglund) method.
Service-triggered relay oscillation measures ultimate gain (Ku) and ultimate period
(Tu), then computes Ziegler-Nichols PD gains. Safety abort on >25deg tilt.

Features:
  - Service /saltybot/autotune_pid (std_srvs/Trigger) starts tuning
  - Relay oscillation method for accurate gain measurement
  - Measures Ku (ultimate gain) and Tu (ultimate period)
  - Computes Z-N PD gains: Kp=0.6*Ku, Kd=0.075*Ku*Tu
  - Real-time safety abort >25° tilt angle
  - JSON telemetry on /saltybot/autotune_info
  - Relay commands on /saltybot/autotune_cmd_vel

Tuning Process:
  1. Settle phase: zero command, allow oscillations to die
  2. Relay oscillation: apply +/-relay_magnitude commands
  3. Measure peaks: detect zero crossings, record extrema
  4. Analysis: calculate Ku from peak amplitude, Tu from period
  5. Gain computation: Ziegler-Nichols formulas
  6. Publish results: Ku, Tu, Kp, Kd

Safety Features:
  - IMU tilt monitoring (abort >25°)
  - Max tuning duration timeout
  - Configurable settle time and oscillation cycles

Published Topics:
  - /saltybot/autotune_info (std_msgs/String) - JSON with Ku, Tu, Kp, Kd
  - /saltybot/autotune_cmd_vel (geometry_msgs/Twist) - Relay control

Subscribed Topics:
  - /imu/data (sensor_msgs/Imu) - IMU tilt safety check
  - /saltybot/balance_feedback (std_msgs/Float32) - Balance feedback

Package: saltybot_pid_autotune
Entry point: pid_autotune_node
Service: /saltybot/autotune_pid

Tests: 20+ unit tests covering:
  - IMU tilt extraction
  - Relay oscillation analysis
  - Ku/Tu measurement
  - Ziegler-Nichols gain computation
  - Peak detection and averaging
  - Safety limits (tilt, timeout)
  - State machine transitions
  - JSON telemetry format

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:47:05 -05:00
4ffa36370d feat: Add Issue #213 - PID auto-tuner (Astrom-Hagglund relay oscillation)
Implements PID auto-tuning ROS2 node using relay feedback (Astrom-Hagglund) method.
Service-triggered relay oscillation measures ultimate gain (Ku) and ultimate period
(Tu), then computes Ziegler-Nichols PD gains. Safety abort on >25deg tilt.

Features:
  - Service /saltybot/autotune_pid (std_srvs/Trigger) starts tuning
  - Relay oscillation method for accurate gain measurement
  - Measures Ku (ultimate gain) and Tu (ultimate period)
  - Computes Z-N PD gains: Kp=0.6*Ku, Kd=0.075*Ku*Tu
  - Real-time safety abort >25° tilt angle
  - JSON telemetry on /saltybot/autotune_info
  - Relay commands on /saltybot/autotune_cmd_vel

Tuning Process:
  1. Settle phase: zero command, allow oscillations to die
  2. Relay oscillation: apply +/-relay_magnitude commands
  3. Measure peaks: detect zero crossings, record extrema
  4. Analysis: calculate Ku from peak amplitude, Tu from period
  5. Gain computation: Ziegler-Nichols formulas
  6. Publish results: Ku, Tu, Kp, Kd

Safety Features:
  - IMU tilt monitoring (abort >25°)
  - Max tuning duration timeout
  - Configurable settle time and oscillation cycles

Published Topics:
  - /saltybot/autotune_info (std_msgs/String) - JSON with Ku, Tu, Kp, Kd
  - /saltybot/autotune_cmd_vel (geometry_msgs/Twist) - Relay control

Subscribed Topics:
  - /imu/data (sensor_msgs/Imu) - IMU tilt safety check
  - /saltybot/balance_feedback (std_msgs/Float32) - Balance feedback

Package: saltybot_pid_autotune
Entry point: pid_autotune_node
Service: /saltybot/autotune_pid

Tests: 20+ unit tests covering:
  - IMU tilt extraction
  - Relay oscillation analysis
  - Ku/Tu measurement
  - Ziegler-Nichols gain computation
  - Peak detection and averaging
  - Safety limits (tilt, timeout)
  - State machine transitions
  - JSON telemetry format

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:46:59 -05:00
7cc4b6742e Merge pull request 'feat(firmware): pan-tilt servo driver (Issue #206)' (#210) from sl-firmware/issue-206-servo into main 2026-03-02 11:45:22 -05:00
532edb835b feat(firmware): Pan-tilt servo driver for camera head (Issue #206)
Implements TIM4 PWM driver for 2-servo camera mount with:
- 50 Hz PWM frequency (standard servo control)
- CH1 (PB6) pan servo, CH2 (PB7) tilt servo
- 0-180° angle range → 500-2500 µs pulse width mapping
- Non-blocking servo_set_angle() for immediate positioning
- servo_sweep() for smooth pan-tilt animation (linear interpolation)
- Independent sweep control per servo (pan and tilt move simultaneously)
- 15 comprehensive unit tests covering all scenarios

Integration:
- servo_init() called at startup after power_mgmt_init()
- servo_tick(now_ms) called every 1ms in main loop
- Ready for camera/gimbal control automation

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:44:56 -05:00
d1a4008451 Merge pull request 'feat(perception): person re-identification node (Issue #201)' (#208) from sl-perception/issue-201-person-reid into main 2026-03-02 11:44:31 -05:00
d143a6d156 chore: remove accidentally committed __pycache__ dirs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:39:21 -05:00
0d07b09949 feat(perception): add person re-ID node (Issue #201)
Two new packages:
- saltybot_person_reid_msgs: PersonAppearance + PersonAppearanceArray msgs
- saltybot_person_reid: MobileNetV2 torso-crop embedder (128-dim L2-norm)
  with 128-bin HSV histogram fallback, cosine-similarity gallery with EMA
  identity updates and configurable age-based pruning, ROS2 node publishing
  PersonAppearanceArray on /saltybot/person_reid at 5 Hz.

Pure-Python helpers (_embedding_model, _reid_gallery) importable without
rclpy — 18/18 unit tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:39:21 -05:00
03e7995e66 Merge pull request 'feat(jetson): CPU/GPU thermal monitor — sysfs + /saltybot/thermal JSON (Issue #205)' (#209) from sl-jetson/issue-205-thermal into main 2026-03-02 11:39:03 -05:00
1600691ec5 Merge pull request 'feat(controls): node watchdog monitor (Issue #203)' (#207) from sl-controls/issue-203-watchdog into main 2026-03-02 11:38:55 -05:00
58bb5ef18e feat(jetson): CPU/GPU thermal monitor — sysfs + /saltybot/thermal JSON (Issue #205)
New saltybot_thermal package with thermal_node: reads all
/sys/class/thermal/thermal_zone* sysfs entries (millidegrees→°C),
publishes /saltybot/thermal JSON at 1 Hz with zones[], max_temp_c,
warn, and throttled flags. Logs ROS2 WARN at ≥75°C, ERROR at ≥85°C.
thermal_root param allows sysfs override for offline testing.
50/50 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:21:18 -05:00
e247389b07 feat: Add Issue #203 - Node watchdog monitor (20Hz heartbeat detection)
Implements node watchdog ROS2 node that monitors heartbeats from critical
systems and triggers safety fallback when motor driver is lost >2s.

Features:
  - Monitor heartbeats from: balance, motor_driver, emergency, docking
  - Alert on /saltybot/node_watchdog (JSON) if heartbeat lost >1s
  - Safety fallback: zero cmd_vel if motor driver lost >2s
  - Republish cmd_vel on /saltybot/cmd_vel_safe with safety checks
  - 20Hz monitoring and publishing frequency
  - Configurable heartbeat timeout thresholds

Heartbeat Topics:
  - /saltybot/balance_heartbeat (std_msgs/UInt32)
  - /saltybot/motor_driver_heartbeat (std_msgs/UInt32)
  - /saltybot/emergency_heartbeat (std_msgs/UInt32)
  - /saltybot/docking_heartbeat (std_msgs/UInt32)
  - /cmd_vel (geometry_msgs/Twist)

Published Topics:
  - /saltybot/node_watchdog (std_msgs/String) - JSON status
  - /saltybot/cmd_vel_safe (geometry_msgs/Twist) - Safety-checked velocity

Package: saltybot_node_watchdog
Entry point: node_watchdog_node
Launch file: node_watchdog.launch.py

Tests: 20+ unit tests covering:
  - Heartbeat reception and timeout detection
  - Motor driver critical timeout (>2s)
  - Safety fallback logic
  - cmd_vel zeroing on motor driver loss
  - Health status JSON serialization
  - Multi-node failure scenarios

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:17:19 -05:00
8ff85601c3 Merge pull request 'feat(firmware): WS2812B LED status indicator (Issue #193)' (#204) from sl-firmware/issue-193-led-driver into main 2026-03-02 11:17:09 -05:00
bfd58fc98c Merge pull request 'feat(social): robot mesh comms — peer announce + person handoff (Issue #171)' (#202) from sl-jetson/issue-171-mesh-comms into main
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 2s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Latency profiling (GPU, Orin) (push) Has been cancelled
2026-03-02 11:17:03 -05:00
bb645c5e72 Merge pull request 'feat(controls): speed limiter node (Issue #194)' (#200) from sl-controls/issue-194-speed-limiter into main 2026-03-02 11:15:14 -05:00
bc45208768 feat: Add Issue #194 - Speed limiter node (50Hz safety constraints)
Implements speed limiter ROS2 node that enforces safety constraints from:
  - Terrain type (via /saltybot/terrain_speed_scale)
  - Battery level (via /saltybot/speed_limit)
  - Emergency system state (via /saltybot/emergency)

Node computes minimum scale factor and applies to /cmd_vel, forwarding
limited commands to /saltybot/cmd_vel_limited. Publishes JSON telemetry
on /saltybot/speed_limit_info at 50Hz (20ms cycle).

Features:
  - Subscription to terrain, battery, and emergency constraints
  - Twist velocity scaling with min-of-three constraint logic
  - JSON telemetry with timestamp and all scale factors
  - 50Hz timer-based publishing for real-time control
  - Emergency state mapping (NOMINAL/RECOVERING/STOPPING/ESCALATED)
  - Comprehensive unit tests (18 test cases)

Package: saltybot_speed_limiter
Entry point: speed_limiter_node
Launch file: speed_limiter.launch.py

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:14:10 -05:00
e5236f781b feat(social): robot mesh comms — peer announce + person handoff (Issue #171)
Some checks failed
social-bot integration tests / Lint (flake8 + pep257) (push) Failing after 9s
social-bot integration tests / Core integration tests (mock sensors, no GPU) (push) Has been skipped
social-bot integration tests / Lint (flake8 + pep257) (pull_request) Failing after 9s
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
New MeshPeer.msg (1 Hz DDS heartbeat: robot_id, social_state, active persons,
greeted names) and MeshHandoff.msg (person context transfer on STATE_LEAVING).
mesh_comms_node subscribes to person_states and orchestrator/state, publishes
announce heartbeat, triggers handoff on LEAVING, tracks peers with timeout
cleanup, and propagates mesh-wide greeting deduplication via /social/mesh/greeted.
73/73 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:14:00 -05:00
b345128427 Merge pull request 'feat(jetson): camera health watchdog — 6 streams, WARNING/ERROR, v4l2 reset (issue #198)' (#199) from sl-perception/issue-198-camera-health into main 2026-03-02 11:12:32 -05:00
e6065e1531 feat(jetson): camera health watchdog node (issue #198)
Adds camera_health_node.py + _camera_state.py to saltybot_bringup:

• _camera_state.py  — pure-Python CameraState dataclass (no ROS2):
                       on_frame(), age_s, fps(window_s), status(),
                       should_reset() + mark_reset() with 30s cooldown

• camera_health_node.py — subscribes 6 image topics (D435i color/depth
                           + 4× IMX219 CSI front/right/rear/left);
                           1 Hz tick: WARNING at >2s silence, ERROR at
                           >10s + v4l2 stream-off/on reset for CSI cams;
                           publishes /saltybot/camera_health JSON with
                           per-camera status, age_s, fps, total_frames

• test/test_camera_health.py — 15 unit tests (15/15 pass, no ROS2 needed)
• setup.py — adds camera_health_monitor console_scripts entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:11:48 -05:00
7678ddebe2 Merge pull request 'feat(jetson): AprilTag landmark detector — DICT_APRILTAG_36h11 10Hz 6-DOF (issue #191)' (#197) from sl-perception/issue-191-apriltag into main 2026-03-02 11:08:44 -05:00
d6d7e7b75a feat(jetson): AprilTag landmark detector — DICT_APRILTAG_36h11 (issue #191)
saltybot_apriltag_msgs (ament_cmake):
• DetectedTag.msg     — tag_id, family, decision_margin, corners[8],
                        center, 6-DOF pose + pose_valid flag
• DetectedTagArray.msg — DetectedTag[], count

saltybot_apriltag (ament_python, single node):
• _aruco_utils.py    — ROS2-free: ArucoBackend (cv2 4.6/4.7+ API shim),
                        rvec_tvec_to_pose (Rodrigues → Shepperd quaternion)
• apriltag_node.py   — 10 Hz timer; subscribes image + latched camera_info;
                        cv2.solvePnP IPPE_SQUARE for 6-DOF pose when K
                        available; publishes /saltybot/apriltags
• test/test_apriltag.py — 11 unit tests (11/11 pass, no ROS2 needed):
                          pose math + rendered tag detection + multi-tag

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 11:07:35 -05:00
fbca191bae feat(firmware): WS2812B NeoPixel LED status indicator driver (Issue #193)
Implements TIM3_CH1 PWM driver for 8-LED NeoPixel ring with:
- 6 state-based animations: boot (blue chase), armed (solid green),
  error (red blink), low battery (yellow pulse), charging (green breathe),
  e_stop (red strobe)
- Non-blocking via 1 ms tick callback
- GRB byte order encoding (WS2812B standard)
- PWM duty values for "0" (~40%) and "1" (~56%) bit encoding
- 10 unit tests covering state transitions, animations, color encoding

Driver integrated into main.c initialization and main loop tick.
Includes buzzer driver (Issue #189) integration.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:06:13 -05:00
aedb8771ad feat(webui): robot event log viewer — emergency/docking/diagnostics (Issue #192)
- Add EventLog component with real-time event streaming
- Color-coded events: red=emergency, blue=docking, cyan=diagnostics
- Filter by event type with toggle buttons
- Auto-scroll to latest event
- Timestamped event cards with details display
- Max 200 event history (FIFO)
- Add MONITORING tab group with Events tab to App.jsx
- Supports /saltybot/emergency, /saltybot/docking_status, /diagnostics topics

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-02 11:04:53 -05:00
1644 changed files with 168043 additions and 3266 deletions

View File

@ -0,0 +1,308 @@
# Issue #469: Terrain Classification Implementation Plan
## Context
SaltyBot currently has good sensor infrastructure (IMU, cameras, RealSense) and a robust velocity control system with the `VelocityRamp` class. However, it lacks terrain awareness for surface type detection and speed adaptation. This feature will enable:
1. **Surface detection** via IMU vibration analysis and camera texture analysis
2. **Automatic speed adaptation** based on terrain type and roughness
3. **Terrain logging** for mapping and future learning
4. **Improved robot safety** by reducing speed on rough/unstable terrain
## Architecture Overview
The implementation follows the existing ROS2 patterns:
```
IMU/Camera Data
[terrain_classifier_node] ← New node
/saltybot/terrain_state (TerrainState.msg)
[terrain_speed_adapter_node] ← New node
Adjusted /cmd_vel_terrain → existing cmd_vel_bridge
Speed-adapted robot motion
Parallel: [terrain_mapper_node] logs data for mapping
```
## Implementation Components
### 1. **Message Definition: TerrainState.msg**
**File to create:** `jetson/ros2_ws/src/saltybot_social_msgs/msg/TerrainState.msg`
Fields:
- `std_msgs/Header header` — timestamp/frame_id
- `uint8 terrain_type` — enum (0=unknown, 1=pavement, 2=grass, 3=gravel, 4=sand, 5=indoor)
- `float32 roughness` — 0.0=smooth, 1.0=very rough
- `float32 confidence` — 0.0-1.0 classification confidence
- `float32 recommended_speed_ratio` — 0.1-1.0 (fraction of max speed)
- `string source` — "imu_vibration" or "camera_texture" or "fused"
**Update files:**
- `jetson/ros2_ws/src/saltybot_social_msgs/CMakeLists.txt` — add TerrainState.msg to rosidl_generate_interfaces()
- `jetson/ros2_ws/src/saltybot_social_msgs/package.xml` — no changes needed (std_msgs already a dependency)
### 2. **Terrain Classifier Node**
**File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_classifier_node.py`
**Purpose:** Analyze IMU and camera data to classify terrain type and estimate roughness.
**Subscribes to:**
- `/camera/imu` (sensor_msgs/Imu) — RealSense IMU at 200 Hz
- `/camera/color/image_raw` (sensor_msgs/Image) — camera RGB at 15 Hz
**Publishes:**
- `/saltybot/terrain_state` (TerrainState.msg) — at 5 Hz
**Key functions:**
- `_analyze_imu_vibration()` — FFT analysis on accel data (window: 200 samples = 1 sec)
- Compute power spectral density in 0-50 Hz band
- Extract features: peak frequency, energy distribution, RMS acceleration
- Roughness = normalized RMS of high-freq components (>10 Hz)
- `_analyze_camera_texture()` — CNN-based texture analysis
- Uses MobileNetV2 pre-trained on ImageNet as feature extractor
- Extracts high-level texture/surface features from camera image
- Lightweight model (~3.5M parameters, ~50-100ms inference on Jetson)
- Outputs feature vector fed to classification layer
- `_classify_terrain()` — decision logic
- Simple rule-based classifier (can be upgraded to CNN)
- Input: [imu_roughness, camera_texture_variance, accel_magnitude]
- Decision tree or thresholds to classify into 5 types
- Output: terrain_type, roughness, confidence
**Node Parameters:**
- `imu_window_size` (int, default 200) — samples for FFT window
- `publish_rate_hz` (float, default 5.0)
- `roughness_threshold` (float, default 0.3) — FFT roughness threshold
- `terrain_timeout_s` (float, default 5.0) — how long to keep previous estimate if no new data
### 3. **Speed Adapter Node**
**File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_speed_adapter_node.py`
**Purpose:** Adapt cmd_vel speed based on terrain state and integration with velocity ramp.
**Subscribes to:**
- `/cmd_vel` (geometry_msgs/Twist) — raw velocity commands
- `/saltybot/terrain_state` (TerrainState.msg) — terrain classification
**Publishes:**
- `/cmd_vel_terrain` (geometry_msgs/Twist) — terrain-adapted velocity
**Logic:**
- Extract target linear velocity from cmd_vel
- Apply terrain speed ratio: `adapted_speed = target_speed × recommended_speed_ratio`
- Preserve angular velocity (steering not affected by terrain)
- Publish adapted command
**Node Parameters:**
- `enable_terrain_adaptation` (bool, default true)
- `min_speed_ratio` (float, default 0.1) — never go below 10% of requested speed
- `debug_logging` (bool, default false)
**Note:** This is a lightweight adapter. The existing `velocity_ramp_node` handles acceleration/deceleration smoothing independently.
### 4. **Terrain Mapper Node** (Logging/Mapping)
**File to create:** `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_mapper_node.py`
**Purpose:** Log terrain detections with robot pose for future mapping.
**Subscribes to:**
- `/saltybot/terrain_state` (TerrainState.msg)
- `/odom` (nav_msgs/Odometry) — robot pose
**Publishes:**
- `/saltybot/terrain_log` (std_msgs/String) — CSV formatted log messages (optional, mainly for logging)
**Logic:**
- Store terrain observations: (timestamp, pose_x, pose_y, terrain_type, roughness, confidence)
- Log to file: `~/.ros/terrain_map_<timestamp>.csv`
- Resample to 1 Hz to avoid spam
**Node Parameters:**
- `log_dir` (string, default "~/.ros/")
- `resample_rate_hz` (float, default 1.0)
### 5. **Launch Configuration**
**File to update:** `jetson/ros2_ws/src/saltybot_bringup/launch/full_stack.launch.py`
Add terrain nodes:
```python
terrain_classifier_node = Node(
package='saltybot_bringup',
executable='terrain_classifier',
name='terrain_classifier',
parameters=[{
'imu_window_size': 200,
'publish_rate_hz': 5.0,
}],
remappings=[
('/imu_in', '/camera/imu'),
('/camera_in', '/camera/color/image_raw'),
],
)
terrain_speed_adapter_node = Node(
package='saltybot_bringup',
executable='terrain_speed_adapter',
name='terrain_speed_adapter',
parameters=[{
'enable_terrain_adaptation': True,
'min_speed_ratio': 0.1,
}],
remappings=[
('/cmd_vel_in', '/cmd_vel'),
('/cmd_vel_out', '/cmd_vel_terrain'),
],
)
terrain_mapper_node = Node(
package='saltybot_bringup',
executable='terrain_mapper',
name='terrain_mapper',
)
```
**Update setup.py entry points:**
```python
'terrain_classifier = saltybot_bringup.terrain_classifier_node:main'
'terrain_speed_adapter = saltybot_bringup.terrain_speed_adapter_node:main'
'terrain_mapper = saltybot_bringup.terrain_mapper_node:main'
```
### 6. **Integration with Existing Stack**
- The existing velocity ramp (`velocity_ramp_node.py`) processes `/cmd_vel_smooth` or `/cmd_vel`
- Optionally, update cmd_vel_bridge to use `/cmd_vel_terrain` if available, else fall back to `/cmd_vel`
- Terrain classification runs independently at 5 Hz (much slower than velocity ramping at 50 Hz)
### 7. **Future CNN Enhancement**
The current implementation uses rule-based classification with IMU FFT and camera edge detection. A future enhancement could add a lightweight CNN for texture classification (e.g., MobileNet) by:
1. Creating a `terrain_classifier_cnn.py` with TensorFlow/ONNX model
2. Replacing decision logic in `terrain_classifier_node.py` with CNN inference
3. Maintaining same message interface
## Implementation Tasks
1. ✅ **Phase 1: Message Definition**
- Create `TerrainState.msg` in saltybot_social_msgs
- Update `CMakeLists.txt`
2. ✅ **Phase 2: Terrain Classifier Node**
- Implement `terrain_classifier_node.py` with IMU FFT analysis
- Implement camera texture analysis
- Decision logic for classification
3. ✅ **Phase 3: Speed Adapter Node**
- Implement `terrain_speed_adapter_node.py`
- Velocity command adaptation
4. ✅ **Phase 4: Terrain Mapper Node**
- Implement `terrain_mapper_node.py` for logging
5. ✅ **Phase 5: Integration**
- Update `full_stack.launch.py` with new nodes
- Update `setup.py` with entry points
- Test integration
## Testing & Verification
**Unit Tests:**
- Test IMU FFT feature extraction with synthetic vibration data
- Test terrain classification decision logic
- Test speed ratio application
- Test CSV logging format
**Integration Tests:**
- Run full stack with simulated IMU/camera data
- Verify terrain messages published at 5 Hz
- Verify cmd_vel_terrain adapts speeds correctly
- Check terrain log file is created and properly formatted
**Manual Testing:**
- Drive robot on different surfaces
- Verify terrain detection changes appropriately
- Verify speed adaptation is smooth (no jerks from ramping)
- Check terrain log CSV has correct format
## Critical Files Summary
**To Create:**
- `jetson/ros2_ws/src/saltybot_social_msgs/msg/TerrainState.msg`
- `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_classifier_node.py`
- `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_speed_adapter_node.py`
- `jetson/ros2_ws/src/saltybot_bringup/saltybot_bringup/terrain_mapper_node.py`
**To Modify:**
- `jetson/ros2_ws/src/saltybot_social_msgs/CMakeLists.txt` (add TerrainState.msg)
- `jetson/ros2_ws/src/saltybot_bringup/launch/full_stack.launch.py` (add nodes)
- `jetson/ros2_ws/src/saltybot_bringup/setup.py` (add entry points)
**Key Dependencies:**
- `numpy` — FFT analysis (already available in saltybot)
- `scipy.signal` — Butterworth filter (optional, for smoothing)
- `cv2` (OpenCV) — image processing (already available)
- `tensorflow` or `tf-lite` — MobileNetV2 pre-trained model for texture CNN
- `rclpy` — ROS2 Python client
**CNN Model Details:**
- Model: MobileNetV2 pre-trained on ImageNet
- Input: 224×224 RGB image (downsampled from camera)
- Output: 1280-dim feature vector from last conv layer before classification
- Strategy: Use pre-trained features directly (transfer learning) for quick MVP, no fine-tuning needed initially
- Alternative: Pre-trained weights can be fine-tuned on terrain image dataset in future iterations
- Inference: ~50-100ms on Jetson Xavier (acceptable at 5 Hz publish rate)
## Terrain Classification Logic (IMU + CNN Fusion)
**Features extracted:**
1. `imu_roughness` = normalized RMS of high-freq (>10 Hz) accel components (0-1)
- Computed from FFT power spectral density in 10-50 Hz band
- Reflects mechanical vibration from surface contact
2. `cnn_texture_features` = 1280-dim feature vector from MobileNetV2
- Pre-trained features capture texture, edge, and surface characteristics
- Reduced to 2-3 principal components via PCA or simple aggregation
3. `accel_magnitude` = RMS of total acceleration (m/s²)
- Helps distinguish stationary (9.81 m/s²) vs. moving
**Classification approach (Version 1):**
- Simple decision tree with IMU-dominant logic + CNN support:
```
if imu_roughness < 0.2 and accel_magnitude < 9.8:
terrain = PAVEMENT (confidence boosted if CNN agrees)
elif imu_roughness < 0.35 and cnn_grainy_score < 0.4:
terrain = GRASS
elif imu_roughness > 0.45 and cnn_granular_score > 0.5:
terrain = GRAVEL
elif cnn_sand_texture_score > 0.6 and imu_roughness > 0.3:
terrain = SAND
else:
terrain = INDOOR
```
- Confidence: weighted combination of IMU and CNN agreement
- Roughness metric: `0.0 = smooth, 1.0 = very rough` derived from IMU FFT energy ratio
**Speed recommendations:**
- Pavement: 1.0 (full speed)
- Grass: 0.8 (20% slower)
- Gravel: 0.5 (50% slower)
- Sand: 0.4 (60% slower)
- Indoor: 0.7 (30% slower by default)
**Future improvement:** Replace decision tree with trained classifier (Random Forest, SVM, or small Dense net) on labeled terrain dataset once collected.
---
This plan follows SaltyBot's established patterns:
- Pure Python libraries for core logic (_terrain_analysis.py)
- ROS2 node wrappers for integration
- Parameter-based configuration in YAML
- Message-based pub/sub architecture
- Integration with existing velocity control pipeline

View File

@ -0,0 +1,162 @@
# .gitea/workflows/ota-release.yml
# Gitea Actions — ESP32 OTA firmware build & release (bd-9kod)
#
# Triggers on signed release tags:
# esp32-balance/vX.Y.Z → builds esp32s3/balance/ (ESP32-S3 Balance board)
# esp32-io/vX.Y.Z → builds esp32s3-io/ (ESP32-S3 IO board)
#
# Uses the official espressif/idf Docker image for reproducible builds.
# Attaches <app>_<version>.bin + <app>_<version>.sha256 to the Gitea release.
# The ESP32 Balance OTA system fetches the .bin from the release asset URL.
name: OTA release — build & attach firmware
on:
push:
tags:
- "esp32-balance/v*"
- "esp32-io/v*"
permissions:
contents: write
jobs:
build-and-release:
name: Build ${{ github.ref_name }}
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.2.2
options: --user root
steps:
# ── 1. Checkout ───────────────────────────────────────────────────────────
- name: Checkout
uses: actions/checkout@v4
# ── 2. Resolve build target from tag ─────────────────────────────────────
# Tag format: esp32-balance/v1.2.3 or esp32-io/v1.2.3
- name: Resolve project from tag
id: proj
shell: bash
run: |
TAG="${GITHUB_REF_NAME}"
case "$TAG" in
esp32-balance/*)
DIR="esp32s3/balance"
APP="esp32s3_balance"
;;
esp32-io/*)
DIR="esp32s3-io"
APP="esp32s3_io"
;;
*)
echo "::error::Unrecognised tag prefix: ${TAG}"
exit 1
;;
esac
VERSION="${TAG#*/}"
echo "dir=${DIR}" >> "$GITHUB_OUTPUT"
echo "app=${APP}" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "Build: ${APP} ${VERSION} from ${DIR}"
# ── 3. Build with ESP-IDF ─────────────────────────────────────────────────
- name: Build firmware (idf.py build)
shell: bash
run: |
. "${IDF_PATH}/export.sh"
cd "${{ steps.proj.outputs.dir }}"
idf.py build
# ── 4. Collect binary & generate checksum ────────────────────────────────
- name: Collect artifacts
id: art
shell: bash
run: |
APP="${{ steps.proj.outputs.app }}"
VER="${{ steps.proj.outputs.version }}"
BIN_SRC="${{ steps.proj.outputs.dir }}/build/${APP}.bin"
BIN_OUT="${APP}_${VER}.bin"
SHA_OUT="${APP}_${VER}.sha256"
cp "$BIN_SRC" "$BIN_OUT"
sha256sum "$BIN_OUT" > "$SHA_OUT"
echo "bin=${BIN_OUT}" >> "$GITHUB_OUTPUT"
echo "sha=${SHA_OUT}" >> "$GITHUB_OUTPUT"
echo "Binary: ${BIN_OUT} ($(wc -c < "$BIN_OUT") bytes)"
echo "Checksum: $(cat "$SHA_OUT")"
# ── 5. Archive artifacts in CI workspace ─────────────────────────────────
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: firmware-${{ steps.proj.outputs.app }}-${{ steps.proj.outputs.version }}
path: |
${{ steps.art.outputs.bin }}
${{ steps.art.outputs.sha }}
# ── 6. Create Gitea release (if needed) & upload assets ──────────────────
# Uses GITHUB_TOKEN (auto-provided, contents:write from permissions block).
# URL-encodes the tag to handle the slash in esp32-balance/vX.Y.Z.
- name: Publish assets to Gitea release
shell: bash
env:
GITEA_URL: https://gitea.vayrette.com
TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ steps.proj.outputs.tag }}
BIN: ${{ steps.art.outputs.bin }}
SHA: ${{ steps.art.outputs.sha }}
run: |
API="${GITEA_URL}/api/v1/repos/${REPO}"
# URL-encode the tag (slash in esp32-balance/vX.Y.Z must be escaped)
TAG_ENC=$(python3 -c "
import urllib.parse, sys
print(urllib.parse.quote(sys.argv[1], safe=''))
" "$TAG")
# Try to fetch an existing release for this tag
RELEASE=$(curl -sf \
-H "Authorization: token ${TOKEN}" \
"${API}/releases/tags/${TAG_ENC}") || true
# If no release yet, create it
if [ -z "$RELEASE" ]; then
echo "Creating release for tag: ${TAG}"
RELEASE=$(curl -sf \
-X POST \
-H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json, sys
print(json.dumps({
'tag_name': sys.argv[1],
'name': sys.argv[1],
'draft': False,
'prerelease': False,
}))
" "$TAG")" \
"${API}/releases")
fi
RELEASE_ID=$(echo "$RELEASE" | python3 -c "
import sys, json; print(json.load(sys.stdin)['id'])
")
echo "Release ID: ${RELEASE_ID}"
# Upload binary and checksum
for FILE in "$BIN" "$SHA"; do
FNAME=$(basename "$FILE")
echo "Uploading: ${FNAME}"
curl -sf \
-X POST \
-H "Authorization: token ${TOKEN}" \
-F "attachment=@${FILE}" \
"${API}/releases/${RELEASE_ID}/assets?name=${FNAME}"
done
echo "Published: ${BIN} + ${SHA} → release ${TAG}"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/audio.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/fan.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/jlink.o Normal file

Binary file not shown.

BIN
.pio/build/f722/src/led.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/ota.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
.pio/build/f722/src/servo.o Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
ee8efb31f6b185f16e4d385971f1a0e3291fe5fd ffc01fb580c81760bdda9a672fe1212be4578e3e

148
AUTONOMOUS_ARMING.md Normal file
View File

@ -0,0 +1,148 @@
# Autonomous Arming (Issue #512)
## Overview
The robot can now be armed and operated autonomously from the Jetson without requiring an RC transmitter. The RC receiver (ELRS) is now optional and serves as an override/kill-switch rather than a requirement.
## Arming Sources
### Jetson Autonomous Arming
- Command: `A\n` (single byte 'A' followed by newline)
<<<<<<< HEAD
- Sent via USB CDC to the ESP32 BALANCE firmware
=======
- Sent via USB Serial (CH343) to the ESP32-S3 firmware
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
- Robot arms after ARMING_HOLD_MS (~500ms) safety hold period
- Works even when RC is not connected or not armed
### RC Arming (Optional Override)
- Command: CH5 switch on ELRS transmitter
- When RC is connected and armed, robot can be armed via RC
- RC and Jetson can both request arming independently
## Safety Features
### Maintained from Original Design
1. **Arming Hold Timer** — 500ms hold before motors enable (prevents accidental arming)
2. **Tilt Safety** — Robot must be within ±10° level to arm
3. **IMU Calibration** — Gyro must be calibrated before arming
4. **Remote E-Stop Override**`safety_remote_estop_active()` blocks all arming
### New for Autonomous Operation
1. **RC Kill Switch** (CH5 OFF when RC connected)
- Triggers emergency stop (motor cutoff) instead of disarm
- Allows Jetson-armed robots to remain armed when RC disconnects
- Maintains safety of kill switch for emergency situations
2. **RC Failsafe**
- If RC signal is lost after being established, robot disarms (500ms timeout)
- Prevents runaway if RC connection drops during flight
- USB-only mode (no RC ever connected) is unaffected
3. **Jetson Timeout** (200ms heartbeat)
- Jetson must send heartbeat (H command) every 500ms
- Prevents autonomous runaway if Jetson crashes/loses connection
- Handled by `jetson_cmd_is_active()` checks
## Command Protocol
<<<<<<< HEAD
### From Jetson to ESP32 BALANCE (USB CDC)
=======
### From Jetson to ESP32-S3 (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
```
A — Request arm (triggers safety hold, then motors enable)
D — Request disarm (immediate motor stop)
E — Emergency stop (immediate motor cutoff, latched)
Z — Clear emergency stop latch
H — Heartbeat (refresh timeout timer, every 500ms)
C<spd>,<str> — Drive command: speed, steer (also refreshes heartbeat)
```
<<<<<<< HEAD
### From ESP32 BALANCE to Jetson (USB CDC)
=======
### From ESP32-S3 to Jetson (USB Serial (CH343))
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
Motor commands are gated by `bal.state == BALANCE_ARMED`:
- When ARMED: Motor commands sent every 20ms (50 Hz)
- When DISARMED: Zero sent every 20ms (prevents ESC timeout)
## Arming State Machine
```
DISARMED
+-- Jetson sends 'A' OR RC CH5 rises (with conditions met)
safety_arm_start() called
(arm hold timer starts)
Wait ARMING_HOLD_MS
safety_arm_ready() returns true
balance_arm() called
ARMED ← (motors now respond to commands)
ARMED
+-- Jetson sends 'D' → balance_disarm()
+-- RC CH5 falls AND RC still alive → balance_disarm()
+-- RC signal lost (failsafe) → balance_disarm()
+-- Tilt fault detected → immediate motor stop
+-- RC kill switch (CH5 OFF) → emergency stop (not disarm)
```
## RC Override Priority
When RC is connected and active:
- **Steer channel**: Blended with Jetson via `mode_manager` (per active mode)
- **Kill switch**: RC CH5 OFF triggers emergency stop (overrides everything)
- **Failsafe**: RC signal loss triggers disarm (prevents runaway)
When RC is disconnected:
- Robot operates under Jetson commands alone
- Emergency stop remains available via 'E' command from Jetson
- No automatic mode change; mission continues autonomously
## Testing Checklist
- [ ] Jetson can arm robot without RC (send 'A' command)
- [ ] Robot motors respond to Jetson drive commands when armed
- [ ] Robot disarms on Jetson 'D' command
- [ ] RC kill switch (CH5 OFF) triggers emergency stop without disarming
- [ ] Robot can be re-armed after RC kill switch via Jetson 'A' command
- [ ] RC failsafe still works (500ms signal loss = disarm)
- [ ] Jetson heartbeat timeout works (500ms without H/C = motors zero)
- [ ] Tilt fault still triggers immediate stop
- [ ] IMU calibration required before arm
- [ ] Arming hold timer (500ms) enforced
## Migration from RC-Only
### Old Workflow (ELRS-Required)
1. Power on robot
2. Arm via RC CH5
3. Send speed/steer commands via RC
4. Disarm via RC CH5
### New Workflow (Autonomous)
1. Power on robot
2. Send heartbeat 'H' every 500ms from Jetson
3. When ready to move, send 'A' command (wait 500ms)
4. Send drive commands 'C<spd>,<str>' every ≤200ms
5. When done, send 'D' command to disarm
### New Workflow (RC + Autonomous Mixed)
1. Power on robot, bring up RC
2. Jetson sends heartbeat 'H'
3. Arm via RC CH5 OR Jetson 'A' (both valid)
4. Control via RC sticks OR Jetson drive commands (blended)
5. Emergency kill: RC CH5 OFF (emergency stop) OR Jetson 'E'
6. Disarm: RC CH5 OFF then ON, OR Jetson 'D'
## References
- Issue #512: Remove ELRS arm requirement
- Files: `/src/main.c` (arming logic), `/lib/USB_CDC/src/usbd_cdc_if.c` (CDC commands)

View File

@ -1,17 +1,36 @@
# SaltyLab Firmware — Agent Playbook # SaltyLab Firmware — Agent Playbook
## Project ## Project
Self-balancing two-wheeled robot: STM32F722 flight controller, hoverboard hub motors, Jetson Nano for AI/SLAM. <<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards + Jetson Orin via CAN. Full spec: `docs/SAUL-TEE-SYSTEM-REFERENCE.md`
| Board | Role |
|-------|------|
| **ESP32-S3 BALANCE** | QMI8658 IMU, PID balance, CAN→VESC (L:68 / R:56), GC9A01 LCD (Waveshare Touch LCD 1.28) |
| **ESP32-S3 IO** | TBS Crossfire RC, ELRS failover, BTS7960 motors, NFC/baro/ToF, WS2812 |
| **Jetson Orin** | AI/SLAM, CANable2 USB→CAN, cmds 0x3000x303, telemetry 0x4000x401 |
> **Legacy:** `src/` and `include/` = archived STM32 HAL — do not extend. New firmware in `esp32/`.
=======
Self-balancing two-wheeled robot: ESP32-S3 ESP32-S3 BALANCE, hoverboard hub motors, Jetson Orin Nano Super for AI/SLAM.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Team ## Team
| Agent | Role | Focus | | Agent | Role | Focus |
|-------|------|-------| |-------|------|-------|
| **sl-firmware** | Embedded Firmware Lead | STM32 HAL, USB CDC debugging, SPI/UART, PlatformIO, DFU bootloader | <<<<<<< HEAD
| **sl-firmware** | Embedded Firmware Lead | ESP32-S3, ESP-IDF, QMI8658, CAN/UART protocol, BTS7960 |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU fusion, balance loop, safety |
| **sl-perception** | Perception / SLAM Engineer | Jetson Orin, RealSense D435i, RPLIDAR, ROS2, Nav2 |
=======
| **sl-firmware** | Embedded Firmware Lead | ESP-IDF, USB Serial (CH343) debugging, SPI/UART, PlatformIO, DFU bootloader |
| **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems | | **sl-controls** | Control Systems Engineer | PID tuning, IMU sensor fusion, real-time control loops, safety systems |
| **sl-perception** | Perception / SLAM Engineer | Jetson Nano, RealSense D435i, RPLIDAR, ROS2, Nav2 | | **sl-perception** | Perception / SLAM Engineer | Jetson Orin Nano Super, RealSense D435i, RPLIDAR, ROS2, Nav2 |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
## Status ## Status
USB CDC TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix). USB Serial (CH343) TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG ordering fix).
## Repo Structure ## Repo Structure
- `projects/saltybot/SALTYLAB.md` — Design doc - `projects/saltybot/SALTYLAB.md` — Design doc
@ -29,11 +48,11 @@ USB CDC TX bug resolved (PR #10 — DCache MPU non-cacheable region + IWDG order
| `saltyrover-dev` | Integration — rover variant | | `saltyrover-dev` | Integration — rover variant |
| `saltytank` | Stable — tracked tank variant | | `saltytank` | Stable — tracked tank variant |
| `saltytank-dev` | Integration — tank variant | | `saltytank-dev` | Integration — tank variant |
| `main` | Shared code only (IMU drivers, USB CDC, balance core, safety) | | `main` | Shared code only (IMU drivers, USB Serial (CH343), balance core, safety) |
### Rules ### Rules
- Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev` - Agents branch FROM `<variant>-dev` and PR back TO `<variant>-dev`
- Shared/infrastructure code (IMU drivers, USB CDC, balance core, safety) goes in `main` - Shared/infrastructure code (IMU drivers, USB Serial (CH343), balance core, safety) goes in `main`
- Variant-specific code (motor topology, kinematics, config) goes in variant branches - Variant-specific code (motor topology, kinematics, config) goes in variant branches
- Stable branches get promoted from `-dev` after review and hardware testing - Stable branches get promoted from `-dev` after review and hardware testing
- **Current SaltyLab team** works against `saltylab-dev` - **Current SaltyLab team** works against `saltylab-dev`

52
TEAM.md
View File

@ -1,12 +1,22 @@
# SaltyLab — Ideal Team # SaltyLab — Ideal Team
## Project ## Project
Self-balancing two-wheeled robot using a drone flight controller (STM32F722), hoverboard hub motors, and eventually a Jetson Nano for AI/SLAM. <<<<<<< HEAD
**SAUL-TEE** — 4-wheel wagon (870×510×550 mm, 23 kg).
Two ESP32-S3 boards (BALANCE + IO) + Jetson Orin. See `docs/SAUL-TEE-SYSTEM-REFERENCE.md`.
## Current Status
- **Hardware:** ESP32-S3 BALANCE (Waveshare Touch LCD 1.28, CH343 USB) + ESP32-S3 IO (bare devkit, JTAG USB)
- **Firmware:** ESP-IDF/PlatformIO target; legacy `src/` STM32 HAL archived
- **Comms:** UART 460800 baud inter-board; CANable2 USB→CAN for Orin; CAN 500 kbps to VESCs (L:68 / R:56)
=======
Self-balancing two-wheeled robot using a drone ESP32-S3 BALANCE (ESP32-S3), hoverboard hub motors, and eventually a Jetson Orin Nano Super for AI/SLAM.
## Current Status ## Current Status
- **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand - **Hardware:** Assembled — FC, motors, ESC, IMU, battery, RC all on hand
- **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB CDC bug - **Firmware:** Balance PID + hoverboard ESC protocol written, but blocked by USB Serial (CH343) bug
- **Blocker:** USB CDC TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB OTG FS — see `USB_CDC_BUG.md` - **Blocker:** USB Serial (CH343) TX stops working when peripheral inits (SPI/UART/GPIO) are added alongside USB on ESP32-S3 — see `legacy/stm32/USB_CDC_BUG.md` for historical context
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
--- ---
@ -14,18 +24,30 @@ Self-balancing two-wheeled robot using a drone flight controller (STM32F722), ho
### 1. Embedded Firmware Engineer (Lead) ### 1. Embedded Firmware Engineer (Lead)
**Must-have:** **Must-have:**
- Deep STM32 HAL experience (F7 series specifically) <<<<<<< HEAD
- Deep ESP32 (Arduino/ESP-IDF) or STM32 HAL experience
- USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts) - USB OTG FS / CDC ACM debugging (TxState, endpoint management, DMA conflicts)
- SPI + UART + USB coexistence on STM32 - SPI + UART + USB coexistence on ESP32
- PlatformIO or bare-metal STM32 toolchain - PlatformIO or bare-metal ESP32 toolchain
- DFU bootloader implementation - DFU bootloader implementation
=======
- Deep ESP-IDF experience (ESP32-S3 specifically)
- USB Serial (CH343) / UART debugging on ESP32-S3
- SPI + UART + USB coexistence on ESP32-S3
- ESP-IDF / Arduino-ESP32 toolchain
- OTA firmware update implementation
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
**Nice-to-have:** **Nice-to-have:**
- Betaflight/iNav/ArduPilot codebase familiarity - ESP32-S3 peripheral coexistence (SPI + UART + USB)
- PID control loop tuning for balance robots - PID control loop tuning for balance robots
- FOC motor control (hoverboard ESC protocol) - FOC motor control (hoverboard ESC protocol)
**Why:** The immediate blocker is a USB peripheral conflict. Need someone who's debugged STM32 USB issues before — this is not a software logic bug, it's a hardware peripheral interaction issue. <<<<<<< HEAD
**Why:** The immediate blocker is a USB peripheral conflict. Need someone who's debugged STM32 USB issues before — ESP32 firmware for the balance loop and I/O needs to be written from scratch.
=======
**Why:** The immediate blocker is a USB peripheral conflict on ESP32-S3. Need someone who's debugged ESP32-S3 USB Serial (CH343) issues before — this is not a software logic bug, it's a hardware peripheral interaction issue.
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
### 2. Control Systems / Robotics Engineer ### 2. Control Systems / Robotics Engineer
**Must-have:** **Must-have:**
@ -43,7 +65,7 @@ Self-balancing two-wheeled robot using a drone flight controller (STM32F722), ho
### 3. Perception / SLAM Engineer (Phase 2) ### 3. Perception / SLAM Engineer (Phase 2)
**Must-have:** **Must-have:**
- Jetson Nano / NVIDIA Jetson platform - Jetson Orin Nano Super / NVIDIA Jetson platform
- Intel RealSense D435i depth camera - Intel RealSense D435i depth camera
- RPLIDAR integration - RPLIDAR integration
- SLAM (ORB-SLAM3, RTAB-Map, or similar) - SLAM (ORB-SLAM3, RTAB-Map, or similar)
@ -54,19 +76,23 @@ Self-balancing two-wheeled robot using a drone flight controller (STM32F722), ho
- Obstacle avoidance - Obstacle avoidance
- Nav2 stack - Nav2 stack
**Why:** Phase 2 goal is autonomous navigation. Jetson Nano with RealSense + RPLIDAR for indoor mapping and person following. **Why:** Phase 2 goal is autonomous navigation. Jetson Orin Nano Super with RealSense + RPLIDAR for indoor mapping and person following.
--- ---
## Hardware Reference ## Hardware Reference
| Component | Details | | Component | Details |
|-----------|---------| |-----------|---------|
| FC | MAMBA F722S (STM32F722RET6, MPU6000) | <<<<<<< HEAD
| FC | ESP32 BALANCE (ESP32RET6, MPU6000) |
=======
| FC | ESP32-S3 BALANCE (ESP32-S3RET6, QMI8658) |
>>>>>>> 291dd68 (feat: remove all STM32/Mamba/BlackPill references — ESP32-S3 only)
| Motors | 2x 8" pneumatic hoverboard hub motors | | Motors | 2x 8" pneumatic hoverboard hub motors |
| ESC | Hoverboard ESC (EFeru FOC firmware) | | ESC | Hoverboard ESC (EFeru FOC firmware) |
| Battery | 36V pack | | Battery | 36V pack |
| RC | BetaFPV ELRS 2.4GHz TX + RX | | RC | BetaFPV ELRS 2.4GHz TX + RX |
| AI Brain | Jetson Nano + Noctua fan | | AI Brain | Jetson Orin Nano Super + Noctua fan |
| Depth | Intel RealSense D435i | | Depth | Intel RealSense D435i |
| LIDAR | RPLIDAR A1M8 | | LIDAR | RPLIDAR A1M8 |
| Spare IMUs | BNO055, MPU6050 | | Spare IMUs | BNO055, MPU6050 |
@ -74,4 +100,4 @@ Self-balancing two-wheeled robot using a drone flight controller (STM32F722), ho
## Repo ## Repo
- Gitea: https://gitea.vayrette.com/seb/saltylab-firmware - Gitea: https://gitea.vayrette.com/seb/saltylab-firmware
- Design doc: `projects/saltybot/SALTYLAB.md` - Design doc: `projects/saltybot/SALTYLAB.md`
- Bug doc: `USB_CDC_BUG.md` - Bug doc: `legacy/stm32/USB_CDC_BUG.md` (archived — STM32 era)

View File

@ -1,44 +0,0 @@
# USB CDC TX Bug — 2026-02-28
## Problem
Balance firmware produces no USB CDC output. Minimal "hello" test firmware works fine.
## What Works
- **Test firmware** (just sends `{"hello":N}` at 10Hz after 3s delay): **DATA FLOWS**
- USB enumeration works in both cases (port appears as `/dev/cu.usbmodemSALTY0011`)
- DFU reboot via RTC backup register works (Betaflight-proven pattern)
## What Doesn't Work
- **Balance firmware**: port opens, no data ever arrives
- Tried: removing init transmit, 3s boot delay, TxState recovery, DTR detection, streaming flags
- None of it helps
## Key Difference Between Working & Broken
- **Working test**: main.c only includes USB CDC headers, HAL, string, stdio
- **Balance firmware**: includes icm42688.h, bmp280.h, balance.h, hoverboard.h, config.h, status.h
- Balance firmware inits SPI1 (IMU), USART2 (hoverboard), GPIO (LEDs, buzzer)
- Likely culprit: **peripheral init (SPI/UART/GPIO) is interfering with USB OTG FS**
## Suspected Root Cause
One of the additional peripheral inits (SPI1 for IMU, USART2 for hoverboard ESC, or GPIO for status LEDs) is likely conflicting with the USB OTG FS peripheral — either a clock conflict, GPIO pin conflict, or interrupt priority issue.
## Hardware
- MAMBA F722S FC (STM32F722RET6)
- Betaflight target: DIAT-MAMBAF722_2022B
- IMU: MPU6000 on SPI1 (PA4/PA5/PA6/PA7)
- USB: OTG FS (PA11/PA12)
- Hoverboard ESC: USART2 (PA2/PA3)
- LEDs: PC14, PC15
- Buzzer: PB2
## Files
- PlatformIO project: `~/Projects/saltylab-firmware/` on mbpm4 (192.168.87.40)
- Working test: was in src/main.c (replaced with balance code)
- Balance main.c backup: src/main.c.bak
- CDC implementation: lib/USB_CDC/src/usbd_cdc_if.c
## To Debug
1. Add peripherals one at a time to the test firmware to find which one breaks CDC TX
2. Check for GPIO pin conflicts with USB OTG FS (PA11/PA12)
3. Check interrupt priorities — USB OTG FS IRQ might be getting starved
4. Check if DCache (disabled via SCB_DisableDCache) is needed for USB DMA

46
android/build.gradle Normal file
View File

@ -0,0 +1,46 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdk 34
namespace 'com.saltylab.uwbtag'
defaultConfig {
applicationId "com.saltylab.uwbtag"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
}
}
buildFeatures {
viewBinding true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'com.google.code.gson:gson:2.10.1'
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- BLE permissions (API 31+) -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Legacy BLE (API < 31) -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<application
android:allowBackup="true"
android:label="UWB Tag Config"
android:theme="@style/Theme.MaterialComponents.DayNight.DarkActionBar">
<activity
android:name=".UwbTagBleActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -0,0 +1,444 @@
package com.saltylab.uwbtag
import android.Manifest
import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.*
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.card.MaterialCardView
import com.google.android.material.switchmaterial.SwitchMaterial
import com.google.android.material.textfield.TextInputEditText
import com.google.gson.Gson
import com.saltylab.uwbtag.databinding.ActivityUwbTagBleBinding
import java.util.UUID
// ---------------------------------------------------------------------------
// GATT service / characteristic UUIDs
// ---------------------------------------------------------------------------
private val SERVICE_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef0")
private val CHAR_CONFIG_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef1") // read/write JSON config
private val CHAR_STATUS_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef2") // notify: tag status string
private val CHAR_BATT_UUID = UUID.fromString("12345678-1234-5678-1234-56789abcdef3") // notify: battery %
private val CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
// BLE scan timeout
private const val SCAN_TIMEOUT_MS = 15_000L
// Permissions request code
private const val REQ_PERMISSIONS = 1001
// ---------------------------------------------------------------------------
// Data model
// ---------------------------------------------------------------------------
data class TagConfig(
val tag_name: String = "UWB_TAG_0001",
val sleep_timeout_s: Int = 300,
val display_brightness: Int = 50,
val uwb_channel: Int = 9,
val ranging_interval_ms: Int = 100,
val battery_report: Boolean = true
)
data class ScannedDevice(
val name: String,
val address: String,
var rssi: Int,
val device: BluetoothDevice
)
// ---------------------------------------------------------------------------
// RecyclerView adapter for scanned devices
// ---------------------------------------------------------------------------
class DeviceAdapter(
private val onConnect: (ScannedDevice) -> Unit
) : RecyclerView.Adapter<DeviceAdapter.VH>() {
private val items = mutableListOf<ScannedDevice>()
fun update(device: ScannedDevice) {
val idx = items.indexOfFirst { it.address == device.address }
if (idx >= 0) {
items[idx] = device
notifyItemChanged(idx)
} else {
items.add(device)
notifyItemInserted(items.size - 1)
}
}
fun clear() {
items.clear()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_ble_device, parent, false)
return VH(view)
}
override fun onBindViewHolder(holder: VH, position: Int) = holder.bind(items[position])
override fun getItemCount() = items.size
inner class VH(view: View) : RecyclerView.ViewHolder(view) {
private val tvName = view.findViewById<TextView>(R.id.tvDeviceName)
private val tvAddress = view.findViewById<TextView>(R.id.tvDeviceAddress)
private val tvRssi = view.findViewById<TextView>(R.id.tvRssi)
private val btnConn = view.findViewById<Button>(R.id.btnConnect)
fun bind(item: ScannedDevice) {
tvName.text = item.name
tvAddress.text = item.address
tvRssi.text = "${item.rssi} dBm"
btnConn.setOnClickListener { onConnect(item) }
}
}
}
// ---------------------------------------------------------------------------
// Activity
// ---------------------------------------------------------------------------
@SuppressLint("MissingPermission") // permissions checked at runtime before any BLE call
class UwbTagBleActivity : AppCompatActivity() {
private lateinit var binding: ActivityUwbTagBleBinding
private val gson = Gson()
private val mainHandler = Handler(Looper.getMainLooper())
// BLE
private val btManager by lazy { getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager }
private val btAdapter by lazy { btManager.adapter }
private var bleScanner: BluetoothLeScanner? = null
private var gatt: BluetoothGatt? = null
private var configChar: BluetoothGattCharacteristic? = null
private var statusChar: BluetoothGattCharacteristic? = null
private var battChar: BluetoothGattCharacteristic? = null
private var isScanning = false
private val deviceAdapter = DeviceAdapter(onConnect = ::connectToDevice)
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUwbTagBleBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
binding.rvDevices.layoutManager = LinearLayoutManager(this)
binding.rvDevices.adapter = deviceAdapter
binding.btnScan.setOnClickListener {
if (isScanning) stopScan() else startScanIfPermitted()
}
binding.btnDisconnect.setOnClickListener { disconnectGatt() }
binding.btnReadConfig.setOnClickListener { readConfig() }
binding.btnWriteConfig.setOnClickListener { writeConfig() }
requestBlePermissions()
}
override fun onDestroy() {
super.onDestroy()
stopScan()
disconnectGatt()
}
// ---------------------------------------------------------------------------
// Permissions
// ---------------------------------------------------------------------------
private fun requestBlePermissions() {
val needed = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!hasPermission(Manifest.permission.BLUETOOTH_SCAN))
needed += Manifest.permission.BLUETOOTH_SCAN
if (!hasPermission(Manifest.permission.BLUETOOTH_CONNECT))
needed += Manifest.permission.BLUETOOTH_CONNECT
} else {
if (!hasPermission(Manifest.permission.ACCESS_FINE_LOCATION))
needed += Manifest.permission.ACCESS_FINE_LOCATION
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), REQ_PERMISSIONS)
}
}
private fun hasPermission(perm: String) =
ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQ_PERMISSIONS &&
grantResults.any { it != PackageManager.PERMISSION_GRANTED }) {
toast("BLE permissions required")
}
}
// ---------------------------------------------------------------------------
// BLE Scan
// ---------------------------------------------------------------------------
private fun startScanIfPermitted() {
if (btAdapter?.isEnabled != true) { toast("Bluetooth is off"); return }
bleScanner = btAdapter.bluetoothLeScanner
val filter = ScanFilter.Builder()
.setDeviceNamePattern("UWB_TAG_.*".toRegex().toPattern())
.build()
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
deviceAdapter.clear()
bleScanner?.startScan(listOf(filter), settings, scanCallback)
isScanning = true
binding.btnScan.text = "Stop"
binding.tvScanStatus.text = "Scanning…"
mainHandler.postDelayed({ stopScan() }, SCAN_TIMEOUT_MS)
}
private fun stopScan() {
bleScanner?.stopScan(scanCallback)
isScanning = false
binding.btnScan.text = "Scan"
binding.tvScanStatus.text = "Scan stopped"
}
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val name = result.device.name ?: return
if (!name.startsWith("UWB_TAG_")) return
val dev = ScannedDevice(
name = name,
address = result.device.address,
rssi = result.rssi,
device = result.device
)
mainHandler.post { deviceAdapter.update(dev) }
}
override fun onScanFailed(errorCode: Int) {
mainHandler.post {
binding.tvScanStatus.text = "Scan failed (code $errorCode)"
isScanning = false
binding.btnScan.text = "Scan"
}
}
}
// ---------------------------------------------------------------------------
// GATT Connection
// ---------------------------------------------------------------------------
private fun connectToDevice(scanned: ScannedDevice) {
stopScan()
binding.tvScanStatus.text = "Connecting to ${scanned.name}"
gatt = scanned.device.connectGatt(this, false, gattCallback, BluetoothDevice.TRANSPORT_LE)
}
private fun disconnectGatt() {
gatt?.disconnect()
gatt?.close()
gatt = null
configChar = null
statusChar = null
battChar = null
mainHandler.post {
binding.cardConfig.visibility = View.GONE
binding.tvScanStatus.text = "Disconnected"
}
}
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
mainHandler.post { binding.tvScanStatus.text = "Connected — discovering services…" }
g.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
mainHandler.post {
binding.cardConfig.visibility = View.GONE
binding.tvScanStatus.text = "Disconnected"
toast("Tag disconnected")
}
gatt?.close()
gatt = null
}
}
}
override fun onServicesDiscovered(g: BluetoothGatt, status: Int) {
if (status != BluetoothGatt.GATT_SUCCESS) {
mainHandler.post { toast("Service discovery failed") }
return
}
val service = g.getService(SERVICE_UUID)
if (service == null) {
mainHandler.post { toast("UWB config service not found on tag") }
return
}
configChar = service.getCharacteristic(CHAR_CONFIG_UUID)
statusChar = service.getCharacteristic(CHAR_STATUS_UUID)
battChar = service.getCharacteristic(CHAR_BATT_UUID)
// Subscribe to status notifications
statusChar?.let { enableNotifications(g, it) }
battChar?.let { enableNotifications(g, it) }
// Initial config read
configChar?.let { g.readCharacteristic(it) }
mainHandler.post {
val devName = g.device.name ?: g.device.address
binding.tvConnectedName.text = "Connected: $devName"
binding.cardConfig.visibility = View.VISIBLE
binding.tvScanStatus.text = "Connected to $devName"
}
}
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) return
if (characteristic.uuid == CHAR_CONFIG_UUID) {
val json = characteristic.value?.toString(Charsets.UTF_8) ?: return
val cfg = runCatching { gson.fromJson(json, TagConfig::class.java) }.getOrNull() ?: return
mainHandler.post { populateFields(cfg) }
}
}
// API 33+ callback
override fun onCharacteristicRead(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int
) {
if (status != BluetoothGatt.GATT_SUCCESS) return
if (characteristic.uuid == CHAR_CONFIG_UUID) {
val json = value.toString(Charsets.UTF_8)
val cfg = runCatching { gson.fromJson(json, TagConfig::class.java) }.getOrNull() ?: return
mainHandler.post { populateFields(cfg) }
}
}
override fun onCharacteristicWrite(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
val msg = if (status == BluetoothGatt.GATT_SUCCESS) "Config written" else "Write failed ($status)"
mainHandler.post { toast(msg) }
}
override fun onCharacteristicChanged(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val value = characteristic.value ?: return
handleNotification(characteristic.uuid, value)
}
// API 33+ callback
override fun onCharacteristicChanged(
g: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
handleNotification(characteristic.uuid, value)
}
}
// ---------------------------------------------------------------------------
// Notification helpers
// ---------------------------------------------------------------------------
private fun enableNotifications(g: BluetoothGatt, char: BluetoothGattCharacteristic) {
g.setCharacteristicNotification(char, true)
val descriptor = char.getDescriptor(CCCD_UUID) ?: return
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
g.writeDescriptor(descriptor)
}
private fun handleNotification(uuid: UUID, value: ByteArray) {
val text = value.toString(Charsets.UTF_8)
mainHandler.post {
when (uuid) {
CHAR_STATUS_UUID -> binding.tvTagStatus.text = "Status: $text"
CHAR_BATT_UUID -> {
val pct = text.toIntOrNull() ?: return@post
binding.tvTagStatus.text = binding.tvTagStatus.text.toString()
.replace(Regex("\\| Batt:.*"), "")
.trimEnd() + " | Batt: $pct%"
}
}
}
}
// ---------------------------------------------------------------------------
// Config read / write
// ---------------------------------------------------------------------------
private fun readConfig() {
val g = gatt ?: run { toast("Not connected"); return }
val c = configChar ?: run { toast("Config char not found"); return }
g.readCharacteristic(c)
}
private fun writeConfig() {
val g = gatt ?: run { toast("Not connected"); return }
val c = configChar ?: run { toast("Config char not found"); return }
val cfg = buildConfigFromFields()
val json = gson.toJson(cfg)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
g.writeCharacteristic(c, json.toByteArray(Charsets.UTF_8),
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)
} else {
@Suppress("DEPRECATION")
c.value = json.toByteArray(Charsets.UTF_8)
@Suppress("DEPRECATION")
g.writeCharacteristic(c)
}
}
// ---------------------------------------------------------------------------
// UI helpers
// ---------------------------------------------------------------------------
private fun populateFields(cfg: TagConfig) {
binding.etTagName.setText(cfg.tag_name)
binding.etSleepTimeout.setText(cfg.sleep_timeout_s.toString())
binding.etBrightness.setText(cfg.display_brightness.toString())
binding.etUwbChannel.setText(cfg.uwb_channel.toString())
binding.etRangingInterval.setText(cfg.ranging_interval_ms.toString())
binding.switchBatteryReport.isChecked = cfg.battery_report
}
private fun buildConfigFromFields() = TagConfig(
tag_name = binding.etTagName.text?.toString() ?: "UWB_TAG_0001",
sleep_timeout_s = binding.etSleepTimeout.text?.toString()?.toIntOrNull() ?: 300,
display_brightness = binding.etBrightness.text?.toString()?.toIntOrNull() ?: 50,
uwb_channel = binding.etUwbChannel.text?.toString()?.toIntOrNull() ?: 9,
ranging_interval_ms = binding.etRangingInterval.text?.toString()?.toIntOrNull() ?: 100,
battery_report = binding.switchBatteryReport.isChecked
)
private fun toast(msg: String) =
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}

View File

@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:title="UWB Tag BLE Config" />
<!-- Scan controls -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<Button
android:id="@+id/btnScan"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Scan" />
<TextView
android:id="@+id/tvScanStatus"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="Tap Scan to find UWB tags"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" />
</LinearLayout>
<!-- Scan results list -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="12dp"
android:text="Nearby Tags"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvDevices"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp"
android:clipToPadding="false" />
<!-- Connected device config panel -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardConfig"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:visibility="gone"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tvConnectedName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Connected: —"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textStyle="bold" />
<Button
android:id="@+id/btnDisconnect"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Disconnect" />
</LinearLayout>
<!-- tag_name -->
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Tag Name">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etTagName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<!-- sleep_timeout_s and uwb_channel (row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:hint="Sleep Timeout (s)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etSleepTimeout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="UWB Channel">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etUwbChannel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- display_brightness and ranging_interval_ms (row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp">
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:hint="Brightness (0-100)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etBrightness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:hint="Ranging Interval (ms)">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etRangingInterval"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<!-- battery_report toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchBatteryReport"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Battery Reporting" />
<!-- Action buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/btnReadConfig"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="Read" />
<Button
android:id="@+id/btnWriteConfig"
style="@style/Widget.MaterialComponents.Button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Write" />
</LinearLayout>
<!-- Status notifications from tag -->
<TextView
android:id="@+id/tvTagStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#1A000000"
android:fontFamily="monospace"
android:padding="8dp"
android:text="Tag status: —"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
app:cardElevation="2dp"
android:clickable="true"
android:focusable="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tvDeviceName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="UWB_TAG_XXXX"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDeviceAddress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="XX:XX:XX:XX:XX:XX"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" />
</LinearLayout>
<TextView
android:id="@+id/tvRssi"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-70 dBm"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="?attr/colorSecondary" />
<Button
android:id="@+id/btnConnect"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Connect" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

118
cad/assembly.scad Normal file
View File

@ -0,0 +1,118 @@
// ============================================
// SaltyLab Full Assembly Visualization
// Shows all parts in position on 2020 spine
// ============================================
include <dimensions.scad>
// Spine height
spine_h = 500;
// Component heights (center of each mount on spine)
h_motor = 0;
h_battery = 50;
h_esc = 100;
h_fc = 170;
h_jetson = 250;
h_realsense = 350;
h_lidar = 430;
// Colors for visualization
module spine() {
color("silver")
translate([-extrusion_w/2, -extrusion_w/2, 0])
cube([extrusion_w, extrusion_w, spine_h]);
}
module wheel(side) {
color("DimGray")
translate([side * 140, 0, 0])
rotate([0, 90, 0])
cylinder(d=200, h=50, center=true, $fn=60);
}
// --- Assembly ---
// Spine
spine();
// Wheels
wheel(-1);
wheel(1);
// Motor mount plate (at base)
color("DodgerBlue", 0.7)
translate([0, 0, h_motor])
import("motor_mount_plate.stl");
// Battery shelf
color("OrangeRed", 0.7)
translate([0, 0, h_battery])
rotate([0, 0, 0])
cube([180, 80, 40], center=true);
// ESC
color("Green", 0.7)
translate([0, 0, h_esc])
cube([80, 50, 15], center=true);
// FC (tiny!)
color("Purple", 0.9)
translate([0, 0, h_fc])
cube([36, 36, 5], center=true);
// Jetson Orin Nano Super
color("LimeGreen", 0.7)
translate([0, 0, h_jetson])
cube([100, 80, 29], center=true);
// RealSense D435i
color("Gray", 0.8)
translate([0, -40, h_realsense])
cube([90, 25, 25], center=true);
// RPLIDAR A1
color("Cyan", 0.7)
translate([0, 0, h_lidar])
cylinder(d=70, h=41, center=true, $fn=40);
// Kill switch (accessible on front)
color("Red")
translate([0, -60, h_esc + 30])
cylinder(d=22, h=10, $fn=30);
// LED ring
color("White", 0.3)
translate([0, 0, h_jetson - 20])
difference() {
cylinder(d=120, h=15, $fn=60);
translate([0, 0, -1])
cylinder(d=110, h=17, $fn=60);
}
// Bumpers
color("Orange", 0.5) {
translate([0, -75, 25])
cube([350, 30, 50], center=true);
translate([0, 75, 25])
cube([350, 30, 50], center=true);
}
// Handle (top)
color("Yellow", 0.7)
translate([0, 0, spine_h + 10])
cube([100, 20, 25], center=true);
// Tether point
color("Red", 0.8)
translate([0, 0, spine_h - 20]) {
difference() {
cylinder(d=30, h=8, $fn=30);
translate([0, 0, -1])
cylinder(d=15, h=10, $fn=30);
}
}
echo("=== SaltyLab Assembly ===");
echo(str("Total height: ", spine_h + 30, "mm"));
echo(str("Width (axle-axle): ", 280 + 50*2, "mm"));
echo(str("Depth: ~", 150, "mm"));

77
cad/battery_shelf.scad Normal file
View File

@ -0,0 +1,77 @@
// ============================================
// SaltyLab Battery Shelf
// 200×100×40mm PETG
// Holds 36V battery pack low on the frame
// Mounts to 2020 extrusion spine
// ============================================
include <dimensions.scad>
shelf_w = 200;
shelf_d = 100;
shelf_h = 40;
floor_h = 3; // Bottom plate
// Battery pocket (with tolerance)
pocket_w = batt_w + tol*2;
pocket_d = batt_d + tol*2;
pocket_h = batt_h + 5; // Slightly taller than battery
// Velcro strap slots
strap_w = 25;
strap_h = 3;
module battery_shelf() {
difference() {
union() {
// Floor
translate([-shelf_w/2, -shelf_d/2, 0])
cube([shelf_w, shelf_d, floor_h]);
// Walls (3 sides front open for wires)
// Left wall
translate([-shelf_w/2, -shelf_d/2, 0])
cube([wall, shelf_d, shelf_h]);
// Right wall
translate([shelf_w/2 - wall, -shelf_d/2, 0])
cube([wall, shelf_d, shelf_h]);
// Back wall
translate([-shelf_w/2, shelf_d/2 - wall, 0])
cube([shelf_w, wall, shelf_h]);
// Front lip (low, keeps battery from sliding out)
translate([-shelf_w/2, -shelf_d/2, 0])
cube([shelf_w, wall, 10]);
// 2020 extrusion mount tabs (top of back wall)
for (x = [-30, 30]) {
translate([x - 10, shelf_d/2 - wall, shelf_h - 15])
cube([20, wall + 10, 15]);
}
}
// Extrusion bolt holes (M5) through back mount tabs
for (x = [-30, 30]) {
translate([x, shelf_d/2 + 5, shelf_h - 7.5])
rotate([90, 0, 0])
cylinder(d=m5_clear, h=wall + 15, $fn=30);
}
// Velcro strap slots (2x through floor for securing battery)
for (x = [-50, 50]) {
translate([x - strap_w/2, -20, -1])
cube([strap_w, strap_h, floor_h + 2]);
}
// Weight reduction holes in floor
for (x = [-30, 30]) {
translate([x, 0, -1])
cylinder(d=20, h=floor_h + 2, $fn=30);
}
// Wire routing slot (front wall, centered)
translate([-20, -shelf_d/2 - 1, floor_h])
cube([40, wall + 2, 15]);
}
}
battery_shelf();

75
cad/bumper.scad Normal file
View File

@ -0,0 +1,75 @@
// ============================================
// SaltyLab Bumper (Front/Rear)
// 350×50×30mm TPU
// Absorbs falls, protects frame and floor
// ============================================
include <dimensions.scad>
bumper_w = 350;
bumper_h = 50;
bumper_d = 30;
bumper_wall = 2.5;
// Honeycomb crush structure for energy absorption
hex_size = 8;
hex_wall = 1.2;
module honeycomb_cell(size, height) {
difference() {
cylinder(d=size, h=height, $fn=6);
translate([0, 0, -1])
cylinder(d=size - hex_wall*2, h=height + 2, $fn=6);
}
}
module bumper() {
difference() {
union() {
// Outer shell (curved front face)
hull() {
translate([-bumper_w/2, 0, 0])
cube([bumper_w, 1, bumper_h]);
translate([-bumper_w/2 + 10, bumper_d - 5, 5])
cube([bumper_w - 20, 1, bumper_h - 10]);
}
}
// Hollow interior (leave outer shell)
hull() {
translate([-bumper_w/2 + bumper_wall, bumper_wall, bumper_wall])
cube([bumper_w - bumper_wall*2, 1, bumper_h - bumper_wall*2]);
translate([-bumper_w/2 + 10 + bumper_wall, bumper_d - 5 - bumper_wall, 5 + bumper_wall])
cube([bumper_w - 20 - bumper_wall*2, 1, bumper_h - 10 - bumper_wall*2]);
}
// Mounting bolt holes (M5, through back face, 4 points)
for (x = [-120, -40, 40, 120]) {
translate([x, -1, bumper_h/2])
rotate([-90, 0, 0])
cylinder(d=m5_clear, h=10, $fn=25);
}
}
// Internal honeycomb ribs for crush absorption
intersection() {
// Bound to bumper volume
hull() {
translate([-bumper_w/2 + bumper_wall, bumper_wall, bumper_wall])
cube([bumper_w - bumper_wall*2, 1, bumper_h - bumper_wall*2]);
translate([-bumper_w/2 + 15, bumper_d - 8, 8])
cube([bumper_w - 30, 1, bumper_h - 16]);
}
// Honeycomb grid
for (x = [-170:hex_size*1.5:170]) {
for (z = [0:hex_size*1.3:60]) {
offset_x = (floor(z / (hex_size*1.3)) % 2) * hex_size * 0.75;
translate([x + offset_x, 0, z])
rotate([-90, 0, 0])
honeycomb_cell(hex_size, bumper_d);
}
}
}
}
bumper();

73
cad/dimensions.scad Normal file
View File

@ -0,0 +1,73 @@
// ============================================
// SaltyLab Common Dimensions & Constants
// ============================================
// --- 2020 Aluminum Extrusion ---
extrusion_w = 20;
extrusion_slot = 6; // T-slot width
extrusion_bore = 5; // Center bore M5
// --- Hub Motors (8" hoverboard) ---
motor_axle_dia = 12;
motor_axle_len = 45;
motor_axle_flat = 10; // Flat-to-flat if D-shaft
motor_body_dia = 200; // ~8 inches
motor_bolt_circle = 0; // Axle-only mount (clamp style)
// --- Drone FC (30.5mm standard) ---
fc_hole_spacing = 25.5; // GEP-F722 AIO v2 (not standard 30.5!)
fc_hole_dia = 3.2; // M3 clearance
fc_board_size = 36; // Typical FC PCB
fc_standoff_h = 5; // Rubber standoff height
// --- Jetson Orin Nano Super ---
jetson_w = 100;
jetson_d = 80;
jetson_h = 29; // With heatsink
jetson_hole_x = 86; // Mounting hole spacing X
jetson_hole_y = 58; // Mounting hole spacing Y
jetson_hole_dia = 2.7; // M2.5 clearance
// --- RealSense D435i ---
rs_w = 90;
rs_d = 25;
rs_h = 25;
rs_tripod_offset = 0; // 1/4-20 centered bottom
rs_mount_dia = 6.5; // 1/4-20 clearance
// --- RPLIDAR A1 ---
lidar_dia = 70;
lidar_h = 41;
lidar_mount_circle = 67; // Bolt circle diameter
lidar_hole_count = 4;
lidar_hole_dia = 2.7; // M2.5
// --- Kill Switch (22mm panel mount) ---
kill_sw_dia = 22;
kill_sw_depth = 35; // Behind-panel depth
// --- Battery (typical 36V hoverboard pack) ---
batt_w = 180;
batt_d = 80;
batt_h = 40;
// --- Hoverboard ESC ---
esc_w = 80;
esc_d = 50;
esc_h = 15;
// --- ESP32-C3 (typical dev board) ---
esp_w = 25;
esp_d = 18;
esp_h = 5;
// --- WS2812B strip ---
led_strip_w = 10; // 10mm wide strip
// --- General ---
wall = 3; // Default wall thickness
m3_clear = 3.2;
m3_insert = 4.2; // Heat-set insert hole
m25_clear = 2.7;
m5_clear = 5.3;
tol = 0.2; // Print tolerance per side

70
cad/esc_mount.scad Normal file
View File

@ -0,0 +1,70 @@
// ============================================
// SaltyLab ESC Mount
// 150×100×15mm PETG
// Hoverboard ESC, mounts to 2020 extrusion
// ============================================
include <dimensions.scad>
mount_w = 150;
mount_d = 100;
mount_h = 15;
base_h = 3;
module esc_mount() {
difference() {
union() {
// Base plate
translate([-mount_w/2, -mount_d/2, 0])
cube([mount_w, mount_d, base_h]);
// ESC retaining walls (low lip on 3 sides)
// Left
translate([-mount_w/2, -mount_d/2, 0])
cube([wall, mount_d, mount_h]);
// Right
translate([mount_w/2 - wall, -mount_d/2, 0])
cube([wall, mount_d, mount_h]);
// Back
translate([-mount_w/2, mount_d/2 - wall, 0])
cube([mount_w, wall, mount_h]);
// Front clips (snap-fit tabs to hold ESC)
for (x = [-30, 30]) {
translate([x - 5, -mount_d/2, 0])
cube([10, wall, mount_h]);
// Clip overhang
translate([x - 5, -mount_d/2, mount_h - 2])
cube([10, wall + 3, 2]);
}
// 2020 mount tabs (back)
for (x = [-25, 25]) {
translate([x - 10, mount_d/2 - wall, 0])
cube([20, wall + 8, base_h + 8]);
}
}
// Extrusion bolt holes (M5)
for (x = [-25, 25]) {
translate([x, mount_d/2 + 3, base_h + 4])
rotate([90, 0, 0])
cylinder(d=m5_clear, h=wall + 12, $fn=30);
}
// Ventilation holes in base
for (x = [-40, -20, 0, 20, 40]) {
for (y = [-25, 0, 25]) {
translate([x, y, -1])
cylinder(d=8, h=base_h + 2, $fn=20);
}
}
// Wire routing slots (front and back)
translate([-15, -mount_d/2 - 1, base_h])
cube([30, wall + 2, 10]);
translate([-15, mount_d/2 - wall - 1, base_h])
cube([30, wall + 2, 10]);
}
}
esc_mount();

57
cad/esp32c3_mount.scad Normal file
View File

@ -0,0 +1,57 @@
// ============================================
// SaltyLab ESP32-C3 Mount
// 30×25×10mm PETG
// Tiny mount for LED controller MCU
// ============================================
include <dimensions.scad>
mount_w = 30;
mount_d = 25;
mount_h = 10;
base_h = 2;
module esp32c3_mount() {
difference() {
union() {
// Base
translate([-mount_w/2, -mount_d/2, 0])
cube([mount_w, mount_d, base_h]);
// Retaining walls (3 sides, front open for USB)
translate([-mount_w/2, -mount_d/2, 0])
cube([wall, mount_d, mount_h]);
translate([mount_w/2 - wall, -mount_d/2, 0])
cube([wall, mount_d, mount_h]);
translate([-mount_w/2, mount_d/2 - wall, 0])
cube([mount_w, wall, mount_h]);
// Clip tabs (front corners)
for (x = [-mount_w/2, mount_w/2 - wall]) {
translate([x, -mount_d/2, mount_h - 2])
cube([wall, 4, 2]);
}
// Zip-tie slot wings
for (x = [-mount_w/2 - 4, mount_w/2 + 1]) {
translate([x, -5, 0])
cube([3, 10, base_h]);
}
}
// Board pocket (recessed)
translate([-esp_w/2 - tol, -esp_d/2 - tol, base_h])
cube([esp_w + tol*2, esp_d + tol*2, mount_h]);
// Zip-tie slots
for (x = [-mount_w/2 - 4, mount_w/2 + 1]) {
translate([x, -2, -1])
cube([3, 4, base_h + 2]);
}
// USB port clearance (front)
translate([-5, -mount_d/2 - 1, base_h])
cube([10, wall + 2, 5]);
}
}
esp32c3_mount();

86
cad/fc_mount.scad Normal file
View File

@ -0,0 +1,86 @@
// ============================================
// SaltyLab Flight Controller Mount
// Vibration-isolated, 30.5mm pattern
// TPU dampers + PETG frame
// ============================================
include <dimensions.scad>
// FC mount attaches to 2020 extrusion via T-slot
// Rubber/TPU grommets isolate FC from frame vibration
mount_w = 45; // Overall width
mount_d = 45; // Overall depth
mount_h = 15; // Total height (base + standoffs)
base_h = 4; // Base plate thickness
// TPU grommet dimensions
grommet_od = 7;
grommet_id = 3.2; // M3 clearance
grommet_h = 5; // Soft mount height
module fc_mount() {
difference() {
union() {
// Base plate
translate([-mount_w/2, -mount_d/2, 0])
cube([mount_w, mount_d, base_h]);
// Standoff posts (PETG, FC sits on TPU grommets on top)
for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
translate([x, y, 0])
cylinder(d=8, h=base_h + grommet_h, $fn=30);
}
}
// 2020 extrusion clamp tabs (sides)
for (side = [-1, 1]) {
translate([side * (extrusion_w/2 + wall), -15, 0])
cube([wall, 30, base_h + 10]);
}
}
// FC mounting holes (M3 through standoffs)
for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
translate([x, y, -1])
cylinder(d=fc_hole_dia, h=base_h + grommet_h + 2, $fn=25);
}
}
// Extrusion channel (20mm wide slot through base)
translate([-extrusion_w/2 - tol, -20, -1])
cube([extrusion_w + tol*2, 40, base_h + 2]);
// Clamp bolt holes (M3, horizontal through side tabs)
for (side = [-1, 1]) {
translate([side * (extrusion_w/2 + wall + 1), 0, base_h + 5])
rotate([0, 90, 0])
cylinder(d=m3_clear, h=wall + 2, center=true, $fn=25);
}
// Center cutout for airflow / weight reduction
translate([0, 0, -1])
cylinder(d=15, h=base_h + 2, $fn=30);
}
}
// TPU grommet (print separately in TPU)
module tpu_grommet() {
difference() {
cylinder(d=grommet_od, h=grommet_h, $fn=30);
translate([0, 0, -1])
cylinder(d=grommet_id, h=grommet_h + 2, $fn=25);
}
}
// Show assembled
fc_mount();
// Show grommets in position (for visualization)
%for (x = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
for (y = [-fc_hole_spacing/2, fc_hole_spacing/2]) {
translate([x, y, base_h])
tpu_grommet();
}
}

59
cad/handle.scad Normal file
View File

@ -0,0 +1,59 @@
// ============================================
// SaltyLab Carry Handle
// 150×30×30mm PETG
// Comfortable grip, mounts on top of spine
// ============================================
include <dimensions.scad>
handle_w = 150;
handle_h = 30;
grip_dia = 25; // Comfortable grip diameter
grip_len = 100; // Grip section length
module handle() {
difference() {
union() {
// Grip bar (rounded for comfort)
translate([-grip_len/2, 0, handle_h])
rotate([0, 90, 0])
cylinder(d=grip_dia, h=grip_len, $fn=40);
// Left support leg
hull() {
translate([-handle_w/2, -10, 0])
cube([20, 20, 3]);
translate([-grip_len/2, 0, handle_h])
rotate([0, 90, 0])
cylinder(d=grip_dia, h=5, $fn=40);
}
// Right support leg
hull() {
translate([handle_w/2 - 20, -10, 0])
cube([20, 20, 3]);
translate([grip_len/2 - 5, 0, handle_h])
rotate([0, 90, 0])
cylinder(d=grip_dia, h=5, $fn=40);
}
}
// 2020 extrusion slot (center of base)
translate([-extrusion_w/2 - tol, -extrusion_w/2 - tol, -1])
cube([extrusion_w + tol*2, extrusion_w + tol*2, 5]);
// M5 bolt holes for extrusion (2x)
for (x = [-30, 30]) {
translate([x, 0, -1])
cylinder(d=m5_clear, h=5, $fn=25);
}
// Finger grooves on grip
for (x = [-30, -10, 10, 30]) {
translate([x, 0, handle_h])
rotate([0, 90, 0])
cylinder(d=grip_dia + 4, h=5, center=true, $fn=40);
}
}
}
handle();

69
cad/jetson_shelf.scad Normal file
View File

@ -0,0 +1,69 @@
// ============================================
// SaltyLab Jetson Orin Nano Super Shelf
// 120×100×15mm PETG
// Mounts Jetson Orin Nano Super to 2020 extrusion
// ============================================
include <dimensions.scad>
shelf_w = 120;
shelf_d = 100;
shelf_h = 15;
base_h = 3;
standoff_h = 8; // Clearance for Jetson underside components
module jetson_shelf() {
difference() {
union() {
// Base plate
translate([-shelf_w/2, -shelf_d/2, 0])
cube([shelf_w, shelf_d, base_h]);
// Jetson standoffs (M2.5, 86mm × 58mm pattern)
for (x = [-jetson_hole_x/2, jetson_hole_x/2]) {
for (y = [-jetson_hole_y/2, jetson_hole_y/2]) {
translate([x, y, 0])
cylinder(d=6, h=base_h + standoff_h, $fn=25);
}
}
// 2020 extrusion clamp (back edge)
translate([-15, shelf_d/2 - wall, 0])
cube([30, wall + 10, base_h + 12]);
// Side rails for Jetson alignment
for (x = [-jetson_w/2 - wall, jetson_w/2]) {
translate([x, -jetson_d/2, base_h + standoff_h])
cube([wall, jetson_d, 4]);
}
}
// Jetson M2.5 holes (through standoffs)
for (x = [-jetson_hole_x/2, jetson_hole_x/2]) {
for (y = [-jetson_hole_y/2, jetson_hole_y/2]) {
translate([x, y, -1])
cylinder(d=jetson_hole_dia, h=base_h + standoff_h + 2, $fn=25);
}
}
// Extrusion bolt hole (M5, through back clamp)
translate([0, shelf_d/2 + 3, base_h + 6])
rotate([90, 0, 0])
cylinder(d=m5_clear, h=wall + 15, $fn=30);
// Extrusion channel slot
translate([-extrusion_w/2 - tol, shelf_d/2 - wall - 1, -1])
cube([extrusion_w + tol*2, wall + 2, base_h + 2]);
// Ventilation / cable routing
for (x = [-25, 0, 25]) {
translate([x, 0, -1])
cylinder(d=15, h=base_h + 2, $fn=25);
}
// USB/Ethernet/GPIO access cutouts (front edge)
translate([-jetson_w/2, -shelf_d/2 - 1, base_h])
cube([jetson_w, wall + 2, shelf_h]);
}
}
jetson_shelf();

View File

@ -0,0 +1,56 @@
// ============================================
// SaltyLab Kill Switch Mount
// 60×60×40mm PETG
// 22mm panel-mount emergency stop button
// Mounts to 2020 extrusion, easily reachable
// ============================================
include <dimensions.scad>
mount_w = 60;
mount_d = 60;
mount_h = 40;
panel_h = 3; // Panel face thickness
module kill_switch_mount() {
difference() {
union() {
// Main body (angled face for visibility)
hull() {
translate([-mount_w/2, 0, 0])
cube([mount_w, mount_d, 1]);
translate([-mount_w/2, 5, mount_h])
cube([mount_w, mount_d - 5, 1]);
}
// 2020 extrusion mount bracket (back)
translate([-15, mount_d, 0])
cube([30, 10, 20]);
}
// Kill switch hole (22mm, through angled face)
translate([0, mount_d/2, mount_h/2])
rotate([10, 0, 0]) // Slight angle for ergonomics
cylinder(d=kill_sw_dia + tol, h=panel_h + 2, center=true, $fn=50);
// Interior cavity (hollow for switch body)
translate([-kill_sw_dia/2 - 3, 5, 3])
cube([kill_sw_dia + 6, mount_d - 10, mount_h - 3]);
// Wire exit hole (bottom)
translate([0, mount_d/2, -1])
cylinder(d=10, h=5, $fn=25);
// Extrusion bolt holes (M5, through back bracket)
for (z = [7, 15]) {
translate([-20, mount_d + 5, z])
rotate([90, 0, 0])
cylinder(d=m5_clear, h=15, center=true, $fn=25);
}
// Label recess ("EMERGENCY STOP" flat area for sticker)
translate([-25, 5, mount_h - 1])
cube([50, 15, 1.5]);
}
}
kill_switch_mount();

Some files were not shown because too many files have changed in this diff Show More