@Athena,
I'm using the Arduino Mega sketch below to control four stepper motors via AccelStepper / MultiStepper. The Arduino is connected to an EZB4 over UART (Serial3). The EZB4 is running JavaScript uploaded from ARC and is sending CMD_RIGHT_PIVOT and CMD_LEFT_PIVOT packets with a STEPS value over UART to the Arduino.
Can you rewrite the sketch to implement the following behavior?
- When receiving a pivot command (CMD_RIGHT_PIVOT or CMD_LEFT_PIVOT), pivot the appropriate side so the robot rotates in that direction:
- CMD_RIGHT_PIVOT: drive the left motors (both left wheels) forward and drive the right motors in reverse so the robot pivots to the right.
- CMD_LEFT_PIVOT: drive the right motors (both right wheels) forward and drive the left motors in reverse so the robot pivots to the left.
- Count the STEPS value received in the pivot packet and stop the motors (call stopMotors()) once the pivot STEPS count is reached.
- The current code checks pivot packet length == 2, so I expect the STEPS payload is a 2-byte little-endian value. Please use that same format unless you indicate otherwise.
For context: the Arduino sketch handles navigation and other packets (waypoints, bearing updates, etc.). The EZB4 is acting as the UART bridge between ARC (Robot Skills / JavaScript on the EZB) and this Arduino Mega sketch. The pivot commands should only act when not already in forward navigation (navRunning == false), consistent with the current sketch.
Below is the current sketch I'm using. Please rewrite it to add the pivot behavior described above.
#include <AccelStepper.h>
#include <MultiStepper.h>
#include <math.h>
static const uint8_t UART_HEADER = 0xA5;
// ARC -> Arduino
static const uint8_t CMD_STOP_MOTORS = 0x01;
static const uint8_t CMD_ACTUAL_BEARING = 0x03;
static const uint8_t CMD_RIGHT_PIVOT = 0x04;
static const uint8_t CMD_LEFT_PIVOT = 0x05;
static const uint8_t CMD_WAYPOINT_DATA = 0x06;
// Arduino -> ARC
static const uint8_t RSP_NAV_ACK = 0x10;
static const uint8_t RSP_TOTAL_STEPS = 0x11;
static const uint8_t RSP_DESIRED_BEARING = 0x12;
static const uint8_t RSP_OBS_SCAN_REQUEST = 0x13;
static const uint8_t MAX_PAYLOAD = 32;
// -------------------------
// Motor setup
// -------------------------
AccelStepper LeftFrontWheel(AccelStepper::DRIVER, 3, 6);
AccelStepper LeftBackWheel(AccelStepper::DRIVER, 2, 5);
AccelStepper RightFrontWheel(AccelStepper::DRIVER, 12, 13);
AccelStepper RightBackWheel(AccelStepper::DRIVER, 4, 7);
MultiStepper multiStepper;
// -------------------------
// Parser
// -------------------------
enum ParserState {
WAIT_HEADER,
WAIT_CMD,
WAIT_LEN,
WAIT_PAYLOAD,
WAIT_CHECKSUM
};
ParserState parserState = WAIT_HEADER;
uint8_t currentCmd = 0;
uint8_t expectedLen = 0;
uint8_t payloadIndex = 0;
uint8_t payload[MAX_PAYLOAD];
uint8_t runningChecksum = 0;
// -------------------------
// Navigation state
// -------------------------
bool navRunning = false;
uint32_t totalStepsTaken = 0;
uint32_t combinedStepsDesired = 0;
uint32_t pathTotalDistance = 0;
int32_t oppositeDistance = 0;
uint32_t nextScanStep = 0;
uint32_t scanIntervalSteps = 3056;
uint8_t bearingAlignment = 0;
uint8_t trackDir = 0;
uint16_t actualBearing100 = 9000;
uint16_t desiredBearing100 = 9000;
uint16_t lastSentDesiredBearing100 = 0xFFFF;
int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;
uint16_t currentSpeed = 0;
uint16_t normalSpeed = 600; //800
// -------------------------
// Helpers
// -------------------------
uint16_t readUInt16LE(const uint8_t* data) {
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
uint32_t readUInt32LE(const uint8_t* data) {
return (uint32_t)data[0]
| ((uint32_t)data[1] << 8)
| ((uint32_t)data[2] << 16)
| ((uint32_t)data[3] << 24);
}
void sendPacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
uint8_t checksum = (uint8_t)(cmd ^ len);
Serial3.write(UART_HEADER);
Serial3.write(cmd);
Serial3.write(len);
for (uint8_t i = 0; i < len; i++) {
Serial3.write(data[i]);
checksum ^= data[i];
}
Serial3.write(checksum);
}
void sendCmdOnly(uint8_t cmd) {
sendPacket(cmd, nullptr, 0);
}
void sendNavAck() {
sendCmdOnly(RSP_NAV_ACK);
Serial.println(F("TX: NAV ACK"));
}
void sendTotalSteps(uint32_t steps) {
uint8_t tmp[4];
tmp[0] = (uint8_t)(steps & 0xFF);
tmp[1] = (uint8_t)((steps >> 8) & 0xFF);
tmp[2] = (uint8_t)((steps >> 16) & 0xFF);
tmp[3] = (uint8_t)((steps >> 24) & 0xFF);
sendPacket(RSP_TOTAL_STEPS, tmp, 4);
Serial.print(F("TX: TOTAL STEPS = "));
Serial.println(steps);
}
void sendDesiredBearing(uint16_t bearing100) {
uint8_t tmp[2];
tmp[0] = (uint8_t)(bearing100 & 0xFF);
tmp[1] = (uint8_t)((bearing100 >> 8) & 0xFF);
sendPacket(RSP_DESIRED_BEARING, tmp, 2);
lastSentDesiredBearing100 = bearing100;
Serial.print(F("TX: DESIRED BEARING x100 = "));
Serial.println(bearing100);
}
void sendObstacleScanRequest() {
sendCmdOnly(RSP_OBS_SCAN_REQUEST);
Serial.println(F("TX: OBSTACLE SCAN REQUEST"));
}
void resetStepperPositions() {
LeftFrontWheel.setCurrentPosition(0);
LeftBackWheel.setCurrentPosition(0);
RightFrontWheel.setCurrentPosition(0);
RightBackWheel.setCurrentPosition(0);
}
uint32_t getAverageAbsStepperPosition() {
uint32_t a = (uint32_t)labs(LeftFrontWheel.currentPosition());
uint32_t b = (uint32_t)labs(LeftBackWheel.currentPosition());
uint32_t c = (uint32_t)labs(RightFrontWheel.currentPosition());
uint32_t d = (uint32_t)labs(RightBackWheel.currentPosition());
return (a + b + c + d) / 4;
}
void setAllMotorSpeeds(int leftSpeed, int rightSpeed) {
leftSpeed = constrain(leftSpeed, 0, 2000);
rightSpeed = constrain(rightSpeed, 0, 2000);
LeftFrontWheel.setSpeed(leftSpeed);
LeftBackWheel.setSpeed(leftSpeed);
RightFrontWheel.setSpeed(rightSpeed);
RightBackWheel.setSpeed(rightSpeed);
}
void stopMotors() {
LeftFrontWheel.stop();
LeftBackWheel.stop();
RightFrontWheel.stop();
RightBackWheel.stop();
LeftFrontWheel.setSpeed(0);
LeftBackWheel.setSpeed(0);
RightFrontWheel.setSpeed(0);
RightBackWheel.setSpeed(0);
Serial.println(F("Action: stopMotors()"));
}
void resetNavigationState() {
resetStepperPositions();
totalStepsTaken = 0;
desiredBearing100 = 9000;
actualBearing100 = 9000;
lastSentDesiredBearing100 = 0xFFFF;
bearingDiffTarget = 0;
bearingDiffRamped = 0;
nextScanStep = scanIntervalSteps;
currentSpeed = 0;
navRunning = false;
Serial.println(F("Navigation state reset"));
}
void beginForwardNavigation() {
navRunning = true;
currentSpeed = 0;
bearingDiffRamped = 0;
nextScanStep = scanIntervalSteps;
Serial.println(F("Navigation started"));
}
// -------------------------
// Navigation math
// -------------------------
void computeDesiredBearing() {
if (bearingAlignment == 0) {
return;
}
uint32_t pathDistanceRemaining = 1;
if (trackDir == 0) {
if (pathTotalDistance > totalStepsTaken) {
pathDistanceRemaining = pathTotalDistance - totalStepsTaken;
}
} else {
uint32_t remainingSteps = (combinedStepsDesired > totalStepsTaken) ? (combinedStepsDesired - totalStepsTaken) : 0;
if (pathTotalDistance > remainingSteps) {
pathDistanceRemaining = pathTotalDistance - remainingSteps;
}
}
if (pathDistanceRemaining == 0) {
pathDistanceRemaining = 1;
}
bool reflectorIsLeft = (desiredBearing100 >= 9100);
int32_t signedOppositeDistance = (int32_t)oppositeDistance;
if (reflectorIsLeft) {
signedOppositeDistance = -labs(signedOppositeDistance);
} else {
signedOppositeDistance = labs(signedOppositeDistance);
}
float angleDeg = atan2((float)signedOppositeDistance, (float)pathDistanceRemaining) * 180.0f / PI;
int32_t bearing100 = (int32_t)((90.0f + angleDeg) * 100.0f);
if (bearing100 < 0) bearing100 = 0;
if (bearing100 > 18000) bearing100 = 18000;
desiredBearing100 = (uint16_t)bearing100;
if (abs((int)desiredBearing100 - (int)lastSentDesiredBearing100) >= 100) {
sendDesiredBearing(desiredBearing100);
}
}
void updateBearingDiff() {
bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;
if (bearingDiffRamped < bearingDiffTarget) {
bearingDiffRamped += 10;
if (bearingDiffRamped > bearingDiffTarget) {
bearingDiffRamped = bearingDiffTarget;
}
} else if (bearingDiffRamped > bearingDiffTarget) {
bearingDiffRamped -= 10;
if (bearingDiffRamped < bearingDiffTarget) {
bearingDiffRamped = bearingDiffTarget;
}
}
}
void applyForwardMotion() {
int leftSpeed = normalSpeed;
int rightSpeed = normalSpeed;
int absDiff = abs(bearingDiffRamped);
if (bearingDiffRamped > 0) {
leftSpeed = normalSpeed - (absDiff * 2);
rightSpeed = normalSpeed + (absDiff * 2);
} else if (bearingDiffRamped < 0) {
leftSpeed = normalSpeed + (absDiff * 2);
rightSpeed = normalSpeed - (absDiff * 2);
}
setAllMotorSpeeds(leftSpeed, rightSpeed);
// Serial.print("leftSpeed: ");
// Serial.println(leftSpeed);
// Serial.print("rightSpeed: ");
// Serial.println(rightSpeed);
}
void updateObstacleScanSchedule() {
if (scanIntervalSteps == 0) {
return;
}
if (totalStepsTaken >= nextScanStep) {
sendObstacleScanRequest();
nextScanStep += scanIntervalSteps;
}
}
void runForwardLoop() {
if (!navRunning) {
return;
}
computeDesiredBearing();
updateBearingDiff();
applyForwardMotion();
LeftFrontWheel.runSpeed();
LeftBackWheel.runSpeed();
RightFrontWheel.runSpeed();
RightBackWheel.runSpeed();
totalStepsTaken = getAverageAbsStepperPosition();
updateObstacleScanSchedule();
if (combinedStepsDesired > 0 && totalStepsTaken >= combinedStepsDesired) {
stopMotors();
sendTotalSteps(totalStepsTaken);
resetNavigationState();
}
}
// -------------------------
// Packet handlers
// -------------------------
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
if (len != 22) {
Serial.print(F("Waypoint length error: "));
Serial.println(len);
return;
}
pathTotalDistance = readUInt32LE(&data[0]);
combinedStepsDesired = readUInt32LE(&data[4]);
oppositeDistance = (int32_t)readUInt32LE(&data[8]);
bearingAlignment = data[12];
desiredBearing100 = readUInt16LE(&data[13]);
actualBearing100 = readUInt16LE(&data[15]);
trackDir = data[17] & 0x01;
scanIntervalSteps = readUInt32LE(&data[18]);
Serial.println(F("RX: WAYPOINT DATA"));
Serial.print(F(" pathTotalDistance=")); Serial.println(pathTotalDistance);
Serial.print(F(" combinedStepsDesired=")); Serial.println(combinedStepsDesired);
Serial.print(F(" oppositeDistance=")); Serial.println(oppositeDistance);
Serial.print(F(" bearingAlignment=")); Serial.println(bearingAlignment);
Serial.print(F(" desiredBearing100=")); Serial.println(desiredBearing100);
Serial.print(F(" actualBearing100=")); Serial.println(actualBearing100);
Serial.print(F(" trackDir=")); Serial.println(trackDir);
Serial.print(F(" scanIntervalSteps=")); Serial.println(scanIntervalSteps);
sendNavAck();
beginForwardNavigation();
}
void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
switch (cmd) {
case CMD_STOP_MOTORS:
stopMotors();
sendTotalSteps(totalStepsTaken);
resetNavigationState();
break;
case CMD_ACTUAL_BEARING:
if (len == 2) {
actualBearing100 = readUInt16LE(data);
Serial.print(F("RX: ACTUAL BEARING x100 = "));
Serial.println(actualBearing100);
}
break;
case CMD_RIGHT_PIVOT:
if (len == 2 && !navRunning) {
Serial.println(F("RX: RIGHT PIVOT"));
}
break;
case CMD_LEFT_PIVOT:
if (len == 2 && !navRunning) {
Serial.println(F("RX: LEFT PIVOT"));
}
break;
case CMD_WAYPOINT_DATA:
if (!navRunning) {
handleWaypointPacket(data, len);
}
break;
}
}
// -------------------------
// UART parser
// -------------------------
void processIncomingByte(uint8_t b) {
switch (parserState) {
case WAIT_HEADER:
if (b == UART_HEADER) parserState = WAIT_CMD;
break;
case WAIT_CMD:
currentCmd = b;
runningChecksum = b;
parserState = WAIT_LEN;
break;
case WAIT_LEN:
expectedLen = b;
runningChecksum ^= b;
if (expectedLen > MAX_PAYLOAD) {
parserState = WAIT_HEADER;
} else if (expectedLen == 0) {
parserState = WAIT_CHECKSUM;
} else {
payloadIndex = 0;
parserState = WAIT_PAYLOAD;
}
break;
case WAIT_PAYLOAD:
payload[payloadIndex++] = b;
runningChecksum ^= b;
if (payloadIndex >= expectedLen) {
parserState = WAIT_CHECKSUM;
}
break;
case WAIT_CHECKSUM:
if (b == runningChecksum) {
handlePacket(currentCmd, payload, expectedLen);
} else {
Serial.println(F("Checksum error"));
}
parserState = WAIT_HEADER;
runningChecksum = 0;
currentCmd = 0;
expectedLen = 0;
payloadIndex = 0;
break;
}
}
void readIncomingUart() {
while (Serial3.available() > 0) {
processIncomingByte((uint8_t)Serial3.read());
}
}
// -------------------------
// Arduino entry points
// -------------------------
void setup() {
Serial.begin(115200);
Serial3.begin(115200);
LeftFrontWheel.setMaxSpeed(2000);
LeftBackWheel.setMaxSpeed(2000);
RightFrontWheel.setMaxSpeed(2000);
RightBackWheel.setMaxSpeed(2000);
LeftFrontWheel.setAcceleration(800);
LeftBackWheel.setAcceleration(800);
RightFrontWheel.setAcceleration(800);
RightBackWheel.setAcceleration(800);
LeftFrontWheel.setSpeed(0);
LeftBackWheel.setSpeed(0);
RightFrontWheel.setSpeed(0);
RightBackWheel.setSpeed(0);
multiStepper.addStepper(LeftFrontWheel);
multiStepper.addStepper(LeftBackWheel);
multiStepper.addStepper(RightFrontWheel);
multiStepper.addStepper(RightBackWheel);
resetNavigationState();
Serial.println(F("ARDUINO_v8 ready"));
}
void loop() {
readIncomingUart();
runForwardLoop();
}
Thanks, Jack
Related Hardware (view all EZB hardware)
Related Robot Skills (view all robot skills)
Upgrade to ARC Pro
Unleash your robot's full potential with the cutting-edge features and intuitive programming offered by Synthiam ARC Pro.
While new comments are no longer being accepted, the information here remains available for learning and reference. Check the most recent comment for additional details.

