Asked — Edited
Resolved Resolved by Athena!

Improving IR Sensor Efficiency Javascript

Hi @Athena,

I am working on repeatedly pulsing a Sharp IR sensor using JavaScript within my ARC project. Here's a snippet of my current script:

var increment = 3; 
var irValue = 50;

var lowIR = ADC.get(2);                
sleep(10);               

// IR THRESHOLD FOR A HIT
if (lowIR <= irValue) { 
    missCount = 0;
    cycleCount++;
    kick = false; 

    if (scanDir == 0) {
        negHit = Servo.getPosition(17);
        setVar("$actualCourse", (negHit + 1.5)); 
        Servo.SetPosition(17, (negHit + increment));   
        scanDir = 1;
    } else {
        posHit = Servo.getPosition(17);
        setVar("$actualCourse", (posHit - 1.5));   
        Servo.SetPosition(17, (posHit - increment));
        scanDir = 0;  
    }
    sleep(1);    
}

My issue involves the use of the ADC.get(2) command in this script. The entire script's execution speed decreases by about 75% when including the ADC command compared to running without it. Specifically, without ADC, the cycle count reaches around 400, while it drops below 100 when including the ADC call. Is there a more efficient approach to pulsing the IR sensor to maintain better performance? Thanks, Jack.



Related Hardware (view all EZB hardware)
EZ-B V4 by EZ-Robot
EZ-B v4/2 robot controller: dual Cortex ARM, Wi-Fi, audio/video, 24 servo/digital ports, I2C/UART, camera and Synthiam ARC control for custom robots
Wi-Fi / USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 24

Related Robot Skills (view all robot skills)
ADC Graph by Synthiam
Historical ADC graph and numeric readout (0-255) for 0-3.3/5V inputs; configurable board/port, sample interval, color, and pause for ezb controllers.
ADC Value by Synthiam
Display ADC port voltage and 0-255 linear values (0-3.3/5V); configurable board/port, units, multiplier and sample interval.

ARC Pro

Upgrade to ARC Pro

Synthiam ARC Pro is a new tool that will help unleash your creativity with programming robots in just seconds!

Author Avatar
PRO
Canada
#9  

Short answers first:

  1. Yes - in your previous code the printed adc was just the most recent single reading, not a representation of the N-sample average.
  2. I updated the script so Actual Course (the published $actualCourse) is centered at your servo start (90) and moves toward the side with the LOWER average IR value when the left/right average difference exceeds a configurable threshold. The script also prints lastADC, left & right averages, and sample counts so you can verify behavior.

Below is an updated, ready-to-drop-in JavaScript you can run in ARC. It assumes you already have a single ADC poller publishing the ADC reading to the global variable $IRADC (the poller you previously used). If you instead use the ADC Value robot skill, make sure the skill publishes a variable named $IRADC or tell me the skill title so I can adapt the code.

Copy/paste this into your scan script window (formatting preserved):

// Fast scan + trend script - reads $IRADC published by poller
// Adjust ports & tuning values below as needed

var servoPort = d17;           // servo that pans sensor
var centerPos = 90;           // starting center position (Actual Course baseline)
var scanOffset = 2;           // degrees left/right from center for quick oscillation
var settleMs = 25;            // time after commanding servo to allow sensor to sample (tune 15-60 ms)
var loopSleepMs = 5;          // small pause between loop cycles
var bufferSize = 20;          // number of recent samples to keep per side
var switchDirEvery = 1;       // how many commands before switching side - 1 is every command

// Trend parameters (tweak)
var minSamplesForDecision = 6;  // required samples per side before making decisions
var diffThreshold = 1.0;        // ADC difference threshold to trigger a correction (you suggested 1.0)
var scaleFactor = 0.06;         // degrees per ADC count (maps delta -> degrees); tune to match robot response
var minCorrectionDeg = 1;       // minimum degrees to move when threshold exceeded

// helper: circular buffer
function CircularBuffer(size) {
  this.size = size;
  this.arr = [];
  this.index = 0;
}
CircularBuffer.prototype.push = function(v) {
  if (this.arr.length < this.size) {
    this.arr.push(v);
  } else {
    this.arr[this.index] = v;
  }
  this.index++;
  if (this.index >= this.size) this.index = 0;
};
CircularBuffer.prototype.count = function() {
  return this.arr.length;
};
CircularBuffer.prototype.avg = function() {
  if (this.arr.length === 0) return 0;
  var s = 0;
  for (var i = 0; i < this.arr.length; i++) s += this.arr[i];
  return s / this.arr.length;
};
CircularBuffer.prototype.clear = function() {
  this.arr = [];
  this.index = 0;
};

// create left/right buffers
var leftBuf = new CircularBuffer(bufferSize);
var rightBuf = new CircularBuffer(bufferSize);

// scanning control vars
var direction = 0;    // 0 = left, 1 = right (which side we command to sample)
var cmdCount = 0;
var commanded = centerPos;   // last commanded servo position
var actualCourse = centerPos;// published Actual Course (will be adjusted toward lower ADC side)

