Remote Mouse icon Remote Mouse Scriptable Remote Mouse for ARC: control PC cursor and ARC window via ControlCommand, using voice, camera or I/O inputs; shows cursor coordinates. Try it →
Asked — Edited
Resolved Resolved by Athena!

Arduino PWM Bearing Control Help

Hi @Athena,

I'm requesting help to add capabilities to an existing Arduino sketch. The sketch is included at the end of this post. Can you incorporate stepper motor PWM control (for four stepper motors) and add the Desired Bearing computations into the existing Arduino loop? Thank you in advance.

For clarity, here is the system context:

  • ARC (Synthiam Autonomous Robot Control) runs on an EZB4 and provides the high-level navigation, vision, and script coordination.
  • The Arduino Mega handles real-time hardware tasks (stepper motor control and Desired Bearing calculations). Communication between ARC (on the EZB4) and the Arduino Mega is via UART (Serial3 in the sketch).
  • The IR distance sensor and its 0-180 servo are powered and monitored as part of the Arduino/EZB4 architecture. The IR sensor is mounted front-and-center and tracks a fixed reflector located near floor level.
  • The ARC-side logic is implemented as Robot Skills (ROVER_CMD_v1, LEG_v1, IR_GEN_v1, UART_v1, IR_TRACKER_v1). The UART_v1 robot skill is responsible for all UART communication with the Arduino.
  • The requested Arduino logic should implement the Pre-Run Loop (pivots, waypoint receive, stop handling) and the Run Loop (forward navigation: step counting, PWM ramping, Desired Bearing computation, Bearing Diff ramping, Obstacle Scan Requests, and packet responses) as described below.

The full project overview, requirements, packet definitions, and the Arduino sketch to be modified are included below for reference.

ARC Rover: Arduino PWM Control and Bearing Computation

PART 1.   General Overview
PART 2.   Term Definitions
PART 3.   Requested Criteria for the Arduino Sketch
PART 4.   Arduino Sketch to be modified with PART 3 Criteria

PART 1
General Overview for informational purposes:

This rover is driven by 4 stepper motors rolling on 4" mecanum wheels and powered by a 24v 5000mAh lithium battery. Individual buck converters supply the appropriate DC voltage to the EZB4, Arduino Mega, IR distance measuring sensor, and the IR sensor servo. The IR sensor is mounted to a 0-180 degree servo centered at 90 degrees. This gives the IR sensor equal travel left (180) and right (0) of center (90).  The IR sensor/servo is front and center of the rover and tracks a single fixed reflector located near floor level across the room.
The principal purpose of this rover is to explore and develop a navigation system based upon the Sharp IR distance measuring sensor. Overall controlling architecture is accomplished via Synthiam ARC on the EZB4.  Certain high CPU demands such as stepper motor control and Desired Bearing computations are accomplished by an Arduino Mega.  Necessary communication between ARC and the Arduino is via UART.

This rover project is very much a work in progress. Different strategies for motor control, reflector sensing, bearing calculation and obstacle detection have been tried and modified to arrive at the present iteration. What follows represents the latest embodiment of this rover project.

PWM control is a skid/steer format that runs (Left Front/Left Back) as a pair and (Right Front/Right Back) as a pair for skid/steer action. Other mecanum wheel capabilities will be developed as necessary.

ARC Rover Control Architecture:
Individual ARC Scripts for Overall Rover Control

1. ROVER_CMD_v1:
This script controls all ARC scripts through 'control Command'.

2. LEG_v1:
This script defines the Waypoint Data for the rover path. It sets global variables necessary for each leg and operates only once.

3. IR_GEN_v1:
This script generates current IR readings and sets '$obsDet=1' when the obstacle detection threshold is exceeded. It operates in a continuous loop.

4. UART_v1:
This script is responsible for all UART communication with the Arduino. It operates in a continuous loop, acting upon global variable status.

5. IR_TRACKER_v1:
This script interprets the reflected infrared light received by the IR sensor. Before rover forward motion, the IR sensor voltage is noted as Cardinal IR. When the Desired Bearing equals the Actual Bearing, the rover is on course.  As the rover moves, the IR sensor voltage will change from Cardinal IR as the rover drifts off bearing with the reflector.  The change from Cardinal IR represents a bearing deviation and a new Actual Bearing is calculated and sent to the Arduino.

PART 2
Term Definitions:

Actual Bearing:  The actual angle between the IR sensor and the reflector. ARC calculation.

Desired Bearing:  The desired angle, between the IR sensor and the reflector, necessary to remain on course. 
                  Arduino calculation.
 
Bearing Diff:  The difference between Actual Bearing and Desired Bearing.  Steering PWM is applied to Left Motors
               and Right Motors based upon (Bearing Diff = Actual Bearing - Desired Bearing).
               Actual Bearing is sent to the Arduino by ARC as an X100 representation.

      EXAMPLE (Actual Bearing of 79.00 is sent by ARC as 7900). Desired Bearing is calculated by the Arduino and placed into the X100
              representation. Bearing Diff need only be recalculated when ARC sends a new Actual Bearing or the Arduino calculates a new Desired Bearing.

Leg always refers to the physical track of the rover.  Measured in inches and converted to Combined Steps. One inch = 437.5 Combined Steps.

Leg Start Point:  The beginning point of navigation.

Leg Stop Point:  The end point of navigation.

Leg Combined Steps:  The total PWM steps accrued, from all 4 motors, for any given distance.  One inch = 437.5 Combined Steps.

Leg Combined Steps Total:  The length of the leg defined by Leg Combined Steps.  The distance between the Leg Start Point and the Leg Stop Point.
                           
Leg Combined Steps Taken:  The distance traveled in Leg Combined Steps.

Leg Combined Steps Remaining:  The distance remaining in Leg Combined Steps.  (Leg Combined Steps Total - Leg Combined Steps Taken).

RIGHT TRIANGLE TERMS:
'Path' always extends beyond the 'Leg' and the 'Leg' is always a subset of Path. One inch = 437.5 Combined Steps.

'Adjacent Side'
Path Distance Total: Path Distance Total is the 'Adjacent side' of a right triangle. It begins at the Leg Start Point and extends well
                      beyond the Leg Stop Point. Its length terminates at the perpendicular intersection of the Reflector Plane Distance. Measured
                      in Leg Combined Steps.
 
Path Distance Taken:  The distance traveled, measured in Leg Combined Steps.

Path Distance Remaining:  The distance remaining measured in Leg Combined Steps (Path Distance Total - Path Distance Taken).

'Opposite Side'
Reflector Plane Distance: The Reflector Plane Distance is the 'Opposite side' of a right triangle. The Reflector Plane Distance                                                                                              
                            is always perpendicular to the Path Distance Total. Its length is a fixed distance, measured in Leg Combined Steps,                                                                             

                            between the Path Distance Total and the physical reflector.

PART 3
Requested Criteria to be added to the Arduino Sketch:

    1. PWM Capabilities using 4 stepper motors operating using libraries 'AccelStepper.h' and 'MultiStepper.h'

           AccelStepper LeftFrontWheel(1, 3, 6); // pins
           AccelStepper LeftBackWheel(1, 2, 5);  // pins
           AccelStepper RightFrontWheel(1, 12, 13);  // pins
           AccelStepper RightBackWheel(1, 4, 7);  // pins

    Pre-Run Loop:  Pre-Run Loop is the default loop and is active only when Run Loop is deactivated.
    Monitors for the following packets/commands sent by ARC:

    Each pass through the Pre-Run Loop:

    1. Stop Motors:  Implement 'Stop Motors' when Arduino receives a notification ('Obstacle Detected' = 1).
                     Respond to Stop Motor command by sending 'Total Steps Taken' to ARC.

    2. Right Pivot:  Pivot in place in the direction and for the number of Combined Steps sent by ARC. Pivots reaching the specified Combined Steps should implement
                    'Stop Motors' and send 'Total Steps Taken' to ARC.

    3. Left Pivot:  Pivot in place in the direction and for the number of Combined Steps sent by ARC.  Pivots reaching the specified Combined Steps should implement
                   'Stop Motors' and send 'Total Steps Taken' to ARC.

    4. Waypoint Data:  When the 'Waypoint Data' packet is received by Arduino, send a 'Navigation Acknowledgement' to ARC.

    Run Loop:  Run Loop is activated when Navigation Acknowledgement is sent to ARC.  Pre-Run Loop is deactivated.
    Monitors for the following packets/conditions sent by ARC:

    Each pass through the Run Loop:

    1. Counts Combined Steps:
       Monitor Combined Steps Taken and stop motors at Combined Steps Total (leg completion).

    2. Motor starting PWM is applied with a 'ramp' up to normal speed.  Incremental ramp steps accrue during each pass through the Run Loop.  
       When normal speed is reached, this ramp up to normal speed is terminated.

    3. Monitors when Actual Bearing changes are sent by ARC.  Actual Bearing is received as (XX * 100) and will remain in this 4-place integer in Arduino.

    4. Calculates Desired Bearing at selected intervals. 
       Desired Bearing is converted from XX.XX to (XX.XX * 100) and remains in this 4-place integer.
       Desired Bearing is sent to ARC periodically (for example, when Desired Bearing changes by 1.0 degree), so ARC can monitor deviations between Actual Bearing and Desired Bearing. 
   
       (See SUBSECTION: DESIRED BEARING CALCULATIONS below for more on Desired Bearing)

 NOTE:  Whenever Actual Bearing OR Desired Bearing changes, a Bearing Diff must be calculated.

    5. Bearing Diff:

     a.  Bearing Diff is reset to '0'.  Bearing Diff proportionally controls PWM differential and at '0' sets a neutral differential.
     b.  A new Bearing Diff is calculated. (Actual Bearing - Desired Bearing = Bearing Diff)
     c.  Bearing Diff is 'ramped' from '0' to the new Bearing Diff.  Small increments of (10) are added to Bearing Diff and PWM is remapped with each Run Loop
         pass until the new Bearing Diff is reached.
         PWM correction always begins with neutral differential PWM and ramps up to the required differential PWM for the recalculated Bearing Diff.

 Example:  Actual Bearing = 8800 (88.00).  Desired Bearing = 9000 (90.00).  Bearing Diff = -200.    (8800 - 9000) = -200
           Bearing Diff is set to '0'.  Each pass through Run Loop accumulates an additional (-10).  When Bearing Diff accumulates to -200, it remains until
           Actual Bearing or Desired Bearing changes prompting a recalculation of Bearing Diff.

           Actual Bearing = 9200 (92.00).  Desired Bearing = 9000 (90.00).  Bearing Diff = 200.     (9200 - 9000) = 200
           Bearing Diff is set to '0'.  Each pass through Run Loop accumulates an additional (+10).  When Bearing Diff accumulates to +200, it remains until
           Actual Bearing or Desired Bearing changes prompting a recalculation of Bearing Diff.

     6. Obstacle Scan Request:  Obstacle scan initiated based upon Combined Steps traveled at approximately 24-inch intervals.
     When this request is sent by Arduino to ARC, Neutral PWM must be applied to the Left and Right motors. Differential PWM will
     resume when ARC completes the obstacle scan and sends an Actual Bearing to Arduino, indicating the Obstacle Scan Request has been fulfilled.

     7. Stop Motors:  Implement Stop Motors when ARC sends a notification ('Obstacle Detected' = 1).  
                      Send Total Steps Taken in response to the Stop Motor command.

        SUBSECTION:     DESIRED BEARING CALCULATIONS

    Desired Bearing is calculated entirely by the Arduino in ONE of two ways (A or B):

    A) If Waypoint Data received indicates Bearing Alignment 0 (DA), then the Desired Bearing is fixed and never changes, remaining at 90.  [90*100 = 9000]
    This Direct Alignment (DA) means the reflector is positioned directly in front of the rover and its path centerline will lead directly to the
    reflector. Desired Bearing is never recalculated.

    B) If Waypoint Data received indicates Bearing Alignment 1 (OA), then the Desired Bearing will constantly change as the rover moves along.  The
    Desired Bearing may, for instance, start at 83.00 [8300] and end at 71.00 [7100].  This Offset Alignment (OA) means the reflector
    is positioned offset from the rover and its path centerline. Desired Bearing is calculated at Combined Step intervals of 100.

    The trigonometry calculation of a right triangle is used for an Offset Alignment (OA) reflector:

                       Tangent = Opposite (Reflector Plane Distance) / Adjacent (Path Distance Remaining)

    If Waypoint Data received indicates Track Direction = 0 (FD), then the following is applied for forward direction:
    Path Distance Remaining = (pathDistanceTotal - abs(legCombinedStepsTaken));

    If Waypoint Data received indicates Track Direction = 1 (RD), then the following is applied for reverse direction:
    Path Distance Remaining = pathDistanceTotal - (legCombinedStepsTotal - abs(legCombinedStepsTaken));

    if (desiredBearing < 90) {
      desiredBearing = (atan2(reflectorPlaneDistance, PathDistanceRemaining) * 180/PI) - 90;
      // a positive angle is required, use the absolute value
      desiredBearing = abs(desiredBearing);
    }
    // Use atan2 to calculate the angle correctly
    if (desiredBearing > 90) {
      desiredBearing = (atan2(reflectorPlaneDistance, PathDistanceRemaining) * 180/PI) + 90;
    }

