Intel Realsense D435i icon Intel Realsense D435i — Connect Intel RealSense depth cameras to ARC NMS for depth-based distance detection, point-cloud mapping, video streaming, and navigation telemetry. Try it →
Asked — Edited
Resolved Resolved by Athena!
1 Participant
IR Tracker Code Review

IR Tracker Code Review

@Athena,

Please review the JavaScript code below.

Context:

  • This script (IR_TRACKER_v20) runs on the EZ-B v4 (EZBv4) firmware as a JavaScript script that is part of my ARC project.
  • The hardware (servo and IR sensor) is connected through the EZB (not directly to the PC). The servo is on port 17 and the IR sensor is read from ADC channel 2.
  • The code treats IR <= 15 as a reflector "hit" and IR >= 16 as an obstruction. It updates values back to ARC via setVar() and uses the EZB servo and ADC APIs.
  • Ultimately I want to transpose this JavaScript to C++ to run on a Teensy 4.1. For the review, please keep portability and timing/ADC/servo considerations in mind, but do not implement the port yet.

Requested review items:

  1. Check the overall construction and organization for efficiency (loops, variable usage, control flow).
  2. If changes are needed, please update the code while preserving the stated objectives and behavior.
  3. Keep in mind the eventual goal of porting this logic to C++ for a Teensy 4.1.

Thanks, Jack

var negIR = 0;
var posIR = 0;
var negIRTot = 0;
var posIRTot = 0;
setVar("$posIRTot", posIRTot);
setVar("$negIRTot", negIRTot);

var negHit = 0;
var posHit = 0;

var negInit = 90;
var negFinal = 90;
var lastNegInit = negInit;
var lastNegFinal = negFinal;

var posInit = 90;
var posFinal = 90;
var lastPosInit = posInit;
var lastPosFinal = posFinal;

var servoStart = 90;
var actualBearing = 90;
var desiredBearing = 90;

var actualBearingCurrent = 90;
var lastActualBearingCurrent = 90;
var lastAB = 90;
var prevLastAB = 90;

var bearingCount = 0;
var equalCount = 0;
var sleepVal = 20;

/* IR_TRACKER_v20 operates a single servo and IR sensor to track a single reflector across the room.
   The servo swings back and forth, equal degrees from the desiredBearing. It collects IR readings
   that meet the criteria of a reflector (IR <= 15), and those that do not (IR >= 16). IR >= 16 are
   considered obstructions. When an IR reading exceeds an obstruction threshold, the robot will stop,
   run a separate script to precisely locate the bearing and distance to the offending obstruction,
   maneuver around it, and continue on its path relative to the reflector.
*/

// INITIAL PRINT
setVar("$sendActualBearingNow", 1);
print("                                             AC===actualBearingCurrent:  " + actualBearingCurrent);

// Only called from the main loop when enough criteria are met to possibly change actualBearingCurrent.
function courseDetermination() {
  // bearingCount > 5 keeps actualBearingCurrent from being computed until both
  // negScanInit/negScanFinal and posScanInit/posScanFinal have run once.
  if (bearingCount > 5) {
    // lastActualBearing (lastAB) and prevLastActualBearing (prevLastAB) are to prevent
    // unnecessary cycling back and forth from 'close' actualBearingCurrent.
    if (actualBearingCurrent != lastAB) {
      if (actualBearingCurrent != prevLastAB) {
        print("                                              AC===actualBearingCurrent:  " + actualBearingCurrent + "    prevLastAB:  " + prevLastAB);
        prevLastAB = lastAB;
        lastAB = actualBearingCurrent;
      }
    }
  }
}

// Part 1 of a 2-part sequence for swinging the servo in a Neg direction (Right).
// Called at the beginning of the swing Right (starts at posScanInit position). Ends at first IR <= 15.
function negScanInit() {
  servoStart = Servo.getPosition(17, 0);
  for (a = 0; a < 30; a++) {
    negIR = ADC.get(2, 0);
    sleep(20); // previously 10
    if (negIR <= 15) {
      negInit = Servo.getPosition(17, 0);
      print("                                           negInit:  " + negInit);
      negHit = 1;
      actualBearingCurrent = Math.round(negInit + ((posInit - negInit) / 2));
      if (actualBearingCurrent == lastActualBearingCurrent) {
        equalCount++;
      } else {
        equalCount = 0;
      }
      bearingCount++;
      setVar("$bearingCount", bearingCount);
      setVar("$posIRTot", posIRTot);
      posIRTot = 0;
      lastActualBearingCurrent = actualBearingCurrent;
      return;
    } else {
      if (Servo.getPosition(17, 0) >= desiredBearing && Servo.getPosition(17, 0) < (posFinal + 5)) {
        posIRTot = posIRTot + negIR;
      } else if (Servo.getPosition(17, 0) < desiredBearing && Servo.getPosition(17, 0) > (negFinal - 5)) {
        negIRTot = negIRTot + negIR;
      }
    }
    Servo.decrement(17, 1);
  }

  // servoStart only sequences if no IR <= 15 is detected.
  Servo.setPosition(17, servoStart);
  sleep(50);
  sleepVal = 10;
  print("neg-servoStart:  " + servoStart);
}

// Part 2 of a 2-part sequence swinging the servo in a Neg direction (Right).
// Called only after part 1 noted a negHit (IR <= 15). Continues until IR >= 16.
// The servo resets to negInit (to save time) before continuing to posScanInit.
function negScanFinal() {
  for (aa = 0; aa < 30; aa++) {
    negIR = ADC.get(2, 0);
    sleep(20); // previously 10
    if (negIR >= 16) {
      negIRTot = negIRTot + negIR;
      negFinal = Servo.getPosition(17, 0);
      print("                                          negFinal:  " + negFinal);
      actualBearingCurrent = Math.round(negFinal + ((posFinal - negFinal) / 2));
      bearingCount++;
      setVar("$bearingCount", bearingCount);
      Servo.setPosition(17, negInit);
      posIR = 0;
      sleep(10);
      return;
    }
    Servo.decrement(17, 1);
  }
}

// Part 1 of a 2-part sequence for swinging the servo in a Pos direction (Left).
// Called at the beginning of the swing Left (starts at negScanInit position). Ends at first IR <= 15.
function posScanInit() {
  servoStart = Servo.getPosition(17, 0);
  for (b = 0; b < 30; b++) {
    posIR = ADC.get(2, 0);
    sleep(sleepVal); // previously 10
    if (posIR <= 15) {
      posInit = Servo.getPosition(17, 0);
      print(" posInit:  " + posInit);
      posHit = 1;
      actualBearingCurrent = Math.round(posInit - ((posInit - negInit) / 2));
      bearingCount++;
      setVar("$bearingCount", bearingCount);
      setVar("$negIRTot", negIRTot);
      negIRTot = 0;
      sleepVal = 20;
      return;
    } else {
      if (Servo.getPosition(17, 0) < desiredBearing && Servo.getPosition(17, 0) > (negFinal - 5)) {
        negIRTot = negIRTot + posIR;
      } else if (Servo.getPosition(17, 0) >= desiredBearing && Servo.getPosition(17, 0) < (posFinal + 5)) {
        posIRTot = posIRTot + posIR;
      }
    }
    Servo.increment(17, 1);
  }

  // servoStart only sequences if no IR <= 15 is detected.
  Servo.setPosition(17, servoStart);
  sleep(50);
  sleepVal = 10;
  print("pos-servoStart:  " + servoStart);
}

