Serial Toolbox icon Serial Toolbox EZB UART parser and Serial-TCP bridge to connect EZ-Robot cameras via Teensy USB, enabling ARC camera control and USB-to-serial bridging. Try it →
Asked — Edited
Resolved Resolved by Athena!

Pivot Stepper Sketch Help

@Athena,

I'm using the Arduino Mega sketch below to control four stepper motors via AccelStepper / MultiStepper. The Arduino is connected to an EZB4 over UART (Serial3). The EZB4 is running JavaScript uploaded from ARC and is sending CMD_RIGHT_PIVOT and CMD_LEFT_PIVOT packets with a STEPS value over UART to the Arduino.

Can you rewrite the sketch to implement the following behavior?

  • When receiving a pivot command (CMD_RIGHT_PIVOT or CMD_LEFT_PIVOT), pivot the appropriate side so the robot rotates in that direction:
    • CMD_RIGHT_PIVOT: drive the left motors (both left wheels) forward and drive the right motors in reverse so the robot pivots to the right.
    • CMD_LEFT_PIVOT: drive the right motors (both right wheels) forward and drive the left motors in reverse so the robot pivots to the left.
  • Count the STEPS value received in the pivot packet and stop the motors (call stopMotors()) once the pivot STEPS count is reached.
    • The current code checks pivot packet length == 2, so I expect the STEPS payload is a 2-byte little-endian value. Please use that same format unless you indicate otherwise.

For context: the Arduino sketch handles navigation and other packets (waypoints, bearing updates, etc.). The EZB4 is acting as the UART bridge between ARC (Robot Skills / JavaScript on the EZB) and this Arduino Mega sketch. The pivot commands should only act when not already in forward navigation (navRunning == false), consistent with the current sketch.

Below is the current sketch I'm using. Please rewrite it to add the pivot behavior described above.

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

static const uint8_t UART_HEADER = 0xA5;

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

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

static const uint8_t MAX_PAYLOAD = 32;

// -------------------------
// Motor setup
// -------------------------
AccelStepper LeftFrontWheel(AccelStepper::DRIVER, 3, 6);
AccelStepper LeftBackWheel(AccelStepper::DRIVER, 2, 5);
AccelStepper RightFrontWheel(AccelStepper::DRIVER, 12, 13);
AccelStepper RightBackWheel(AccelStepper::DRIVER, 4, 7);

MultiStepper multiStepper;

// -------------------------
// Parser
// -------------------------
enum ParserState {
  WAIT_HEADER,
  WAIT_CMD,
  WAIT_LEN,
  WAIT_PAYLOAD,
  WAIT_CHECKSUM
};

ParserState parserState = WAIT_HEADER;
uint8_t currentCmd = 0;
uint8_t expectedLen = 0;
uint8_t payloadIndex = 0;
uint8_t payload[MAX_PAYLOAD];
uint8_t runningChecksum = 0;

// -------------------------
// Navigation state
// -------------------------
bool navRunning = false;

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

uint32_t nextScanStep = 0;
uint32_t scanIntervalSteps = 3056;

uint8_t bearingAlignment = 0;
uint8_t trackDir = 0;

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

int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;

uint16_t currentSpeed = 0;
uint16_t normalSpeed = 600; //800

// -------------------------
// Helpers
// -------------------------
uint16_t readUInt16LE(const uint8_t* data) {
  return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

uint32_t readUInt32LE(const uint8_t* data) {
  return (uint32_t)data[0]
       | ((uint32_t)data[1] << 8)
       | ((uint32_t)data[2] << 16)
       | ((uint32_t)data[3] << 24);
}

void sendPacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  uint8_t checksum = (uint8_t)(cmd ^ len);
  Serial3.write(UART_HEADER);
  Serial3.write(cmd);
  Serial3.write(len);
  for (uint8_t i = 0; i < len; i++) {
    Serial3.write(data[i]);
    checksum ^= data[i];
  }
  Serial3.write(checksum);
}

void sendCmdOnly(uint8_t cmd) {
  sendPacket(cmd, nullptr, 0);
}

void sendNavAck() {
  sendCmdOnly(RSP_NAV_ACK);
  Serial.println(F("TX: NAV ACK"));
}

void sendTotalSteps(uint32_t steps) {
  uint8_t tmp[4];
  tmp[0] = (uint8_t)(steps & 0xFF);
  tmp[1] = (uint8_t)((steps >> 8) & 0xFF);
  tmp[2] = (uint8_t)((steps >> 16) & 0xFF);
  tmp[3] = (uint8_t)((steps >> 24) & 0xFF);
  sendPacket(RSP_TOTAL_STEPS, tmp, 4);
  Serial.print(F("TX: TOTAL STEPS = "));
  Serial.println(steps);
}

void sendDesiredBearing(uint16_t bearing100) {
  uint8_t tmp[2];
  tmp[0] = (uint8_t)(bearing100 & 0xFF);
  tmp[1] = (uint8_t)((bearing100 >> 8) & 0xFF);
  sendPacket(RSP_DESIRED_BEARING, tmp, 2);
  lastSentDesiredBearing100 = bearing100;
  Serial.print(F("TX: DESIRED BEARING x100 = "));
  Serial.println(bearing100);
}

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

void resetStepperPositions() {
  LeftFrontWheel.setCurrentPosition(0);
  LeftBackWheel.setCurrentPosition(0);
  RightFrontWheel.setCurrentPosition(0);
  RightBackWheel.setCurrentPosition(0);
}

uint32_t getAverageAbsStepperPosition() {
  uint32_t a = (uint32_t)labs(LeftFrontWheel.currentPosition());
  uint32_t b = (uint32_t)labs(LeftBackWheel.currentPosition());
  uint32_t c = (uint32_t)labs(RightFrontWheel.currentPosition());
  uint32_t d = (uint32_t)labs(RightBackWheel.currentPosition());
  return (a + b + c + d) / 4;
}

void setAllMotorSpeeds(int leftSpeed, int rightSpeed) {
  leftSpeed = constrain(leftSpeed, 0, 2000);
  rightSpeed = constrain(rightSpeed, 0, 2000);
  LeftFrontWheel.setSpeed(leftSpeed);
  LeftBackWheel.setSpeed(leftSpeed);
  RightFrontWheel.setSpeed(rightSpeed);
  RightBackWheel.setSpeed(rightSpeed);
}

void stopMotors() {
  LeftFrontWheel.stop();
  LeftBackWheel.stop();
  RightFrontWheel.stop();
  RightBackWheel.stop();

  LeftFrontWheel.setSpeed(0);
  LeftBackWheel.setSpeed(0);
  RightFrontWheel.setSpeed(0);
  RightBackWheel.setSpeed(0);

  Serial.println(F("Action: stopMotors()"));
}

void resetNavigationState() {
  resetStepperPositions();
  totalStepsTaken = 0;
  desiredBearing100 = 9000;
  actualBearing100 = 9000;
  lastSentDesiredBearing100 = 0xFFFF;
  bearingDiffTarget = 0;
  bearingDiffRamped = 0;
  nextScanStep = scanIntervalSteps;
  currentSpeed = 0;
  navRunning = false;
  Serial.println(F("Navigation state reset"));
}

void beginForwardNavigation() {
  navRunning = true;
  currentSpeed = 0;
  bearingDiffRamped = 0;
  nextScanStep = scanIntervalSteps;
  Serial.println(F("Navigation started"));
}