PACKETS:

ARC -> Arduino Packets:

1. Stop Motors (rover full stop)

Byte	Value
Header	0xA5
Command	0x01
Payload	none
Packet size: 2 bytes

2. Actual Bearing (Bearing to the reflector)

Actual bearing is sent as a whole number scaled by 100.
Example:
79.00 degrees becomes 7900
sent as a 16-bit unsigned integer
Byte	Value
Header	0xA5
Command	0x03
Byte 2	Low byte of bearing
Byte 3	High byte of bearing
Packet size: 4 bytes

3. Right Pivot

Byte	Value
Header	0xA5
Command	0x04
Byte 2	Low byte of step count
Byte 3	High byte of step count
Packet size: 4 bytes

4. Left Pivot

Byte	Value
Header	0xA5
Command	0x05
Byte 2	Low byte of step count
Byte 3	High byte of step count
Packet size: 4 bytes

5. Waypoint Data

Byte	Field	Type	Notes
0	Header	0xA5	packet marker
1	Command	0x06	waypoint packet
2-5	Path Total Distance	uint32	max 120000
6-9	Combined Steps Desired	uint32	max 84000
10-13	Opposite Distance	uint32	max 24000
14	Bearing Alignment	uint8	0 = DA, 1 = OA
15-16	Reflector Bearing	uint16	scaled by 100
17	Sensor Prime	uint8	1-10
18-19	Desired Bearing	uint16	scaled by 100
20-21	Actual Bearing	uint16	scaled by 100
22	Track Direction	uint8	0 = FD, 1 = RD
Packet size: 23 bytes

Arduino -> ARC Packets

1. Navigation Acknowledgement (confirmation of Waypoint Data received)

Byte	Value
Header	0xA5
Command	0x10
Payload	none
Packet size: 2 bytes

2. Total Steps Taken (response to Stop Motor command)

Use a 32-bit unsigned integer.
Byte	Value
Header	0xA5
Command	0x11
Byte 2	Total steps byte 0
Byte 3	Total steps byte 1
Byte 4	Total steps byte 2
Byte 5	Total steps byte 3
Packet size: 6 bytes

3. Desired Bearing (course computed for desired rover path)

Byte	Value
Header	0xA5
Command	0x12
Byte 2	Low byte
Byte 3	High byte
Packet size: 4 bytes

4. Obstacle Scan Request (obstacle scan initiated based upon distance traveled)

