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

Join the ARC Pro community and gain access to a wealth of resources and support, ensuring your robot's success.

Author Avatar
PRO
Canada
#17  

Thanks Jack - good diagnostics. I found the rounding / smoothing interaction was preventing any integer change to actualCourse (so actualCourse === centerPos and the code block never ran). I updated the logic so:

  • desired is computed from the delta and moveDeg (move toward the LOWER average).
  • actualCourse is computed using smoothing, but if smoothing would round to no change, the script forces at least a 1 move in the correct direction (so centerPos will actually update).
  • centerPos is then updated only after Servo.waitForMove() completes and the buffers are cleared (so the next mini-sweeps are centered on the new position).
  • Circular buffers are implemented robustly, debug print shows lastADC/L_avg/R_avg/counts/cmd, and the script reads your poller variable $IRADC via getVar().

Drop this into a JavaScript Script skill in ARC (keep your ADC poller running and writing $IRADC). I kept integer actualCourse output (toString()) as you requested.

Code:

// Fast scan + trend script (reads $IRADC published by your ADC poller)
// - Ensures centerPos updates when minSamplesForDecision is met
// - Exponential smoothing with forced-minimum integer move to avoid no-op rounding
// - Uses Servo.waitForMove when re-centering (slower but guaranteed)
// Tune values below for your robot / WiFi environment

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 (0 = no change, 1 = full jump)
var centerWaitMs = 350;           // timeout for Servo.waitForMove when centering (ms)

var adcVarName = "$IRADC";        // global var written by your 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.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;
};
// ------------------------------------------------------------------
// State
var leftBuf = new CircularBuffer(bufferSize);
var rightBuf = new CircularBuffer(bufferSize);
var cmdCount = 0;
var direction = 0; // 0 = left (center - offset), 1 = right (center + offset)
var adc = 0;
var commanded = centerPos;

print("Start");

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

  // short settle to let ADC sample while servo is moving a small amount
  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 into buffer for the side we just commanded
      if (direction === 0) {
        leftBuf.push(adc);
      } else {
        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 +
        " centerPos:" + centerPos);

  // Decision logic when enough samples exist
  if (leftCount >= minSamplesForDecision && rightCount >= minSamplesForDecision) {
    var delta = rightAvg - leftAvg; // >0 => right avg higher than left
    if (Math.abs(delta) > diffThreshold) {
      // compute how many degrees to move (at least minCorrectionDeg)
      var moveDeg = Math.max(minCorrectionDeg, Math.round(Math.abs(delta) * scaleFactor));

      // desired: move TOWARD the LOWER average (stronger reflection)
      var desired;
      if (leftAvg < rightAvg) {
        desired = clampAngle(centerPos - moveDeg);
      } else {
        desired = clampAngle(centerPos + moveDeg);
      }

      // compute actualCourse using smoothing, but force at least 1 degree if smoothing would round to no-op
      var deltaCenter = desired - centerPos;
      var smoothedStep = Math.round(deltaCenter * smoothingAlpha);
      var actualCourse;
      if (smoothedStep === 0) {
        // force minimum integer move in the direction of desired
        actualCourse = centerPos + Math.sign(deltaCenter) * Math.max(1, Math.min(Math.abs(deltaCenter), minCorrectionDeg));
      } else {
        actualCourse = centerPos + smoothedStep;
      }
      actualCourse = clampAngle(Math.round(actualCourse));

      // If actualCourse differs from current centerPos, apply re-centering
      if (actualCourse !== centerPos) {
        // publish actualCourse for Arduino (string)
        setVar("$actualCourse", actualCourse.toString());

        // center servo and wait for motion to complete (guaranteed)
        Servo.setPosition(servoPort, actualCourse);
        Servo.waitForMove(servoPort, 0, centerWaitMs); // (port, ezbIndex, timeoutMS)

        // update centerPos and clear buffers for fresh sampling around the new center
        centerPos = actualCourse;
        leftBuf.clear();
        rightBuf.clear();

        print("Adjusting ActualCourse ->" + actualCourse + "(desired:" + desired + " movedDeg:" + moveDeg + ")");
      } // end if actualCourse changed
    } // end if diff big enough
  } // end if samples enough

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