// -------------------------
// Navigation math
// -------------------------
void computeDesiredBearing() {
  if (bearingAlignment == 0) {
    return;
  }

  uint32_t pathDistanceRemaining = 1;
  if (trackDir == 0) {
    if (pathTotalDistance > totalStepsTaken) {
      pathDistanceRemaining = pathTotalDistance - totalStepsTaken;
    }
  } else {
    uint32_t remainingSteps = (combinedStepsDesired > totalStepsTaken) ? (combinedStepsDesired - totalStepsTaken) : 0;
    if (pathTotalDistance > remainingSteps) {
      pathDistanceRemaining = pathTotalDistance - remainingSteps;
    }
  }

  if (pathDistanceRemaining == 0) {
    pathDistanceRemaining = 1;
  }

  bool reflectorIsLeft = (desiredBearing100 >= 9100);
  int32_t signedOppositeDistance = (int32_t)oppositeDistance;
  if (reflectorIsLeft) {
    signedOppositeDistance = -labs(signedOppositeDistance);
  } else {
    signedOppositeDistance = labs(signedOppositeDistance);
  }

  float angleDeg = atan2((float)signedOppositeDistance, (float)pathDistanceRemaining) * 180.0f / PI;
  int32_t bearing100 = (int32_t)((90.0f + angleDeg) * 100.0f);
  if (bearing100 < 0) bearing100 = 0;
  if (bearing100 > 18000) bearing100 = 18000;

  desiredBearing100 = (uint16_t)bearing100;
  if (abs((int)desiredBearing100 - (int)lastSentDesiredBearing100) >= 100) {
    sendDesiredBearing(desiredBearing100);
  }
}

void updateBearingDiff() {
  bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;
  if (bearingDiffRamped < bearingDiffTarget) {
    bearingDiffRamped += 10;
    if (bearingDiffRamped > bearingDiffTarget) {
      bearingDiffRamped = bearingDiffTarget;
    }
  } else if (bearingDiffRamped > bearingDiffTarget) {
    bearingDiffRamped -= 10;
    if (bearingDiffRamped < bearingDiffTarget) {
      bearingDiffRamped = bearingDiffTarget;
    }
  }
}

void applyForwardMotion() {
  int leftSpeed = normalSpeed;
  int rightSpeed = normalSpeed;

  int absDiff = abs(bearingDiffRamped);
  if (bearingDiffRamped > 0) {
    leftSpeed = normalSpeed - (absDiff * 2);
    rightSpeed = normalSpeed + (absDiff * 2);
  } else if (bearingDiffRamped < 0) {
    leftSpeed = normalSpeed + (absDiff * 2);
    rightSpeed = normalSpeed - (absDiff * 2);
  }

  setAllMotorSpeeds(leftSpeed, rightSpeed);

  // Serial.print("leftSpeed:  ");
  // Serial.println(leftSpeed);
  // Serial.print("rightSpeed:  ");
  // Serial.println(rightSpeed);
}

void updateObstacleScanSchedule() {
  if (scanIntervalSteps == 0) {
    return;
  }

  if (totalStepsTaken >= nextScanStep) {
    sendObstacleScanRequest();
    nextScanStep += scanIntervalSteps;
  }
}

void runForwardLoop() {
  if (!navRunning) {
    return;
  }

  computeDesiredBearing();
  updateBearingDiff();
  applyForwardMotion();

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

  totalStepsTaken = getAverageAbsStepperPosition();
  updateObstacleScanSchedule();

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

// -------------------------
// Packet handlers
// -------------------------
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
  if (len != 22) {
    Serial.print(F("Waypoint length error: "));
    Serial.println(len);
    return;
  }

  pathTotalDistance = readUInt32LE(&data[0]);
  combinedStepsDesired = readUInt32LE(&data[4]);
  oppositeDistance = (int32_t)readUInt32LE(&data[8]);
  bearingAlignment = data[12];
  desiredBearing100 = readUInt16LE(&data[13]);
  actualBearing100 = readUInt16LE(&data[15]);
  trackDir = data[17] & 0x01;
  scanIntervalSteps = readUInt32LE(&data[18]);

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

  sendNavAck();
  beginForwardNavigation();
}

void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  switch (cmd) {
    case CMD_STOP_MOTORS:
      stopMotors();
      sendTotalSteps(totalStepsTaken);
      resetNavigationState();
      break;

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

    case CMD_RIGHT_PIVOT:
      if (len == 2 && !navRunning) {
        Serial.println(F("RX: RIGHT PIVOT"));
      }
      break;

    case CMD_LEFT_PIVOT:
      if (len == 2 && !navRunning) {
        Serial.println(F("RX: LEFT PIVOT"));
      }
      break;

    case CMD_WAYPOINT_DATA:
      if (!navRunning) {
        handleWaypointPacket(data, len);
      }
      break;
  }
}

// -------------------------
// UART parser
// -------------------------
void processIncomingByte(uint8_t b) {
  switch (parserState) {
    case WAIT_HEADER:
      if (b == UART_HEADER) parserState = WAIT_CMD;
      break;

    case WAIT_CMD:
      currentCmd = b;
      runningChecksum = b;
      parserState = WAIT_LEN;
      break;

    case WAIT_LEN:
      expectedLen = b;
      runningChecksum ^= b;
      if (expectedLen > MAX_PAYLOAD) {
        parserState = WAIT_HEADER;
      } else if (expectedLen == 0) {
        parserState = WAIT_CHECKSUM;
      } else {
        payloadIndex = 0;
        parserState = WAIT_PAYLOAD;
      }
      break;

    case WAIT_PAYLOAD:
      payload[payloadIndex++] = b;
      runningChecksum ^= b;
      if (payloadIndex >= expectedLen) {
        parserState = WAIT_CHECKSUM;
      }
      break;

    case WAIT_CHECKSUM:
      if (b == runningChecksum) {
        handlePacket(currentCmd, payload, expectedLen);
      } else {
        Serial.println(F("Checksum error"));
      }
      parserState = WAIT_HEADER;
      runningChecksum = 0;
      currentCmd = 0;
      expectedLen = 0;
      payloadIndex = 0;
      break;
  }
}

void readIncomingUart() {
  while (Serial3.available() > 0) {
    processIncomingByte((uint8_t)Serial3.read());
  }
}

// -------------------------
// Arduino entry points
// -------------------------
void setup() {
  Serial.begin(115200);
  Serial3.begin(115200);

  LeftFrontWheel.setMaxSpeed(2000);
  LeftBackWheel.setMaxSpeed(2000);
  RightFrontWheel.setMaxSpeed(2000);
  RightBackWheel.setMaxSpeed(2000);

  LeftFrontWheel.setAcceleration(800);
  LeftBackWheel.setAcceleration(800);
  RightFrontWheel.setAcceleration(800);
  RightBackWheel.setAcceleration(800);

  LeftFrontWheel.setSpeed(0);
  LeftBackWheel.setSpeed(0);
  RightFrontWheel.setSpeed(0);
  RightBackWheel.setSpeed(0);

  multiStepper.addStepper(LeftFrontWheel);
  multiStepper.addStepper(LeftBackWheel);
  multiStepper.addStepper(RightFrontWheel);
  multiStepper.addStepper(RightBackWheel);

  resetNavigationState();
  Serial.println(F("ARDUINO_v8 ready"));
}

void loop() {
  readIncomingUart();
  runForwardLoop();
}

Thanks, Jack



Related Hardware (view all EZB hardware)
Synthiam ARC enables Arduino Mega 2560 control with 8 ADC, 13 PWM, 21 digital ports and 24 servos; includes firmware, shield and tutorials
USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 21

Related Robot Skills (view all robot skills)
Script by Synthiam
Multi-language ARC Script: build, run and debug Blockly, JavaScript, EZ-Script or Python with Intellisense, run/save/load and Roboscratch support

ARC Pro

Upgrade to ARC Pro

Unleash your robot's full potential with the cutting-edge features and intuitive programming offered by Synthiam ARC Pro.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#9  

Absolutely, Jack. Since the pivot behavior is already repeatable, the main improvement here is to make the forward navigation speed handling more gradual and less aggressive.

What I’m changing in Arduino_v10:

  • adds a true speed ramp from 0 up to normalSpeed
  • slows how fast speed changes when bearing error changes
  • adds a deadband so tiny bearing noise doesn’t constantly twitch the motors
  • limits how much bearing correction can influence speed
  • keeps your pivot behavior unchanged
  • keeps your existing UART protocol unchanged

Arduino_v10

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

// =========================
// Protocol
// =========================
static const uint8_t UART_HEADER = 0xA5; // ARC -> Arduino

static const uint8_t CMD_STOP_MOTORS    = 0x01;
static const uint8_t CMD_ACTUAL_BEARING = 0x03;
static const uint8_t CMD_RIGHT_PIVOT    = 0x04;
static const uint8_t CMD_LEFT_PIVOT     = 0x05;
static const uint8_t CMD_WAYPOINT_DATA  = 0x06;

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