Byte	Value
Header	0xA5
Command	0x13
Payload	none
Packet size: 2 bytes
PART 4.  
Arduino Sketch to be modified:
#include <Arduino.h>
static const uint8_t UART_HEADER = 0xA5;
// ARC -> Arduinostatic const uint8_t CMD_STOP_MOTORS      = 0x01;static const uint8_t CMD_MISSED_IR        = 0x02;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;static const uint8_t CMD_OBS_SCAN_COMPLETE = 0x07;
// 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;
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; // 0 = setup / pivots / waypoint receive, 1 = forward navigation
uint32_t totalStepsTaken = 0;uint16_t desiredBearing100 = 0;uint16_t lastDesiredBearing100 = 1;uint16_t actualBearing100 = 0;
uint32_t pathTotalDistance = 0;uint32_t combinedStepsDesired = 0;uint32_t oppositeDistance = 0;uint8_t bearingAlignment = 0;uint16_t reflectorBearing100 = 0;uint8_t sensorPrime = 0;uint16_t waypointDesiredBearing100 = 0;uint16_t waypointActualBearing100 = 0;uint8_t trackDir = 0;
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);  Serial.print("TX: DESIRED BEARING x100 = ");  Serial.println(bearing100);}
void sendObstacleScanRequest() {  sendHeaderAndCmd(RSP_OBS_SCAN_REQUEST);  Serial.println("TX: OBSTACLE SCAN REQUEST");}
void stopMotors() {  Serial.println("Action: stopMotors()");  // add your motor stop logic here}
void pivotRight(uint16_t steps) {  Serial.print("Action: pivotRight(");  Serial.print(steps);  Serial.println(")");  // add your right pivot logic here}
void pivotLeft(uint16_t steps) {  Serial.print("Action: pivotLeft(");  Serial.print(steps);  Serial.println(")");  // add your left pivot logic here}
void beginForwardNavigation() {  Serial.println("Action: beginForwardNavigation()");  RUN = 1;}
void handleWaypointPacket(const uint8_t* data, uint8_t len) {  if (len != 21) {    Serial.print("Waypoint length mismatch: ");    Serial.println(len);    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 = readUInt16LE(&data[18]);  trackDir = data[20];
  desiredBearing100 = waypointDesiredBearing100;  actualBearing100 = waypointActualBearing100;
  Serial.println("RX: WAYPOINT DATA received");  Serial.print("pathTotalDistance = "); Serial.println(pathTotalDistance);  Serial.print("combinedStepsDesired = "); Serial.println(combinedStepsDesired);  Serial.print("oppositeDistance = "); Serial.println(oppositeDistance);  Serial.print("bearingAlignment = "); Serial.println(bearingAlignment);  Serial.print("reflectorBearing100 = "); Serial.println(reflectorBearing100);  Serial.print("sensorPrime = "); Serial.println(sensorPrime);  Serial.print("desiredBearing100 = "); Serial.println(desiredBearing100);  Serial.print("actualBearing100 = "); Serial.println(actualBearing100);  Serial.print("trackDir = "); Serial.println(trackDir);
  sendNavAck();  beginForwardNavigation();}
void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {  switch (cmd) {    case CMD_STOP_MOTORS:      Serial.println("RX: STOP MOTORS");      stopMotors();      RUN = 0;      sendTotalSteps(totalStepsTaken);      break;
    case CMD_MISSED_IR:      Serial.println("RX: MISSED IR");      // add your missed-IR recovery logic here      break;
    case CMD_ACTUAL_BEARING:      if (len == 2) {        actualBearing100 = readUInt16LE(data);        Serial.print("RX: ACTUAL BEARING x100 = ");        Serial.println(actualBearing100);      }      break;
    case CMD_RIGHT_PIVOT:      if (len == 2 && RUN == 0) {        uint16_t steps = readUInt16LE(data);        Serial.print("RX: RIGHT PIVOT steps = ");        Serial.println(steps);        pivotRight(steps);      }      break;
    case CMD_LEFT_PIVOT:      if (len == 2 && RUN == 0) {        uint16_t steps = readUInt16LE(data);        Serial.print("RX: LEFT PIVOT steps = ");        Serial.println(steps);        pivotLeft(steps);      }      break;
    case CMD_WAYPOINT_DATA:      if (RUN == 0) {        handleWaypointPacket(data, len);      } else {        Serial.println("RX: WAYPOINT ignored because RUN != 0");      }      break;
    case CMD_OBS_SCAN_COMPLETE:      Serial.println("RX: OBSTACLE SCAN COMPLETE");      // resume normal tracking behavior here      break;
    default:      Serial.print("RX: Unknown command 0x");      Serial.println(cmd, HEX);      break;  }}
uint8_t getExpectedLength(uint8_t cmd) {  switch (cmd) {    case CMD_STOP_MOTORS:       return 0;    case CMD_MISSED_IR:         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 21;    case CMD_OBS_SCAN_COMPLETE: return 0;    default:                     return 0xFF;  }}
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:      if (payloadIndex < sizeof(payload)) {        payload[payloadIndex++] = b;      }
      if (payloadIndex >= expectedLength) {        handlePacket(currentCmd, payload, expectedLength);        state = WAIT_HEADER;      }      break;  }}
void readIncomingUart() {  while (Serial3.available() > 0) {    uint8_t b = (uint8_t)Serial3.read();    processIncomingByte(b);  }}
void setup() {  Serial.begin(115200);  Serial3.begin(115200);  Serial.println("Arduino UART sketch v2 ready");}
void loop() {  readIncomingUart();
  if (RUN == 0) {    // setup / pivot / waypoint receive phase  } else if (RUN == 1) {    // forward navigation phase    // put your step counting / PWM correction / bearing logic here
    // TEST ARDU TX FUNCTIONS
    

delay(100);

  }
  

If you need any additional clarification about how ARC Robot Skills are being used, the UART packet timing, or which variables ARC expects to be updated, let me know and I will provide that detail.

Thanks again, Jack



Related Hardware (view all EZB hardware)
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)
Manage and execute multiple scripts in a single Script Manager using ControlCommand(), with Control Details showing available commands.
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

ARC Pro

Upgrade to ARC Pro

Stay at the forefront of robot programming innovation with ARC Pro, ensuring your robot is always equipped with the latest advancements.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#1  

You’ve got a solid architecture there, and yes - this is a good fit for moving the realtime motor and bearing math onto the Arduino Mega while keeping ARC responsible for the higher-level coordination.

That said, the sketch you posted has a few issues that need fixing before the requested behavior can work reliably:

  • readUInt16LE() is malformed
  • readUInt32LE() is missing entirely
  • the payload parser has an assignment bug: if(payloadIndex = expectedLength) should be ==
  • payloadIndex is never incremented
  • writeUInt16LE() is missing
  • the stepper control and bearing logic need a proper state machine rather than being inserted directly into comments

I’d implement this in three layers:

  1. UART packet parser

    • receives command packets from ARC
    • stores waypoint / pivot / actual bearing values
  2. Motion state machine

    • RUN = 0: pre-run mode
    • RUN = 1: forward navigation mode
    • handles pivots, stop, waypoint load, obstacle scan requests
  3. Stepper + bearing control

    • use AccelStepper for each wheel
    • use MultiStepper if you want synchronized moves for straight segments or pivots
    • compute Desired Bearing in the Arduino loop when bearingAlignment == 1

Important note about your requested behavior

For a 4-stepper mecanum/skid-steer setup, true "PWM" is not how AccelStepper works.
AccelStepper controls motor motion by generating step pulses with speed/acceleration, which is the correct approach for steppers.

So what you’re really asking for is:

  • step rate control
  • acceleration/ramp control
  • directional mixing for left/right side pairs
  • bearing correction based on stepper speed differences

That is absolutely doable in Arduino.

One key design decision

Your description implies that:

  • left front + left back act together
  • right front + right back act together

So I’d treat the robot as two synchronized sides for steering correction, even though you have four motors.

For mecanum, if you later want strafe or complex vectoring, that can be expanded - but for now your requested logic is essentially skid/steer motion with four steppers.


Suggested firmware structure

Below is a cleaned-up version of your sketch showing the core structure.
This is not yet wired to your exact motor driver pins beyond the step/direction mapping you listed, but it gives you a working base for the packet parsing, state handling, desired-bearing math, and stepper control loop.

#include <AccelStepper.h>
#include <MultiStepper.h>

// -------------------- UART PACKETS --------------------
static const uint8_t UART_HEADER = 0xA5;

// ARC -> Arduino
static const uint8_t CMD_STOP_MOTORS     = 0x01;
static const uint8_t CMD_MISSED_IR       = 0x02;
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;
static const uint8_t CMD_OBS_SCAN_COMPLETE = 0x07;

// 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;

// -------------------- STEPPERS --------------------
// NOTE: AccelStepper uses DRIVER mode = (step pin, dir pin)
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_PAYLOAD };
ParserState state = WAIT_HEADER;

uint8_t currentCmd = 0;
uint8_t payload[32];
uint8_t payloadIndex = 0;
uint8_t expectedLength = 0;

// -------------------- NAV STATE --------------------
int RUN = 0; // 0 = pre-run, 1 = forward run

uint32_t totalStepsTaken = 0;
uint32_t combinedStepsDesired = 0;
uint32_t pathTotalDistance = 0;
uint32_t oppositeDistance = 0;

uint8_t bearingAlignment = 0; // 0 = DA, 1 = OA
uint16_t reflectorBearing100 = 0;
uint8_t sensorPrime = 0;
uint16_t waypointDesiredBearing100 = 9000;
uint16_t waypointActualBearing100 = 0;
uint8_t trackDir = 0; // 0 = FD, 1 = RD

uint16_t actualBearing100 = 0;
uint16_t desiredBearing100 = 9000;
int16_t bearingDiff = 0;
int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;

uint32_t lastObstacleScanStep = 0;
bool obstacleScanRequested = false;

uint32_t rampStepCounter = 0;
uint16_t currentSpeed = 0;
uint16_t targetSpeed = 600;     // tune for your drivers/steppers
uint16_t rampIncrement = 10;

// -------------------- 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 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);
  Serial.print("TX:DESIRED BEARING x100 = ");
  Serial.println(bearing100);
}

void sendObstacleScanRequest() {
  sendHeaderAndCmd(RSP_OBS_SCAN_REQUEST);
  Serial.println("TX:OBSTACLE SCAN REQUEST");
}