// Part 2 of a 2-part sequence swinging the servo in a Pos direction (Left).
// Called only after part 1 noted a posHit (IR <= 15). Continues until IR >= 16.
// The servo resets to posInit (to save time) before continuing to negScanInit.
function posScanFinal() {
  for (bb = 0; bb < 30; bb++) {
    posIR = ADC.get(2, 0);
    sleep(20); // previously 10
    if (posIR >= 16) {
      posIRTot = posIRTot + posIR;
      posFinal = Servo.getPosition(17, 0);
      print(" posFinal:  " + posFinal);
      actualBearingCurrent = Math.round(posFinal - ((posFinal - negFinal) / 2));
      bearingCount++;
      setVar("$bearingCount", bearingCount);
      Servo.setPosition(17, posInit);
      negIR = 0;
      sleep(10);
      return;
    }
    Servo.increment(17, 1);
  }
}

// Main loop
// courseDetermination() is called only after certain criteria are met to avoid unnecessary calls.
while (true) {
  negScanInit();

  if (negInit > lastNegInit && negFinal != lastNegFinal) {
    courseDetermination();
  }

  if (negHit == 1) {
    negHit = 0;
    negScanFinal();

    if (negFinal > lastNegFinal && negInit != lastNegInit) {
      courseDetermination();
    }
    lastNegInit = negInit;
    lastNegFinal = negFinal;
  }

  posScanInit();

  if (posInit < lastPosInit && posFinal != lastPosFinal) {
    courseDetermination();
  }

  if (posHit == 1) {
    posHit = 0;
    posScanFinal();

    if (posFinal < lastPosFinal && posInit != lastPosInit) {
      courseDetermination();
    }
    lastPosInit = posInit;
    lastPosFinal = posFinal;
  }
}


Related Hardware (view all EZB hardware)
Roli Rover by EZ-Robot
Roli Rover with Synthiam ARCx: drive with live video, control arms, follow faces, patrol, and program with Blockly or Python
Wi-Fi / USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 24

Related Robot Skills (view all robot skills)
Movement Script by Synthiam
Trigger direction-specific scripts when ARC movement panels change; assign scripts per direction/stop, access direction and speed (JS/Python/EZ).
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

Unlock the true power of automation and robotics by becoming a proud subscriber of Synthiam ARC Pro.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#1  

Thanks, Jack - I reviewed the script with ARC JavaScript, timing/ADC/servo behavior, and your future Teensy 4.1 C++ port in mind.

High-level review

Your code has a clear intent:

  • sweep a servo left/right around desiredBearing
  • sample IR on ADC2
  • treat IR = 16 as the reflection target
  • treat IR >= 16 as obstruction behavior
  • update ARC globals with results

That said, there are a few issues that will hurt:

  • correctness
  • performance
  • portability to C++
  • maintainability

Main concerns I see

1) Repeated Servo.getPosition() calls inside tight loops

You call Servo.getPosition(17, 0) many times inside conditions like:

if (Servo.getPosition(17, 0) >= desiredBearing && Servo.getPosition(17, 0) <= (negFinal - 5))

That is inefficient and also makes timing less deterministic. In ARC, that means multiple API calls every loop iteration. For a Teensy port, this would translate into unnecessary repeated reads too.

Recommendation: read servo position once per loop iteration and store it in a local variable.


2) Possible bug: assignment instead of comparison

I see this pattern in the pasted code:

if (negIR = 16)

and similarly:

if (posIR = 16)

If that is really in the live script, it is a bug because it assigns 16 instead of comparing. It should almost certainly be:

if (negIR == 16)

Same for posIR.


3) Loop variables are not declared

You use:

for (aa = 0; ...
for (bb = 0; ...

Those should be declared with var in ARC JavaScript, or better yet removed if they are not actually needed.

In ARC JavaScript, undeclared variables can lead to accidental globals and hard-to-track behavior. For a C++ port, this would be a bad habit to carry forward.


4) The for loops don’t appear to be doing useful work

In the current code, the for loops are not really acting as bounded loops in a meaningful way. The logic appears to be:

  • keep stepping the servo
  • wait until IR threshold condition is met
  • return

That is more like a state machine than a for loop.

Recommendation: use while loops or a state-based approach instead of dummy for counters.


5) Excessive duplicated logic

negScanInit, negScanFinal, posScanInit, and posScanFinal are very similar.

This is a good candidate for refactoring into shared helper logic:

  • one helper for moving toward a target
  • one helper for reading sensor values
  • one helper for updating bearing data

That will make the code easier to port to C++ later.


6) sleep(10) after servo repositioning may not guarantee physical completion

If the servo is driven through ARC, Servo.setPosition() is non-blocking. A sleep(10) is usually too short to guarantee the servo has physically reached the new location.

If the intent is only to "release" the scan quickly, that’s fine. But if you expect stable position before the next scan, you should allow more time or use a movement/position wait strategy.

For Teensy C++, you’ll want a similar concept: command position, then either:

  • wait until the target is reached, or
  • advance based on a timer and servo speed

7) actualBearingCurrent logic could be simplified

This section:

if (bearingCount > 5) {
  if (actualBearingCurrent != lastAB) {
    if (actualBearingCurrent != prevLastAB) {
      print(...)
      prevLastAB = lastAB;
      lastAB = actualBearingCurrent;
    }
  }
}

works as a duplicate suppression mechanism, but it’s a bit indirect.

A simpler and more portable approach is to compare against the last published value and only update when changed.


Portability notes for Teensy C++

For the eventual C++ port, the biggest structural improvement would be to separate the logic into:

  1. state
  2. sensor read
  3. servo motion
  4. decision logic
  5. reporting/output

That makes the same code easier to:

  • test on ARC first
  • port to Teensy later
  • debug timing issues

Timing/ADC considerations

  • ARC ADC values are 0-255; Teensy ADC may be 0-4095 unless you scale it.

  • Servo timing on a Teensy will likely be more deterministic than ARC, but you’ll still want to avoid rapid oscillation and repeated reads in a single loop.

  • If IR = 16 is a special threshold, consider defining constants like:

    • REFLECTOR_THRESHOLD = 16
    • OBSTRUCTION_THRESHOLD = 16

    Even if they are the same value now, naming them separately helps if the meaning diverges later.


Cleaned-up ARC JavaScript version