static const uint8_t MAX_PAYLOAD = 32;

// =========================
// Motor setup
// =========================
AccelStepper LeftFrontWheel(AccelStepper::DRIVER, 3, 6);
AccelStepper LeftBackWheel(AccelStepper::DRIVER, 2, 5);
AccelStepper RightFrontWheel(AccelStepper::DRIVER, 12, 13);
AccelStepper RightBackWheel(AccelStepper::DRIVER, 4, 7);

MultiStepper multiStepper;

// =========================
// Parser
// =========================
enum ParserState { WAIT_HEADER, WAIT_CMD, WAIT_LEN, WAIT_PAYLOAD, WAIT_CHECKSUM };
ParserState parserState = WAIT_HEADER;
uint8_t currentCmd = 0;
uint8_t expectedLen = 0;
uint8_t payloadIndex = 0;
uint8_t payload[MAX_PAYLOAD];
uint8_t runningChecksum = 0;

// =========================
// Navigation state
// =========================
bool navRunning = false;
uint32_t totalStepsTaken = 0;
uint32_t combinedStepsDesired = 0;
uint32_t pathTotalDistance = 0;
int32_t oppositeDistance = 0;
uint32_t nextScanStep = 0;
uint32_t scanIntervalSteps = 3056;
uint8_t bearingAlignment = 0;
uint8_t trackDir = 0;
uint16_t actualBearing100 = 9000;
uint16_t desiredBearing100 = 9000;
uint16_t lastSentDesiredBearing100 = 0xFFFF;
int16_t bearingDiffTarget = 0;
int16_t bearingDiffRamped = 0;

uint16_t currentSpeed = 0;
uint16_t normalSpeed = 600;

// =========================
// Forward ramp / smoothing
// =========================
uint16_t rampedSpeed = 0;
static const uint16_t MIN_FORWARD_SPEED = 120;     // start moving gently
static const uint16_t SPEED_RAMP_STEP = 12;         // speed increase per update
static const uint16_t SPEED_DECAY_STEP = 6;         // speed decrease per update
static const uint16_t MAX_BEARING_SPEED_ADJUST = 180; // clamp steering effect
static const int16_t  BEARING_DEADBAND = 3;         // ignore tiny bearing error
static const int16_t  BEARING_UPDATE_SMOOTH = 1;    // ramp bearing error slowly

// =========================
// Pivot state
// =========================
enum PivotMode { PIVOT_NONE, PIVOT_RIGHT, PIVOT_LEFT };
PivotMode pivotMode = PIVOT_NONE;

uint32_t pivotStepsDesired = 0;
uint32_t pivotStepsTaken = 0;
long pivotStartLF = 0;
long pivotStartLB = 0;
long pivotStartRF = 0;
long pivotStartRB = 0;

static const uint16_t PIVOT_SPEED = 600;

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

uint32_t abs32(long v) {
  return (v < 0) ? (uint32_t)(-v) : (uint32_t)v;
}

void sendPacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  uint8_t checksum = cmd ^ len;
  Serial3.write(UART_HEADER);
  Serial3.write(cmd);
  Serial3.write(len);

  for (uint8_t i = 0; i < len; i++) {
    Serial3.write(data[i]);
    checksum ^= data[i];
  }

  Serial3.write(checksum);
}

void sendCmdOnly(uint8_t cmd) {
  sendPacket(cmd, nullptr, 0);
}

void sendTotalSteps(uint32_t steps) {
  uint8_t tmp[4];
  tmp[0] = (uint8_t)(steps & 0xFF);
  tmp[1] = (uint8_t)((steps >> 8) & 0xFF);
  tmp[2] = (uint8_t)((steps >> 16) & 0xFF);
  tmp[3] = (uint8_t)((steps >> 24) & 0xFF);
  sendPacket(RSP_TOTAL_STEPS, tmp, 4);
  Serial.print(F("TX: TOTAL STEPS = "));
  Serial.println(steps);
}

void sendDesiredBearing(uint16_t bearing100) {
  uint8_t tmp[2];
  tmp[0] = (uint8_t)(bearing100 & 0xFF);
  tmp[1] = (uint8_t)((bearing100 >> 8) & 0xFF);
  sendPacket(RSP_DESIRED_BEARING, tmp, 2);
  lastSentDesiredBearing100 = bearing100;
  Serial.print(F("TX: DESIRED BEARING x100 = "));
  Serial.println(bearing100);
}

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

void clearIncomingUart() {
  while (Serial3.available() > 0) {
    Serial3.read();
  }
}

void resetStepperPositions() {
  LeftFrontWheel.setCurrentPosition(0);
  LeftBackWheel.setCurrentPosition(0);
  RightFrontWheel.setCurrentPosition(0);
  RightBackWheel.setCurrentPosition(0);
}

void resetPivotState() {
  pivotMode = PIVOT_NONE;
  pivotStepsDesired = 0;
  pivotStepsTaken = 0;
  pivotStartLF = 0;
  pivotStartLB = 0;
  pivotStartRF = 0;
  pivotStartRB = 0;
}

uint32_t getAverageAbsStepperPosition() {
  uint32_t a = (uint32_t)labs(LeftFrontWheel.currentPosition());
  uint32_t b = (uint32_t)labs(LeftBackWheel.currentPosition());
  uint32_t c = (uint32_t)labs(RightFrontWheel.currentPosition());
  uint32_t d = (uint32_t)labs(RightBackWheel.currentPosition());
  return (a + b + c + d) / 4;
}

void setAllMotorSpeeds(int leftSpeed, int rightSpeed) {
  leftSpeed = constrain(leftSpeed, -2000, 2000);
  rightSpeed = constrain(rightSpeed, -2000, 2000);
  LeftFrontWheel.setSpeed(leftSpeed);
  LeftBackWheel.setSpeed(leftSpeed);
  RightFrontWheel.setSpeed(rightSpeed);
  RightBackWheel.setSpeed(rightSpeed);
}

void stopMotors() {
  LeftFrontWheel.stop();
  LeftBackWheel.stop();
  RightFrontWheel.stop();
  RightBackWheel.stop();

  LeftFrontWheel.setSpeed(0);
  LeftBackWheel.setSpeed(0);
  RightFrontWheel.setSpeed(0);
  RightBackWheel.setSpeed(0);

  Serial.println(F("Action: stopMotors()"));
}

void finishPivot() {
  stopMotors();
  clearIncomingUart();
  resetStepperPositions();
  resetPivotState();
  Serial.println(F("Pivot complete and reset"));
}

void resetNavigationState() {
  resetStepperPositions();
  totalStepsTaken = 0;
  desiredBearing100 = 9000;
  actualBearing100 = 9000;
  lastSentDesiredBearing100 = 0xFFFF;
  bearingDiffTarget = 0;
  bearingDiffRamped = 0;
  nextScanStep = scanIntervalSteps;
  currentSpeed = 0;
  rampedSpeed = 0;
  navRunning = false;
  Serial.println(F("Navigation state reset"));
}

void beginForwardNavigation() {
  navRunning = true;
  currentSpeed = 0;
  rampedSpeed = 0;
  bearingDiffRamped = 0;
  nextScanStep = scanIntervalSteps;
  Serial.println(F("Navigation started"));
}

void beginPivot(PivotMode mode, uint32_t steps) {
  if (steps == 0) {
    Serial.println(F("Pivot ignored: steps = 0"));
    return;
  }

  clearIncomingUart();
  resetStepperPositions();

  pivotMode = mode;
  pivotStepsDesired = steps;
  pivotStepsTaken = 0;

  pivotStartLF = LeftFrontWheel.currentPosition();
  pivotStartLB = LeftBackWheel.currentPosition();
  pivotStartRF = RightFrontWheel.currentPosition();
  pivotStartRB = RightBackWheel.currentPosition();

  if (mode == PIVOT_RIGHT) {
    setAllMotorSpeeds((int)PIVOT_SPEED, -(int)PIVOT_SPEED);
    Serial.println(F("Action: beginPivot(RIGHT)"));
  } else if (mode == PIVOT_LEFT) {
    setAllMotorSpeeds(-(int)PIVOT_SPEED, (int)PIVOT_SPEED);
    Serial.println(F("Action: beginPivot(LEFT)"));
  }
}

