| Setting | Value |
|---|---|
| Baud rate | 115200 |
| Data bits | 8 |
| Parity | None |
| Stop bits | 1 |
| Line ending | \n (newline) or \r\n |
| Command format | COMMAND,param1,param2\n |
| Buffer size | 32 bytes max per command |
| Encoding | ASCII text |
Move a fixed distance, then stop automatically. Speed: 20-255. Ticks: encoder counts.
| Command | Format | Description | Example |
|---|---|---|---|
FWD |
FWD,speed,ticks |
Drive forward | FWD,100,1719 (~10cm) |
BWD |
BWD,speed,ticks |
Drive backward | BWD,80,860 (~5cm) |
LEFT |
LEFT,speed,ticks |
Strafe left | LEFT,80,1719 |
RIGHT |
RIGHT,speed,ticks |
Strafe right | RIGHT,80,1719 |
TURN |
TURN,speed,ticks |
Rotate (positive ticks=CCW, negative=CW) | TURN,60,500 (CCW) or TURN,60,-500 (CW) |
DIAGFL |
DIAGFL,speed,ticks |
Diagonal forward-left | DIAGFL,80,1000 |
DIAGFR |
DIAGFR,speed,ticks |
Diagonal forward-right | DIAGFR,80,1000 |
DIAGBL |
DIAGBL,speed,ticks |
Diagonal backward-left | DIAGBL,80,1000 |
DIAGBR |
DIAGBR,speed,ticks |
Diagonal backward-right | DIAGBR,80,1000 |
Preemption: Sending a new movement command while MOVING replaces the current motion immediately. The old command gets a
DONEresponse before the new one starts.
Distance conversion: ticks = distance_mm x 17.19 (4320 CPR / (pi x 80mm wheel))
| Command | Format | Description | Response |
|---|---|---|---|
STOP |
STOP |
Emergency stop. Brakes all motors, cancels any operation. Always safe to send in any state. | DONE |
READ |
READ |
Read all 4 encoder values. Works in any state without interrupting. | Four int32 values, one per line |
VEL |
VEL,vx,vy,wz |
Continuous velocity (see Velocity Mode) | OK (first time), then ODOM,... at 20Hz |
CALIB |
CALIB |
Run motor calibration (~40 sec): dead-zone + forward + reverse | CALIB,... progress, then DONE |
| Command | Format | Description | Response |
|---|---|---|---|
TMOTOR |
TMOTOR,motor,pwm |
Drive one motor with raw PWM (no PID). Motor: 0-3 or FL/FR/RL/RR. PWM: -255 to 255. |
TEST,FL,pwm:150,FL:xxx,... at 20Hz |
TENC |
TENC |
Stream all encoders at 20Hz. No motors driven. Spin wheels by hand to test. | ENC,... at 20Hz |
| Message | Meaning | When |
|---|---|---|
READY |
Arduino booted and initialized | Once at startup |
Robot initialized |
All subsystems ready | After READY |
OK |
Command accepted, execution started | After MOVE or first VEL |
DONE |
Operation completed successfully | After movement finishes or STOP |
BUSY |
Cannot process — robot is busy | Sending MOVE during CALIBRATING, TESTING, or ERROR |
ERROR: Stalled |
A wheel got stuck (no progress for 5s) | During MOVING |
ERROR: Timeout |
Movement exceeded 30 seconds | During MOVING |
ERROR: Watchdog |
No VEL command received within 200ms | During VEL mode |
ERROR: I2C fault |
PCA9685 communication failed 5 times | Hardware problem |
ERROR: Buffer overflow |
Command longer than 32 characters | Malformed input |
ERROR: Unknown |
Unrecognized command | Typo or wrong syntax |
STALL,FL,pos=xxx |
Which motor stalled and where | During MOVING |
ENC,FL:n,FR:n,RL:n,RR:n,t_us:n |
Encoder telemetry | 1Hz idle, 20Hz moving |
ODOM,vx,vy,wz |
Robot velocity (mm/s, mm/s, mrad/s) | 20Hz in VEL mode |
CALIB,start,phase:deadzone |
Calibration begun, starting dead-zone scan | Start of CALIB |
CALIB,dz,FL:45 |
Dead-zone found for motor (PWM value) | During dead-zone phase |
CALIB,phase:forward |
Starting forward speed measurement | After dead-zone |
CALIB,fwd,N/3,FL:n,... |
Forward session N result (ticks/period) | During forward phase |
CALIB,fwd,done,FL:n,... |
Final forward max tick rates | Forward complete |
CALIB,phase:reverse |
Starting reverse speed measurement | After forward |
CALIB,rev,N/3,FL:n,... |
Reverse session N result (ticks/period) | During reverse phase |
CALIB,rev,done,FL:n,... |
Final reverse max tick rates | Reverse complete |
CALIB,saved |
All data written to EEPROM (v2 format) | After all phases |
CALIB,loaded,fwd,FL:n,... |
Forward data loaded from EEPROM | At boot |
CALIB,loaded,rev,FL:n,... |
Reverse data loaded from EEPROM | At boot |
CALIB,loaded,dz,FL:n,... |
Dead-zone data loaded from EEPROM | At boot |
CALIB,aborted |
Calibration stopped by STOP | During CALIB |
Moving: remain=n |
Debug: ticks remaining | 1Hz during position move |
ENC_RESET |
Encoders zeroed | At boot, before new motion, and during TMOTOR/TENC |
TMOTOR,FL,pwm:N |
Acknowledge test motor command | After TMOTOR command |
TENC,started |
Encoder test started | After TENC command |
VEL,vx,vy,wz sets continuous motor velocities using mecanum inverse kinematics. Unlike position commands, VEL mode runs forever until you send STOP or the watchdog triggers.
| Parameter | Range | Meaning | Unit |
|---|---|---|---|
| vx | -500 to 500 | Forward (+) / Backward (-) | mm/s |
| vy | -500 to 500 | Left strafe (+) / Right strafe (-) | mm/s |
| wz | -2500 to 2500 | Counter-clockwise (+) / Clockwise (-) | mrad/s |
Same units as ODOM output — input and output use identical units (mm/s, mrad/s), so a ROS2 node can directly feed velocity commands without unit conversion. Max linear speed is ~436 mm/s, max angular speed is ~2180 mrad/s (hardware-limited by motor max RPM).
VEL,vx,vy,wzis parsed byserial_cmd.cpp(values are mm/s, mm/s, mrad/s)robot.cppcallsMecanum::computeFromVelocity(vx, vy, wz, tickRates)— mecanum IK converts robot velocity to per-wheel tick-rate targets (ticks/control-period) using wheel geometry and encoder CPRMotion::setMotorTickRates(tickRates)setsvelSetpoint[i]directly (no PWM conversion needed)- Every 20ms,
Motion::update()runs the PID for each motor against itsvelSetpoint - Every 50ms,
robot.cppcomputes encoder deltas -> forward kinematics -> sendsODOM,vx_mm,vy_mm,wz_mrad
- Watchdog: The PC must send VEL commands at least every 200ms. If commands stop (PC crashed, cable disconnected), the robot auto-stops and sends
ERROR: Watchdog. The GUI sends VEL at 10Hz (every 100ms), well within the 200ms window. - PID controlled: Motor speeds are not just "set and forget" PWM — each motor has a PID loop tracking its target speed using encoder feedback.
- Preemption: Each new VEL command immediately replaces the previous velocity targets. No need to send STOP between VEL commands.
- First VEL initializes PID and ODOM: On the first VEL command (transition from non-VEL mode), two things happen:
- PID initialization:
motion.cppreads the current encoder positions and sets each motor'sprevTicksto the current value. This prevents the PID from seeing a huge stale delta on the first tick (which would cause a PWM spike). The integral accumulators are also zeroed. - ODOM baseline:
robot.cpprecords the current encoder positions as the ODOM delta baseline and sendsOK. Subsequent VEL commands updatevelSetpointwithout resetting the PID (to maintain smooth tracking). They also don't get individualOKresponses to avoid flooding the serial line.
- PID initialization:
- ODOM output:
ODOM,vx,vy,wzreports actual measured robot velocity in real-world units: mm/s for linear, mrad/s for angular. This can be consumed by ROS2 for localization.
VEL,200,0,0 # Go forward at 200 mm/s
VEL,0,150,0 # Strafe left at 150 mm/s
VEL,0,0,500 # Rotate counter-clockwise at 500 mrad/s (~28.6 deg/s)
VEL,200,100,0 # Forward + left strafe = diagonal
VEL,150,0,300 # Forward while turning = arc
VEL,0,0,0 # Stop (but watchdog timer still running — better to use STOP)
STOP # Full stop, exit VEL mode, disable watchdogThe parser in serial_cmd.cpp is designed for minimal RAM usage on the 2KB ATmega328P:
- No
Stringclass — uses a fixed 32-bytechar[]buffer. The ArduinoStringclass fragments the heap and can crash on low-memory MCUs. - Character-by-character reading —
update()reads one byte at a time fromSerial.available()and appends to the buffer. When\nor\ris received, it null-terminates and callsparse(). - Prefix matching —
match()compares the buffer against command names character by character (notstrcmp, to avoid reading past the comma delimiter). - Lookup table — movement commands use a static array of
{name, Direction}pairs. To add a new direction, you add one line to the table. - Overflow protection — if the buffer is full and more characters arrive, an overflow flag is set. The entire command is discarded and
ERROR: Buffer overflowis sent when\narrives.
< (PC sends) > (Arduino responds)
> CALIB,loaded,fwd,FL:153,RL:150,RR:155,FR:159
> CALIB,loaded,rev,FL:150,RL:148,RR:153,FR:157
> CALIB,loaded,dz,FL:45,RL:42,RR:48,FR:44
> READY
> Robot initialized
> ENC,FL:0,FR:0,RL:0,RR:0,t_us:12345 # 1Hz idle telemetry
< FWD,100,1719
> MOVE cmd: dir=1 spd=100 ticks=1719
> OK
> ENC,FL:200,FR:195,RL:202,RR:198,t_us:... # 20Hz telemetry
> ENC,FL:500,FR:495,RL:502,RR:498,t_us:...
> Moving: remain=1219 # 1Hz debug
> ...
> DONE # Target reached
< VEL,200,0,0
> OK
> ODOM,195,3,-2 # 20Hz odometry (mm/s, mm/s, mrad/s)
> ODOM,198,2,-1
< VEL,200,100,0 # Change direction
> ODOM,192,95,-3 # Now strafing too
< STOP
> DONE
< TMOTOR,FL,150
> TMOTOR,FL,pwm:150
> TEST,FL,pwm:150,FL:250,FR:0,RL:0,RR:0 # Only FL moves
< STOP
> DONE
< CALIB
> CALIB,start,phase:deadzone
> CALIB,dz,FL:45
> CALIB,dz,RL:42
> CALIB,dz,RR:48
> CALIB,dz,FR:44
> CALIB,phase:forward
> CALIB,fwd,1/3,FL:120,RL:118,RR:122,FR:125
> CALIB,fwd,2/3,FL:121,RL:117,RR:123,FR:124
> CALIB,fwd,3/3,FL:120,RL:118,RR:122,FR:125
> CALIB,fwd,done,FL:153,RL:150,RR:155,FR:159
> CALIB,phase:reverse
> CALIB,rev,1/3,FL:118,RL:116,RR:120,FR:123
> CALIB,rev,2/3,FL:119,RL:115,RR:121,FR:122
> CALIB,rev,3/3,FL:118,RL:116,RR:120,FR:123
> CALIB,rev,done,FL:150,RL:148,RR:153,FR:157
> CALIB,saved
> DONE