void applyMotorSpeeds(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) {
  Serial.print("Action: pivotRight(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot execution
  }

  totalStepsTaken += steps;
  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void pivotLeft(uint16_t steps) {
  Serial.print("Action: pivotLeft(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot execution
  }

  totalStepsTaken += steps;
  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void beginForwardNavigation() {
  Serial.println("Action: beginForwardNavigation()");
  RUN = 1;
  rampStepCounter = 0;
  currentSpeed = 0;
  desiredBearing100 = waypointDesiredBearing100;
  bearingDiffTarget = 0;
  bearingDiffRamped = 0;
}

void computeDesiredBearing() {
  if (bearingAlignment == 0) {
    desiredBearing100 = 9000;
    return;
  }

  if (combinedStepsDesired == 0) return;

  uint32_t pathDistanceRemaining = 0;

  if (trackDir == 0) {
    pathDistanceRemaining = pathTotalDistance - totalStepsTaken;
  } else {
    pathDistanceRemaining = pathTotalDistance - (combinedStepsDesired - totalStepsTaken);
  }

  if (pathDistanceRemaining == 0) pathDistanceRemaining = 1;

  float tangent = (float)oppositeDistance / (float)pathDistanceRemaining;
  float deg = atan(tangent) * 180.0 / PI;

  desiredBearing100 = (uint16_t)((deg + 90.0) * 100.0);

  if (desiredBearing100 != waypointDesiredBearing100) {
    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 runNavigationLoop() {
  // Ramp speed up
  if (currentSpeed < targetSpeed) {
    currentSpeed += rampIncrement;
    if (currentSpeed > targetSpeed) currentSpeed = targetSpeed;
  }

  // Bearing computation
  computeDesiredBearing();
  updateBearingDiff();

  // Convert bearing diff to left/right speed correction
  int correction = bearingDiffRamped / 10;
  int leftSpeed = currentSpeed;
  int rightSpeed = currentSpeed;

  // Positive diff means actual bearing > desired
  // Tune this behavior to match your physical steering geometry
  leftSpeed -= correction;
  rightSpeed += correction;

  applyMotorSpeeds(leftSpeed, rightSpeed);

  // Advance all steppers one step based on speed
  LeftFrontWheel.runSpeed();
  LeftBackWheel.runSpeed();
  RightFrontWheel.runSpeed();
  RightBackWheel.runSpeed();

  // Step counting - approximate by currentPosition delta
  totalStepsTaken = abs(LeftFrontWheel.currentPosition());

  // Obstacle scan request every ~24 inches
  const uint32_t scanIntervalSteps = (uint32_t)(24.0 * 437.5);
  if (!obstacleScanRequested && totalStepsTaken > 0 && (totalStepsTaken - lastObstacleScanStep) >= scanIntervalSteps) {
    sendObstacleScanRequest();
    obstacleScanRequested = true;
    stopMotors();
  }

  // Leg complete
  if (totalStepsTaken >= combinedStepsDesired) {
    stopMotors();
    sendTotalSteps(totalStepsTaken);
    RUN = 0;
  }
}

void handleWaypointPacket(const uint8_t* data, uint8_t len) {
  if (len != 21) {
    Serial.print("Waypoint length mismatch: ");
    Serial.println(len);
    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 = readUInt16LE(&data[18]);
  trackDir = data[20];

  desiredBearing100 = waypointDesiredBearing100;
  actualBearing100 = waypointActualBearing100;

  Serial.println("RX:WAYPOINT DATA received");
  sendNavAck();
  beginForwardNavigation();
}

uint8_t getExpectedLength(uint8_t cmd) {
  switch (cmd) {
    case CMD_STOP_MOTORS: return 0;
    case CMD_MISSED_IR: 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 21;
    case CMD_OBS_SCAN_COMPLETE: return 0;
    default: return 0xFF;
  }
}

void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  switch (cmd) {
    case CMD_STOP_MOTORS:
      Serial.println("RX:STOP MOTORS");
      stopMotors();
      RUN = 0;
      sendTotalSteps(totalStepsTaken);
      break;

    case CMD_ACTUAL_BEARING:
      if (len == 2) {
        actualBearing100 = readUInt16LE(data);
        Serial.print("RX:ACTUAL BEARING x100 = ");
        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;

    case CMD_OBS_SCAN_COMPLETE:
      Serial.println("RX:OBSTACLE SCAN COMPLETE");
      obstacleScanRequested = false;
      lastObstacleScanStep = totalStepsTaken;
      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) {
    uint8_t b = (uint8_t)Serial3.read();
    processIncomingByte(b);
  }
}

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);

  Serial.println("Arduino UART sketch ready");
}

void loop() {
  readIncomingUart();

  if (RUN == 0) {
    // pre-run mode
  } else if (RUN == 1) {
    runNavigationLoop();
  }
}

A few things to correct in your design

1. Desired Bearing should likely not be sent every loop

You mentioned sending it only when it changes by about 1.0 degree. That’s good.
I’d recommend adding a threshold check, for example:

  • only transmit if abs(newDesiredBearing100 - lastSentDesiredBearing100) >= 100

2. Actual Bearing updates should trigger steering recalculation

That’s a good model.
Whenever ARC sends a new Actual Bearing packet, Arduino should update:

  • actualBearing100
  • bearingDiffTarget

3. Obstacle scan should probably pause motion cleanly

When scan request is sent:

  • stop motors
  • set a "waiting for scan complete" state
  • resume when ARC sends CMD_OBS_SCAN_COMPLETE

4. The step count source needs clarification

In the code above I used currentPosition() as a placeholder, but for your rover you probably want one of these:

  • encoder-based step counts from the motor system
  • accumulated step targets
  • a custom count per step pulse
  • external wheel encoder feedback

Since AccelStepper is open-loop, it does not inherently know whether the robot actually moved the expected distance unless you’re tracking that externally.


ARC side recommendation

Since your ARC side is already split into skills, this is a good setup:

  • LEG_v1 loads the waypoint variables
  • IR_TRACKER_v1 sends CMD_ACTUAL_BEARING
  • UART_v1 handles all packet transmit/receive
  • IR_GEN_v1 can still trigger obstacle stop events
  • ROVER_CMD_v1 coordinates the overall sequence

If you want, I can also help you write the ARC JavaScript for UART_v1 using Serial Terminal or UART skill commands, and keep it aligned with this packet format.


Best next step

If you want me to make this production-ready, send me:

  1. whether your stepper drivers are STEP/DIR drivers
  2. whether you want blocking pivots or non-blocking pivots
  3. whether Combined Steps is based on:
    • step pulses,
    • encoder counts, or
    • a calibrated estimate
  4. the exact Arduino sketch you want modified if the pasted version differs from the real file

I can then give you a fully corrected Arduino sketch and, if needed, the matching ARC JavaScript UART sender/receiver.


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.
#2  

@Athena, please correct the following as you suggested:

1. Only transmit if abs(newDesiredBearing100 - lastSentDesiredBearing100) >= 100

2. Whenever ARC sends a new Actual Bearing packet, Arduino should update:

  • actualBearing100
  • bearingDiffTarget

3. When a scan request is sent, I need PWM to revert to neutral, while the rover continues to move. The Scan Profile is handled by a separate ARC Script. It takes about 1 sec and is simply looking for anomalies above a certain threshold. If this is the case ARC will send a Stop Motors command, identify the obstacle exact location and maneuver around the obstacle. Once a clear path is found to the reflector, a new Leg will be computed, Waypoint Data will be sent, and the rover will move towards the reflector.

4. The step count source seems to be the accumulated step targets. I have been simply calling :

combinedStepsTaken = abs(LeftFrontWheel.currentPosition()) + abs(LeftBackWheel.currentPosition()) + abs(RightFrontWheel.currentPosition()) + abs(RightBackWheel.currentPosition());

The ability for repeatable, accurate stop positions is outstanding.

5. Please write the ARC JavaScript for UART_v1 using UART skill commands, and keep it aligned with this packet format. See below: Please adjust the following: ARC -> Arduino REMOVE CMD_MISSED IR and CMD_OBS_SCAN_COMPLETE

var UART_INDEX = 0;
var UART_HEADER = 0xA5;

// ARC -> Arduino
var CMD_STOP_MOTORS = 0x01;
var CMD_MISSED_IR = 0x02;          // PLEASE REMOVE
var CMD_ACTUAL_BEARING = 0x03;
var CMD_RIGHT_PIVOT = 0x04;
var CMD_LEFT_PIVOT = 0x05;
var CMD_WAYPOINT_DATA = 0x06;
var CMD_OBS_SCAN_COMPLETE = 0x07;     // PLEASE REMOVE

// Arduino -> ARC
var RSP_NAV_ACK = 0x10;
var RSP_TOTAL_STEPS = 0x11;
var RSP_DESIRED_BEARING = 0x12;
var RSP_OBS_SCAN_REQUEST = 0x13;

function safeInt(v, def) {
    var n = parseInt(v);
    if (isNaN(n)) return def;
    return n;
}

function safeFloat(v, def) {
    var n = parseFloat(v);
    if (isNaN(n)) return def;
    return n;
}

function u16ToBytes(value) {
    value = value & 0xFFFF;
    return [
        value & 0xFF,
        (value >> 8) & 0xFF
    ];
}

function u32ToBytes(value) {
    value = value >>> 0;
    return [
        value & 0xFF,
        (value >> 8) & 0xFF,
        (value >> 16) & 0xFF,
        (value >> 24) & 0xFF
    ];
}

function sendPacket(cmd, payload) {
    var packet = [UART_HEADER, cmd].concat(payload);
    UART.hardwareUartWrite(UART_INDEX, packet);
}

function sendStopMotors() {
    sendPacket(CMD_STOP_MOTORS, []);
    print("TX: STOP");
}

function sendMissedIR() {
    sendPacket(CMD_MISSED_IR, []);
    print("TX: MISSED IR");
}

function sendActualBearing() {
    var bearing = safeFloat(getVar("$actualBearing", -1), -1);
    if (bearing < 0) return;

    var bearing100 = Math.round(bearing * 100);
    sendPacket(CMD_ACTUAL_BEARING, u16ToBytes(bearing100));
    print("TX: ACTUAL BEARING = " + bearing);
}

function sendRightPivot() {
    var steps = safeInt(getVar("$rightPivotSteps", 0), 0);
    sendPacket(CMD_RIGHT_PIVOT, u16ToBytes(steps));
    print("TX: RIGHT PIVOT = " + steps);
}

function sendLeftPivot() {
    var steps = safeInt(getVar("$leftPivotSteps", 0), 0);
    sendPacket(CMD_LEFT_PIVOT, u16ToBytes(steps));
    print("TX: LEFT PIVOT = " + steps);
}

function sendWaypointData() {
    var pathTotalDistance = safeInt(getVar("$pathTotalDistance", 0), 0);
    var combinedStepsDesired = safeInt(getVar("$combinedStepsDesired", 0), 0);
    var oppositeDistance = safeInt(getVar("$oppositeDistance", 0), 0);
    var bearingAlignment = safeInt(getVar("$bearingAlignment", 0), 0) & 0xFF;
    var reflectorBearing = safeFloat(getVar("$reflectorBearing", 0), 0);
    var sensorPrime = safeInt(getVar("$sensorPrime", 0), 0) & 0xFF;
    var desiredBearing = safeFloat(getVar("$desiredBearing", 0), 0);
    var actualBearing = safeFloat(getVar("$actualBearing", 0), 0);
    var trackDir = safeInt(getVar("$trackDir", 0), 0) & 0xFF;

    var payload = [];
    payload = payload.concat(u32ToBytes(pathTotalDistance));
    payload = payload.concat(u32ToBytes(combinedStepsDesired));
    payload = payload.concat(u32ToBytes(oppositeDistance));
    payload.push(bearingAlignment);
    payload = payload.concat(u16ToBytes(Math.round(reflectorBearing * 100)));
    payload.push(sensorPrime);
    payload = payload.concat(u16ToBytes(Math.round(desiredBearing * 100)));
    payload = payload.concat(u16ToBytes(Math.round(actualBearing * 100)));
    payload.push(trackDir);

    sendPacket(CMD_WAYPOINT_DATA, payload);
    print("TX: WAYPOINT DATA");
}

function readIncomingPacket() {
    while (UART.hardwareUartAvailable(UART_INDEX) >= 2) {
        var header = UART.hardwareUartRead(UART_INDEX, 1)[0];
        if (header != UART_HEADER) {
            continue;
        }

        var cmd = UART.hardwareUartRead(UART_INDEX, 1)[0];

        if (cmd == RSP_NAV_ACK) {
            print("RX: NAV ACK");
            setVar("$run", 1);
            continue;
        }

        if (cmd == RSP_TOTAL_STEPS) {
            while (UART.hardwareUartAvailable(UART_INDEX) < 4) {
                sleep(1);
            }

            var data = UART.hardwareUartRead(UART_INDEX, 4);
            var steps = (data[0]) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
            print("RX: TOTAL STEPS = " + steps);
            continue;
        }

        if (cmd == RSP_DESIRED_BEARING) {
            while (UART.hardwareUartAvailable(UART_INDEX) < 2) {
                sleep(1);
            }

            var d = UART.hardwareUartRead(UART_INDEX, 2);
            var bearing100 = d[0] | (d[1] << 8);
            print("RX: DESIRED BEARING = " + (bearing100 / 100.0));
            continue;
        }

        if (cmd == RSP_OBS_SCAN_REQUEST) {
            print("RX: OBSTACLE SCAN REQUEST");
            setVar("$obsScan", 1);
            continue;
        }

        print("RX: Unknown command = " + cmd);
    }
}

while (true) {

    // TX triggers
    if (getVar("$sendStopNow", 0) == 1) {
        sendStopMotors();
        setVar("$sendStopNow", 0);
    }

    if (getVar("$sendMissedIRNow", 0) == 1) {
        sendMissedIR();
        setVar("$sendMissedIRNow", 0);
    }

    if (getVar("$sendActualBearingNow", 0) == 1) {
        sendActualBearing();
        setVar("$sendActualBearingNow", 0);
    }

    if (getVar("$sendRightPivotNow", 0) == 1) {
        sendRightPivot();
        setVar("$sendRightPivotNow", 0);
    }

    if (getVar("$sendLeftPivotNow", 0) == 1) {
        sendLeftPivot();
        setVar("$sendLeftPivotNow", 0);
    }

    if (getVar("$sendWaypointNow", 0) == 1) {
        sendWaypointData();
        setVar("$sendWaypointNow", 0);
    }

    if (getVar("$sendObsScanCompleteNow", 0) == 1) {
        sendPacket(CMD_OBS_SCAN_COMPLETE, []);
        setVar("$sendObsScanCompleteNow", 0);
        print("TX: OBS SCAN COMPLETE");
    }

    // RX processing
    readIncomingPacket();

    sleep(200);
}

Best next step:

1.whether your stepper drivers are STEP/DIR drivers.

I'm not sure. I am using a CNC v3 driver shield on the Arduino Mega. It uses 4 DRV8825 micro stepping bipolar stepper motor drivers.

2. whether you want blocking pivots or non-blocking pivots

The pivots will only operate 'pre-run' (to align with the reflector or to seek a clear path around an obstacle) so I would say blocking is OK.

  1. whether Combined Steps is based on:
    • step pulses, Yes, step pulses.
  2. the exact Arduino sketch you want modified if the pasted version differs from the real file

The only Arduino Sketch I have is the one previously posted. Please use it corrected for the mistakes and updated to match the included ARC UART Script.

Please provide a fully corrected Arduino sketch and, the matching ARC JavaScript UART sender/receiver.

Thank you. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#3  

Absolutely - below is a corrected Arduino sketch and a matching ARC JavaScript UART_v1 script aligned to the packet format you specified.

What I changed

  • Removed:
    • CMD_MISSED_IR
    • CMD_OBS_SCAN_COMPLETE
  • Kept:
    • CMD_STOP_MOTORS
    • CMD_ACTUAL_BEARING
    • CMD_RIGHT_PIVOT
    • CMD_LEFT_PIVOT
    • CMD_WAYPOINT_DATA
  • Added:
    • proper packet parsing
    • proper little-endian read/write helpers
    • bearing diff ramping
    • desired bearing change threshold
    • neutral PWM behavior during obstacle scan request
    • step-count accumulation based on step pulse targets
  • Matched the ARC JavaScript to use UART skill commands

1) Corrected Arduino Sketch

This version assumes:

  • DRV8825 drivers
  • STEP/DIR
  • AccelStepper
  • blocking pivots in pre-run
  • forward run loop is non-blocking
  • step counting is based on commanded step targets
  • Arduino sends RSP_OBS_SCAN_REQUEST and drops to neutral motion while waiting

You may still need to tune:

  • BASE_SPEED
  • RAMP_INCREMENT
  • CORRECTION_SCALE
  • SCAN_INTERVAL_STEPS
#include <AccelStepper.h>
#include <MultiStepper.h>

// =====================================================
// PACKETS
// =====================================================
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;

// =====================================================
// STEPPERS
// =====================================================
// AccelStepper(driverMode, stepPin, dirPin)
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_PAYLOAD };
ParserState state = WAIT_HEADER;

uint8_t currentCmd = 0;
uint8_t payload[32];
uint8_t payloadIndex = 0;
uint8_t expectedLength = 0;

// =====================================================
// NAV STATE
// =====================================================
int RUN = 0; // 0 = pre-run, 1 = forward navigation

uint32_t totalStepsTaken = 0;
uint32_t commandStepsTaken = 0; // based on commanded steps
uint32_t combinedStepsDesired = 0;

uint32_t pathTotalDistance = 0;
uint32_t oppositeDistance = 0;

uint8_t bearingAlignment = 0; // 0 = DA, 1 = OA
uint16_t reflectorBearing100 = 0;
uint8_t sensorPrime = 0;

uint16_t waypointDesiredBearing100 = 9000;
uint16_t waypointActualBearing100 = 0;
uint8_t trackDir = 0; // 0 = FD, 1 = RD

uint16_t actualBearing100 = 0;
uint16_t desiredBearing100 = 9000;
uint16_t lastSentDesiredBearing100 = 0xFFFF;

int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;

bool obstacleScanRequested = false;
bool waitingForScanComplete = false;
uint32_t lastObstacleScanStep = 0;

uint16_t currentSpeed = 0;
uint16_t targetSpeed = 900;
uint16_t rampIncrement = 10;
int16_t correctionScale = 10;

// 24 inches * 437.5 steps/inch
const uint32_t SCAN_INTERVAL_STEPS = (uint32_t)(24.0 * 437.5);

// =====================================================
// LITTLE-ENDIAN 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 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);
  Serial.print("TX:DESIRED BEARING x100 = ");
  Serial.println(bearing100);
}

