Asked
,
#include <AccelStepper.h>#include <MultiStepper.h>
static const uint8_t UART_HEADER = 0xA5;
// ARC -> Arduinostatic 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 -> ARCstatic 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());
if (!obstacleScanRequested && totalStepsTaken >= nextScanStep) { sendObstacleScanRequest(); obstacleScanRequested = true; 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); } 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(); }}
This is an Arduino sketch that communicates over Serial3 using a simple UART protocol (header 0xA5). It should trigger sendObstacleScanRequest() every 10,500 steps, but it only seems to occur at the first 10,500-step mark and not subsequently. Is this line of code correct?
if (!obstacleScanRequested && totalStepsTaken >= nextScanStep) { sendObstacleScanRequest(); obstacleScanRequested = true; scanNeutralMode = true; nextScanStep += scanIntervalSteps; }
For reference, Serial3 is the serial port used to communicate with ARC (or a host controller/EZB) in this setup.
Thanks, Jack
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)
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
Task Script
by Synthiam Inc.
Split complex robot tasks into sequential scripts, synchronizing asynchronous skills via NEXT/CANCEL commands for staged automation.
Requires ARC
v8
Upgrade to ARC Pro
ARC Pro is your gateway to a community of like-minded robot enthusiasts and professionals, all united by a passion for advanced robot programming.
📚 This thread has been preserved as a helpful resource!
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.

Yes - the issue is very likely that
obstacleScanRequestedis never being cleared back tofalseafter the first request.Right now your logic does this:
That means:
obstacleScanRequestedbecomestruefalseWhy it only happens once
Your code increments
nextScanStep, but the!obstacleScanRequestedgate blocks future requests forever.Fix options
Option 1: Reset the flag when the scan response is received
If ARC or your host sends a response back after the scan, that response handler should clear the flag:
That would allow the next 10,500-step trigger to fire.
Option 2: Remove the flag entirely
If you only want one request per interval, the
nextScanStepcheck is enough by itself:This works only if
runForwardLoop()runs often enough and you don’t need a "wait for response" lockout.Option 3: Use a response/timeout state machine
A safer approach is:
obstacleScanRequested = trueobstacleScanRequestedThat’s usually the best pattern for serial robot navigation.
One more thing to check
You calculate:
If your steppers are not all moving the way you expect, this could jump in unexpected ways. But that would not explain "only once" as clearly as the boolean flag does.
Recommended change
If you want repeated obstacle scan requests every 10,500 steps, I’d start with this:
Then, wherever you process the scan reply:
ARC side note
If this Arduino is talking to ARC over
Serial3, make sure your ARC side skill is reading the custom protocol consistently. In ARC, the best place to handle this is usually a Script skill using JavaScript or Python, or a custom robot skill if you’re building a reusable interface.If you want, I can help you turn this into a cleaner state machine so the scan request/reply cycle is reliable and easier to debug.
Also, if you’d like, I can review the
runForwardLoop()portion too - there may be a second issue there affecting the repeat trigger.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. Please turn this into a cleaner state machine so the scan request/reply cycle is reliable and easier to debug. Also, please review the
runForwardLoop()portion too - there may be a second issue there affecting the repeat trigger. Thanks. JackAbsolutely - a cleaner scan request / reply state machine will make this much more reliable and much easier to debug.
What’s going wrong right now
Your current logic mixes:
That makes it easy for one boolean to block the whole cycle.
The biggest improvement is to replace the single flag approach with an explicit scan state.
Suggested scan state machine
Use an enum like this:
Then track:
nextScanStepwhen the next scan should happenscanRequestStepwhen the request was sentscanTimeoutMsfallback if reply never arrivesCleaner flow
1) When moving forward
SCAN_WAITING_REPLY2) When scan reply arrives
3) If no reply arrives in time
Example implementation
Here is a cleaner version you can adapt.
Global state
Send scan request
Handle scan reply
When your reply comes back from ARC or another controller, call something like this:
If your reply includes data, add it into that function.
Timeout check
Cleaner scan trigger in
runForwardLoop()Replace this:
with this:
That’s the key improvement.
Review of
runForwardLoop()logicYour current loop has a likely second issue:
Why this may be a problem
That value is the sum of all 4 wheels, not the actual distance traveled by the robot.
If each wheel moves 10,500 steps, this total becomes around:
That means your threshold logic may be firing earlier or later than expected depending on how the motors are configured.
Better approach
Use one representative wheel or average the wheels:
If all 4 steppers are mechanically synchronized, averaging is usually more meaningful than summing.
Another likely issue:
stopMotors()andresetNavigationState()In your pivot functions you do:
But in
resetNavigationState()you set:So if a pivot or stop happens, you are completely resetting the navigation state. That’s fine if it’s intentional, but it can interfere with repeat scanning if you expect the robot to continue the same run after a pause or correction.
If the robot should continue navigating after a scan, you should not fully reset navigation on every scan event.
Recommended structure
Use these separate functions:
beginForwardNavigation()requestObstacleScan()handleObstacleScanReply()updateScanTimeout()runForwardLoop()stopNavigation()for full reset onlyThat keeps each responsibility isolated.
Suggested
runForwardLoop()skeletonHelpful debug prints
These will make the behavior much easier to trace:
And for scan state:
Important note about ARC
Since this protocol is between your Arduino firmware and ARC, I’d recommend keeping the scan reply handling in one place only:
handleObstacleScanReply()If you want, I can also help you write the ARC JavaScript side that listens for
RSP_OBS_SCAN_REQUESTand sends the response back cleanly.Next step
If you paste your full
runForwardLoop()function and the ARC-side serial handling, I can rewrite both into a complete working version with: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 Very Helpful. Thank you. Jack