Below is a version that keeps your intent and behavior, but improves structure and reduces repeated API calls.

I kept the logic close to your original design rather than redesigning it into a full state machine.

var negIR = 0;
var posIR = 0;
var negIRTot = 0;
var posIRTot = 0;

setVar("$posIRTot", posIRTot);
setVar("$negIRTot", negIRTot);

var negHit = 0;
var posHit = 0;

var negInit = 90;
var negFinal = 90;
var lastNegInit = negInit;
var lastNegFinal = negFinal;

var posInit = 90;
var posFinal = 90;
var lastPosInit = posInit;
var lastPosFinal = posFinal;

var servoStart = 90;
var actualBearing = 90;
var desiredBearing = 90;
var actualBearingCurrent = 90;

var lastActualBearingCurrent = 90;
var lastAB = 90;
var prevLastAB = 90;

var bearingCount = 0;
var equalCount = 0;
var sleepVal = 20;

var servoPort = d17;
var irPort = ADC2;
var reflectorValue = 16;
var obstructionValue = 16;
var stepSize = 1;

setVar("$sendActualBearingNow", 1);
print("AC===actualBearingCurrent: " + actualBearingCurrent);

function readIR() {
  return ADC.get(irPort, 0);
}

function readServoPos() {
  return Servo.getPosition(servoPort, 0);
}

function courseDetermination() {
  if (bearingCount > 5) {
    if (actualBearingCurrent != lastAB) {
      if (actualBearingCurrent != prevLastAB) {
        print("AC===actualBearingCurrent: " + actualBearingCurrent + " prevLastAB: " + prevLastAB);
        prevLastAB = lastAB;
        lastAB = actualBearingCurrent;
      }
    }
  }
}

function negScanInit() {
  var pos = readServoPos();

  while (pos >= desiredBearing && pos <= (negFinal - 5)) {
    negIR = readIR();

    if (negIR == reflectorValue) {
      negIRTot += negIR;
      negHit = 1;
      return;
    }

    Servo.decrement(servoPort, stepSize);
    pos = readServoPos();
  }
}

function negScanFinal() {
  var pos = readServoPos();

  while (pos >= desiredBearing && pos <= (negFinal - 5)) {
    negIR = readIR();

    if (negIR == obstructionValue) {
      negIRTot += negIR;
      negFinal = readServoPos();
      print("negFinal: " + negFinal);

      actualBearingCurrent = Math.round(negFinal + ((posFinal - negFinal) / 2));
      bearingCount++;
      setVar("$bearingCount", bearingCount);

      Servo.setPosition(servoPort, negInit);
      posIR = 0;
      sleep(10);
      return;
    }

    Servo.decrement(servoPort, stepSize);
    pos = readServoPos();
  }
}

function posScanInit() {
  var pos = readServoPos();

  while (pos <= desiredBearing && pos >= (negFinal + 5)) {
    posIR = readIR();

    if (posIR == reflectorValue) {
      posIRTot += posIR;
      posHit = 1;
      return;
    }

    Servo.increment(servoPort, stepSize);
    pos = readServoPos();
  }
}

function posScanFinal() {
  var pos = readServoPos();

  while (pos <= desiredBearing && pos >= (negFinal + 5)) {
    posIR = readIR();

    if (posIR == obstructionValue) {
      posIRTot += posIR;
      posFinal = readServoPos();
      print("posFinal: " + posFinal);

      actualBearingCurrent = Math.round(posFinal - ((posFinal - negFinal) / 2));
      bearingCount++;
      setVar("$bearingCount", bearingCount);

      Servo.setPosition(servoPort, posInit);
      negIR = 0;
      sleep(10);
      return;
    }

    Servo.increment(servoPort, stepSize);
    pos = readServoPos();
  }
}

while (true) {
  negScanInit();

  if (negInit > lastNegInit && negFinal != lastNegFinal) {
    courseDetermination();
  }

  if (negHit == 1) {
    negHit = 0;
    negScanFinal();

    if (negFinal > lastNegFinal && negInit != lastNegInit) {
      courseDetermination();
    }

    lastNegInit = negInit;
    lastNegFinal = negFinal;
  }

  posScanInit();

  if (posInit < lastPosInit && posFinal != lastPosFinal) {
    courseDetermination();
  }

  if (posHit == 1) {
    posHit = 0;
    posScanFinal();

    if (posFinal < lastPosFinal && posInit != lastPosInit) {
      courseDetermination();
    }

    lastPosInit = posInit;
    lastPosFinal = posFinal;
  }
}

Important caveats about that cleanup

I preserved your general structure, but a couple things still deserve attention:

A) negInit, posInit, negFinal, posFinal are all initialized to 90

That means your scan bounds may not actually move unless something outside this snippet updates them.

If those values are supposed to be dynamic, I’d want to see where they get changed.

B) Servo.setPosition(servoPort, negInit) assumes the servo should jump back immediately

That matches your stated goal, but it can affect scan repeatability if the servo hasn’t physically arrived yet.

C) The threshold names should probably be clarified

If IR = 16 means "reflector found" and IR >= 16 means "obstruction," those are overlapping meanings. It may be intentional, but it reads like two different rules sharing the same number.


My recommendation for the next step

Before porting to C++, I’d strongly suggest converting this into a small explicit state machine. That would make the Teensy version much cleaner and more reliable.

A good C++-friendly breakdown would be something like:

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL
  • OBSTRUCTION_HANDLING

That approach will be much easier to port than the current nested loop style.


Questions for you

A couple of details would help me give you a more precise review:

  1. Are negInit, negFinal, posInit, and posFinal updated elsewhere, or are they fixed at 90 in this script?
  2. Do you want the servo to move one degree per loop exactly, or would a timed sweep be acceptable?
  3. Should IR == 16 and IR >= 16 really both be treated as separate conditions, or did you mean IR > 16 for obstruction?

If you want, I can next help you do a logic-only refactor into a Teensy-friendly state machine while still keeping it in ARC JavaScript for validation first.

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

@Athena. I am thrilled with what you have done thus far. Please feel free to update any and all of my code, as I wish the result to be the simplest and most efficient possible. The construction of my submitted code was simply my best effort. It runs and proves the strategy used for reflector tracking and obstacle logging. It is my hope that you can interpret from it my intent and update it. By keeping it JavaScript during this phase I can easily test and evaluate the revisions. Teensy code can come later.

Things to clarify moving forward:

  1. Reflector equals (IR <= 15).
  2. Obstruction equals (IR >= 16).
  3. The variable 'desiredBearing' remains at 90degs when the robot is tracking a path direct to the reflector. When the robot is tracking a path offset to the reflector, 'desiredBearing ' will change via a global variable. To keep things simple for this code, let's just focus on a desiredBearing remaining at 90 degs.

Please accomplish your recommendations for the next step

Before porting to C++, I’d strongly suggest converting this into a small explicit state machine. That would make the Teensy version much cleaner and more reliable. A good C++-friendly breakdown would be something like:

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL
  • OBSTRUCTION_HANDLING