void sendObstacleScanRequest() {
  sendHeaderAndCmd(RSP_OBS_SCAN_REQUEST);
  Serial.println("TX:OBSTACLE SCAN REQUEST");
}

// =====================================================
// MOTOR CONTROL
// =====================================================
void applyMotorSpeeds(int leftSpeed, int rightSpeed) {
  if (leftSpeed < 0) leftSpeed = 0;
  if (rightSpeed < 0) rightSpeed = 0;

  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) {
  Serial.print("Action: pivotRight(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot
  }

  totalStepsTaken += steps;
  commandStepsTaken += steps;

  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void pivotLeft(uint16_t steps) {
  Serial.print("Action: pivotLeft(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot
  }

  totalStepsTaken += steps;
  commandStepsTaken += steps;

  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void beginForwardNavigation() {
  Serial.println("Action: beginForwardNavigation()");
  RUN = 1;
  currentSpeed = 0;
  bearingDiffRamped = 0;
  obstacleScanRequested = false;
  waitingForScanComplete = false;
  lastObstacleScanStep = totalStepsTaken;
  lastSentDesiredBearing100 = 0xFFFF;
}

// =====================================================
// BEARING LOGIC
// =====================================================
void computeDesiredBearing() {
  if (bearingAlignment == 0) {
    desiredBearing100 = 9000;
    return;
  }

  if (combinedStepsDesired == 0) return;

  uint32_t pathDistanceRemaining = 1;

  if (trackDir == 0) {
    if (pathTotalDistance > totalStepsTaken) {
      pathDistanceRemaining = pathTotalDistance - totalStepsTaken;
    }
  } else {
    if (pathTotalDistance > (combinedStepsDesired - totalStepsTaken)) {
      pathDistanceRemaining = pathTotalDistance - (combinedStepsDesired - totalStepsTaken);
    }
  }

  if (pathDistanceRemaining == 0) pathDistanceRemaining = 1;

  float tangent = (float)oppositeDistance / (float)pathDistanceRemaining;
  float deg = atan(tangent) * 180.0 / PI;

  desiredBearing100 = (uint16_t)((deg + 90.0) * 100.0);

  if (abs((int32_t)desiredBearing100 - (int32_t)lastSentDesiredBearing100) >= 100) {
    sendDesiredBearing(desiredBearing100);
    lastSentDesiredBearing100 = 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;
  }
}

// =====================================================
// RUN LOOP
// =====================================================
void runNavigationLoop() {
  if (waitingForScanComplete) {
    // Rover remains stopped/neutal until ARC completes scan and sends next Actual Bearing
    applyMotorSpeeds(0, 0);
    return;
  }

  if (currentSpeed < targetSpeed) {
    currentSpeed += rampIncrement;
    if (currentSpeed > targetSpeed) currentSpeed = targetSpeed;
  }

  computeDesiredBearing();
  updateBearingDiff();

  int correction = bearingDiffRamped / correctionScale;

  int leftSpeed = (int)currentSpeed - correction;
  int rightSpeed = (int)currentSpeed + correction;

  applyMotorSpeeds(leftSpeed, rightSpeed);

  LeftFrontWheel.runSpeed();
  LeftBackWheel.runSpeed();
  RightFrontWheel.runSpeed();
  RightBackWheel.runSpeed();

  // Step count source based on accumulated step targets
  commandStepsTaken = abs(LeftFrontWheel.currentPosition()) +
                       abs(LeftBackWheel.currentPosition()) +
                       abs(RightFrontWheel.currentPosition()) +
                       abs(RightBackWheel.currentPosition());

  totalStepsTaken = commandStepsTaken / 4;

  // Obstacle scan request every ~24 inches
  if (!obstacleScanRequested &&
      totalStepsTaken > 0 &&
      (totalStepsTaken - lastObstacleScanStep) >= SCAN_INTERVAL_STEPS) {
    sendObstacleScanRequest();
    obstacleScanRequested = true;
    waitingForScanComplete = true;
    stopMotors();
    return;
  }

  // Leg complete
  if (totalStepsTaken >= combinedStepsDesired) {
    stopMotors();
    sendTotalSteps(totalStepsTaken);
    RUN = 0;
  }
}

// =====================================================
// WAYPOINT PACKET
// =====================================================
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
  if (len != 21) {
    Serial.print("Waypoint length mismatch: ");
    Serial.println(len);
    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 = readUInt16LE(&data[18]);
  trackDir = data[20];

  desiredBearing100 = waypointDesiredBearing100;
  actualBearing100 = waypointActualBearing100;

  Serial.println("RX:WAYPOINT DATA received");
  Serial.print("pathTotalDistance = "); Serial.println(pathTotalDistance);
  Serial.print("combinedStepsDesired = "); Serial.println(combinedStepsDesired);
  Serial.print("oppositeDistance = "); Serial.println(oppositeDistance);
  Serial.print("bearingAlignment = "); Serial.println(bearingAlignment);
  Serial.print("reflectorBearing100 = "); Serial.println(reflectorBearing100);
  Serial.print("sensorPrime = "); Serial.println(sensorPrime);
  Serial.print("desiredBearing100 = "); Serial.println(desiredBearing100);
  Serial.print("actualBearing100 = "); Serial.println(actualBearing100);
  Serial.print("trackDir = "); Serial.println(trackDir);

  sendNavAck();
  beginForwardNavigation();
}

// =====================================================
// PACKET HANDLING
// =====================================================
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 21;
    default:                 return 0xFF;
  }
}

void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  switch (cmd) {
    case CMD_STOP_MOTORS:
      Serial.println("RX:STOP MOTORS");
      stopMotors();
      RUN = 0;
      waitingForScanComplete = false;
      sendTotalSteps(totalStepsTaken);
      break;

    case CMD_ACTUAL_BEARING:
      if (len == 2) {
        actualBearing100 = readUInt16LE(data);
        Serial.print("RX:ACTUAL BEARING x100 = ");
        Serial.println(actualBearing100);

        // Update bearing diff immediately when ARC sends new actual bearing
        bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;
      }
      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) {
    uint8_t b = (uint8_t)Serial3.read();
    processIncomingByte(b);
  }
}

// =====================================================
// SETUP / LOOP
// =====================================================
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);

  Serial.println("Arduino UART sketch ready");
}

void loop() {
  readIncomingUart();

  if (RUN == 0) {
    // pre-run mode
  } else if (RUN == 1) {
    runNavigationLoop();
  }
}