uint32_t getPivotProgressSteps() {
  uint32_t lf = abs32(LeftFrontWheel.currentPosition() - pivotStartLF);
  uint32_t lb = abs32(LeftBackWheel.currentPosition() - pivotStartLB);
  uint32_t rf = abs32(RightFrontWheel.currentPosition() - pivotStartRF);
  uint32_t rb = abs32(RightBackWheel.currentPosition() - pivotStartRB);
  return (lf + lb + rf + rb) / 4;
}

void updatePivotLoop() {
  if (pivotMode == PIVOT_NONE) return;

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

  pivotStepsTaken = getPivotProgressSteps();

  if (pivotStepsTaken >= pivotStepsDesired) {
    finishPivot();
    Serial.print(F("Pivot complete, steps="));
    Serial.println(pivotStepsTaken);
  }
}

// =========================
// Navigation math
// =========================
void computeDesiredBearing() {
  if (bearingAlignment == 0) return;

  uint32_t pathDistanceRemaining = 1;

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

  if (pathDistanceRemaining == 0) pathDistanceRemaining = 1;

  bool reflectorIsLeft = (desiredBearing100 >= 9100);
  int32_t signedOppositeDistance = (int32_t)oppositeDistance;

  if (reflectorIsLeft) {
    signedOppositeDistance = -labs(signedOppositeDistance);
  } else {
    signedOppositeDistance = labs(signedOppositeDistance);
  }

  float angleDeg = atan2((float)signedOppositeDistance, (float)pathDistanceRemaining) * 180.0f / PI;
  int32_t bearing100 = (int32_t)((90.0f + angleDeg) * 100.0f);

  if (bearing100 < 0) bearing100 = 0;
  if (bearing100 > 18000) bearing100 = 18000;

  desiredBearing100 = (uint16_t)bearing100;

  if (abs((int)desiredBearing100 - (int)lastSentDesiredBearing100) >= 100) {
    sendDesiredBearing(desiredBearing100);
  }
}

void updateBearingDiff() {
  bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;

  if (bearingDiffTarget > -BEARING_DEADBAND && bearingDiffTarget < BEARING_DEADBAND) {
    bearingDiffTarget = 0;
  }

  if (bearingDiffRamped < bearingDiffTarget) {
    bearingDiffRamped += BEARING_UPDATE_SMOOTH;
    if (bearingDiffRamped > bearingDiffTarget) bearingDiffRamped = bearingDiffTarget;
  } else if (bearingDiffRamped > bearingDiffTarget) {
    bearingDiffRamped -= BEARING_UPDATE_SMOOTH;
    if (bearingDiffRamped < bearingDiffTarget) bearingDiffRamped = bearingDiffTarget;
  }
}

void applyForwardMotion() {
  int16_t diff = bearingDiffRamped;
  int16_t absDiff = abs(diff);

  if (absDiff < BEARING_DEADBAND) {
    absDiff = 0;
  }

  // smoother startup ramp
  if (rampedSpeed < normalSpeed) {
    if (rampedSpeed == 0) {
      rampedSpeed = MIN_FORWARD_SPEED;
    } else {
      rampedSpeed += SPEED_RAMP_STEP;
    }

    if (rampedSpeed > normalSpeed) {
      rampedSpeed = normalSpeed;
    }
  } else if (rampedSpeed > normalSpeed) {
    rampedSpeed -= SPEED_DECAY_STEP;
    if (rampedSpeed < normalSpeed) rampedSpeed = normalSpeed;
  }

  // reduce aggressive steering effect
  int32_t steerAdjust = absDiff * 1;
  if (steerAdjust > MAX_BEARING_SPEED_ADJUST) {
    steerAdjust = MAX_BEARING_SPEED_ADJUST;
  }

  int leftSpeed = rampedSpeed;
  int rightSpeed = rampedSpeed;

  if (diff > 0) {
    leftSpeed = (int)rampedSpeed - (int)steerAdjust;
    rightSpeed = (int)rampedSpeed + (int)steerAdjust;
  } else if (diff < 0) {
    leftSpeed = (int)rampedSpeed + (int)steerAdjust;
    rightSpeed = (int)rampedSpeed - (int)steerAdjust;
  }

  leftSpeed = constrain(leftSpeed, 0, (int)normalSpeed);
  rightSpeed = constrain(rightSpeed, 0, (int)normalSpeed);

  setAllMotorSpeeds(leftSpeed, rightSpeed);

  Serial.print(F("FORWARD rampedSpeed="));
  Serial.print(rampedSpeed);
  Serial.print(F(" diff="));
  Serial.print(diff);
  Serial.print(F(" left="));
  Serial.print(leftSpeed);
  Serial.print(F(" right="));
  Serial.println(rightSpeed);
}

void updateObstacleScanSchedule() {
  if (totalStepsTaken >= nextScanStep) {
    sendObstacleScanRequest();
    nextScanStep += scanIntervalSteps;
  }
}

void runForwardLoop() {
  if (!navRunning) return;

  computeDesiredBearing();
  updateBearingDiff();
  applyForwardMotion();

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

  totalStepsTaken = getAverageAbsStepperPosition();
  updateObstacleScanSchedule();

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

// =========================
// Packet handlers
// =========================
void handleWaypointPacket(const uint8_t* data, uint8_t len) {
  if (len != 22) {
    Serial.print(F("Waypoint length error: "));
    Serial.println(len);
    return;
  }

  pathTotalDistance = readUInt32LE(&data[0]);
  combinedStepsDesired = readUInt32LE(&data[4]);
  oppositeDistance = (int32_t)readUInt32LE(&data[8]);
  bearingAlignment = data[12];
  desiredBearing100 = readUInt16LE(&data[13]);
  actualBearing100 = readUInt16LE(&data[15]);
  trackDir = data[17] & 0x01;
  scanIntervalSteps = readUInt32LE(&data[18]);

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

  sendCmdOnly(RSP_NAV_ACK);
  beginForwardNavigation();
}

void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {
  switch (cmd) {
    case CMD_STOP_MOTORS:
      stopMotors();
      sendTotalSteps(totalStepsTaken);
      resetNavigationState();
      break;

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

    case CMD_RIGHT_PIVOT:
      if (len == 2 && !navRunning) {
        uint16_t steps = readUInt16LE(data);
        Serial.print(F("RX: RIGHT PIVOT steps="));
        Serial.println(steps);
        beginPivot(PIVOT_RIGHT, steps);
      }
      break;

    case CMD_LEFT_PIVOT:
      if (len == 2 && !navRunning) {
        uint16_t steps = readUInt16LE(data);
        Serial.print(F("RX: LEFT PIVOT steps="));
        Serial.println(steps);
        beginPivot(PIVOT_LEFT, steps);
      }
      break;

    case CMD_WAYPOINT_DATA:
      if (!navRunning) {
        handleWaypointPacket(data, len);
      }
      break;
  }
}

// =========================
// UART parser
// =========================
void processIncomingByte(uint8_t b) {
  switch (parserState) {
    case WAIT_HEADER:
      if (b == UART_HEADER) parserState = WAIT_CMD;
      break;

    case WAIT_CMD:
      currentCmd = b;
      runningChecksum = b;
      parserState = WAIT_LEN;
      break;

    case WAIT_LEN:
      expectedLen = b;
      runningChecksum ^= b;
      if (expectedLen > MAX_PAYLOAD) {
        parserState = WAIT_HEADER;
      } else if (expectedLen == 0) {
        parserState = WAIT_CHECKSUM;
      } else {
        payloadIndex = 0;
        parserState = WAIT_PAYLOAD;
      }
      break;

    case WAIT_PAYLOAD:
      payload[payloadIndex++] = b;
      runningChecksum ^= b;
      if (payloadIndex >= expectedLen) {
        parserState = WAIT_CHECKSUM;
      }
      break;

    case WAIT_CHECKSUM:
      if (b == runningChecksum) {
        handlePacket(currentCmd, payload, expectedLen);
      } else {
        Serial.println(F("Checksum error"));
      }
      parserState = WAIT_HEADER;
      runningChecksum = 0;
      currentCmd = 0;
      expectedLen = 0;
      payloadIndex = 0;
      break;
  }
}

void readIncomingUart() {
  while (Serial3.available() > 0) {
    processIncomingByte((uint8_t)Serial3.read());
  }
}

// =========================
// Arduino entry points
// =========================
void setup() {
  delay(1000); // give USB/serial time to settle

  Serial.begin(115200);
  Serial3.begin(115200);

  LeftFrontWheel.setMaxSpeed(2000);
  LeftBackWheel.setMaxSpeed(2000);
  RightFrontWheel.setMaxSpeed(2000);
  RightBackWheel.setMaxSpeed(2000);

  LeftFrontWheel.setAcceleration(800);
  LeftBackWheel.setAcceleration(800);
  RightFrontWheel.setAcceleration(800);
  RightBackWheel.setAcceleration(800);

  LeftFrontWheel.setSpeed(0);
  LeftBackWheel.setSpeed(0);
  RightFrontWheel.setSpeed(0);
  RightBackWheel.setSpeed(0);

  multiStepper.addStepper(LeftFrontWheel);
  multiStepper.addStepper(LeftBackWheel);
  multiStepper.addStepper(RightFrontWheel);
  multiStepper.addStepper(RightBackWheel);

  resetNavigationState();
  resetPivotState();
  clearIncomingUart();

  Serial.println(F("ARDUINO_v10 ready"));
}

void loop() {
  readIncomingUart();

  if (navRunning) {
    runForwardLoop();
  } else {
    updatePivotLoop();
  }
}

What changed for smoother motion

1) Real startup ramp