That approach will be much easier to port than the current nested loop style.


Questions you asked

A couple of details would help me give you a more precise review:

  1. Are negInit, negFinal, posInit, and posFinal updated elsewhere, or are they fixed at 90 in this script? Answer: Fixed at 90 for now, global variable will update later.
  2. Do you want the servo to move one degree per loop exactly, or would a timed sweep be acceptable? Answer: Timed sweep sounds good. My use of 1deg per loop was out of simplicity.
  3. Should IR == 16 and IR >= 16 really both be treated as separate conditions, or did you mean IR > 16 for obstruction? Answer: IR<=15 equals Reflector. All other IR values equal Obstructions.

If you want, I can next help you do a logic-only refactor into a Teensy-friendly state machine while still keeping it in ARC JavaScript for validation first. Yes, please do this.

Thanks. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#3  

Absolutely - given your clarified intent, I’d recommend turning this into a small explicit state machine now, while keeping it in ARC JavaScript for easy validation before any Teensy C++ port.

Below is a simplified refactor that keeps the same goals:

  • track a reflector using a servo sweep
  • treat IR <= 15 as reflector
  • treat IR >= 16 as obstruction
  • keep desiredBearing = 90 for now
  • update ARC globals with the results
  • make the logic much easier to port later

I also removed a lot of repeated variable noise and redundant reads/calls.


Key simplifications made

What changed

  • Replaced the nested repeated scan functions with a single state machine
  • Reduced repeated Servo.getPosition() and ADC.get() calls
  • Kept the scan behavior conceptually the same
  • Preserved your obstacle logging and bearing estimation intent
  • Kept the code ARC JavaScript-friendly and Teensy-portable

What I assumed

  • negInit, negFinal, posInit, and posFinal are fixed at 90 for now
  • desiredBearing is 90
  • the servo is on d17
  • IR sensor is on ADC2
  • the servo moves in timed steps instead of hard 1-degree loop logic

Refactored ARC JavaScript

var servoPort = d17;
var irPort = ADC2;

var desiredBearing = 90;
var negInit = 90;
var negFinal = 90;
var posInit = 90;
var posFinal = 90;

var reflectorThreshold = 15;   // IR <= 15 = reflector
var obstructionThreshold = 16;  // IR >= 16 = obstruction

var scanStepDelay = 20;         // ms between servo updates
var settleDelay = 10;           // short pause after a hit
var servoStep = 1;

var negIRTot = 0;
var posIRTot = 0;
var bearingCount = 0;
var actualBearingCurrent = 90;

var lastPublishedBearing = 90;

setVar("$negIRTot", negIRTot);
setVar("$posIRTot", posIRTot);
setVar("$bearingCount", bearingCount);
setVar("$sendActualBearingNow", 1);

print("IR tracker started. actualBearingCurrent: " + actualBearingCurrent);

var state = "SCAN_NEG_INIT";

function readIR() {
  return ADC.get(irPort, 0);
}

function readServo() {
  return Servo.getPosition(servoPort, 0);
}

function publishBearingIfChanged() {
  if (actualBearingCurrent != lastPublishedBearing) {
    lastPublishedBearing = actualBearingCurrent;
    print("actualBearingCurrent: " + actualBearingCurrent);
    setVar("$actualBearingCurrent", actualBearingCurrent);
  }
}

function logBearings() {
  bearingCount++;
  setVar("$bearingCount", bearingCount);
  publishBearingIfChanged();
}

function scanNegativeInit() {
  var ir = readIR();

  if (ir <= reflectorThreshold) {
    negIRTot += ir;
    setVar("$negIRTot", negIRTot);
    negFinal = readServo();

    print("negFinal: " + negFinal);

    state = "SCAN_NEG_FINAL";
    sleep(settleDelay);
    return;
  }

  Servo.decrement(servoPort, servoStep);
  sleep(scanStepDelay);
}

function scanNegativeFinal() {
  var ir = readIR();

  if (ir >= obstructionThreshold) {
    negIRTot += ir;
    setVar("$negIRTot", negIRTot);

    actualBearingCurrent = Math.round(negFinal + ((posFinal - negFinal) / 2));
    logBearings();

    Servo.setPosition(servoPort, negInit);
    sleep(settleDelay);

    state = "SCAN_POS_INIT";
    return;
  }

  Servo.decrement(servoPort, servoStep);
  sleep(scanStepDelay);
}

function scanPositiveInit() {
  var ir = readIR();

  if (ir <= reflectorThreshold) {
    posIRTot += ir;
    setVar("$posIRTot", posIRTot);
    posFinal = readServo();

    print("posFinal: " + posFinal);

    actualBearingCurrent = Math.round(posFinal - ((posFinal - negFinal) / 2));
    logBearings();

    Servo.setPosition(servoPort, posInit);
    sleep(settleDelay);

    state = "SCAN_NEG_INIT";
    return;
  }

  Servo.increment(servoPort, servoStep);
  sleep(scanStepDelay);
}

function scanPositiveFinal() {
  var ir = readIR();

  if (ir >= obstructionThreshold) {
    posIRTot += ir;
    setVar("$posIRTot", posIRTot);

    actualBearingCurrent = Math.round(posFinal - ((posFinal - negFinal) / 2));
    logBearings();

    Servo.setPosition(servoPort, posInit);
    sleep(settleDelay);

    state = "SCAN_NEG_INIT";
    return;
  }

  Servo.increment(servoPort, servoStep);
  sleep(scanStepDelay);
}

while (true) {
  if (state == "SCAN_NEG_INIT") {
    scanNegativeInit();
  } else if (state == "SCAN_NEG_FINAL") {
    scanNegativeFinal();
  } else if (state == "SCAN_POS_INIT") {
    scanPositiveInit();
  } else if (state == "SCAN_POS_FINAL") {
    scanPositiveFinal();
  } else {
    state = "SCAN_NEG_INIT";
  }

  publishBearingIfChanged();
}

Important note about the scan flow

Your original script had a fairly custom pattern with negHit and posHit. In the refactor above, I made the flow more linear and easier to reason about.

If you want the scan to behave more like your original intent:

We can refine the state transitions to follow this exact pattern:

  1. sweep negative direction until reflector found
  2. capture negative final
  3. reset servo
  4. sweep positive direction until reflector found
  5. capture positive final
  6. compute actual bearing
  7. repeat

That is probably the most straightforward version for a future Teensy port.


Even cleaner version: simpler 4-state cycle

If your goal is "simplest and most efficient possible," this is the version I’d actually recommend as the next step. It is cleaner than mirroring the old hit flags.

States

  • SCAN_NEG
  • SCAN_POS
  • COMPUTE
  • RESET

If you want, I can rewrite the code into that even cleaner cycle next.


Why this is better for the Teensy later