2) ARC JavaScript for UART_v1

This is a cleaned-up and aligned ARC script using UART skill commands.

Notes

  • Assumes the UART skill name is "UART_v1"
  • Uses UART.hardwareUartWrite(...) and UART.hardwareUartRead...
  • Only sends the packets you requested
  • Removed CMD_MISSED_IR and CMD_OBS_SCAN_COMPLETE
var UART_INDEX = 0;

var UART_HEADER = 0xA5;

// ARC -> Arduino
var CMD_STOP_MOTORS = 0x01;
var CMD_ACTUAL_BEARING = 0x03;
var CMD_RIGHT_PIVOT = 0x04;
var CMD_LEFT_PIVOT = 0x05;
var CMD_WAYPOINT_DATA = 0x06;

// Arduino -> ARC
var RSP_NAV_ACK = 0x10;
var RSP_TOTAL_STEPS = 0x11;
var RSP_DESIRED_BEARING = 0x12;
var RSP_OBS_SCAN_REQUEST = 0x13;

function safeInt(v, def) {
  var n = parseInt(v);
  if (isNaN(n)) return def;
  return n;
}

function safeFloat(v, def) {
  var n = parseFloat(v);
  if (isNaN(n)) return def;
  return n;
}

function u16ToBytes(value) {
  value = value & 0xFFFF;
  return [
    value & 0xFF,
    (value >> 8) & 0xFF
  ];
}

function u32ToBytes(value) {
  value = value >>> 0;
  return [
    value & 0xFF,
    (value >> 8) & 0xFF,
    (value >> 16) & 0xFF,
    (value >> 24) & 0xFF
  ];
}

function sendPacket(cmd, payload) {
  var packet = [UART_HEADER, cmd].concat(payload);
  UART.hardwareUartWrite(UART_INDEX, packet);
}

function sendStopMotors() {
  sendPacket(CMD_STOP_MOTORS, []);
  print("TX:STOP MOTORS");
}

function sendActualBearing() {
  var bearing = safeFloat(getVar("$actualBearing", -1), -1);
  if (bearing < 0) return;

  var bearing100 = Math.round(bearing * 100.0);
  sendPacket(CMD_ACTUAL_BEARING, u16ToBytes(bearing100));
  print("TX:ACTUAL BEARING = " + bearing.toFixed(2));
}

function sendRightPivot() {
  var steps = safeInt(getVar("$rightPivotSteps", 0), 0);
  sendPacket(CMD_RIGHT_PIVOT, u16ToBytes(steps));
  print("TX:RIGHT PIVOT = " + steps);
}

function sendLeftPivot() {
  var steps = safeInt(getVar("$leftPivotSteps", 0), 0);
  sendPacket(CMD_LEFT_PIVOT, u16ToBytes(steps));
  print("TX:LEFT PIVOT = " + steps);
}

function sendWaypointData() {
  var pathTotalDistance = safeInt(getVar("$pathTotalDistance", 0), 0);
  var combinedStepsDesired = safeInt(getVar("$combinedStepsDesired", 0), 0);
  var oppositeDistance = safeInt(getVar("$oppositeDistance", 0), 0);
  var bearingAlignment = safeInt(getVar("$bearingAlignment", 0), 0);
  var reflectorBearing100 = safeInt(getVar("$reflectorBearing100", 0), 0);
  var sensorPrime = safeInt(getVar("$sensorPrime", 1), 1);
  var desiredBearing100 = safeInt(getVar("$desiredBearing100", 9000), 9000);
  var actualBearing100 = safeInt(getVar("$actualBearing100", 9000), 9000);
  var trackDir = safeInt(getVar("$trackDir", 0), 0);

  var payload = [];
  payload = payload.concat(u32ToBytes(pathTotalDistance));
  payload = payload.concat(u32ToBytes(combinedStepsDesired));
  payload = payload.concat(u32ToBytes(oppositeDistance));
  payload.push(bearingAlignment & 0xFF);
  payload = payload.concat(u16ToBytes(reflectorBearing100));
  payload.push(sensorPrime & 0xFF);
  payload = payload.concat(u16ToBytes(desiredBearing100));
  payload = payload.concat(u16ToBytes(actualBearing100));
  payload.push(trackDir & 0xFF);

  sendPacket(CMD_WAYPOINT_DATA, payload);
  print("TX:WAYPOINT DATA");
}

function processIncomingPacket() {
  while (UART.hardwareUartAvailable(UART_INDEX) >= 2) {
    var header = UART.hardwareUartRead(UART_INDEX, 1)[0];
    if (header != UART_HEADER) {
      continue;
    }

    var cmd = UART.hardwareUartRead(UART_INDEX, 1)[0];

    if (cmd == RSP_NAV_ACK) {
      print("RX:NAV ACK");
      setVar("$run", 1);
      continue;
    }

    if (cmd == RSP_TOTAL_STEPS) {
      while (UART.hardwareUartAvailable(UART_INDEX) < 4) {
        sleep(1);
      }
      var data = UART.hardwareUartRead(UART_INDEX, 4);
      var steps = (data[0]) |
                  (data[1] << 8) |
                  (data[2] << 16) |
                  (data[3] << 24);
      print("RX:TOTAL STEPS = " + steps);
      setVar("$totalStepsTaken", steps);
      continue;
    }

    if (cmd == RSP_DESIRED_BEARING) {
      while (UART.hardwareUartAvailable(UART_INDEX) < 2) {
        sleep(1);
      }
      var d = UART.hardwareUartRead(UART_INDEX, 2);
      var bearing100 = d[0] | (d[1] << 8);
      print("RX:DESIRED BEARING = " + (bearing100 / 100.0).toFixed(2));
      setVar("$desiredBearing100", bearing100);
      continue;
    }

    if (cmd == RSP_OBS_SCAN_REQUEST) {
      print("RX:OBSTACLE SCAN REQUEST");
      setVar("$obsScan", 1);
      continue;
    }

    print("RX:UNKNOWN CMD = " + cmd);
  }
}

while (true) {
  if (getVar("$sendStopNow", 0) == 1) {
    sendStopMotors();
    setVar("$sendStopNow", 0);
  }

  if (getVar("$sendActualBearingNow", 0) == 1) {
    sendActualBearing();
    setVar("$sendActualBearingNow", 0);
  }

  if (getVar("$sendRightPivotNow", 0) == 1) {
    sendRightPivot();
    setVar("$sendRightPivotNow", 0);
  }

  if (getVar("$sendLeftPivotNow", 0) == 1) {
    sendLeftPivot();
    setVar("$sendLeftPivotNow", 0);
  }

  if (getVar("$sendWaypointNow", 0) == 1) {
    sendWaypointData();
    setVar("$sendWaypointNow", 0);
  }

  processIncomingPacket();
  sleep(200);
}

3) A couple of important corrections

Obstacle scan behavior

You asked for:

  • scan request sent
  • PWM returns to neutral
  • rover continues to move

Given your current workflow, I treated this as:

  • Arduino sends scan request
  • Arduino neutralizes motion
  • ARC scan script runs
  • ARC decides whether to stop/avoid or continue
  • ARC then sends the next Actual Bearing and/or Stop Motors

If you truly want the rover to keep moving during the scan, then the Arduino should not force stopMotors() on scan request. In that case, you would instead:

  • keep current speed
  • temporarily set bearingDiffRamped = 0
  • hold neutral differential only
  • continue forward until ARC sends stop or updated bearing

If you want, I can give you that alternate version too.

Step count

Your formula:

combinedStepsTaken = abs(LeftFrontWheel.currentPosition()) + abs(LeftBackWheel.currentPosition()) + abs(RightFrontWheel.currentPosition()) + abs(RightBackWheel.currentPosition());

is fine as a raw aggregate pulse count, but for leg completion I recommend using:

totalStepsTaken = commandStepsTaken / 4;

because each wheel contributes to the same traveled distance.


4) Recommendation for the ARC-side scripts

Since you already have:

  • ROVER_CMD_v1
  • LEG_v1
  • IR_GEN_v1
  • UART_v1
  • IR_TRACKER_v1

the cleanest flow is:

  1. LEG_v1 computes waypoint values
  2. UART_v1 sends CMD_WAYPOINT_DATA
  3. Arduino replies NAV ACK
  4. ARC sets $run = 1
  5. IR_TRACKER_v1 keeps sending CMD_ACTUAL_BEARING
  6. Arduino updates desired bearing and steering
  7. On scan request, ARC runs the scan profile and decides next action

5) One thing I’d like to confirm

Before you upload this, can you confirm:

  • Are your DRV8825 step pins wired exactly as:
    • LF: step 3, dir 6
    • LB: step 2, dir 5
    • RF: step 12, dir 13
    • RB: step 4, dir 7
  • Do you want the obstacle scan request to:
    • pause motion
    • or keep moving with neutral differential

If you want, I can do the next pass and give you:

  • a non-blocking pivot version
  • a cleaner packet state machine
  • and a matching ARC ROVER_CMD_v1 / LEG_v1 orchestration script

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.
#4  

@Athena. I want the rover to keep moving during the scan:

  • keep current speed
  • temporarily set bearingDiffRamped = 0
  • hold neutral differential only
  • continue forward until ARC sends stop or updated bearing

Please write the Arduino to keep moving during the scan.

STEP COUNT:

totalStepsTaken = commandStepsTaken / 4;

Please write totalStepsTaken = commandStepsTaken/4

Please implement your recommendation for ARC side scripts: the cleanest flow is:

  1. LEG_v1 computes waypoint values
  2. UART_v1 sends CMD_WAYPOINT_DATA
  3. Arduino replies NAV ACK
  4. ARC sets $run = 1
  5. IR_TRACKER_v1 keeps sending CMD_ACTUAL_BEARING
  6. Arduino updates desired bearing and steering
  7. On scan request, ARC runs the scan profile and decides next action
  • Are your DRV8825 step pins wired exactly as:
    • LF: step 3, dir 6
    • LB: step 2, dir 5
    • RF: step 12, dir 13
    • RB: step 4, dir 7

Yes, they are wired as noted. For a final Arduino sketch and ARC script, could you please make the following changes to the Waypoint Packet?

