Asked
— Edited
@Athena - can you review the Arduino sketch below and explain why the left motors (LeftFront and LeftBack) do not run until sendObstacleScanRequest() has been called?
Context: the sketch communicates with ARC over Serial3 using a simple UART protocol (header 0xA5), so the Arduino is acting as the EZB / hardware controller receiving navigation/waypoint commands from ARC (for example, commands sent by a Robot Skill). The four motors are driven as steppers using AccelStepper and MultiStepper. I’ve included the full sketch below.
Thanks, Jack
#include <AccelStepper.h>
#include <MultiStepper.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;
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;
enum ParserState { WAIT_HEADER, WAIT_CMD, WAIT_PAYLOAD };
ParserState state = WAIT_HEADER;
uint8_t currentCmd = 0;
uint8_t payload[32];
uint8_t payloadIndex = 0;
uint8_t expectedLength = 0;
int RUN = 0;
uint32_t totalStepsTaken = 0;
uint32_t combinedStepsDesired = 0;
uint32_t pathTotalDistance = 0;
uint32_t oppositeDistance = 0;
uint8_t bearingAlignment = 0;
uint16_t reflectorBearing100 = 0;
uint8_t sensorPrime = 0;
uint16_t waypointDesiredBearing100 = 9000;
uint16_t waypointActualBearing100 = 9000;
uint8_t trackDir = 0;
uint16_t actualBearing100 = 9000;
uint16_t desiredBearing100 = 9000;
uint16_t lastSentDesiredBearing100 = 0xFFFF;
int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;
bool obstacleScanRequested = false;
bool scanNeutralMode = false;
uint32_t nextScanStep = 10500;
const uint32_t scanIntervalSteps = 10500;
uint16_t currentSpeed = 0;
uint16_t normalSpeed = 800;
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 writeUInt16LE(uint16_t value) {
Serial3.write((uint8_t)(value & 0xFF));
Serial3.write((uint8_t)((value >> 8) & 0xFF));
}
void writeUInt32LE(uint32_t value) {
Serial3.write((uint8_t)(value & 0xFF));
Serial3.write((uint8_t)((value >> 8) & 0xFF));
Serial3.write((uint8_t)((value >> 16) & 0xFF));
Serial3.write((uint8_t)((value >> 24) & 0xFF));
}
void sendHeaderAndCmd(uint8_t cmd) {
Serial3.write(UART_HEADER);
Serial3.write(cmd);
}
void sendNavAck() {
sendHeaderAndCmd(RSP_NAV_ACK);
Serial.println("TX: NAV ACK");
}
void sendTotalSteps(uint32_t steps) {
sendHeaderAndCmd(RSP_TOTAL_STEPS);
writeUInt32LE(steps);
Serial.print("TX: TOTAL STEPS = ");
Serial.println(steps);
}
void sendDesiredBearing(uint16_t bearing100) {
sendHeaderAndCmd(RSP_DESIRED_BEARING);
writeUInt16LE(bearing100);
lastSentDesiredBearing100 = bearing100;
Serial.print("TX: DESIRED BEARING x100 = ");
Serial.println(bearing100);
}
void sendObstacleScanRequest() {
sendHeaderAndCmd(RSP_OBS_SCAN_REQUEST);
Serial.println("TX: OBSTACLE SCAN REQUEST");
}
void resetStepperPositions() {
LeftFrontWheel.setCurrentPosition(0);
LeftBackWheel.setCurrentPosition(0);
RightFrontWheel.setCurrentPosition(0);
RightBackWheel.setCurrentPosition(0);
}
void resetNavigationState() {
resetStepperPositions();
totalStepsTaken = 0;
desiredBearing100 = 9000;
actualBearing100 = 9000;
lastSentDesiredBearing100 = 0xFFFF;
bearingDiffTarget = 0;
bearingDiffRamped = 0;
obstacleScanRequested = false;
scanNeutralMode = false;
nextScanStep = scanIntervalSteps;
currentSpeed = 0;
RUN = 0;
Serial.println("Navigation state reset");
Serial.print("desiredBearing100: ");
Serial.println(desiredBearing100);
Serial.print("totalStepsTaken: ");
Serial.println(totalStepsTaken);
Serial.print("combinedStepsDesired: ");
Serial.println(combinedStepsDesired);
Serial.print("pathTotalDistance: ");
Serial.println(pathTotalDistance);
}
void setAllMotorSpeeds(int leftSpeed, int rightSpeed) {
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("Action: stopMotors()");
}
void pivotRight(uint16_t steps) {
long positions[4];
positions[0] = LeftFrontWheel.currentPosition() + steps;
positions[1] = LeftBackWheel.currentPosition() + steps;
positions[2] = RightFrontWheel.currentPosition() - steps;
positions[3] = RightBackWheel.currentPosition() - steps;
multiStepper.moveTo(positions);
while (multiStepper.run()) { }
totalStepsTaken += steps;
stopMotors();
sendTotalSteps(totalStepsTaken);
resetNavigationState();
}
void pivotLeft(uint16_t steps) {
long positions[4];
positions[0] = LeftFrontWheel.currentPosition() - steps;
positions[1] = LeftBackWheel.currentPosition() - steps;
positions[2] = RightFrontWheel.currentPosition() + steps;
positions[3] = RightBackWheel.currentPosition() + steps;
multiStepper.moveTo(positions);
while (multiStepper.run()) { }
totalStepsTaken += steps;
stopMotors();
sendTotalSteps(totalStepsTaken);
resetNavigationState();
}
void beginForwardNavigation() {
RUN = 1;
currentSpeed = 0;
bearingDiffRamped = 0;
obstacleScanRequested = false;
scanNeutralMode = false;
nextScanStep = scanIntervalSteps;
}
void computeDesiredBearing() {
if (bearingAlignment == 0) {
// desiredBearing100 = 9000;
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;
}
float tangent = (float)oppositeDistance / (float)pathDistanceRemaining;
float deg = atan(tangent) * 180.0 / PI;
desiredBearing100 = (uint16_t)((deg + 90.0) * 100.0);
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 runForwardLoop() {
if (currentSpeed < normalSpeed) {
currentSpeed += 10;
if (currentSpeed > normalSpeed) currentSpeed = normalSpeed;
}
computeDesiredBearing();
updateBearingDiff();
int leftSpeed = currentSpeed;
int rightSpeed = currentSpeed;
if (!scanNeutralMode) {
int correction = bearingDiffRamped / 10;
leftSpeed -= correction;
rightSpeed += correction;
}
setAllMotorSpeeds(leftSpeed, rightSpeed);
LeftFrontWheel.runSpeed();
LeftBackWheel.runSpeed();
RightFrontWheel.runSpeed();
RightBackWheel.runSpeed();
totalStepsTaken =
abs(LeftFrontWheel.currentPosition()) +
abs(LeftBackWheel.currentPosition()) +
abs(RightFrontWheel.currentPosition()) +
abs(RightBackWheel.currentPosition()) / 4;
// if (!obstacleScanRequested && totalStepsTaken >= nextScanStep) {
// sendObstacleScanRequest();
// obstacleScanRequested = true;
// scanNeutralMode = true;
// nextScanStep += scanIntervalSteps;
// }
if (totalStepsTaken >= nextScanStep) {
sendObstacleScanRequest();
scanNeutralMode = true;
nextScanStep += scanIntervalSteps;
}
if (totalStepsTaken >= combinedStepsDesired && combinedStepsDesired > 0) {
stopMotors();
sendTotalSteps(totalStepsTaken);
resetNavigationState();
}
}
uint8_t getExpectedLength(uint8_t cmd) {
switch (cmd) {
case CMD_STOP_MOTORS: return 0;
case CMD_ACTUAL_BEARING: return 2;
case CMD_RIGHT_PIVOT: return 2;
case CMD_LEFT_PIVOT: return 2;
case CMD_WAYPOINT_DATA: return 18;
default: return 0xFF;
}
}
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
if (len != 18) return;
pathTotalDistance = readUInt32LE(&data[0]);
combinedStepsDesired = readUInt32LE(&data[4]);
oppositeDistance = readUInt32LE(&data[8]);
bearingAlignment = data[12];
reflectorBearing100 = readUInt16LE(&data[13]);
sensorPrime = data[15];
waypointDesiredBearing100 = readUInt16LE(&data[16]);
waypointActualBearing100 = actualBearing100;
trackDir = 0;
desiredBearing100 = waypointDesiredBearing100;
actualBearing100 = waypointActualBearing100;
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("actualBearing100: ");
Serial.println(actualBearing100);
}
break;
case CMD_RIGHT_PIVOT:
if (len == 2 && RUN == 0) pivotRight(readUInt16LE(data));
break;
case CMD_LEFT_PIVOT:
if (len == 2 && RUN == 0) pivotLeft(readUInt16LE(data));
break;
case CMD_WAYPOINT_DATA:
if (RUN == 0) handleWaypointPacket(data, len);
break;
}
}
void processIncomingByte(uint8_t b) {
switch (state) {
case WAIT_HEADER:
if (b == UART_HEADER) state = WAIT_CMD;
break;
case WAIT_CMD:
currentCmd = b;
expectedLength = getExpectedLength(currentCmd);
if (expectedLength == 0xFF) {
state = WAIT_HEADER;
} else if (expectedLength == 0) {
handlePacket(currentCmd, nullptr, 0);
state = WAIT_HEADER;
} else {
payloadIndex = 0;
state = WAIT_PAYLOAD;
}
break;
case WAIT_PAYLOAD:
payload[payloadIndex++] = b;
if (payloadIndex >= expectedLength) {
handlePacket(currentCmd, payload, expectedLength);
state = WAIT_HEADER;
}
break;
}
}
void readIncomingUart() {
while (Serial3.available() > 0) {
processIncomingByte((uint8_t)Serial3.read());
}
}
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);
multiStepper.addStepper(LeftFrontWheel);
multiStepper.addStepper(LeftBackWheel);
multiStepper.addStepper(RightFrontWheel);
multiStepper.addStepper(RightBackWheel);
resetNavigationState();
Serial.println("Arduino_v5 ready");
}
void loop() {
readIncomingUart();
if (RUN == 1) {
runForwardLoop();
}
}
Related Hardware (view all EZB hardware)
Arduino Due/Mega
by Arduino
Synthiam ARC enables Arduino Mega 2560 control with 8 ADC, 13 PWM, 21 digital ports and 24 servos; includes firmware, shield and tutorials
USB
Servos
24
Camera
Audio
UART
3
I2C
ADC
8
Digital
21
Related Robot Skills (view all robot skills)
Wavefront
by rz90208
Wavefront grid planner computes collision-free, deterministic paths from start to goal using known occupancy maps for indoor robots.
Requires ARC
v2
Script
by Synthiam
Multi-language ARC Script: build, run and debug Blockly, JavaScript, EZ-Script or Python with Intellisense, run/save/load and Roboscratch support