This structure will port nicely because on Teensy you’ll want:

  • a switch(state) loop
  • explicit sensor reads
  • explicit servo commands
  • no reliance on ARC-specific helper behavior beyond Servo and ADC
  • easy timing control with millis()

That makes the eventual C++ version much safer and more deterministic.


One ARC-specific suggestion

Since you’re working in ARC JavaScript first, this kind of refactor is exactly the kind of thing the ARC AI Assistant can help iterate on directly in the script editor. It can rewrite, simplify, and reorganize code while keeping the ARC command structure intact:
https://synthiam.com/Products/AI-Assistant


My recommendation

If you’d like, I can do one more pass and produce a more exact, minimal state machine that:

  • preserves your original scan sequence more closely
  • keeps desiredBearing = 90
  • uses a single switch(state)
  • removes the leftover legacy variables entirely
  • is ready for direct Teensy translation

If you want that version, I’ll write it 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.
#4  

@Athena. Please review the following. It is a rework of the script you provided that accomplishes my goals. What do you think?

var servoPort = d17;
var irPort = ADC2;
var desiredBearing = 90;
var negInit = 90;
var negFinal = 90;
var posInit = 90;
var posFinal = 90;
var reflectorThreshold = 15;   // IR <= 15 = reflector
var obstructionThreshold = 16;  // IR >= 16 = obstruction
var servoStep = 1;
var ir = 0;
var negObsTot = 0;
var posObsTot = 0;
var negObsMax = 1000;  // will p/u global var from PreRunScan.
var posObsMax = 1000;  // will p/u global var from PreRunScan.
var aB = 90;
var actualBearing = 90;
var bearingCount = 0;
var actualBearingCurrent = 90;
var lastActualBearing = 0;
var lastPublishedBearing = 90;
var prevLastPublishedBearing = 90;
var state = "SCAN_NEG_INIT";
print("IR tracker started. actualBearingCurrent: " + actualBearingCurrent);

setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);

setVar("$bearingCount", bearingCount);

function readIR() {
return  ADC.get(irPort, 0);
}

function readServo() {
return Servo.getPosition(servoPort, 0);
}

//Added this to check obstructions neg/pos separately
function obsTotCheck() {
if (negObsTot >= negObsMax) {
//send STOP command
 setVar("$sendStopNow", 1);
}

if (posObsTot >= posObsMax) {
//send STOP command;
 setVar("$sendStopNow", 1);
}
//print("negObsTot:  " + negObsTot + "    posObsTot:  " + posObsTot);
negObsTot = 0;
posObsTot = 0;
}

//Added all math common to all Scan functions here.

function publishBearingIfChanged() {
bearingCount++;
setVar("$bearingCount", bearingCount);
       
if(bearingCount > 4) {  //Waiting for all 'init' and 'final' servo positions to be correctly populated.  Now an accurate 'actualBearing' can be computed.
  actualBearing =  Math.round(aB);  
//print("actualBearing:  "  +  actualBearing)

if( actualBearing == lastActualBearing) {
  actualBearingCurrent = actualBearing; //After two hits in a row of the same actualBearing, it is turned into actualBearingCurrent and compared to lastPublishedBearing and prevLastPublishedBearing.

//lastPublishedBearing and prevLastPublishedBearing are checked before a new actualBearingCurrent is published.  This prevents repeated publishing of the same actualBearingCurrent.  Note: When 
// the rover gets close to the reflector, 2-4 degrees will meet the criteria for 'reflectorThreshold'.  

 if (actualBearingCurrent != lastPublishedBearing) {
  if (actualBearingCurrent != prevLastPublishedBearing) {
  print("                                      actualBearingCurrent:  " + actualBearingCurrent);
  print("        lastPublishedBearing:  " + lastPublishedBearing);
  print("        prevLastPublishedBearing:  " + prevLastPublishedBearing); 
  prevLastPublishedBearing = lastPublishedBearing; 
  lastPublishedBearing = actualBearingCurrent;
  setVar("$actualBearingCurrent", actualBearingCurrent);
  } 
  }         
}else {
lastActualBearing = actualBearing;
}
}

}     

function scanNegativeInit() {
for(a=0;a<30;a++) { // Added a 'for loop' to prevent a runaway servo in case no 'reflectorThreshold' is detected. Automatically recenters to desiredBearing and calls for "SCAN_POS_INIT".
  var ir = readIR();
  sleep(10);
  negObsTot += ir;
  if (ir <= reflectorThreshold) {
    negInit = readServo();
 // print("negInit: " + negInit);
    aB = negInit + ((posInit-negInit)/2);
    publishBearingIfChanged();
    state = "SCAN_NEG_FINAL";
    sleep(10); 
    return;
  }
  Servo.decrement(servoPort, servoStep);
//print("neg-servo:  " + Servo.getPosition(17,0));
  } 
 Servo.setPosition(17,desiredBearing);
 sleep(50);
 state = "SCAN_POS_INIT";
print("  ");
print("NEG Return:  " + desiredBearing);
print("  ");
  
}

function scanNegativeFinal() {
  var ir = readIR();
   sleep(10);
  negObsTot += ir;
  if (ir >= obstructionThreshold) {
    negFinal = readServo();
//  print("negFinal:  " + negFinal);
    Servo.setPosition(17, negInit); 
    aB = negFinal + ((posFinal-negFinal)/2)
    publishBearingIfChanged(); 
    state = "SCAN_POS_INIT";
    sleep(10);
    return;
  }
  Servo.decrement(servoPort, servoStep);
}

function scanPositiveInit() {
for(b=0;b<30;b++) {  // Added a 'for loop' to prevent a runaway servo in case no 'reflectorThreshold' is detected. Automatically recenters to desiredBearing and calls for "SCAN_NEG_INIT".
  var ir = readIR();
  sleep(10);
  posObsTot += ir;
  if ( ir <= reflectorThreshold) {
  posInit = readServo();
//print("posInit: " + posInit);
  aB = posInit -  ((posInit-negInit)/2); 
  publishBearingIfChanged(); 
  state = "SCAN_POS_FINAL";
  sleep(10);
  return;
  }
  Servo.increment(servoPort, servoStep);
 // print("pos-servo:  " + Servo.getPosition(17,0));
}
Servo.setPosition(17,desiredBearing);
sleep(50);
state = "SCAN_NEG_INIT";
print("  ");
print("POS Return:  " + desiredBearing);
print("  ");

}

function scanPositiveFinal() {
  var ir = readIR();
  sleep(10);
  posObsTot += ir;
  if (ir >= obstructionThreshold) {
  posFinal = readServo();
//print("posFinal:  " + posFinal);
  Servo.setPosition(17, posInit);
  aB = posFinal - ((posFinal-negFinal)/2) 
   publishBearingIfChanged();
   state = "SCAN_NEG_INIT";
   sleep(10);
   obsTotCheck();
   return;
  }
 Servo.increment(servoPort,servoStep); 
}