What I changed vs. the previous script

  • Ensured actualCourse calculation can't be a no-op due to rounding when desired != centerPos.
  • Only update centerPos after servo move completes.
  • Clear buffers after re-centering so new trends form around the new center.
  • Robust circular buffers + clear debug prints.

Tuning notes for WiFi (since you confirmed WiFi):

  • Keep your ADC poller interval between 20-50 ms (25 ms is a good starting point).
  • If you see too much jitter, reduce smoothingAlpha (e.g., 0.2). If too slow to respond, increase smoothingAlpha (e.g., 0.6).
  • If corrections are weak, increase scaleFactor; if over-correcting, reduce it.
  • minSamplesForDecision = 2 is OK for faster responses; raise it (4-6) if you want more stability at the cost of responsiveness.

Next steps I can do

  • Add a quick calibration routine that records left/center/right averages to auto-calc scaleFactor and diffThreshold.
  • Reduce the debug prints or log to file if the console is too noisy.
  • Convert the script to publish $actualCourse as an integer (already done) or a float if you later need it.

Try this version and paste a sample of the print log (like you did) - if centerPos still doesn’t update I’ll quickly instrument a few extra debug prints to show desired, deltaCenter, smoothedStep, and moveDeg so we can see exactly why a move wasn’t forced.


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

Thank you @Athena.  The script runs well, however when the IR value rises above a certain threshold, I am not getting reliable "recapture" when the servo is panned.   I suspect the quality of my IR values are not to be trusted at my reception rate.  Thank you for the wonderful help along the way! Jack

Author Avatar
PRO
Canada
#19  

Great details - thanks Jack. Your diagnosis is likely correct: the IR readings are noisy or intermittently sampled during motion, so the detector sometimes fails to "recapture" after the pan. Below are the causes, practical fixes, and a ready-to-paste JavaScript example that implements robust per-position sampling + median/average filtering + hysteresis and consecutive-sample confirmation to make recapture much more reliable.

Key causes

  • Quantization/noise on single ADC reads (0-255) - small changes get lost.
  • Sampling while servo still moving (or while emitter off) bad readings.
  • Ambient IR / sunlight causing volatile spikes.
  • Single-sample thresholding (no debouncing) false negatives.

What to try (software first)

  1. Use multiple samples per commanded position

    • Take N samples while servo is stationary and use median or average.
    • Median is robust to spikes; average smooths noise.
  2. Require K-of-N or consecutive samples above threshold

    • Avoid acting on a single sample; require confirmation.
  3. Add hysteresis

    • Use separate thresholds for detection and release to avoid chatter.
  4. Increase ADC resolution or oversample

    • Use ADC.get12Bit(port) in the poller (returns 0-4095) if possible, then scale.
    • Or oversample (take many quick reads and average) to reduce quantization noise.
  5. Ensure servo is settled before sampling

    • Use Servo.waitForMove() or increase settleMs and take samples after settling.
  6. Exponential smoothing / low-pass filter

    • Smooth long-term trend but still use per-position median/confirm for detection.
  7. Increase scanOffset / slower micro-sweeps

    • Make sure the sweep crosses the beam more reliably and gives sensor enough time.
  8. Hardware improvements (if needed)

    • Shield sensor from ambient light, add lens, use modulated IR emitter + demodulated receiver for robustness.

Example: robust per-position sampling + median + hysteresis + K-of-N confirmation

  • Paste this into a JavaScript Script skill in ARC.
  • It reads getVar("$IRADC") (your poller), does per-position sampling (N samples), uses median and average, requires K consecutive medians above threshold to declare detection and recapture.
// Robust recapture scan - per-position median sampling + hysteresis + confirmation
// Configure:
var servoPort = d17;
var centerPos = 90;
var scanOffset = 2;
var settleMs = 40;           // ms to let servo settle (increase if noisy)
var samplesPerPos = 7;      // odd number recommended for median
var sampleSpacingMs = 8;    // ms between ADC samples
var confirmNeeded = 2;      // consecutive medians above detectThreshold to lock
var detectThreshold = 60;   // detection threshold (0-255). tune to your sensor
var releaseThreshold = 48;  // hysteresis: lower threshold to release (avoid chatter)
var switchDirEvery = 1;
var loopSleepMs = 5;
var adcVarName = "$IRADC";  // poller must write this
// Internal:
var direction = 0;
var cmdCount = 0;
var lockCount = 0;
var locked = false;
var lastMedian = 0;