Yes - the behavior is coming from the sketch logic, not ARC itself.
Short answer
Your left motors are being starved until
sendObstacleScanRequest()runs becauserunForwardLoop()is the only place where motor speeds are continuously updated andrunSpeed()is called.Until that loop is actively running, the stepper drivers won’t be serviced often enough for
AccelStepperto generate motion.But there’s a second, more important issue:
Your code only enters forward-running mode after the waypoint packet is handled
beginForwardNavigation()sets:RUN = 1currentSpeedSo the real question is: why do the left motors appear dead until the first obstacle scan request?
That points to the speed-correction logic and the scan-neutral behavior.
What I see in the sketch
1) Motor motion only happens inside
runForwardLoop()This block is critical:
And inside
runForwardLoop()you do all of this:setAllMotorSpeeds(leftSpeed, rightSpeed);runSpeed()on all 4 motorsSo if
RUNis not 1, nothing moves.2) Your left motors are probably being driven to
0or negative speed before the scan requestThis section is suspicious:
If
bearingDiffRampedis positive enough, the correction can push leftSpeed down to zero or below.Because you’re using:
runSpeed()does not accelerate or recover a stalled motor if the speed is 0. It only steps when the speed is set properly.So the left side may look "stalled" while the right side continues or the system waits for the scan event.
3)
scanNeutralModechanges the correction behaviorOnce this happens:
then this block no longer applies correction:
That means both sides go back to the same
currentSpeed, which makes the left motors suddenly start moving.So the symptom fits this pattern:
There is also a bug in your step averaging line
This line has operator precedence trouble:
Only the last term is divided by 4.
It should probably be:
If ARC is using
totalStepsTakento decide when scans happen, this bug could make scan timing very wrong.Another issue:
currentSpeedmay never ramp up the way you expectYou do:
But I don’t see the full
runForwardLoop()definition here, so if there’s any early return, or ifcurrentSpeedis reset unexpectedly, the motors may never get enough speed to overcome static friction.That often shows up more on one side than the other.
Most likely root cause
The most likely cause is:
So the obstacle scan request is not fixing the motors - it’s disabling the correction that was suppressing the left side.
What I recommend checking first
Add debug prints inside the loop right before
setAllMotorSpeeds():If you see
leftSpeed <= 0before the scan request, that confirms it.Safer fix
Clamp the speeds before sending them to the steppers:
If you need reverse capability, handle negative values intentionally instead of feeding them directly to
runSpeed().One more thing to verify
Make sure your
runForwardLoop()is actually being called continuously and not only after a scan event. If you can paste that function, I can review it line by line - that’s probably where the exact left-side stall is happening.Relevant ARC note
Since this is acting like a hardware controller for ARC, your setup fits the kind of microcontroller offload ARC is designed for: https://synthiam.com/Support/ARC-Overview/arc-and-micros-for-closed-loop-functions
If you want, I can also help you:
If you paste
runForwardLoop()and the exact serial packet flow from ARC, I’ll pinpoint the fault more precisely.I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
@Athena. Can you please rewrite this into cleaner Arduino/ARC UART protocol code?
Absolutely - and I’d strongly recommend cleaning this up into a small framed protocol + a state machine so ARC and the Arduino stay easy to debug.
Below is a much cleaner pattern you can use for ARC Arduino UART with:
What I changed conceptually
Your current sketch mixes:
That makes bugs hard to isolate.
Cleaner structure
Split into 4 layers:
Protocol
Parser
Serial3Navigation state
Motor service loop
Recommended packet format
Use this for all messages:
0xA5= headerCMD= command byteLEN= payload lengthPAYLOAD= 0..N bytesCHECKSUM= XOR of CMD, LEN, and payload bytesThis is much safer than parsing only by fixed lengths.
Cleaner Arduino sketch template
Here is a rewritten version you can build on.
Why this version is better
1) Safer protocol
You now have:
That makes bad serial reads much easier to reject.
2) No hidden dependence on scan events
runForwardLoop()runs every loop, not only under a side effect.3) Speed values are clamped
So the left side can’t get driven into nonsense values.
4) Packet parsing is reusable
You can add more commands later without rewriting the parser.
Important note about your current code
Your original
readUInt16LE()is broken in the pasted sketch:It should be:
That alone could corrupt bearing values and affect motor correction.
ARC side recommendation
On the ARC side, use the Script skill to send packets with a clean helper function.
If you want, I can also write the ARC JavaScript sender for this protocol so ARC can send:
CMD_WAYPOINT_DATACMD_ACTUAL_BEARINGCMD_STOP_MOTORSusing
Serial3/COM with a matching checksum.ARC docs for scripting:
Best next step
If you want, I can do one of these next:
protocol.hnavigation.inoIf you paste the ARC side packet code too, I can make both ends match exactly.
I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.