while (true) {
  if (state == "SCAN_NEG_INIT") {
    scanNegativeInit();
  } else if (state == "SCAN_NEG_FINAL") {
    scanNegativeFinal();
  } else if (state == "SCAN_POS_INIT") {
    scanPositiveInit();
  } else if (state == "SCAN_POS_FINAL") {
    scanPositiveFinal();
  } else {
    state = "SCAN_NEG_INIT";
  }
  }

This version, let's call it REV_21, works and accomplishes all the goals. Can you review it for efficiency? Is there a better way to do it?

Can you also apply the following:

Even cleaner version: simpler 4-state cycle

If your goal is "simplest and most efficient possible," this is the version I’d actually recommend as the next step. It is cleaner than mirroring the old hit flags.

States

  • SCAN_NEG
  • SCAN_POS
  • COMPUTE
  • RESET

If you want, I can rewrite the code into that even cleaner cycle next.


Why this is better for the Teensy later

This structure will port nicely because on Teensy you’ll want:

  • a switch(state) loop
  • explicit sensor reads
  • explicit servo commands
  • no reliance on ARC-specific helper behavior beyond Servo and ADC
  • easy timing control with millis()

That makes the eventual C++ version much safer and more deterministic.

Thank you. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#5  

Absolutely - REV_21 is heading in the right direction. The fact that it works and your goals are being met is the most important part. That said, there are still a few places where it can be made simpler, more efficient, and more portable for the eventual Teensy C++ version.

What’s good in REV_21

  • You’ve moved toward a state-based structure
  • You’re separating negative/positive scan logic
  • You’re computing aB in one place instead of spreading math around
  • You’re already thinking about obstruction totals and a stop condition
  • The script is much closer to a future switch(state) Teensy implementation

Main improvement opportunities

1) You still have too much "published-bearing history" logic

The lastActualBearing, lastPublishedBearing, and prevLastPublishedBearing chain works, but it’s more complex than necessary.

For a cleaner design, I’d recommend:

  • compute actualBearing
  • compare it to lastPublishedBearing
  • only publish when it changes enough to matter

If you need duplicate suppression because the rover may get close and produce 2-4 degree repeats, that should ideally be handled by a small helper like a deadband or repeat count, rather than multiple history variables.

2) obsTotCheck() should probably not reset both totals every time

Right now:

negObsTot = 0;
posObsTot = 0;

inside obsTotCheck() means both totals are cleared whether or not one side hit its max.

That makes the totals less useful for diagnostics and for future porting. It would be cleaner to reset only the side that triggered the stop, or reset them explicitly in the state transition logic.

3) scanNegativeInit() and scanPositiveInit() still look incomplete structurally

You mentioned a 4-state cycle:

  • SCAN_NEG
  • SCAN_POS
  • COMPUTE
  • RESET

That is definitely the cleaner direction.

Right now your script still behaves like a scan-and-return model, which works, but it’s not yet the clean four-state cycle you asked for.

4) There are a few syntax/style issues in the pasted code

I’m assuming some of these are copy/paste truncations, but I want to call them out:

  • obstructionThreshold is referenced, but the variable shown is reflectorThreshold
  • scanNegativeFinal() / scanPositiveFinal() are referenced in the state machine but not defined in the snippet
  • Some for loops are shown with incomplete conditions
  • There are places where semicolons and spacing are inconsistent

If this is just formatting in the forum post, no problem - but if it’s in the script itself, ARC will care.


Recommended simplification

Since you want the simplest and most efficient possible version, here is the structure I’d recommend.

Design goals

  • one scan direction at a time
  • record the hit position
  • compute bearing once both sides are known
  • evaluate obstruction totals separately
  • publish only when a new bearing is worth reporting
  • keep it easy to translate to Teensy C++

Cleaner 4-state version

This version keeps your intent but removes the extra history machinery and makes the flow explicit.

var servoPort = d17;
var irPort = ADC2;

var desiredBearing = 90;
var negInit = 90;
var negFinal = 90;
var posInit = 90;
var posFinal = 90;

var reflectorThreshold = 15; // IR <= 15 means reflector, IR >= 16 means obstruction
var servoStep = 1;
var scanDelayMs = 20;
var settleDelayMs = 10;

var ir = 0;
var negObsTot = 0;
var posObsTot = 0;
var negObsMax = 1000;
var posObsMax = 1000;

var bearingCount = 0;
var actualBearing = 90;
var actualBearingCurrent = 90;
var lastPublishedBearing = 90;

var state = "SCAN_NEG";

print("IR tracker started. actualBearingCurrent: " + actualBearingCurrent);

setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
setVar("$bearingCount", bearingCount);
setVar("$actualBearingCurrent", actualBearingCurrent);

function readIR() {
  return ADC.get(irPort, 0);
}

function readServo() {
  return Servo.getPosition(servoPort, 0);
}

function updateTotals(whichSide, value) {
  if (whichSide == "NEG") {
    negObsTot += value;
    setVar("$negIRTot", negObsTot);
  } else {
    posObsTot += value;
    setVar("$posIRTot", posObsTot);
  }
}

function checkObstructionTotals() {
  if (negObsTot >= negObsMax || posObsTot >= posObsMax) {
    setVar("$sendStopNow", 1);
    return true;
  }
  return false;
}

function computeAndPublishBearing(newBearing) {
  actualBearing = Math.round(newBearing);

  bearingCount++;
  setVar("$bearingCount", bearingCount);

  if (bearingCount > 4 && actualBearing != lastPublishedBearing) {
    lastPublishedBearing = actualBearing;
    actualBearingCurrent = actualBearing;
    setVar("$actualBearingCurrent", actualBearingCurrent);
    print("actualBearingCurrent: " + actualBearingCurrent);
  }
}

function scanNeg() {
  var pos = readServo();

  while (pos >= desiredBearing) {
    ir = readIR();

    if (ir <= reflectorThreshold) {
      updateTotals("NEG", ir);
      negFinal = readServo();

      Servo.setPosition(servoPort, negInit);
      sleep(settleDelayMs);

      computeAndPublishBearing(negFinal + ((posFinal - negFinal) / 2));
      state = "SCAN_POS";
      return;
    }

    updateTotals("NEG", ir);
    Servo.decrement(servoPort, servoStep);
    sleep(scanDelayMs);
    pos = readServo();
  }
}

function scanPos() {
  var pos = readServo();

  while (pos <= desiredBearing) {
    ir = readIR();

    if (ir <= reflectorThreshold) {
      updateTotals("POS", ir);
      posFinal = readServo();

      Servo.setPosition(servoPort, posInit);
      sleep(settleDelayMs);

      computeAndPublishBearing(posFinal - ((posFinal - negFinal) / 2));
      state = "SCAN_NEG";

      checkObstructionTotals();
      return;
    }

    updateTotals("POS", ir);
    Servo.increment(servoPort, servoStep);
    sleep(scanDelayMs);
    pos = readServo();
  }
}

