saltylab-firmware/src/jetson_cmd.c
sl-jetson 22aaeb02cf feat: Jetson→STM32 command protocol — /cmd_vel to serial (Phase 2)
STM32 firmware (C):
- include/jetson_cmd.h: protocol constants (HB_TIMEOUT_MS=500,
  SPEED_MAX_DEG=4°), API for jetson_cmd_process/is_active/steer/sp_offset
- src/jetson_cmd.c: main-loop parser for buffered C<spd>,<str> frames;
  setpoint offset = speed/1000 * 4°; steer clamped ±1000
- lib/USB_CDC/src/usbd_cdc_if.c: add H (heartbeat) and C (drive cmd) to
  CDC_Receive ISR — follows existing pattern: H updates jetson_hb_tick in
  ISR, C copied to jetson_cmd_buf for main-loop sscanf (avoids sscanf in IRQ)
- src/main.c: integrate jetson_cmd — process buffered frame, apply setpoint
  offset around balance_update(), inject steer into motor_driver_update()
  only when heartbeat alive (fallback: steer=0, setpoint unchanged)

ROS2 (Python):
- saltybot_cmd_node.py: full bidirectional node — owns serial port, handles
  telemetry RX → topics AND /cmd_vel TX → C<spd>,<str>\n + H\n heartbeat
  200ms timer; sends C0,0\n on shutdown; speed/steer_scale configurable
- serial_bridge_node.py: add write_serial() helper for extensibility
- launch/bridge.launch.py: mode arg (bidirectional|rx_only) selects node
- config/bridge_params.yaml: heartbeat_period, speed_scale, steer_scale docs
- test/test_cmd.py: 13 tests — zero, full fwd/rev, turn clamping, combined
- setup.py: saltybot_cmd_node entry point

All 21 tests pass (8 parse + 13 cmd).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 21:07:15 -05:00

56 lines
1.8 KiB
C

#include "jetson_cmd.h"
#include <stdio.h>
/*
* Parsed drive state — updated by jetson_cmd_process() in the main loop.
* Raw fields are ints parsed from "C<speed>,<steer>\n".
*/
static volatile int16_t jcmd_speed = 0; /* -1000..+1000 */
static volatile int16_t jcmd_steer = 0; /* -1000..+1000 */
/* Clamp helper (avoids including math.h) */
static int16_t clamp16(int v, int lo, int hi) {
if (v < lo) return (int16_t)lo;
if (v > hi) return (int16_t)hi;
return (int16_t)v;
}
/*
* Called from main loop when jetson_cmd_ready is set.
* Parses jetson_cmd_buf — safe to use sscanf here (not in ISR).
* The ISR only copies bytes and sets the ready flag.
*/
void jetson_cmd_process(void) {
int speed = 0, steer = 0;
/* buf format: "C<speed>,<steer>" — skip leading 'C' */
if (sscanf((const char *)jetson_cmd_buf + 1, "%d,%d", &speed, &steer) == 2) {
jcmd_speed = clamp16(speed, -1000, 1000);
jcmd_steer = clamp16(steer, -1000, 1000);
}
/* If parse fails, keep previous values — don't zero-out mid-motion */
}
/*
* Returns true if the last heartbeat (H or C command) arrived within
* JETSON_HB_TIMEOUT_MS. jetson_hb_tick is updated in the ISR.
*/
bool jetson_cmd_is_active(uint32_t now_ms) {
/* jetson_hb_tick == 0 means we've never received any command */
if (jetson_hb_tick == 0) return false;
return (now_ms - jetson_hb_tick) < JETSON_HB_TIMEOUT_MS;
}
/* Steer command for motor_driver_update() */
int16_t jetson_cmd_steer(void) {
return jcmd_steer;
}
/*
* Convert speed command to balance setpoint offset (degrees).
* Positive speed → lean forward → robot moves forward.
* Scaled linearly: speed=1000 → +JETSON_SPEED_MAX_DEG degrees.
*/
float jetson_cmd_sp_offset(void) {
return ((float)jcmd_speed / 1000.0f) * JETSON_SPEED_MAX_DEG;
}