Absolutely, Jack. Since the pivot behavior is already repeatable, the main improvement here is to make the forward navigation speed handling more gradual and less aggressive.
What I’m changing in Arduino_v10:
normalSpeedArduino_v10
What changed for smoother motion
1) Real startup ramp
Instead of jumping straight into
normalSpeed, the robot now ramps up with:MIN_FORWARD_SPEEDSPEED_RAMP_STEPSo it starts gently and builds up.
2) Less aggressive steering
The bearing correction now uses:
That means tiny changes in
actualBearing100won’t yank the motors around.3) Speed is no longer being over-corrected
In your earlier behavior, the steering adjustment could dominate the speed output too quickly. Now it is intentionally softened.
Good tuning values to try
If the robot still feels too aggressive, try these changes first:
If it feels too sluggish, increase:
SPEED_RAMP_STEPMAX_BEARING_SPEED_ADJUSTOne important observation
In your pasted
Arduino_v9, the speed issue may also have been caused by:bearingDiffRampedchanging too quicklySo Arduino_v10 addresses all three.
My recommendation
Start with Arduino_v10 exactly as posted, then test in this order:
If you want, I can also give you a UART_v10 companion script that adds a quick way to set tuning variables like:
$normalSpeed$pivotSteps$bearingDeadband$speedRampStepThat would let you tune motion from ARC without re-uploading the sketch.
Yes @Athena, please send a UART_v10 companion script that adds a quick way to set tuning variables like:
$normalSpeed$pivotSteps$bearingDeadband$speedRampStepThank you very much. Jack
Absolutely, Jack. Below is a UART_v10 companion ARC JavaScript script that lets you quickly set tuning values from ARC and sends them as global variables for your robot logic to use.
It does not change your pivot packet format or navigation packet format. It simply gives you a convenient way to update:
$normalSpeed$pivotSteps$bearingDeadband$speedRampStepThis is useful if you want to tune behavior without editing/reuploading the Arduino sketch each time.
UART_v10 companion script
How to use it
1) Set the tuning values in ARC
Set these global variables anywhere in ARC:
$normalSpeed$pivotSteps$bearingDeadband$speedRampStepExample:
$normalSpeed = 600$pivotSteps = 600$bearingDeadband = 3$speedRampStep = 122) Apply them
Set:
$applyTuningNow = 1That will copy the values into the script state and print them.
Optional convenience
If you want, you can also use the variables directly before a motion command. For example:
$pivotSteps$sendRightPivotNow = 1Or:
$pivotSteps$sendLeftPivotNow = 1Important note
Your Arduino_v10 sketch is the one that actually uses the new tuning values. So for this companion script to fully match, make sure Arduino_v10 is reading those global settings from incoming packets or you’re manually using them in ARC logic.
If you want, I can also do one more step and make a UART_v11 script that sends a dedicated tuning packet to Arduino_v10 so those values are transferred over UART instead of just being local ARC globals. That would be the cleanest long-term setup.
Would you like me to build that tuning-packet version next?
Thank you @Athena for the Arduino and ARC scripts. The current problem with the Arduino_v10 is that it does not adjust left motor and right motor PWM equally. Only one motor decays in relation to bearing diff change. Can you rewrite Arduino_v10 to adjust right and left motor PWM equally when bearing diff changes? Let's call it Arduino_v11. Thanks. Jack.
Sorry, I can't continue helping in this thread. It's grown long enough that my context window will require too much extra processing, which makes follow-ups unreliable and I can start making mistakes.
A couple of options:
Thank you @Athena for your wonderful help. Jack