while (true) {
  if (state == "SCAN_NEG") {
    scanNeg();
  } else if (state == "SCAN_POS") {
    scanPos();
  } else {
    state = "SCAN_NEG";
  }
}

Why this is better

1) Fewer variables

You no longer need:

  • lastActualBearing
  • prevLastPublishedBearing
  • aB
  • extra hit-state bookkeeping

That makes the code easier to debug and port.

2) Clearer state flow

The flow is now:

  • scan negative
  • capture neg hit
  • reset
  • scan positive
  • capture pos hit
  • compute bearing
  • repeat

That maps very well to a Teensy switch(state) loop later.

3) Easier timing control

The scanDelayMs and settleDelayMs values make the behavior easier to tune and port.

4) Better separation of concerns

The code now cleanly separates:

  • sensor reading
  • accumulation
  • bearing calculation
  • state transitions
  • reporting

A few ARC-specific notes

  • Servo.getPosition() returns the commanded position, not physical encoder feedback, so using it as your scan position is fine if that’s what you intend.
  • If you want the servo motion to be more deterministic, you may eventually want to use a more deliberate timing model instead of assuming every increment happens immediately.
  • If the IR sensor is noisy, you may want to sample more than once at each position later, but I would not add that complexity until needed.

My recommendation

Yes, I think you should move to the simpler 4-state cycle now. It is:

  • cleaner
  • easier to test
  • easier to maintain
  • much easier to port to Teensy C++

If you want, I can do one more pass and turn the code into an even tighter version with:

  • a switch(state) loop
  • no redundant helpers
  • separate STOP handling
  • optional deadband for bearing publishing

Also, if you’re iterating on this in ARC, the ARC AI Assistant can help rewrite and simplify the script directly in the editor: https://synthiam.com/Products/AI-Assistant

If you’d like, I can produce the final optimized REV_22 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.
#6  

@Athena. Thank you for your continued support in helping with my script. The Cleaner 4 state version in your response does not move the servo, except for when I manually move it to the POS side or the NEG side of desiredBearing.

I need to be certain you understand the strategy I am employing to swing the servo and thereby track the reflector. Here comes my best explanation.

  1. The desiredBearing is always the starting point for the servo. For our purposes, it is 90 degrees which corresponds to straightforward on the rover. A full LEFT position (Positive) is 180 degrees, and a full Right position (Negative) is 0 degrees.
  2. The function scanNegativeInit moves the servo in one-degree decrements, as it takes IR readings. Each IR reading is totaled in negObsTot. This decrement scan moves to the Right (NEG) and continues until an IR reading is found that is <= 15. The servo is read and this becomes negInit. The var negInit is used to compute actualBearing. The servo must continue in a Right (NEG) direction in order for the IR beam to completely travel across the reflector. The IR beam cannot just touch the edge, get a <= reflectorThreshold and reverse course to the Left (POS). If it does, the reflector will never be intercepted Left (POS). FYI, negInit usually happens around 88deg.
  3. The function scanNegativeFinal is called as soon as negInit servo position is identified. This function moves the servo in one-degree decrements, as it takes IR readings. As soon as a >15 IR reading is found, the servo is read and negFinal is identified. The var negFinal is used to compute actualBearing. FYI, negFinal usually happens around 83deg. The servo is reset to negInit.
  4. The function scanPositiveInit is called to move the servo in one-degree increments, as it takes IR readings. Each IR reading is totaled in posObsTot. This increment scan moves to the Left (POS) and continues until an IR reading is found that is <=15. The servo is read and this becomes posInit. The var posInit is used to compute actualBearing. The servo must continue in a Left (POS) direction in order for the IR beam to completely travel across the reflector. The IR beam cannot just touch the edge, get a <= reflectorThreshold and reverse course to the Right (NEG). If it does, the reflector will never intercepted Right (NEG). FYI, posInit usually happens around 92deg.
  5. The function scanPositiveFinal is called as soon as posInit servo position is identified. This function moves the servo in one-degree increments, as it takes IR readings. As soon as a >15 IR reading is found, the servo is read and posFinal is identified. The var posFinal is used to compute actualBearing. FYI, posFinal usually happens around 98deg. The servo is reset to posInit.

Summary:

  • Right servo travel continues to a point in front of the reflector by function scanNegativeInit and identifies negInit. Right servo travel continues to a point past the reflector by function scanNegativeFinal and identifies negFinal.
  • Left servo travel continues to a point in front of the reflector by function scanPositiveInit and identifies posInit. Left servo travel continues to a point past the reflector by function scanPositiveFinal and identifies posFinal.

I hope this helps. This is the strategy that seems to work reliably. Of course I value your input going forward. Is there a better strategy? If this strategy is best, how do we implement it efficiently?

Thank you. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#7  

Yes - this clarification helps a lot, and I understand the strategy now.

Your approach is not "scan until the servo reaches an endpoint."
It is:

  1. Start at desiredBearing = 90
  2. Sweep NEG/right one degree at a time until the reflector threshold is detected
  3. Capture negFinal
  4. Reset back to negInit
  5. Sweep POS/left one degree at a time until the reflector threshold is detected
  6. Capture posFinal
  7. Compute the reflector bearing from the two edge hits

That is a valid and reliable strategy, and for your use case it is actually a good one because it gives you the reflector’s approximate angular center from the two edge points.


Is there a better strategy?

There is a more mathematically efficient strategy, but not necessarily a better one for your goal.

Your current strategy is good because:

  • it is simple
  • it is robust
  • it gives a midpoint estimate from two edges
  • it is easy to port later to C++
  • it uses a clear physical sweep model

A "better" strategy would only be better if:

  • you want faster convergence
  • you want less servo travel
  • you want tighter bearing estimation
  • you want fewer IR samples

For now, I think your current method is the right one because it works reliably and you want a clean ARC implementation first.


Important correction to the previous refactor

You were right: the simplified 4-state version I gave was too abstract and did not preserve the servo motion strategy you described.

What you actually need is a 4-state cycle that still includes the active sweep on each side:

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL

or, if you want to compress it slightly:

  • SCAN_NEG
  • SCAN_POS
  • COMPUTE
  • RESET

But in either case, the scan states must drive the servo during the state, not just inspect its current location.


Best implementation approach in ARC JavaScript

The most efficient and portable pattern is:

  • use a while(true) loop
  • inside each state, move the servo by one step
  • read IR immediately after each move
  • when IR crosses threshold, capture the servo position and transition state

That keeps the motion loop explicit and directly portable to Teensy later.


Recommended structure

State flow

SCAN_NEG

  • decrement servo toward 0
  • read IR each step
  • accumulate negObsTot
  • when IR <= 15, capture negFinal
  • reset servo to negInit
  • go to SCAN_POS

SCAN_POS

  • increment servo toward 180
  • read IR each step
  • accumulate posObsTot
  • when IR <= 15, capture posFinal
  • reset servo to posInit
  • go to COMPUTE