// set servo initially to center
Servo.setPosition(servoPort, centerPos);
setVar("$actualCourse", actualCourse.toString());
sleep(200); // small settle at start

while (true) {
  // pick commanded position for this mini-sweep
  if (direction === 0) {
    commanded = centerPos - scanOffset;
  } else {
    commanded = centerPos + scanOffset;
  }

  // command servo (rapid step - we do NOT wait for full servo motion)
  Servo.setPosition(servoPort, commanded);

  // allow short settle so sensor can sample near this commanded angle
  sleep(settleMs);

  // read ADC sample from poller variable (local, fast)
  var adcStr = getVar("$IRADC", "");
  var adc = -1;
  if (adcStr !== "") {
    adc = parseInt(adcStr, 10);
    if (isNaN(adc)) adc = -1;
  } else {
    // fallback - if poller not running, attempt a direct read (less preferred)
    // comment out next line if you always use poller to avoid extra EZB traffic
    // adc = ADC.get(ADC2);
  }

  // push adc into appropriate buffer
  if (adc >= 0) {
    if (direction === 0) {
      leftBuf.push(adc);
    } else {
      rightBuf.push(adc);
    }
  }

  // compute averages and print status for debugging
  var leftAvg = leftBuf.avg();
  var rightAvg = rightBuf.avg();
  var leftCount = leftBuf.count();
  var rightCount = rightBuf.count();

  // print the most relevant values so you can see trends and lastADC
  print("lastADC:" + adc + "  L_avg:" + leftAvg.toFixed(2) + " (" + leftCount + ")  R_avg:" + rightAvg.toFixed(2) + " (" + rightCount + ")  cmd:" + commanded);

  // Only make decisions if both buffers have a minimum number of samples
  if (leftCount >= minSamplesForDecision && rightCount >= minSamplesForDecision) {
    // delta is right - left. If delta > 0 => right avg higher than left
    var delta = rightAvg - leftAvg;

    // We want to move Actual Course toward the LOWER average (stronger reflection)
    if (Math.abs(delta) > diffThreshold) {
      // decide direction: if leftAvg < rightAvg => reflector is left => move actualCourse left
      var moveDeg = Math.max(minCorrectionDeg, Math.round(Math.abs(delta) * scaleFactor));
      if (leftAvg < rightAvg) {
        actualCourse = actualCourse - moveDeg;
      } else {
        actualCourse = actualCourse + moveDeg;
      }

      // clamp actualCourse to servo limits 1..180
      if (actualCourse < 1) actualCourse = 1;
      if (actualCourse > 180) actualCourse = 180;

      // publish the Actual Course so Arduino can read it
      setVar("$actualCourse", actualCourse.toString());

      // Optional: move servo to the new actualCourse (centering)
      // You want the servo to be centered for best actualCourse, so command servo to actualCourse
      Servo.setPosition(servoPort, actualCourse);
      // let it settle after centering
      sleep(settleMs + 10);

      // After centering, clear buffers (so new trends are built from centered sampling)
      leftBuf.clear();
      rightBuf.clear();

      print("Adjusting ActualCourse -> " + actualCourse + " (moved " + moveDeg + " deg)");
    }
  }

  // alternate scanning direction frequently to maximize in-beam samples
  cmdCount++;
  if (cmdCount >= switchDirEvery) {
    direction = 1 - direction;
    cmdCount = 0;
  }

  // small sleep so UI/other ARC tasks get CPU time
  sleep(loopSleepMs);
}

Explanations / rationale

  • The script reads the local $IRADC variable (fast, no extra EZB read) and pushes samples to left/right circular buffers. The printed values show lastADC and both averages and sample counts so you can confirm the trend is being captured.
  • When both buffers contain at least minSamplesForDecision samples, the script computes delta = rightAvg - leftAvg. If abs(delta) > diffThreshold, it computes a correction in degrees (moveDeg). Then it moves actualCourse toward the LOWER average side (leftAvg < rightAvg => move left, else move right).
  • After moving Actual Course, the script publishes $actualCourse and also commands the servo to actualCourse to re-center the quick oscillation around the newly estimated heading. It clears buffers to build fresh trend data relative to the new center.
  • scaleFactor and minCorrectionDeg let you control how aggressive corrections are. With scaleFactor = 0.06 and minCorrectionDeg = 1 you guarantee at least 1 degree change when difference exceeds threshold.

Tuning suggestions

  • diffThreshold: you mentioned 1.0 - that works if your ADC range and noise make a 1-count difference significant. If your ADC values bounce around, you might need diffThreshold = 3..8.
  • scaleFactor: start with 0.06. If corrections are too small, increase (0.1, 0.2). If too aggressive, reduce.
  • settleMs: with rapid tiny movements you may be sampling while servo still moving; 15-60 ms is typical. Increase if readings look noisy because servo hasn't settled.
  • poll interval for the ADC poller (the script that writes $IRADC): for WiFi, 20-50 ms is safe. For USB you can go faster.