remove Reflector Bearing. remove Sensor Prime. These variables will be kept in ARC and not sent in the Waypoint Packet. What do you mean with an orchestration script?

  • and a matching ARC ROVER_CMD_v1 / LEG_v1 orchestration script

I'll probably want it. Thanks. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#5  

Absolutely - that makes the motion model much cleaner.

Since you want the rover to keep moving during the scan, the Arduino should:

  • keep current speed
  • set bearingDiffRamped = 0 only while the scan is active
  • continue forward motion
  • resume steering corrections as soon as ARC sends a new CMD_ACTUAL_BEARING

And yes, removing Reflector Bearing and Sensor Prime from the waypoint packet is the right move if ARC is already maintaining those values.

What I mean by "orchestration script"

An orchestration script is just the top-level coordinator that decides which ARC robot skill runs next and when.

In your case, that would be ROVER_CMD_v1, which might:

  1. tell LEG_v1 to load the next waypoint
  2. tell UART_v1 to send the waypoint packet
  3. wait for NAV ACK
  4. set $run = 1
  5. let IR_TRACKER_v1 keep sending bearing updates
  6. react to scan requests or stop events

So it’s not a special ARC feature - just the script that orchestrates the workflow.


Updated Arduino Sketch

This version:

  • keeps moving during scan requests
  • neutralizes only the differential during scan
  • removes Reflector Bearing and Sensor Prime from the waypoint packet
  • uses totalStepsTaken = commandStepsTaken / 4;
  • keeps the packet format aligned with your updated plan
#include <AccelStepper.h>
#include <MultiStepper.h>

// =====================================================
// PACKETS
// =====================================================
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;

// =====================================================
// STEPPERS
// =====================================================
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_PAYLOAD };
ParserState state = WAIT_HEADER;

uint8_t currentCmd = 0;
uint8_t payload[32];
uint8_t payloadIndex = 0;
uint8_t expectedLength = 0;

// =====================================================
// NAV STATE
// =====================================================
int RUN = 0; // 0 = pre-run, 1 = forward navigation

uint32_t totalStepsTaken = 0;
uint32_t commandStepsTaken = 0;

uint32_t combinedStepsDesired = 0;
uint32_t pathTotalDistance = 0;
uint32_t oppositeDistance = 0;

uint8_t bearingAlignment = 0; // 0 = DA, 1 = OA
uint8_t trackDir = 0;          // 0 = FD, 1 = RD

uint16_t waypointDesiredBearing100 = 9000;
uint16_t waypointActualBearing100 = 0;

uint16_t actualBearing100 = 0;
uint16_t desiredBearing100 = 9000;
uint16_t lastSentDesiredBearing100 = 0xFFFF;

int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;

bool obstacleScanRequested = false;
uint32_t lastObstacleScanStep = 0;
bool scanModeActive = false;

uint16_t currentSpeed = 0;
uint16_t targetSpeed = 900;
uint16_t rampIncrement = 10;
int16_t correctionScale = 10;

const uint32_t SCAN_INTERVAL_STEPS = (uint32_t)(24.0 * 437.5);

// =====================================================
// LITTLE-ENDIAN 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 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);
  Serial.print("TX:DESIRED BEARING x100 = ");
  Serial.println(bearing100);
}

void sendObstacleScanRequest() {
  sendHeaderAndCmd(RSP_OBS_SCAN_REQUEST);
  Serial.println("TX:OBSTACLE SCAN REQUEST");
}

// =====================================================
// MOTOR CONTROL
// =====================================================
void applyMotorSpeeds(int leftSpeed, int rightSpeed) {
  if (leftSpeed < 0) leftSpeed = 0;
  if (rightSpeed < 0) rightSpeed = 0;

  LeftFrontWheel.setSpeed(leftSpeed);
  LeftBackWheel.setSpeed(leftSpeed);
  RightFrontWheel.setSpeed(rightSpeed);
  RightBackWheel.setSpeed(rightSpeed);
}

void stopMotors() {
  LeftFrontWheel.setSpeed(0);
  LeftBackWheel.setSpeed(0);
  RightFrontWheel.setSpeed(0);
  RightBackWheel.setSpeed(0);
  Serial.println("Action: stopMotors()");
}

void pivotRight(uint16_t steps) {
  Serial.print("Action: pivotRight(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot
  }

  commandStepsTaken += (uint32_t)steps * 4UL;
  totalStepsTaken = commandStepsTaken / 4UL;

  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void pivotLeft(uint16_t steps) {
  Serial.print("Action: pivotLeft(");
  Serial.print(steps);
  Serial.println(")");

  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()) {
    // blocking pivot
  }

  commandStepsTaken += (uint32_t)steps * 4UL;
  totalStepsTaken = commandStepsTaken / 4UL;

  stopMotors();
  sendTotalSteps(totalStepsTaken);
}

void beginForwardNavigation() {
  Serial.println("Action: beginForwardNavigation()");
  RUN = 1;
  currentSpeed = 0;
  bearingDiffRamped = 0;
  bearingDiffTarget = 0;
  scanModeActive = false;
  obstacleScanRequested = false;
  lastObstacleScanStep = 0;
}

// =====================================================
// BEARING LOGIC
// =====================================================
void computeDesiredBearing() {
  if (bearingAlignment == 0) {
    desiredBearing100 = 9000;
    return;
  }

  uint32_t pathDistanceRemaining = 1;

  if (trackDir == 0) {
    if (pathTotalDistance > totalStepsTaken) {
      pathDistanceRemaining = pathTotalDistance - totalStepsTaken;
    }
  } else {
    if (pathTotalDistance > (combinedStepsDesired - totalStepsTaken)) {
      pathDistanceRemaining = pathTotalDistance - (combinedStepsDesired - totalStepsTaken);
    }
  }

  float tangent = (float)oppositeDistance / (float)pathDistanceRemaining;
  float deg = atan(tangent) * 180.0 / PI;
  desiredBearing100 = (uint16_t)((deg + 90.0) * 100.0);

  if (abs((int32_t)desiredBearing100 - (int32_t)lastSentDesiredBearing100) >= 100) {
    sendDesiredBearing(desiredBearing100);
    lastSentDesiredBearing100 = 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 updateMotion() {
  if (currentSpeed < targetSpeed) {
    currentSpeed += rampIncrement;
    if (currentSpeed > targetSpeed) currentSpeed = targetSpeed;
  }

  if (scanModeActive) {
    bearingDiffRamped = 0;
  } else {
    computeDesiredBearing();
    updateBearingDiff();
  }

  int correction = bearingDiffRamped / correctionScale;

  int leftSpeed = (int)currentSpeed - correction;
  int rightSpeed = (int)currentSpeed + correction;

  applyMotorSpeeds(leftSpeed, rightSpeed);

  LeftFrontWheel.runSpeed();
  LeftBackWheel.runSpeed();
  RightFrontWheel.runSpeed();
  RightBackWheel.runSpeed();

  commandStepsTaken =
    abs(LeftFrontWheel.currentPosition()) +
    abs(LeftBackWheel.currentPosition()) +
    abs(RightFrontWheel.currentPosition()) +
    abs(RightBackWheel.currentPosition());

  totalStepsTaken = commandStepsTaken / 4UL;

  if (!obstacleScanRequested && totalStepsTaken > 0 && (totalStepsTaken - lastObstacleScanStep) >= SCAN_INTERVAL_STEPS) {
    obstacleScanRequested = true;
    scanModeActive = true;
    sendObstacleScanRequest();
  }

  if (totalStepsTaken >= combinedStepsDesired) {
    stopMotors();
    sendTotalSteps(totalStepsTaken);
    RUN = 0;
  }
}

// =====================================================
// WAYPOINT PACKET
// Format now removes:
//   - Reflector Bearing
//   - Sensor Prime
// =====================================================
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
  // New length:
  // pathTotalDistance 4
  // combinedStepsDesired 4
  // oppositeDistance 4
  // bearingAlignment 1
  // desiredBearing100 2
  // actualBearing100 2
  // trackDir 1
  // total = 18 bytes
  if (len != 18) {
    Serial.print("Waypoint length mismatch: ");
    Serial.println(len);
    return;
  }

  pathTotalDistance = readUInt32LE(&data[0]);
  combinedStepsDesired = readUInt32LE(&data[4]);
  oppositeDistance = readUInt32LE(&data[8]);
  bearingAlignment = data[12];
  waypointDesiredBearing100 = readUInt16LE(&data[13]);
  waypointActualBearing100 = readUInt16LE(&data[15]);
  trackDir = data[17];

  desiredBearing100 = waypointDesiredBearing100;
  actualBearing100 = waypointActualBearing100;

  Serial.println("RX:WAYPOINT DATA received");
  Serial.print("pathTotalDistance = "); Serial.println(pathTotalDistance);
  Serial.print("combinedStepsDesired = "); Serial.println(combinedStepsDesired);
  Serial.print("oppositeDistance = "); Serial.println(oppositeDistance);
  Serial.print("bearingAlignment = "); Serial.println(bearingAlignment);
  Serial.print("desiredBearing100 = "); Serial.println(desiredBearing100);
  Serial.print("actualBearing100 = "); Serial.println(actualBearing100);
  Serial.print("trackDir = "); Serial.println(trackDir);

  sendNavAck();
  beginForwardNavigation();
}

// =====================================================
// PACKET HANDLING
// =====================================================
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 handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  switch (cmd) {
    case CMD_STOP_MOTORS:
      Serial.println("RX:STOP MOTORS");
      stopMotors();
      RUN = 0;
      scanModeActive = false;
      obstacleScanRequested = false;
      sendTotalSteps(totalStepsTaken);
      break;

    case CMD_ACTUAL_BEARING:
      if (len == 2) {
        actualBearing100 = readUInt16LE(data);
        Serial.print("RX:ACTUAL BEARING x100 = ");
        Serial.println(actualBearing100);

        // Update immediately when ARC sends actual bearing
        bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;

        // Resume steering if scan mode was active
        scanModeActive = false;
      }
      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) {
    uint8_t b = (uint8_t)Serial3.read();
    processIncomingByte(b);
  }
}

// =====================================================
// SETUP / LOOP
// =====================================================
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);

  Serial.println("Arduino UART sketch ready");
}

void loop() {
  readIncomingUart();

  if (RUN == 0) {
    // pre-run mode
  } else if (RUN == 1) {
    updateMotion();
  }
}