Instead of jumping straight into normalSpeed, the robot now ramps up with:

  • MIN_FORWARD_SPEED
  • SPEED_RAMP_STEP

So it starts gently and builds up.

2) Less aggressive steering

The bearing correction now uses:

  • a small deadband
  • a slower bearing ramp
  • a capped steering adjustment

That means tiny changes in actualBearing100 won’t yank the motors around.

3) Speed is no longer being over-corrected

In your earlier behavior, the steering adjustment could dominate the speed output too quickly. Now it is intentionally softened.


Good tuning values to try

If the robot still feels too aggressive, try these changes first:

static const uint16_t MIN_FORWARD_SPEED = 90;
static const uint16_t SPEED_RAMP_STEP = 8;
static const uint16_t SPEED_DECAY_STEP = 4;
static const uint16_t MAX_BEARING_SPEED_ADJUST = 120;
static const int16_t BEARING_DEADBAND = 4;

If it feels too sluggish, increase:

  • SPEED_RAMP_STEP
  • MAX_BEARING_SPEED_ADJUST

One important observation

In your pasted Arduino_v9, the speed issue may also have been caused by:

  • bearing corrections being too large too early
  • bearingDiffRamped changing too quickly
  • no meaningful speed ramp from a low starting point

So Arduino_v10 addresses all three.


My recommendation

Start with Arduino_v10 exactly as posted, then test in this order:

  1. straight forward motion
  2. small bearing corrections
  3. longer forward navigation run
  4. pivot check, just to confirm it still behaves the same

If you want, I can also give you a UART_v10 companion script that adds a quick way to set tuning variables like:

  • $normalSpeed
  • $pivotSteps
  • $bearingDeadband
  • $speedRampStep

That would let you tune motion from ARC without re-uploading the sketch.

I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#10  

Yes @Athena, please send a UART_v10 companion script that adds a quick way to set tuning variables like:

  • $normalSpeed
  • $pivotSteps
  • $bearingDeadband
  • $speedRampStep

Thank you very much. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#11  

Absolutely, Jack. Below is a UART_v10 companion ARC JavaScript script that lets you quickly set tuning values from ARC and sends them as global variables for your robot logic to use.

It does not change your pivot packet format or navigation packet format. It simply gives you a convenient way to update:

  • $normalSpeed
  • $pivotSteps
  • $bearingDeadband
  • $speedRampStep

This is useful if you want to tune behavior without editing/reuploading the Arduino sketch each time.


UART_v10 companion script

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;

// -------------------------
// RX parser
// -------------------------
var rxState = 0;
var rxCmd = 0;
var rxLen = 0;
var rxIndex = 0;
var rxPayload = [];
var rxChecksum = 0;

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

function clamp(v, min, max) {
  if (v < min) return min;
  if (v > max) return max;
  return v;
}

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 bytesToU16LE(b0, b1) {
  return (b0 & 0xFF) | ((b1 & 0xFF) << 8);
}

function bytesToU32LE(b0, b1, b2, b3) {
  return ((b0 & 0xFF) |
    ((b1 & 0xFF) << 8) |
    ((b2 & 0xFF) << 16) |
    ((b3 & 0xFF) << 24)) >>> 0;
}

function checksumXor(cmd, len, payload) {
  var c = (cmd ^ len) & 0xFF;
  for (var i = 0; i < payload.length; i++) {
    c ^= (payload[i] & 0xFF);
  }
  return c & 0xFF;
}

function sendPacket(cmd, payload) {
  var len = payload.length & 0xFF;
  var checksum = checksumXor(cmd, len, payload);

  UART.hardwareUartWrite(UART_INDEX, [UART_HEADER, cmd, len]);
  if (len > 0) {
    UART.hardwareUartWrite(UART_INDEX, payload);
  }
  UART.hardwareUartWrite(UART_INDEX, [checksum]);
}

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

function sendActualBearing() {
  var bearing100 = safeInt(getVar("$actualBearing100", 9000), 9000);
  bearing100 = clamp(bearing100, 0, 18000);

  sendPacket(CMD_ACTUAL_BEARING, u16ToBytes(bearing100));
  print("TX: ACTUAL BEARING x100 = " + bearing100);
}

function sendWaypointData() {
  var pathTotalDistance = safeInt(getVar("$pathTotalDistance", 0), 0) >>> 0;
  var combinedStepsDesired = safeInt(getVar("$combinedStepsDesired", 0), 0) >>> 0;
  var oppositeDistance = safeInt(getVar("$oppositeDistance", 0), 0) >>> 0;
  var bearingAlignment = safeInt(getVar("$bearingAlignment", 0), 0) & 0xFF;
  var trackDir = safeInt(getVar("$trackDir", 0), 0);
  trackDir = (trackDir === 0) ? 0 : 1;
  var desiredBearing100 = clamp(safeInt(getVar("$desiredBearing100", 9000), 9000), 0, 18000);
  var actualBearing100 = clamp(safeInt(getVar("$actualBearing100", 9000), 9000), 0, 18000);
  var scanIntervalSteps = safeInt(getVar("$scanIntervalSteps", 3056), 3056) >>> 0;

  setVar("$desiredBearing100", desiredBearing100);
  setVar("$actualBearing100", actualBearing100);

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

  if (payload.length !== 22) {
    print("Waypoint payload length error: " + payload.length);
    return;
  }

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

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

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

// -------------------------
// Tuning helper
// -------------------------
function applyTuningFromGlobals() {
  var normalSpeed = safeInt(getVar("$normalSpeed", 600), 600);
  var pivotSteps = safeInt(getVar("$pivotSteps", 600), 600);
  var bearingDeadband = safeInt(getVar("$bearingDeadband", 3), 3);
  var speedRampStep = safeInt(getVar("$speedRampStep", 12), 12);

  setVar("$normalSpeed", normalSpeed);
  setVar("$pivotSteps", pivotSteps);
  setVar("$bearingDeadband", bearingDeadband);
  setVar("$speedRampStep", speedRampStep);

  print("Tuning applied:");
  print(" $normalSpeed=" + normalSpeed);
  print(" $pivotSteps=" + pivotSteps);
  print(" $bearingDeadband=" + bearingDeadband);
  print(" $speedRampStep=" + speedRampStep);
}

// Optional: apply and send a waypoint after tuning
function applyTuningAndSendWaypoint() {
  applyTuningFromGlobals();
  sendWaypointData();
}

// -------------------------
// RX handlers
// -------------------------
function requestObstacleScan() {
  if (getVar("$obsScanActive", 0) == 1) {
    print("RX: OBSTACLE SCAN REQUEST ignored (scan already active)");
    return;
  }
  setVar("$obsScanRequested", 1);
  setVar("$obsScanActive", 1);
  print("RX: OBSTACLE SCAN REQUEST -> $obsScanRequested=1, $obsScanActive=1");
}

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

  if (cmd == RSP_TOTAL_STEPS) {
    if (payload.length == 4) {
      var steps = bytesToU32LE(payload[0], payload[1], payload[2], payload[3]);
      setVar("$totalStepsTaken", steps);
      setVar("$legComplete", 1);
      print("RX: TOTAL STEPS = " + steps);
    }
    return;
  }

  if (cmd == RSP_DESIRED_BEARING) {
    if (payload.length == 2) {
      var bearing100 = bytesToU16LE(payload[0], payload[1]);
      setVar("$desiredBearing100", bearing100);
      print("RX: DESIRED BEARING x100 = " + bearing100);
    }
    return;
  }

  if (cmd == RSP_OBS_SCAN_REQUEST) {
    requestObstacleScan();
    return;
  }

  print("RX: Unknown cmd=" + cmd + " len=" + payload.length);
}