// helper functions
function clampAngle(a){ if(a < 0) return 0; if(a > 180) return 180; return a; }
function median(arr){
  var s = arr.slice().sort(function(a,b){return a-b;});
  return s[Math.floor(s.length/2)];
}
function avg(arr){
  if(arr.length === 0) return 0;
  var s = 0; for(var i=0;i<arr.length;i++) s+=arr[i];
  return s/arr.length;
}

// main loop
while(true){
  // choose commanded position for mini-sweep
  var commanded = (direction === 0) ? clampAngle(centerPos - scanOffset) : clampAngle(centerPos + scanOffset);
  Servo.setPosition(servoPort, commanded);
  // Prefer waiting for motion to finish to get stable readings
  Servo.waitForMove(servoPort, 0, 400); // timeout 400ms (tune)
  // small extra settle in case waitForMove returns early
  sleep(settleMs);

  // collect multiple samples and compute median
  var samples = [];
  for(var i=0;i<samplesPerPos;i++){
    var s = getVar(adcVarName, "");
    if(s !== ""){
      var v = parseInt(s,10);
      if(!isNaN(v)) samples.push(v);
    }
    sleep(sampleSpacingMs);
  }
  // If poller sometimes misses values, keep going but ensure we have at least 3 samples
  if(samples.length < 3){
    // optional: log and skip
    print("Not enough samples at pos " + commanded + " samples:" + samples.length);
  }
  var med = median(samples);
  var mean = Math.round(avg(samples));
  lastMedian = med;

  // Debug print
  print("cmd:" + commanded + " med:" + med + " avg:" + mean + " sz:" + samples.length + " center:" + centerPos);

  // Detection logic with hysteresis and confirmation
  if(!locked){
    // if above detectThreshold, increment lockCount, else reset
    if(med >= detectThreshold){
      lockCount++;
      if(lockCount >= confirmNeeded){
        locked = true;
        print("LOCK ACQUIRED at " + commanded + " med:" + med);
        // Optionally: recenter around current commanded or compute desired center
        // For this example, set centerPos to current commanded (or fine tune)
        centerPos = commanded;
        setVar("$actualCourse", centerPos.toString());
        // Optionally wait or perform fine-sweep here
      }
    } else {
      lockCount = 0;
    }
  } else {
    // already locked. release only when med falls below releaseThreshold
    if(med <= releaseThreshold){
      locked = false;
      lockCount = 0;
      print("RELEASE at " + commanded + " med:" + med);
      // Optionally: start recovery behavior (expand sweep, slower scan)
    }
  }

  // alternate direction
  cmdCount++;
  if(cmdCount >= switchDirEvery){ direction = 1 - direction; cmdCount = 0; }

  sleep(loopSleepMs);
}

How to tune quickly

  • Run the script and watch med and avg prints while you move a target left/center/right.
  • Pick detectThreshold near the level you consistently see with target present minus a small margin.
  • Set releaseThreshold ~ 0.7-0.9 * detectThreshold for hysteresis.
  • If readings are sparse, increase settleMs and sampleSpacingMs or increase samplesPerPos.
  • If resolution is too coarse, change your poller to use ADC.get12Bit(port) and scale values (0-4095) - then increase thresholds accordingly.

If you want, I can

  • Provide a modified poller example that uses ADC.get12Bit and publishes scaled $IRADC12 (and the script adjusted to use it).
  • Add a recapture routine that expands sweep amplitude and slows sweep when lock is lost, then tightens in when signal returns.
  • Convert the median confirmation into a winner-take-all centroid calculation for more precise center locking.

Quick questions

  1. What is your poller interval and does it use ADC.get12Bit or ADC.get?
  2. Are you pulsing the Sharp IR emitter under script control, or is the sensor passive? (If pulsing, sampling should be synchronized to emitter ON.)
  3. Do you want me to add an automatic threshold calibration routine (ambient baseline + detection level)?

Tell me which follow-up you prefer and I’ll produce the exact poller + scan combo tuned for your WiFi environment.


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.