4) ARC JavaScript: cleaned and matched to the new packet format

This version:

  • removes CMD_MISSED_IR
  • removes CMD_OBS_SCAN_COMPLETE
  • updates waypoint packet to 18 bytes
  • includes NAV ACK, TOTAL STEPS, DESIRED BEARING, OBS SCAN REQUEST
  • sets up a simple orchestration flow with ROVER_CMD_v1
var UART_INDEX = 0;
var UART_HEADER = 0xA5;

// ARC -> Arduino
var CMD_STOP_MOTORS = 0x01;
var CMD_ACTUAL_BEARING = 0x03;
var CMD_RIGHT_PIVOT = 0x04;
var CMD_LEFT_PIVOT = 0x05;
var CMD_WAYPOINT_DATA = 0x06;

// Arduino -> ARC
var RSP_NAV_ACK = 0x10;
var RSP_TOTAL_STEPS = 0x11;
var RSP_DESIRED_BEARING = 0x12;
var RSP_OBS_SCAN_REQUEST = 0x13;

function safeInt(v, def) {
  var n = parseInt(v);
  if (isNaN(n)) return def;
  return n;
}

function safeFloat(v, def) {
  var n = parseFloat(v);
  if (isNaN(n)) return def;
  return n;
}

function u16ToBytes(value) {
  value = value & 0xFFFF;
  return [
    value & 0xFF,
    (value >> 8) & 0xFF
  ];
}

function u32ToBytes(value) {
  value = value >>> 0;
  return [
    value & 0xFF,
    (value >> 8) & 0xFF,
    (value >> 16) & 0xFF,
    (value >> 24) & 0xFF
  ];
}

function sendPacket(cmd, payload) {
  var packet = [UART_HEADER, cmd].concat(payload);
  UART.hardwareUartWrite(UART_INDEX, packet);
}

function sendStopMotors() {
  sendPacket(CMD_STOP_MOTORS, []);
  print("TX:STOP MOTORS");
}

function sendActualBearing() {
  var bearing = safeFloat(getVar("$actualBearing", -1), -1);
  if (bearing < 0) return;

  var bearing100 = Math.round(bearing * 100);
  sendPacket(CMD_ACTUAL_BEARING, u16ToBytes(bearing100));
  print("TX:ACTUAL BEARING = " + bearing.toFixed(2));
}

function sendRightPivot() {
  var steps = safeInt(getVar("$rightPivotSteps", 0), 0);
  sendPacket(CMD_RIGHT_PIVOT, u16ToBytes(steps));
  print("TX:RIGHT PIVOT = " + steps);
}

function sendLeftPivot() {
  var steps = safeInt(getVar("$leftPivotSteps", 0), 0);
  sendPacket(CMD_LEFT_PIVOT, u16ToBytes(steps));
  print("TX:LEFT PIVOT = " + steps);
}

function sendWaypointData() {
  var pathTotalDistance = safeInt(getVar("$pathTotalDistance", 0), 0);
  var combinedStepsDesired = safeInt(getVar("$combinedStepsDesired", 0), 0);
  var oppositeDistance = safeInt(getVar("$oppositeDistance", 0), 0);
  var bearingAlignment = safeInt(getVar("$bearingAlignment", 0), 0);
  var desiredBearing100 = safeInt(getVar("$desiredBearing100", 9000), 9000);
  var actualBearing100 = safeInt(getVar("$actualBearing100", 9000), 9000);
  var trackDir = safeInt(getVar("$trackDir", 0), 0);

  var payload = [];
  payload = payload.concat(u32ToBytes(pathTotalDistance));
  payload = payload.concat(u32ToBytes(combinedStepsDesired));
  payload = payload.concat(u32ToBytes(oppositeDistance));
  payload.push(bearingAlignment & 0xFF);
  payload = payload.concat(u16ToBytes(desiredBearing100));
  payload = payload.concat(u16ToBytes(actualBearing100));
  payload.push(trackDir & 0xFF);

  sendPacket(CMD_WAYPOINT_DATA, payload);
  print("TX:WAYPOINT DATA");
}

function readIncomingPacket() {
  if (UART.hardwareUartAvailable(UART_INDEX) < 2) return;

  var header = UART.hardwareUartRead(UART_INDEX, 1)[0];
  if (header != UART_HEADER) return;

  var cmd = UART.hardwareUartRead(UART_INDEX, 1)[0];

  if (cmd == RSP_NAV_ACK) {
    print("RX:NAV ACK");
    setVar("$run", 1);
    return;
  }

  if (cmd == RSP_TOTAL_STEPS) {
    while (UART.hardwareUartAvailable(UART_INDEX) < 4) sleep(1);
    var data = UART.hardwareUartRead(UART_INDEX, 4);
    var steps = (data[0]) | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);
    if (steps < 0) steps = steps >>> 0;
    print("RX:TOTAL STEPS = " + steps);
    setVar("$totalStepsTaken", steps);
    return;
  }

  if (cmd == RSP_DESIRED_BEARING) {
    while (UART.hardwareUartAvailable(UART_INDEX) < 2) sleep(1);
    var d = UART.hardwareUartRead(UART_INDEX, 2);
    var bearing100 = d[0] | (d[1] << 8);
    print("RX:DESIRED BEARING = " + (bearing100 / 100.0).toFixed(2));
    setVar("$desiredBearing100", bearing100);
    return;
  }

  if (cmd == RSP_OBS_SCAN_REQUEST) {
    print("RX:OBSTACLE SCAN REQUEST");
    setVar("$obsScan", 1);
    return;
  }

  print("RX:UNKNOWN CMD = " + cmd);
}

5) Suggested ROVER_CMD_v1 orchestration script

This is the coordinator concept you asked about. It assumes your other scripts use global variables like $run, $obsScan, $sendWaypointNow, etc.

print("ROVER_CMD_v1 started");

setVar("$run", 0);
setVar("$obsScan", 0);

controlCommand("LEG_v1", "ScriptStartWait");
controlCommand("UART_v1", "ScriptStart");

setVar("$sendWaypointNow", 1);

while (getVar("$run", 0) == 0) {
  sleep(50);
}

print("Run mode active");

while (true) {
  if (getVar("$obsScan", 0) == 1) {
    print("Scan requested - running scan profile");

    controlCommand("IR_GEN_v1", "ScriptStart");
    // your scan routine can set variables and/or decide next move

    setVar("$obsScan", 0);
  }

  if (getVar("$legComplete", 0) == 1) {
    print("Leg complete");
    break;
  }

  sleep(50);
}

6) What I still need from you

Before I refine this into a final production-ready pass, I’d like to confirm one thing:

Do you want UART_v1 to:

  1. just send packets on trigger variables and read responses in a loop, or
  2. run continuously as a background listener while other scripts trigger sends?

Your earlier examples suggest option 2 is what you want, but I can format it either way.

If you want, I can next give you:

  • a clean ARC UART_v1 background listener
  • a matching LEG_v1 waypoint builder
  • and a ROVER_CMD_v1 controller that ties the whole rover workflow together neatly.

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.
#6  

@Athena.

UART_v1:

I thought UART_v1 would run in a loop and send or receive when triggered by incoming packet or global variable changes. I think this is option (1). I'm not sure how option(2) would work. Can you explain the difference between option 1 and 2?

  1. just send packets on trigger variables and read responses in a loop, or
  2. run continuously as a background listener while other scripts trigger sends?

Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#7  

Yes - the distinction is mostly who owns the loop.

Option 1: UART_v1 runs in a loop and acts on trigger variables

This is the cleaner fit for ARC when you want one script to handle both transmit and receive.

How it works

  • UART_v1 is started once.
  • It runs a while (true) loop.
  • Each pass:
    • checks global variables like $sendWaypointNow, $sendStopNow, $sendActualBearingNow
    • sends packets when those flags are set
    • reads any incoming UART bytes and processes packets
  • Other scripts do not talk directly to the UART hardware; they just set globals.

Example flow

  • LEG_v1 sets waypoint globals
  • ROVER_CMD_v1 sets $sendWaypointNow = 1
  • UART_v1 notices it, sends CMD_WAYPOINT_DATA
  • Arduino replies NAV ACK
  • UART_v1 reads that and sets $run = 1

Pros

  • Simple
  • Easier to debug
  • One place owns UART traffic
  • Works well with packet-based robot coordination

Cons

  • You need to keep the loop responsive
  • It should sleep briefly each pass so it does not hog CPU

Option 2: UART_v1 is a background listener, while other scripts trigger sends independently

This means:

  • one script is dedicated to receiving and parsing UART all the time
  • other scripts may also send UART packets directly when needed

How it works

  • A receiver loop is always running
  • Another script might independently call UART write functions to send commands
  • So send and receive responsibilities are split across scripts

Pros

  • Can be useful if you have a lot of asynchronous traffic
  • Lets multiple scripts initiate UART sends

Cons

  • Harder to manage
  • Higher risk of packet collisions or interleaving
  • More difficult to debug
  • Usually unnecessary unless you have a complex communication design

Recommendation for your project

For your rover, Option 1 is the better choice.

Why:

  • Your packet system is structured and command-driven
  • You already have a top-level orchestration script
  • UART traffic is centralized and predictable
  • ARC scripts can set flags and let UART_v1 handle transmission/response parsing

So the likely architecture is:

  • ROVER_CMD_v1 = coordinator
  • LEG_v1 = builds waypoint data
  • IR_TRACKER_v1 = updates actual bearing globals
  • IR_GEN_v1 = obstacle detection logic
  • UART_v1 = single loop handling all UART sends/receives

Practical implication

UART_v1 should:

  • start once
  • loop forever
  • check trigger globals
  • send packets when requested
  • read incoming bytes every pass
  • update ARC globals from received packets

That is the pattern I’d use here.

If you want, I can now write the final ARC UART_v1 script in this style, plus the matching ROVER_CMD_v1 and LEG_v1 scripts so the whole flow is consistent.


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.
#8  

@Athena.

UART_v1 should:

  • start once
  • loop forever
  • check trigger globals
  • send packets when requested
  • read incoming bytes every pass
  • update ARC globals from received packets

Please write the final ARC UART_v1 script in this style, plus the matching ROVER_CMD_v1 and LEG_v1 scripts so the whole flow is consistent.

Thank you. Jack

 
📚 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.