function processIncomingBytes() {
  var available = UART.hardwareUartAvailable(UART_INDEX);

  while (available > 0) {
    var b = UART.hardwareUartRead(UART_INDEX, 1)[0] & 0xFF;
    available--;

    if (rxState == 0) {
      if (b == UART_HEADER) rxState = 1;
      continue;
    }

    if (rxState == 1) {
      rxCmd = b;
      rxChecksum = rxCmd;
      rxState = 2;
      continue;
    }

    if (rxState == 2) {
      rxLen = b;
      rxChecksum ^= rxLen;
      rxPayload = [];
      rxIndex = 0;
      rxState = (rxLen == 0) ? 4 : 3;
      continue;
    }

    if (rxState == 3) {
      rxPayload.push(b);
      rxChecksum ^= b;
      rxIndex++;
      if (rxIndex >= rxLen) rxState = 4;
      continue;
    }

    if (rxState == 4) {
      var cs = b & 0xFF;
      if (cs == (rxChecksum & 0xFF)) {
        handleRxPacket(rxCmd, rxPayload);
      } else {
        print("RX checksum error. cmd=" + rxCmd + " expected=" + (rxChecksum & 0xFF) + " got=" + cs);
      }

      rxState = 0;
      rxCmd = 0;
      rxLen = 0;
      rxIndex = 0;
      rxPayload = [];
      rxChecksum = 0;
      continue;
    }
  }
}

// -------------------------
// Main
// -------------------------
print("UART_v10 companion started");

while (true) {
  // Quick tuning sync
  if (getVar("$applyTuningNow", 0) == 1) {
    applyTuningFromGlobals();
    setVar("$applyTuningNow", 0);
  }

  // Existing one-shot actions
  if (getVar("$sendWaypointNow", 0) == 1 && getVar("$waypointSent", 0) == 0) {
    sendWaypointData();
    setVar("$sendWaypointNow", 0);
    setVar("$waypointSent", 1);
  }

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

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

  processIncomingBytes();
  sleep(20);
}

How to use it

1) Set the tuning values in ARC

Set these global variables anywhere in ARC:

  • $normalSpeed
  • $pivotSteps
  • $bearingDeadband
  • $speedRampStep

Example:

  • $normalSpeed = 600
  • $pivotSteps = 600
  • $bearingDeadband = 3
  • $speedRampStep = 12

2) Apply them

Set:

  • $applyTuningNow = 1

That will copy the values into the script state and print them.


Optional convenience

If you want, you can also use the variables directly before a motion command. For example:

  • set $pivotSteps
  • set $sendRightPivotNow = 1

Or:

  • set $pivotSteps
  • set $sendLeftPivotNow = 1

Important note

Your Arduino_v10 sketch is the one that actually uses the new tuning values. So for this companion script to fully match, make sure Arduino_v10 is reading those global settings from incoming packets or you’re manually using them in ARC logic.

If you want, I can also do one more step and make a UART_v11 script that sends a dedicated tuning packet to Arduino_v10 so those values are transferred over UART instead of just being local ARC globals. That would be the cleanest long-term setup.

Would you like me to build that tuning-packet version next?

I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#12  

Thank you @Athena for the Arduino and ARC scripts. The current problem with the Arduino_v10 is that it does not adjust left motor and right motor PWM equally. Only one motor decays in relation to bearing diff change. Can you rewrite Arduino_v10 to adjust right and left motor PWM equally when bearing diff changes? Let's call it Arduino_v11. Thanks. Jack.

//Arduino_v10

#include <AccelStepper.h>#include <MultiStepper.h>#include <math.h>
// =========================// Protocol// =========================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 -> 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;
static const uint8_t MAX_PAYLOAD = 32;
// =========================// Motor setup// =========================AccelStepper LeftFrontWheel(AccelStepper::DRIVER, 3, 6);AccelStepper LeftBackWheel(AccelStepper::DRIVER, 2, 5);AccelStepper RightFrontWheel(AccelStepper::DRIVER, 12, 13);AccelStepper RightBackWheel(AccelStepper::DRIVER, 4, 7);
MultiStepper multiStepper;
// =========================// Parser// =========================enum ParserState { WAIT_HEADER, WAIT_CMD, WAIT_LEN, WAIT_PAYLOAD, WAIT_CHECKSUM };ParserState parserState = WAIT_HEADER;uint8_t currentCmd = 0;uint8_t expectedLen = 0;uint8_t payloadIndex = 0;uint8_t payload[MAX_PAYLOAD];uint8_t runningChecksum = 0;
// =========================// Navigation state// =========================bool navRunning = false;uint32_t totalStepsTaken = 0;uint32_t combinedStepsDesired = 0;uint32_t pathTotalDistance = 0;int32_t oppositeDistance = 0;uint32_t nextScanStep = 0;uint32_t scanIntervalSteps = 3056;uint8_t bearingAlignment = 0;uint8_t trackDir = 0;uint16_t actualBearing100 = 9000;uint16_t desiredBearing100 = 9000;uint16_t lastSentDesiredBearing100 = 0xFFFF;int16_t bearingDiffTarget = 0;int16_t bearingDiffRamped = 0;
uint16_t currentSpeed = 0;uint16_t normalSpeed = 600;
// =========================// Forward ramp / smoothing// =========================uint16_t rampedSpeed = 0;static const uint16_t MIN_FORWARD_SPEED = 60;     // start moving gently120static const uint16_t SPEED_RAMP_STEP = 2;         // speed increase per update12static const uint16_t SPEED_DECAY_STEP = 2;         // speed decrease per update6static const uint16_t MAX_BEARING_SPEED_ADJUST = 180; // clamp steering effectstatic const int16_t  BEARING_DEADBAND = 3;         // ignore tiny bearing errorstatic const int16_t  BEARING_UPDATE_SMOOTH = 1;    // ramp bearing error slowly
// =========================// Pivot state// =========================enum PivotMode { PIVOT_NONE, PIVOT_RIGHT, PIVOT_LEFT };PivotMode pivotMode = PIVOT_NONE;
uint32_t pivotStepsDesired = 0;uint32_t pivotStepsTaken = 0;long pivotStartLF = 0;long pivotStartLB = 0;long pivotStartRF = 0;long pivotStartRB = 0;
static const uint16_t PIVOT_SPEED = 600;
// =========================// 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);}
uint32_t abs32(long v) {  return (v < 0) ? (uint32_t)(-v) : (uint32_t)v;}
void sendPacket(uint8_t cmd, const uint8_t* data, uint8_t len) {  uint8_t checksum = cmd ^ len;  Serial3.write(UART_HEADER);  Serial3.write(cmd);  Serial3.write(len);
  for (uint8_t i = 0; i < len; i++) {    Serial3.write(data[i]);    checksum ^= data[i];  }
  Serial3.write(checksum);}