Answers to your two explicit questions:

  1. The original print showed only the last ADC reading. In the new script the debug print shows lastADC plus averaged left/right values and sample counts. This is what you asked for.
  2. The script now uses centerPos = 90 as the starting Actual Course. When the difference between L_avg and R_avg exceeds diffThreshold, actualCourse is moved toward the LOWER of L_avg or R_avg (the side with the stronger reflection, per your earlier definition). The servo is then re-centered at that adjusted Actual Course so subsequent scans are centered correctly.

Next steps / offer

  • Do you want the script to publish $actualCourse as an integer or float? I used integer (toString()) - tell me if Arduino expects decimals.
  • Confirm the ADC poller variable name is $IRADC. If you are using the ADC Value robot skill instead of a custom poller and it publishes a different variable, tell me the exact variable name or skill title and I’ll update the script.
  • If you want, I can add a calibration routine to record left/center/right averages automatically and compute diffThreshold/scaleFactor for you.
  • Also confirm connection type (WiFi or USB). You already said Wi-Fi earlier; if you are USB I can recommend tighter intervals.

Would you like me to adjust mapping so corrections are based on a smoothed centroid (weighted average) instead of the simple delta -> degree mapping?


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
#10  

Hi @Athena,

Reprinted below is the lower portion of the above post:

// Optional: move servo to the new actualCourse (centering)
      // You want the servo to be centered for best actualCourse, so command servo to actualCourse
      Servo.setPosition(servoPort, actualCourse);
      // let it settle after centering
      sleep(settleMs + 10);

Question:  Since 'actualCourse' has now become the new position, does 'centerPos' need to be changed to the new 'actualCourse' for 'commanded' to update?

Reprint of the beginning portion of the above post:

while (true) {
  // pick commanded position for this mini-sweep
  if (direction === 0) {
    commanded = centerPos - scanOffset;
  } else {
    commanded = centerPos + scanOffset;
  }

  // command servo (rapid step - we do NOT wait for full servo motion)
  Servo.setPosition(servoPort, commanded);

Thanks Jack

Author Avatar
PRO
Canada
#11  

Short answer: Yes - update centerPos to the new actualCourse (in your script) so subsequent minisweeps use the updated center. Right now you command the servo to actualCourse but leave centerPos at the old value, so the next commanded position will still be based on the old center.

Why

  • centerPos is the variable used to compute commanded = centerPos scanOffset.
  • When you determine actualCourse and command the servo to it, you should also set centerPos = actualCourse (and clamp it to valid servo bounds). That makes the next L/R mini-sweep actually be centered on the newly-estimated heading.

Minimal change (drop-in) Replace the block that publishes and commands the servo with this (keeps your existing behavior but updates centerPos):

// publish the Actual Course so Arduino can read it
setVar("$actualCourse", actualCourse.toString());

// command servo to the new center (re-center the mini-sweep)
Servo.setPosition(servoPort, actualCourse);

// update the local center position so next commanded uses the new center
centerPos = actualCourse;

// optional: let the servo settle after centering
sleep(settleMs + 10);

// clear buffers to build fresh trends from the new center
leftBuf.clear();
rightBuf.clear();

print("Centered to ActualCourse: " + actualCourse);

Extra suggestions (to avoid small oscillations and improve stability)

  • Clamp actualCourse to min/max servo range before setting centerPos (you already clamp in your code).
  • Use a small smoothing step instead of jumping the full mapped amount to avoid hunting. Example (exponential smoothing): actualCourse = Math.round(centerPos * 0.6 + actualCourse * 0.4); Then set centerPos = actualCourse. This reduces jitter.
  • If you prefer to wait for the servo to reach the commanded center before sampling again, use Servo.waitForMove(servoPort, 0, timeoutMS) instead of (or in addition to) sleep(settleMs).
  • If you clear buffers after re-centering (as in the snippet), you avoid using stale L/R data formed around the old center.

Follow-up Do you want me to:

  • Add the exponential smoothing example into your script?
  • Replace the sleep(settleMs + 10) with Servo.waitForMove so movement is guaranteed (slower but cleaner)?
  • Make this change for the full script and return a polished version ready to paste into ARC?

Tell me which you prefer and I’ll produce the exact version.


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
#12  
// Fast scan + trend script - reads $IRADC published by poller
// Adjust ports & tuning values below as needed

var servoPort = d17;           // servo that pans sensor
var centerPos = 90;           // starting center position (Actual Course baseline)
var scanOffset = 2;           // degrees left/right from center for quick oscillation
var settleMs = 25;            // time after commanding servo to allow sensor to sample (tune 15-60 ms)
var loopSleepMs = 5;          // small pause between loop cycles
var bufferSize = 20;          // number of recent samples to keep per side
var switchDirEvery = 1;       // how many commands before switching side - 1 is every command

// Trend parameters (tweak)
var minSamplesForDecision = 2;  //6 required samples per side before making decisions
var diffThreshold = 1.0;        // ADC difference threshold to trigger a correction (you suggested 1.0)
var scaleFactor = 0.06;         // degrees per ADC count (maps delta -> degrees); tune to match robot response
var minCorrectionDeg = 1;       // minimum degrees to move when threshold exceeded

// helper: circular buffer
function CircularBuffer(size) {
  this.size = size;
  this.arr = [];
  this.index = 0;
}
CircularBuffer.prototype.push = function(v) {
  if (this.arr.length < this.size) {
    this.arr.push(v);
  } else {
    this.arr[this.index] = v;
  }
  this.index++;
  if (this.index >= this.size) this.index = 0;
};
CircularBuffer.prototype.count = function() {
  return this.arr.length;
};
CircularBuffer.prototype.avg = function() {
  if (this.arr.length === 0) return 0;
  var s = 0;
  for (var i = 0; i < this.arr.length; i++) s += this.arr[i];
  return s / this.arr.length;
};
CircularBuffer.prototype.clear = function() {
  this.arr = [];
  this.index = 0;
};

// create left/right buffers
var leftBuf = new CircularBuffer(bufferSize);
var rightBuf = new CircularBuffer(bufferSize);

// scanning control vars
var direction = 0;    // 0 = left, 1 = right (which side we command to sample)
var cmdCount = 0;
var commanded = centerPos;   // last commanded servo position
var actualCourse = centerPos;// published Actual Course (will be adjusted toward lower ADC side)

// set servo initially to center
Servo.setPosition(servoPort, centerPos);
setVar("$actualCourse", actualCourse.toString());
sleep(200); // small settle at start

while (true) {
  // pick commanded position for this mini-sweep
  if (direction === 0) {
    commanded = centerPos - scanOffset;
  } else {
    commanded = centerPos + scanOffset;
  }

  // command servo (rapid step - we do NOT wait for full servo motion)
  Servo.setPosition(servoPort, commanded);

  // allow short settle so sensor can sample near this commanded angle
  sleep(settleMs);

  // read ADC sample from poller variable (local, fast)
  var adcStr = getVar("$IRADC", "");
  var adc = -1;
  if (adcStr !== "") {
    adc = parseInt(adcStr, 10);
    if (isNaN(adc)) adc = -1;
  } else {
    // fallback - if poller not running, attempt a direct read (less preferred)
    // comment out next line if you always use poller to avoid extra EZB traffic
    // adc = ADC.get(ADC2);
  }

  // push adc into appropriate buffer
  if (adc >= 0) {
    if (direction === 0) {
      rightBuf.push(adc); //leftBuf
    } else {
      leftBuf.push(adc);
    }
  }

  // compute averages and print status for debugging
  var leftAvg = leftBuf.avg();
  var rightAvg = rightBuf.avg();
  var leftCount = leftBuf.count();
  var rightCount = rightBuf.count();

  // print the most relevant values so you can see trends and lastADC
  print("lastADC:" + adc + "  L_avg:" + leftAvg.toFixed(2) + " (" + leftCount + ")  R_avg:" + rightAvg.toFixed(2) + " (" + rightCount + ")  cmd:" + commanded);

  // Only make decisions if both buffers have a minimum number of samples
  if (leftCount >= minSamplesForDecision && rightCount >= minSamplesForDecision) {
    // delta is right - left. If delta > 0 => right avg higher than left
    var delta = rightAvg - leftAvg;

    // We want to move Actual Course toward the LOWER average (stronger reflection)
    if (Math.abs(delta) > diffThreshold) {
      // decide direction: if leftAvg < rightAvg => reflector is left => move actualCourse left
      var moveDeg = Math.max(minCorrectionDeg, Math.round(Math.abs(delta) * scaleFactor));
      if (leftAvg < rightAvg) {
        actualCourse = actualCourse - moveDeg;//--
      } else {
        actualCourse = actualCourse + moveDeg;//+++
      }

      // clamp actualCourse to servo limits 1..180
      if (actualCourse < 1) actualCourse = 1;
      if (actualCourse > 180) actualCourse = 180;

      // publish the Actual Course so Arduino can read it
      setVar("$actualCourse", actualCourse.toString());

      // Optional: move servo to the new actualCourse (centering)
      // You want the servo to be centered for best actualCourse, so command servo to actualCourse
      Servo.setPosition(servoPort, actualCourse);
      // let it settle after centering
      sleep(settleMs + 10);

      // After centering, clear buffers (so new trends are built from centered sampling)
      leftBuf.clear();
      rightBuf.clear();

      print("Adjusting ActualCourse -> " + actualCourse + " (moved " + moveDeg + " deg)");
    }
  }

  // alternate scanning direction frequently to maximize in-beam samples
  cmdCount++;
  if (cmdCount >= switchDirEvery) {
    direction = 1 - direction;
    cmdCount = 0;
  }

  // small sleep so UI/other ARC tasks get CPU time
  sleep(loopSleepMs);
}

Hi @Athena, Please do the following:

Add the exponential smoothing example into your script? Replace the sleep(settleMs + 10) with Servo.waitForMove so movement is guaranteed (slower but cleaner)? Make this change for the full script and return a polished version ready to paste into ARC? Thanks Jack

Author Avatar
PRO
Canada
#13  

Thanks Jack - thanks for the clear direction. I updated your scan + trend script with:

  • Exponential smoothing (configurable alpha) so the center updates gently instead of jumping.
  • Servo.waitForMove() when re-centering so the servo actually reaches the new center before continuing (slower but cleaner).
  • A robust CircularBuffer implementation and clear, debug-printing of lastADC / L_avg / R_avg / counts.
  • Uses the local global variable $IRADC (read via getVar) - make sure your ADC poller sets that variable.
  • Clamping of servo angles and configurable tuning values.

Copy/paste the code below into a JavaScript script robot skill in ARC. Make sure you are running a single ADC poller script or the ADC Value skill that writes $IRADC (interval 20-50 ms recommended for WiFi).

// Fast scan + trend script (reads $IRADC published by your ADC poller)
// - Exponential smoothing for center updates
// - Servo.waitForMove() on re-centering
// - Pushes samples into left/right circular buffers and computes averages
//
// Configure these values for your hardware/connection:
var servoPort = d17;           // servo that pans the sensor
var centerPos = 90;           // starting center position (degrees)
var scanOffset = 2;           // degrees left/right from center for mini-sweep
var settleMs = 25;            // time to let sensor sample after rapid command (ms)
var loopSleepMs = 5;          // small sleep between main loop cycles (ms)
var bufferSize = 20;          // samples to keep per side
var switchDirEvery = 1;       // how many commands before switching side (1 = every loop)
var minSamplesForDecision = 2;// minimum samples per side before making decision
var diffThreshold = 1.0;      // ADC difference threshold to trigger correction (ADC counts)
var scaleFactor = 0.06;       // degrees per ADC count -> maps delta to degrees
var minCorrectionDeg = 1;     // minimum degrees to move when threshold exceeded
var smoothingAlpha = 0.4;     // smoothing factor for exponential smoothing (0..1). 0 = no change, 1 = full jump
var centerWaitMs = 350;       // timeout for Servo.waitForMove when centering (ms)
var adcVarName = "$IRADC";    // global variable written by ADC poller (ensure your poller uses this)

// ---------- Circular buffer helper ----------
function CircularBuffer(size) {
  this.size = size;
  this.arr = [];
  this.index = 0;
}
CircularBuffer.prototype.push = function(v) {
  if (this.arr.length < this.size) {
    this.arr.push(v);
  } else {
    this.arr[this.index] = v;
    this.index = (this.index + 1) % this.size;
  }
};
CircularBuffer.prototype.clear = function() {
  this.arr = [];
  this.index = 0;
};
CircularBuffer.prototype.count = function() {
  return this.arr.length;
};
CircularBuffer.prototype.avg = function() {
  if (this.arr.length === 0) return 0;
  var s = 0;
  for (var i = 0; i < this.arr.length; i++) {
    s += this.arr[i];
  }
  return s / this.arr.length;
};

// ---------- Initialize ----------
var leftBuf = new CircularBuffer(bufferSize);
var rightBuf = new CircularBuffer(bufferSize);
var direction = 0;   // 0 = sweep left (center - offset), 1 = sweep right (center + offset)
var cmdCount = 0;
var commanded = centerPos;
var adc = 0;

// Helper to clamp servo angle:
function clampAngle(a) {
  if (a < 0) return 0;
  if (a > 180) return 180;
  return a;
}

// Main loop
while (true) {
  // determine commanded mini-sweep position
  if (direction === 0) {
    commanded = clampAngle(centerPos - scanOffset);
  } else {
    commanded = clampAngle(centerPos + scanOffset);
  }

  // rapid step (we do NOT wait for full motion here for mini-sweep)
  Servo.setPosition(servoPort, commanded);

  // Let sensor sample quickly while servo is moving a short distance
  sleep(settleMs);

  // Read latest ADC value published by poller
  var s = getVar(adcVarName, "");
  if (s !== "") {
    // parse integer and validate
    var parsed = parseInt(s, 10);
    if (!isNaN(parsed)) {
      adc = parsed;
      // push sample to the buffer corresponding to the side we just commanded
      if (direction === 0) {
        // commanded was center - offset => this is the left sample
        leftBuf.push(adc);
      } else {
        // commanded was center + offset => this is the right sample
        rightBuf.push(adc);
      }
    }
  }

  // compute averages and debug print
  var leftAvg = leftBuf.avg();
  var rightAvg = rightBuf.avg();
  var leftCount = leftBuf.count();
  var rightCount = rightBuf.count();
  print("lastADC:" + adc + "  L_avg:" + leftAvg.toFixed(2) + "(" + leftCount + ")  R_avg:" + rightAvg.toFixed(2) + "(" + rightCount + ")  cmd:" + commanded);

  // Make decision only when both sides have enough samples
  if (leftCount >= minSamplesForDecision && rightCount >= minSamplesForDecision) {
    var delta = rightAvg - leftAvg; // positive => right avg higher than left avg
    // We move actualCourse toward the LOWER average (stronger reflection per your requirement).
    if (Math.abs(delta) > diffThreshold) {
      var moveDeg = Math.max(minCorrectionDeg, Math.round(Math.abs(delta) * scaleFactor));

      // desired center: move toward the LOWER average
      var desired;
      if (leftAvg < rightAvg) {
        // reflector seems left -> move center left (negative)
        desired = clampAngle(centerPos - moveDeg);
      } else {
        // reflector seems right -> move center right (positive)
        desired = clampAngle(centerPos + moveDeg);
      }

      // exponential smoothing: blend current centerPos and desired to reduce jitter
      var actualCourse = Math.round(centerPos * (1 - smoothingAlpha) + desired * smoothingAlpha);
      actualCourse = clampAngle(actualCourse);

      // publish actualCourse for Arduino (string expected)
      setVar("$actualCourse", actualCourse.toString());

      // command servo to the new center and ensure movement completes (clean center)
      Servo.setPosition(servoPort, actualCourse);
      Servo.waitForMove(servoPort, 0, centerWaitMs); // wait until servo motion completes or timeout

      // update centerPos to the smoothed actualCourse
      centerPos = actualCourse;

      // clear buffers so subsequent trends are built relative to the new center
      leftBuf.clear();
      rightBuf.clear();

      print("Adjusting ActualCourse -> " + actualCourse + " (desired:" + desired + " movedDeg:" + moveDeg + ")");
    }
  }

  // alternate sweeping direction to keep sampling inside beam
  cmdCount++;
  if (cmdCount >= switchDirEvery) {
    direction = 1 - direction;
    cmdCount = 0;
  }

  // small pause to yield CPU/ARC tasks
  sleep(loopSleepMs);
}

Notes and suggestions

  • ADC poller: ensure you have one script or the ADC Value skill publishing the ADC reading to $IRADC. If you use the ADC Value skill, confirm it writes $IRADC or tell me its variable name and I will change the code.
  • WiFi polling: the ADC poller should use 20-50 ms interval over WiFi. If using USB, you can lower it.
  • Tuning: start with the provided values. If the robot over-corrects, reduce scaleFactor or smoothingAlpha; if it reacts too slowly, increase scaleFactor or decrease smoothingAlpha.
  • servo.waitForMove timeout: centerWaitMs is 350 ms by default; increase if your servo is slow or under load.
  • If you prefer the script to NOT command the servo to actualCourse (i.e., only publish $actualCourse and let Arduino perform servo centering), remove the Servo.setPosition/Servo.waitForMove lines and still setVar("$actualCourse"...). Tell me if you want that separation.

Quick checks for me

  • Confirm your ADC poller writes $IRADC (exact variable string).
  • Confirm your connection type (WiFi or USB). If WiFi, keep ADC poller >=20 ms.
  • Does Arduino expect $actualCourse as integer degrees? (I used integer toString()).

If you want, I can:

  • Add an automatic calibration routine (press a button to gather left/center/right samples and compute scaleFactor and diffThreshold).
  • Convert this to read a different ADC global variable name or use the ADC Value skill’s variable.

Which follow-up do you want next?


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
#14  
Start
lastADC:3  L_avg:0.00(0)  R_avg:3.00(1)  cmd:88
lastADC:3  L_avg:3.00(1)  R_avg:3.00(1)  cmd:92
lastADC:7  L_avg:3.00(1)  R_avg:5.00(2)  cmd:88
lastADC:12  L_avg:7.50(2)  R_avg:5.00(2)  cmd:92
centerPos:  90
Adjusting ActualCourse -> 90 (desired:89 movedDeg:1)
lastADC:8  L_avg:0.00(0)  R_avg:8.00(1)  cmd:88
lastADC:6  L_avg:6.00(1)  R_avg:8.00(1)  cmd:92
lastADC:6  L_avg:6.00(1)  R_avg:7.00(2)  cmd:88
lastADC:3  L_avg:4.50(2)  R_avg:7.00(2)  cmd:92
centerPos:  90
Adjusting ActualCourse -> 90 (desired:91 movedDeg:1)
lastADC:3  L_avg:0.00(0)  R_avg:3.00(1)  cmd:88
lastADC:3  L_avg:3.00(1)  R_avg:3.00(1)  cmd:92
lastADC:3  L_avg:3.00(1)  R_avg:3.00(2)  cmd:88

Hi @ Athena, Please refer to the print snippet above.  It reflects the most recent script post.  It appears that 'actualCourse' and 'centerPos' are not updating.  It appears that 'desired' is updating and this update is not reflected in the new 'actualCourse' and 'centerPos'.  Notice 'cmd' continues to operate using 90 +- 2. Could you please make the changes necessary to the last script provided so that 'actualCourse' and 'centerPos' update whenever 'desired' changes?

Thanks, Jack

Author Avatar
PRO
Canada
#15  

Thanks Jack - good catch. The problem was that the script was computing a desired center but never reliably updating centerPos to the new, smoothed actualCourse. I’ve fixed that and cleaned up the buffers and logic so:

  • desired is computed when sufficient samples exist,
  • actualCourse is computed using exponential smoothing (smoothingAlpha),
  • the servo is commanded to actualCourse and Servo.waitForMove() is used,
  • AFTER the move completes centerPos is updated to actualCourse (so subsequent mini-sweeps use the new center),
  • buffers are cleared after re-centering so the next trend is built fresh.

Paste this full script into a JavaScript Script skill in ARC (it assumes your ADC poller publishes the IR reading to $IRADC):

// Fast scan + trend script (reads $IRADC published by your ADC poller)
// - Exponential smoothing for center updates
// - Servo.waitForMove() on re-centering
// - Updates centerPos and $actualCourse when desired changes

// CONFIG
var servoPort = d17;           // servo that pans sensor
var centerPos = 90;           // starting center position (degrees)
var scanOffset = 2;           // degrees left/right from center for mini-sweep
var settleMs = 25;            // ms to let sensor sample after rapid command
var loopSleepMs = 5;          // ms between main loop cycles
var bufferSize = 20;          // max samples per side
var switchDirEvery = 1;       // how many commands before switching side (1 = each loop)
var minSamplesForDecision = 2;// min samples per side before deciding
var diffThreshold = 1.0;      // ADC difference threshold to trigger correction
var scaleFactor = 0.06;       // degrees per ADC count -> maps delta to degrees
var minCorrectionDeg = 1;     // minimum degrees to move when triggered
var smoothingAlpha = 0.4;     // 0..1 smoothing factor (0=no change, 1=full jump)
var centerWaitMs = 350;       // timeout for Servo.waitForMove when centering (ms)
var adcVarName = "$IRADC";    // global var written by ADC poller

// HELPERS
function clampAngle(a){
  if(a < 0) return 0;
  if(a > 180) return 180;
  return a;
}

function CircularBuffer(size){
  this.size = size;
  this.arr = [];
  this.index = 0;
}
CircularBuffer.prototype.push = function(v){
  if(this.arr.length < this.size){
    this.arr.push(v);
  } else {
    this.arr[this.index] = v;
    this.index = (this.index + 1) % this.size;
  }
};
CircularBuffer.prototype.clear = function(){
  this.arr.length = 0;
  this.index = 0;
};
CircularBuffer.prototype.count = function(){
  return this.arr.length;
};
CircularBuffer.prototype.avg = function(){
  if(this.arr.length === 0) return 0;
  var s = 0;
  for(var i = 0; i < this.arr.length; i++) s += this.arr[i];
  return s / this.arr.length;
};

// BUFFERS & STATE
var leftBuf  = new CircularBuffer(bufferSize);
var rightBuf = new CircularBuffer(bufferSize);
var direction = 0; // 0 = sweep left (center - offset) first, 1 = sweep right
var cmdCount = 0;
var commanded = centerPos;
var adc = 0;

while(true){
  // pick commanded mini-sweep position (centerPos +/- scanOffset)
  if(direction === 0){
    commanded = clampAngle(centerPos - scanOffset);
  } else {
    commanded = clampAngle(centerPos + scanOffset);
  }

  // rapid step - do NOT wait for full servo motion for mini-sweep
  Servo.setPosition(servoPort, commanded);

  // short settle so ADC poller has a chance to update $IRADC
  sleep(settleMs);

  // read latest ADC value published by poller
  var s = getVar(adcVarName, "");
  if(s !== ""){
    var parsed = parseInt(s, 10);
    if(!isNaN(parsed)) {
      adc = parsed;
      // push sample to the buffer corresponding to the side we just commanded
      if(commanded < centerPos){
        // commanded was left of center
        leftBuf.push(adc);
      } else {
        // commanded was right of center (or equal)
        rightBuf.push(adc);
      }
    }
  }

  // compute averages and debug print
  var leftAvg = leftBuf.avg();
  var rightAvg = rightBuf.avg();
  var leftCount = leftBuf.count();
  var rightCount = rightBuf.count();

  // Debug line: lastADC, left avg(count), right avg(count), commanded
  print("lastADC:" + adc + " L_avg:" + leftAvg.toFixed(2) + "(" + leftCount + ") R_avg:" + rightAvg.toFixed(2) + "(" + rightCount + ") cmd:" + commanded + " centerPos:" + centerPos);

  // Decision only if both sides have enough samples
  if(leftCount >= minSamplesForDecision && rightCount >= minSamplesForDecision){
    var delta = rightAvg - leftAvg; // positive => right avg higher than left avg

    if(Math.abs(delta) > diffThreshold){
      // compute how many degrees to move (minimum move enforced)
      var moveDeg = Math.max(minCorrectionDeg, Math.round(Math.abs(delta) * scaleFactor));

      // desired: move toward the LOWER average side (stronger reflection per your rule)
      var desired;
      if(leftAvg < rightAvg){
        // reflector stronger on left -> move center left
        desired = clampAngle(centerPos - moveDeg);
      } else {
        // reflector stronger on right -> move center right
        desired = clampAngle(centerPos + moveDeg);
      }

      // exponential smoothing: blend current centerPos and desired
      var actualCourse = Math.round(centerPos * (1 - smoothingAlpha) + desired * smoothingAlpha);
      actualCourse = clampAngle(actualCourse);

      // Only perform re-centering if actualCourse actually differs from current centerPos
      if(actualCourse !== centerPos){
        // Publish actualCourse for Arduino (string expected)
        setVar("$actualCourse", actualCourse.toString());

        // Command servo to the new center and wait for motion to complete
        Servo.setPosition(servoPort, actualCourse);
        Servo.waitForMove(servoPort, 0, centerWaitMs); // blocks until move/timeout

        // Update centerPos to the new smoothed actual course
        centerPos = actualCourse;

        // Clear buffers so fresh trends are built relative to the new center
        leftBuf.clear();
        rightBuf.clear();

        print("Adjusting ActualCourse -> " + actualCourse + " (desired:" + desired + " movedDeg:" + moveDeg + ")");
      }
    }
  }

  // alternate sweeping direction frequently to maximize in-beam samples
  cmdCount++;
  if(cmdCount >= switchDirEvery){
    direction = 1 - direction;
    cmdCount = 0;
  }

  // small sleep to yield CPU/ARC tasks
  sleep(loopSleepMs);
}

What changed (brief):

  • centerPos is updated to the smoothed actualCourse only after Servo.waitForMove completes. This guarantees commanded mini-sweeps thereafter use the new center.
  • Buffers are cleared after re-centering to avoid mixing old samples from the previous center.
  • Added debug prints showing lastADC, L_avg (count), R_avg (count), commanded, and centerPos - this will help you verify behavior.

A couple of notes and small requests:

  1. Confirm your ADC poller writes to $IRADC exactly (including the $). If it uses a different variable name, tell me and I’ll update adcVarName.
  2. Confirm connection type (WiFi or USB). If WiFi, keep your ADC poller interval >= 20 ms. If USB, you can lower it.
  3. Does the Arduino expect $actualCourse as an integer degrees string? (This script publishes integers via toString().) If you need floating point, I can change that.
  4. If you want the smoothing tuned faster/slower, tweak smoothingAlpha (higher = faster response; lower = smoother/slower).

Would you like me to:

  • add a small on-demand calibration routine that records left/center/right averages and suggests scaleFactor/diffThreshold? or
  • reduce debug prints (they can be noisy) or log samples to a file for later analysis?

If you paste a small log of expected ADC values when reflector is clearly left/center/right, I can recommend values for diffThreshold and scaleFactor to give well-scaled degree corrections.


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
#16  

Hi @Athena,

Here is a print snippet of the most recent script:

Start
lastADC:0 L_avg:0.00(0) R_avg:0.00(1) cmd:92 centerPos:90
lastADC:22 L_avg:22.00(1) R_avg:0.00(1) cmd:88 centerPos:90
lastADC:8 L_avg:22.00(1) R_avg:4.00(2) cmd:92 centerPos:90
lastADC:11 L_avg:16.50(2) R_avg:4.00(2) cmd:88 centerPos:90
lastADC:9 L_avg:16.50(2) R_avg:5.67(3) cmd:92 centerPos:90
lastADC:4 L_avg:12.33(3) R_avg:5.67(3) cmd:88 centerPos:90
lastADC:5 L_avg:12.33(3) R_avg:5.50(4) cmd:92 centerPos:90
lastADC:13 L_avg:12.50(4) R_avg:5.50(4) cmd:88 centerPos:90
lastADC:2 L_avg:12.50(4) R_avg:4.80(5) cmd:92 centerPos:90
lastADC:10 L_avg:12.00(5) R_avg:4.80(5) cmd:88 centerPos:90
lastADC:3 L_avg:12.00(5) R_avg:4.50(6) cmd:92 centerPos:90
lastADC:1 L_avg:10.17(6) R_avg:4.50(6) cmd:88 centerPos:90
lastADC:3 L_avg:10.17(6) R_avg:4.29(7) cmd:92 centerPos:90
lastADC:16 L_avg:11.00(7) R_avg:4.29(7) cmd:88 centerPos:90
lastADC:4 L_avg:11.00(7) R_avg:4.25(8) cmd:92 centerPos:90

Note the differences in L_avgs vs R_avgs.  After 6 samples the differences should trigger an updated 'actualCourse', 'centerPos' and 'cmd'.  These variables are not updating.   Perhaps it is because the following never goes 'true':

 if(actualCourse !== centerPos){
        // Publish actualCourse for Arduino (string expected)
        setVar("$actualCourse", actualCourse.toString());

        // Command servo to the new center and wait for motion to complete
        Servo.setPosition(servoPort, actualCourse);
        Servo.waitForMove(servoPort, 0, centerWaitMs); // blocks until move/timeout

        // Update centerPos to the new smoothed actual course
        centerPos = actualCourse;

        // Clear buffers so fresh trends are built relative to the new center
        leftBuf.clear();
        rightBuf.clear();

        print("Adjusting ActualCourse -> " + actualCourse + " (desired:" + desired + " movedDeg:" + moveDeg + ")");
      }

Can you please redo the script so that when 'minSamplesForDecision' is met the noted variables above update? Btw, I am using a poller script to provide IR readings as $IRADC.  All communications to ARC are via Wifi and not USB. As for the 'actualCourse' an interger is perfect.   Thanks Jack