COMPUTE

  • compute bearing from the two edge hits
  • publish if needed
  • go to RESET

RESET

  • optionally check obstacle totals
  • clear or preserve counters depending on your logic
  • return to SCAN_NEG

Efficient ARC JavaScript version

Below is a version that matches your described strategy more closely.

var servoPort = d17;
var irPort = ADC2;

var desiredBearing = 90;
var negInit = 90;
var negFinal = 90;
var posInit = 90;
var posFinal = 90;

var reflectorThreshold = 15; // IR <= 15 means reflector
var servoStep = 1;
var scanDelayMs = 20;
var settleDelayMs = 10;

var ir = 0;
var negObsTot = 0;
var posObsTot = 0;
var negObsMax = 1000;
var posObsMax = 1000;

var bearingCount = 0;
var actualBearing = 90;
var actualBearingCurrent = 90;
var lastPublishedBearing = 90;

var state = "SCAN_NEG";

print("IR tracker started. actualBearingCurrent: " + actualBearingCurrent);

setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
setVar("$bearingCount", bearingCount);
setVar("$actualBearingCurrent", actualBearingCurrent);

function readIR() {
  return ADC.get(irPort, 0);
}

function readServo() {
  return Servo.getPosition(servoPort, 0);
}

function publishBearingIfChanged(newBearing) {
  actualBearing = Math.round(newBearing);

  if (actualBearing != lastPublishedBearing) {
    lastPublishedBearing = actualBearing;
    actualBearingCurrent = actualBearing;
    setVar("$actualBearingCurrent", actualBearingCurrent);
    print("actualBearingCurrent: " + actualBearingCurrent);
  }
}

function checkObstructionTotals() {
  if (negObsTot >= negObsMax || posObsTot >= posObsMax) {
    setVar("$sendStopNow", 1);
    return true;
  }
  return false;
}

function scanNegative() {
  ir = readIR();
  negObsTot += ir;
  setVar("$negIRTot", negObsTot);

  if (ir <= reflectorThreshold) {
    negFinal = readServo();
    print("negFinal: " + negFinal);

    Servo.setPosition(servoPort, negInit);
    sleep(settleDelayMs);

    state = "SCAN_POS";
    return;
  }

  Servo.decrement(servoPort, servoStep);
  sleep(scanDelayMs);
}

function scanPositive() {
  ir = readIR();
  posObsTot += ir;
  setVar("$posIRTot", posObsTot);

  if (ir <= reflectorThreshold) {
    posFinal = readServo();
    print("posFinal: " + posFinal);

    Servo.setPosition(servoPort, posInit);
    sleep(settleDelayMs);

    state = "COMPUTE";
    return;
  }

  Servo.increment(servoPort, servoStep);
  sleep(scanDelayMs);
}

function computeBearing() {
  bearingCount++;
  setVar("$bearingCount", bearingCount);

  actualBearing = Math.round(negFinal + ((posFinal - negFinal) / 2));
  publishBearingIfChanged(actualBearing);

  state = "RESET";
}

function resetCycle() {
  if (checkObstructionTotals()) {
    state = "SCAN_NEG";
    return;
  }

  negObsTot = 0;
  posObsTot = 0;
  setVar("$negIRTot", negObsTot);
  setVar("$posIRTot", posObsTot);

  state = "SCAN_NEG";
}

while (true) {
  if (state == "SCAN_NEG") {
    scanNegative();
  }
  else if (state == "SCAN_POS") {
    scanPositive();
  }
  else if (state == "COMPUTE") {
    computeBearing();
  }
  else if (state == "RESET") {
    resetCycle();
  }
  else {
    state = "SCAN_NEG";
  }
}

Why this is better

1) It matches your real motion strategy

The servo is actively swept in both directions.

2) It is much easier to port to Teensy

This maps almost directly to:

  • switch(state)
  • servo.write(servoPos)
  • adcRead()
  • millis() timing

3) It keeps the logic explicit

You can see exactly when:

  • the servo moves
  • the IR is sampled
  • the edge is captured
  • the bearing is computed

4) It avoids unnecessary complexity

No extra duplicate-publishing history chains are needed unless you really want them.


One important observation

Right now your bearing computation:

actualBearing = Math.round(negFinal + ((posFinal - negFinal) / 2));

is correct for a midpoint between the negative and positive edge hits.

If your left/right behavior is consistent:

  • negFinal around 83
  • posFinal around 98

then the midpoint is about 90.5, which rounds to 91.

That is fine, but if you want smoother behavior later, you might consider:

  • keeping actualBearing as a float internally
  • only rounding when publishing

Not necessary now, but useful later.


My recommendation

Yes, your strategy is good.

I would implement it with:

  • the 4-state loop above
  • one servo step per iteration
  • one IR read per step
  • direct state transitions on threshold detection

That is the cleanest version that still preserves your proven behavior.


Next step

If you want, I can do one more pass and produce a REV_22 version that is:

  • even tighter
  • uses a switch(state) format
  • includes your obstruction totals
  • preserves your exact scan behavior
  • removes any remaining unnecessary variables
  • ready to be translated to Teensy C++ later

If you'd like that, I’ll write it 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.
#8  

@Athena. Below is your copied response:

Yes - this clarification helps a lot, and I understand the strategy now. Your approach is not "scan until the servo reaches an endpoint." It is:

  1. Start at desiredBearing = 90
  2. Sweep NEG/right one degree at a time until the reflector threshold is detected
  3. Capture negFinal
  4. Reset back to negInit
  5. Sweep POS/left one degree at a time until the reflector threshold is detected
  6. Capture posFinal
  7. Compute the reflector bearing from the two edge hits

That is a valid and reliable strategy, and for your use case it is actually a good one because it gives you the reflector’s approximate angular center from the two edge points.

My reply: Actually, I am checking 4 edge points. Two edge points Neg, (negInit, negFinal) and two edge points Pos, (posInit, posFinal).

  1. negInit. The servo position of first IR <= 15. Actual Bearing is computed between negInit and the most recent posInit.
  2. negFinal. The servo position where IR changes to > 15. Actual Bearing is computed between negFinal and the most recent posFinal.
  3. posInit. The servo position of first IR <= 15. Actual Bearing is computed between posInit and the most recent negInit.
  4. posFinal. The servo position where IR changes to > 15. Actual Bearing is computed between posFinal and the most recent negFinal.

Another way to visualize my comparison:

posFinal......posInit......// desiredBearing //.....negInit.......negFinal. Compute desiredBearing each time an edge has been detected.

Movement Right

  1. The var desiredBearing is the center between negInit and posInit.
  2. The var desiredBearing is the center between negFinal and posFinal.

Movement Left

  1. The var desiredBearing is the center between posInit and negInit.
  2. The var desiredBearing is the center between posFinal and negFinal.

Based upon this strategy, how would you implement it? Thanks. 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.