@Athena,
Attached is JavaScript Rev22 of an IR tracker for a rover. Can you review it?
Context: this is an embedded ARC JavaScript script running in an ARC ARC project. It appears to use an EZB for hardware I/O (servo on port d17 and IR on ADC2). The script reads the IR ADC and drives a servo to find reflector edges, then computes a bearing from those edges. The script is not part of a robot skill in the project (it's embedded scripting), but it interacts directly with the EZB servo and ADC ports.
Rev22:
var servoPort = d17;
var irPort = ADC2;
var desiredBearing = 90;
var actualBearing = 90;
var negInit = -1;
var negFinal = -1;
var posInit = -1;
var posFinal = -1;
var negObsTot = 0;
var posObsTot = 0;
var negObsMax = 1000;
var posObsMax = 1000;
var bearingCount = 0;
var lastPublishedBearing = -1;
var reflectorThreshold = 15;
var servoStep = 1;
var scanDelayMs = 20;
var settleDelayMs = 10;
var state = "SCAN_NEG_INIT";
print("IR tracker started. desiredBearing: " + desiredBearing);
setVar("$desiredBearing", desiredBearing);
setVar("$actualBearing", actualBearing);
setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
setVar("$bearingCount", bearingCount);
function readIR() {
return ADC.get(irPort, 0);
}
function readServo() {
return Servo.getPosition(servoPort, 0);
}
function publishBearing(newBearing) {
actualBearing = Math.round(newBearing);
if (actualBearing != lastPublishedBearing) {
lastPublishedBearing = actualBearing;
desiredBearing = actualBearing;
setVar("$actualBearing", actualBearing);
setVar("$desiredBearing", desiredBearing);
print("actualBearing: " + actualBearing + " desiredBearing: " + desiredBearing);
}
}
function updateBearingFromEdges() {
var candidate = -1;
// Prefer the most recently valid completed pair.
if (negFinal >= 0 && posFinal >= 0) {
candidate = (negFinal + posFinal) / 2;
} else if (negInit >= 0 && posInit >= 0) {
candidate = (negInit + posInit) / 2;
} else if (negInit >= 0 && posFinal >= 0) {
candidate = (negInit + posFinal) / 2;
} else if (negFinal >= 0 && posInit >= 0) {
candidate = (negFinal + posInit) / 2;
}
if (candidate >= 0) {
bearingCount++;
setVar("$bearingCount", bearingCount);
publishBearing(candidate);
}
}
function checkObstructionTotals() {
if (negObsTot >= negObsMax || posObsTot >= posObsMax) {
setVar("$sendStopNow", 1);
return true;
}
return false;
}
function scanStepNegative() {
var ir = readIR();
negObsTot += ir;
setVar("$negIRTot", negObsTot);
if (ir <= reflectorThreshold) {
return true;
}
Servo.decrement(servoPort, servoStep);
sleep(scanDelayMs);
return false;
}
function scanStepPositive() {
var ir = readIR();
posObsTot += ir;
setVar("$posIRTot", posObsTot);
if (ir <= reflectorThreshold) {
return true;
}
Servo.increment(servoPort, servoStep);
sleep(scanDelayMs);
return false;
}
function resetTotalsIfNeeded() {
if (checkObstructionTotals()) {
return;
}
negObsTot = 0;
posObsTot = 0;
setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
}
while (true) {
if (state == "SCAN_NEG_INIT") {
if (scanStepNegative()) {
negInit = readServo();
print("negInit: " + negInit);
updateBearingFromEdges();
Servo.setPosition(servoPort, desiredBearing);
sleep(settleDelayMs);
state = "SCAN_NEG_FINAL";
}
}
else if (state == "SCAN_NEG_FINAL") {
if (scanStepNegative()) {
negFinal = readServo();
print("negFinal: " + negFinal);
updateBearingFromEdges();
Servo.setPosition(servoPort, desiredBearing);
sleep(settleDelayMs);
state = "SCAN_POS_INIT";
}
}
else if (state == "SCAN_POS_INIT") {
if (scanStepPositive()) {
posInit = readServo();
print("posInit: " + posInit);
updateBearingFromEdges();
Servo.setPosition(servoPort, desiredBearing);
sleep(settleDelayMs);
state = "SCAN_POS_FINAL";
}
}
else if (state == "SCAN_POS_FINAL") {
if (scanStepPositive()) {
posFinal = readServo();
print("posFinal: " + posFinal);
updateBearingFromEdges();
Servo.setPosition(servoPort, desiredBearing);
sleep(settleDelayMs);
resetTotalsIfNeeded();
state = "SCAN_NEG_INIT";
}
}
else {
state = "SCAN_NEG_INIT";
}
}
I do not think the following function is correct - see my notes in the comments:
function updateBearingFromEdges() {
var candidate = -1;
// Prefer the most recently valid completed pair.
if (negFinal >= 0 && posFinal >= 0) {
candidate = (negFinal + posFinal) / 2; // This is correct. Equal distance from negFinal and posFinal will be the center position (actual bearing).
} else if (negInit >= 0 && posInit >= 0) {
candidate = (negInit + posInit) / 2; // This is correct. Equal distance from negInit and posInit will be the center position (actual bearing).
// -----------------------------------------------------------------------------------------------
} else if (negInit >= 0 && posFinal >= 0) { // Incorrect below:
candidate = (negInit + posFinal) / 2; // This section is not valid. Equal distance from negInit and posFinal will not be the center position (actual bearing).
} else if (negFinal >= 0 && posInit >= 0) {
candidate = (negFinal + posInit) / 2; // This section is not valid. Equal distance from negFinal and posInit will not be the center position (actual bearing).
}
if (candidate >= 0) {
bearingCount++;
setVar("$bearingCount", bearingCount);
publishBearing(candidate);
}
}
By the way, the script will not move the [servo] if started at desiredBearing (90). The only way I can get the [servo] to move is to first move it off 90; then it recenters itself at 90 and the [servo] stops. The [servo] should continually sweep NEG and POS.
To review:
- NEG movement identifies two servo positions: negInit and negFinal.
- At negInit, actualBearing = ((negInit + posInit) / 2).
- At negFinal, actualBearing = ((negFinal + posFinal) / 2).
- POS movement identifies two servo positions: posInit and posFinal.
- At posInit, actualBearing = ((posInit + negInit) / 2).
- At posFinal, actualBearing = ((posFinal + negFinal) / 2).
Can you rewrite Rev22 to correct function updateBearingFromEdges()? Also, can you fix the [servo] movement so it continually sweeps NEG and POS?
Thank you, Jack

Absolutely - thanks for clarifying. You’re right: in your model,
ir <= reflectorThresholdis the edge capture condition, not the trigger to start the sweep. That means the scan should record the first low sample asnegInit/posInit, continue through the target, then record the next low sample asnegFinal/posFinal.Below is a cleaner Rev24 that follows that concept and uses a true edge-tracking state machine.
Rev24 - ARC JavaScript true edge-tracking scanner
How this detects rising / peak / falling edge
Here’s the idea in plain language:
In your implementation, the important part is not the literal "peak" of the sensor value - it’s the center estimate formed between the two captured edge positions.
Why this version is better
1. State-based gating
Each state only fires once:
SCAN_NEG_INITSCAN_NEG_FINALSCAN_POS_INITSCAN_POS_FINALThat prevents repeated captures while the IR remains low across multiple loop cycles.
2. Threshold debounce
lowCountRequiredmakes the script wait for a couple of consecutive low readings before confirming an edge.That helps avoid false triggers from noisy IR readings.
3. Continuous sweep
The servo keeps moving:
Servo.decrement()Servo.increment()So it won’t stall at the center unless the servo physically reaches its limit.
4. Bearing only after valid edge pairs
updateBearingFromEdges()runs afternegFinalorposFinalis captured, which matches your request.One note on the edge math
You mentioned a true edge-tracking scanner, so this version treats the low-threshold region as the reflector band and uses:
If you want, I can take this one step further and make the bearing logic explicitly use only the matching pair for each sweep, rather than allowing fallback combinations.
That would make the scan stricter and easier to reason about.
If you’d like, I can now write a Rev25 that:
@Athena. Thank you for your continued support. The logic for function ' updateBearingFromEdges(edgeName) ':
negInitactualBearing = (negInit + posInit) / 2negFinalactualBearing = (negFinal + posFinal) / 2posInitactualBearing = (posInit + negInit) / 2posFinalactualBearing = (posFinal + negFinal) / 2The logic for function ' scanStep() ':
The script below follows the logic stated above. Please review the following script for efficiency, accuracy, and simplicity. REV 25
Can you please add a simple way to prevent a runaway servo for NEG sweep if negInit is never found or for a POS sweep if posInit is never found?
Criteria for the runaway servo prevention:
Thank you. Jack
Yes - that’s a good safety addition.
Your current Rev25 logic is close, but the runaway protection should be state-based, not just based on time. That means if the script is scanning NEG or POS and never finds the threshold capture point, it should bail out once the servo exceeds a safe range beyond
desiredBearing.What to add
For your criteria, the simplest approach is:
maxSweepOffset = 20desiredBearing - 20desiredBearing + 20Servo.setPosition(servoPort, desiredBearing)setVar("$sendStopNow", 1)Recommended safety helper
Add this helper:
Add bounds for NEG and POS
Add these variables near the top:
If you want to recalculate them anytime
desiredBearingchanges, wrap them in a function:Call that once at startup and again whenever
desiredBearingis changed.Updated
moveServoForSweep()Here’s the safer version:
A small but important note
Your existing code uses:
That is fine only if you intentionally want to jump back to the initial edge position. For a continuous sweep,
Servo.increment()/Servo.decrement()is usually cleaner and less likely to create jumps.Best practice for your scan logic
You’ll want your
scanStep()to:desiredBearingOne more refinement
Because
desiredBearingis being updated bypublishBearing(), the abort limits should be recalculated after a new bearing is published.Example:
If you want, I can do this next
I can rewrite your full Rev25 into a cleaner Rev26 with:
If you’d like, I can produce the full updated code block now.
@Athena. Thank you.
Can you rewrite the full Rev25 into a cleaner Rev26 with:
Thank you. Jack
Sorry, I can't continue helping in this thread. It's grown long enough that my context window will require too much extra processing, which makes follow-ups unreliable and I can start making mistakes.
A couple of options:
@Athena. Thank you. Jack