void sendCmdOnly(uint8_t cmd) {  sendPacket(cmd, nullptr, 0);}
void sendTotalSteps(uint32_t steps) {  uint8_t tmp[4];  tmp[0] = (uint8_t)(steps & 0xFF);  tmp[1] = (uint8_t)((steps >> 8) & 0xFF);  tmp[2] = (uint8_t)((steps >> 16) & 0xFF);  tmp[3] = (uint8_t)((steps >> 24) & 0xFF);  sendPacket(RSP_TOTAL_STEPS, tmp, 4);  Serial.print(F("TX: TOTAL STEPS = "));  Serial.println(steps);}
void sendDesiredBearing(uint16_t bearing100) {  uint8_t tmp[2];  tmp[0] = (uint8_t)(bearing100 & 0xFF);  tmp[1] = (uint8_t)((bearing100 >> 8) & 0xFF);  sendPacket(RSP_DESIRED_BEARING, tmp, 2);  lastSentDesiredBearing100 = bearing100;  Serial.print(F("TX: DESIRED BEARING x100 = "));  Serial.println(bearing100);}
void sendObstacleScanRequest() {  sendCmdOnly(RSP_OBS_SCAN_REQUEST);  Serial.println(F("TX: OBSTACLE SCAN REQUEST"));}
void clearIncomingUart() {  while (Serial3.available() > 0) {    Serial3.read();  }}
void resetStepperPositions() {  LeftFrontWheel.setCurrentPosition(0);  LeftBackWheel.setCurrentPosition(0);  RightFrontWheel.setCurrentPosition(0);  RightBackWheel.setCurrentPosition(0);}
void resetPivotState() {  pivotMode = PIVOT_NONE;  pivotStepsDesired = 0;  pivotStepsTaken = 0;  pivotStartLF = 0;  pivotStartLB = 0;  pivotStartRF = 0;  pivotStartRB = 0;}
uint32_t getAverageAbsStepperPosition() {  uint32_t a = (uint32_t)labs(LeftFrontWheel.currentPosition());  uint32_t b = (uint32_t)labs(LeftBackWheel.currentPosition());  uint32_t c = (uint32_t)labs(RightFrontWheel.currentPosition());  uint32_t d = (uint32_t)labs(RightBackWheel.currentPosition());  return (a + b + c + d) / 4;}
void setAllMotorSpeeds(int leftSpeed, int rightSpeed) {  leftSpeed = constrain(leftSpeed, -2000, 2000);  rightSpeed = constrain(rightSpeed, -2000, 2000);  LeftFrontWheel.setSpeed(leftSpeed);  LeftBackWheel.setSpeed(leftSpeed);  RightFrontWheel.setSpeed(rightSpeed);  RightBackWheel.setSpeed(rightSpeed);}
void stopMotors() {  LeftFrontWheel.stop();  LeftBackWheel.stop();  RightFrontWheel.stop();  RightBackWheel.stop();
  LeftFrontWheel.setSpeed(0);  LeftBackWheel.setSpeed(0);  RightFrontWheel.setSpeed(0);  RightBackWheel.setSpeed(0);
  Serial.println(F("Action: stopMotors()"));}
void finishPivot() {  stopMotors();  clearIncomingUart();  resetStepperPositions();  resetPivotState();  Serial.println(F("Pivot complete and reset"));}
void resetNavigationState() {  resetStepperPositions();  totalStepsTaken = 0;  desiredBearing100 = 9000;  actualBearing100 = 9000;  lastSentDesiredBearing100 = 0xFFFF;  bearingDiffTarget = 0;  bearingDiffRamped = 0;  nextScanStep = scanIntervalSteps;  currentSpeed = 0;  rampedSpeed = 0;  navRunning = false;  Serial.println(F("Navigation state reset"));}
void beginForwardNavigation() {  navRunning = true;  currentSpeed = 0;  rampedSpeed = 0;  bearingDiffRamped = 0;  nextScanStep = scanIntervalSteps;  Serial.println(F("Navigation started"));}
void beginPivot(PivotMode mode, uint32_t steps) {  if (steps == 0) {    Serial.println(F("Pivot ignored: steps = 0"));    return;  }
  clearIncomingUart();  resetStepperPositions();
  pivotMode = mode;  pivotStepsDesired = steps;  pivotStepsTaken = 0;
  pivotStartLF = LeftFrontWheel.currentPosition();  pivotStartLB = LeftBackWheel.currentPosition();  pivotStartRF = RightFrontWheel.currentPosition();  pivotStartRB = RightBackWheel.currentPosition();
  if (mode == PIVOT_RIGHT) {    setAllMotorSpeeds((int)PIVOT_SPEED, -(int)PIVOT_SPEED);    Serial.println(F("Action: beginPivot(RIGHT)"));  } else if (mode == PIVOT_LEFT) {    setAllMotorSpeeds(-(int)PIVOT_SPEED, (int)PIVOT_SPEED);    Serial.println(F("Action: beginPivot(LEFT)"));  }}
uint32_t getPivotProgressSteps() {  uint32_t lf = abs32(LeftFrontWheel.currentPosition() - pivotStartLF);  uint32_t lb = abs32(LeftBackWheel.currentPosition() - pivotStartLB);  uint32_t rf = abs32(RightFrontWheel.currentPosition() - pivotStartRF);  uint32_t rb = abs32(RightBackWheel.currentPosition() - pivotStartRB);  return (lf + lb + rf + rb) / 4;}
void updatePivotLoop() {  if (pivotMode == PIVOT_NONE) return;
  LeftFrontWheel.runSpeed();  LeftBackWheel.runSpeed();  RightFrontWheel.runSpeed();  RightBackWheel.runSpeed();
  pivotStepsTaken = getPivotProgressSteps();
  if (pivotStepsTaken >= pivotStepsDesired) {    finishPivot();    Serial.print(F("Pivot complete, steps="));    Serial.println(pivotStepsTaken);  }}
// =========================// Navigation math// =========================void computeDesiredBearing() {  if (bearingAlignment == 0) return;
  uint32_t pathDistanceRemaining = 1;
  if (trackDir == 0) {    if (pathTotalDistance > totalStepsTaken) {      pathDistanceRemaining = pathTotalDistance - totalStepsTaken;    }  } else {    uint32_t remainingSteps = (combinedStepsDesired > totalStepsTaken) ? (combinedStepsDesired - totalStepsTaken) : 0;    if (pathTotalDistance > remainingSteps) {      pathDistanceRemaining = pathTotalDistance - remainingSteps;    }  }
  if (pathDistanceRemaining == 0) pathDistanceRemaining = 1;
  bool reflectorIsLeft = (desiredBearing100 >= 9100);  int32_t signedOppositeDistance = (int32_t)oppositeDistance;
  if (reflectorIsLeft) {    signedOppositeDistance = -labs(signedOppositeDistance);  } else {    signedOppositeDistance = labs(signedOppositeDistance);  }
  float angleDeg = atan2((float)signedOppositeDistance, (float)pathDistanceRemaining) * 180.0f / PI;  int32_t bearing100 = (int32_t)((90.0f + angleDeg) * 100.0f);
  if (bearing100 < 0) bearing100 = 0;  if (bearing100 > 18000) bearing100 = 18000;
  desiredBearing100 = (uint16_t)bearing100;
  if (abs((int)desiredBearing100 - (int)lastSentDesiredBearing100) >= 100) {    sendDesiredBearing(desiredBearing100);  }}
void updateBearingDiff() {  bearingDiffTarget = (int16_t)actualBearing100 - (int16_t)desiredBearing100;
  if (bearingDiffTarget > -BEARING_DEADBAND && bearingDiffTarget < BEARING_DEADBAND) {    bearingDiffTarget = 0;  }
  if (bearingDiffRamped < bearingDiffTarget) {    bearingDiffRamped += BEARING_UPDATE_SMOOTH;    if (bearingDiffRamped > bearingDiffTarget) bearingDiffRamped = bearingDiffTarget;  } else if (bearingDiffRamped > bearingDiffTarget) {    bearingDiffRamped -= BEARING_UPDATE_SMOOTH;    if (bearingDiffRamped < bearingDiffTarget) bearingDiffRamped = bearingDiffTarget;  }}
void applyForwardMotion() {  int16_t diff = bearingDiffRamped;  int16_t absDiff = abs(diff);
  if (absDiff < BEARING_DEADBAND) {    absDiff = 0;  }
  // smoother startup ramp  if (rampedSpeed < normalSpeed) {    if (rampedSpeed == 0) {      rampedSpeed = MIN_FORWARD_SPEED;    } else {      rampedSpeed += SPEED_RAMP_STEP;    }
    if (rampedSpeed > normalSpeed) {      rampedSpeed = normalSpeed;    }  } else if (rampedSpeed > normalSpeed) {    rampedSpeed -= SPEED_DECAY_STEP;    if (rampedSpeed < normalSpeed) rampedSpeed = normalSpeed;  }
  // reduce aggressive steering effect  int32_t steerAdjust = absDiff * 1;  if (steerAdjust > MAX_BEARING_SPEED_ADJUST) {    steerAdjust = MAX_BEARING_SPEED_ADJUST;  }
  int leftSpeed = rampedSpeed;  int rightSpeed = rampedSpeed;
  if (diff > 0) {    leftSpeed = (int)rampedSpeed - (int)steerAdjust;    rightSpeed = (int)rampedSpeed + (int)steerAdjust;  } else if (diff < 0) {    leftSpeed = (int)rampedSpeed + (int)steerAdjust;    rightSpeed = (int)rampedSpeed - (int)steerAdjust;  }
  leftSpeed = constrain(leftSpeed, 0, (int)normalSpeed);  rightSpeed = constrain(rightSpeed, 0, (int)normalSpeed);
  setAllMotorSpeeds(leftSpeed, rightSpeed);
  Serial.print(F("FORWARD rampedSpeed="));  Serial.print(rampedSpeed);  Serial.print(F(" diff="));  Serial.print(diff); Serial.print(F(" left="));  Serial.print(leftSpeed);  Serial.print(F(" right="));  Serial.println(rightSpeed);}
void updateObstacleScanSchedule() {  if (totalStepsTaken >= nextScanStep) {    sendObstacleScanRequest();    nextScanStep += scanIntervalSteps;  }}
void runForwardLoop() {  if (!navRunning) return;
  computeDesiredBearing();  updateBearingDiff();  applyForwardMotion();
  LeftFrontWheel.runSpeed();  LeftBackWheel.runSpeed();  RightFrontWheel.runSpeed();  RightBackWheel.runSpeed();
  totalStepsTaken = getAverageAbsStepperPosition();  updateObstacleScanSchedule();
  if (combinedStepsDesired > 0 && totalStepsTaken >= combinedStepsDesired) {    stopMotors();    sendTotalSteps(totalStepsTaken);    resetNavigationState();  }}
// =========================// Packet handlers// =========================void handleWaypointPacket(const uint8_t* data, uint8_t len) {  if (len != 22) {    Serial.print(F("Waypoint length error: "));    Serial.println(len);    return;  }
  pathTotalDistance = readUInt32LE(&data[0]);  combinedStepsDesired = readUInt32LE(&data[4]);  oppositeDistance = (int32_t)readUInt32LE(&data[8]);  bearingAlignment = data[12];  desiredBearing100 = readUInt16LE(&data[13]);  actualBearing100 = readUInt16LE(&data[15]);  trackDir = data[17] & 0x01;  scanIntervalSteps = readUInt32LE(&data[18]);
  Serial.println(F("RX: WAYPOINT DATA"));  Serial.print(F(" pathTotalDistance=")); Serial.println(pathTotalDistance);  Serial.print(F(" combinedStepsDesired=")); Serial.println(combinedStepsDesired);  Serial.print(F(" oppositeDistance=")); Serial.println(oppositeDistance);  Serial.print(F(" bearingAlignment=")); Serial.println(bearingAlignment);  Serial.print(F(" desiredBearing100=")); Serial.println(desiredBearing100);  Serial.print(F(" actualBearing100=")); Serial.println(actualBearing100);  Serial.print(F(" trackDir=")); Serial.println(trackDir);  Serial.print(F(" scanIntervalSteps=")); Serial.println(scanIntervalSteps);
  sendCmdOnly(RSP_NAV_ACK);  beginForwardNavigation();}
void handlePacket(uint8_t cmd, const uint8_t* data, uint8_t len) {  switch (cmd) {    case CMD_STOP_MOTORS:      stopMotors();      sendTotalSteps(totalStepsTaken);      resetNavigationState();      break;
    case CMD_ACTUAL_BEARING:      if (len == 2) {        actualBearing100 = readUInt16LE(data);        Serial.print(F("RX: ACTUAL BEARING x100 = "));        Serial.println(actualBearing100);      }      break;
    case CMD_RIGHT_PIVOT:      if (len == 2 && !navRunning) {        uint16_t steps = readUInt16LE(data);        Serial.print(F("RX: RIGHT PIVOT steps="));        Serial.println(steps);        beginPivot(PIVOT_RIGHT, steps);      }      break;
    case CMD_LEFT_PIVOT:      if (len == 2 && !navRunning) {        uint16_t steps = readUInt16LE(data);        Serial.print(F("RX: LEFT PIVOT steps="));        Serial.println(steps);        beginPivot(PIVOT_LEFT, steps);      }      break;
    case CMD_WAYPOINT_DATA:      if (!navRunning) {        handleWaypointPacket(data, len);      }      break;  }}
// =========================// UART parser// =========================void processIncomingByte(uint8_t b) {  switch (parserState) {    case WAIT_HEADER:      if (b == UART_HEADER) parserState = WAIT_CMD;      break;
    case WAIT_CMD:      currentCmd = b;      runningChecksum = b;      parserState = WAIT_LEN;      break;
    case WAIT_LEN:      expectedLen = b;      runningChecksum ^= b;      if (expectedLen > MAX_PAYLOAD) {        parserState = WAIT_HEADER;      } else if (expectedLen == 0) {        parserState = WAIT_CHECKSUM;      } else {        payloadIndex = 0;        parserState = WAIT_PAYLOAD;      }      break;
    case WAIT_PAYLOAD:      payload[payloadIndex++] = b;      runningChecksum ^= b;      if (payloadIndex >= expectedLen) {        parserState = WAIT_CHECKSUM;      }      break;
    case WAIT_CHECKSUM:      if (b == runningChecksum) {        handlePacket(currentCmd, payload, expectedLen);      } else {        Serial.println(F("Checksum error"));      }      parserState = WAIT_HEADER;      runningChecksum = 0;      currentCmd = 0;      expectedLen = 0;      payloadIndex = 0;      break;  }}
void readIncomingUart() {  while (Serial3.available() > 0) {    processIncomingByte((uint8_t)Serial3.read());  }}
// =========================// Arduino entry points// =========================void setup() {  delay(1000); // give USB/serial time to settle
  Serial.begin(115200);  Serial3.begin(115200);
  LeftFrontWheel.setMaxSpeed(2000);  LeftBackWheel.setMaxSpeed(2000);  RightFrontWheel.setMaxSpeed(2000);  RightBackWheel.setMaxSpeed(2000);
  LeftFrontWheel.setAcceleration(800);  LeftBackWheel.setAcceleration(800);  RightFrontWheel.setAcceleration(800);  RightBackWheel.setAcceleration(800);
  LeftFrontWheel.setSpeed(0);  LeftBackWheel.setSpeed(0);  RightFrontWheel.setSpeed(0);  RightBackWheel.setSpeed(0);
  multiStepper.addStepper(LeftFrontWheel);  multiStepper.addStepper(LeftBackWheel);  multiStepper.addStepper(RightFrontWheel);  multiStepper.addStepper(RightBackWheel);
  resetNavigationState();  resetPivotState();  clearIncomingUart();
  Serial.println(F("ARDUINO_v10 ready"));}
void loop() {  readIncomingUart();
  if (navRunning) {    runForwardLoop();  } else {    updatePivotLoop();  }}
Author Avatar
PRO
Canada
Twitter YouTube GitHub
#13  

Sorry, I can't continue helping in this thread. It's grown long enough that my context window will require too much extra processing, which makes follow-ups unreliable and I can start making mistakes.

A couple of options:

  • Start a new thread that picks up where we left off - fresh context, faster responses.
  • Or get unlimited interaction with me through ARC's built-in AI Assistant agent: Synthiam AI Assistant
I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#14  

Thank you @Athena for your wonderful help. 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.