Thank you @Athena for your help. Please review REV 27.4 below. This JavaScript runs as an ARC script / robot skill inside an ARC project and appears to use an EZB for hardware I/O (servo on d17 and IR on ADC2).
//REV 27.4 JavaScript
var servoPort = d17;
var irPort = ADC2;
var desiredBearing = 90;
var actualBearing = 90;
var actualBearingCurrent = 90;
var lastActualBearing = -1;
var lastPublishedBearing = -1;
var prevLastPublishedBearing = -1;
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 reflectorThreshold = 15;
var servoStep = 1;
var resetDelayMs = 50;
var settleDelayMs = 10;
var runawayCount = 0;
var runawayMargin = 20;
var negAbortLimit = desiredBearing - runawayMargin;
var posAbortLimit = desiredBearing + runawayMargin;
var state = "NEG_SEARCH_INIT";
var stopRequested = false;
setVar("$desiredBearing", desiredBearing);
setVar("$actualBearing", actualBearing);
setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
setVar("$bearingCount", bearingCount);
print("IR tracker Rev27.4 started.");
function readIR() {
return ADC.get(irPort, 0);
}
function readServo() {
return Servo.getPosition(servoPort, 0);
}
function refreshAbortLimits() {
negAbortLimit = desiredBearing - runawayMargin;
posAbortLimit = desiredBearing + runawayMargin;
}
function resetNegEdges() {
negInit = -1;
negFinal = -1;
}
function resetPosEdges() {
posInit = -1;
posFinal = -1;
}
function publishBearing(newBearing) {
var rounded = Math.round(newBearing);
if (rounded == actualBearingCurrent) {
return;
}
actualBearingCurrent = rounded;
if (actualBearingCurrent == lastPublishedBearing) {
return;
}
if (actualBearingCurrent == prevLastPublishedBearing) {
return;
}
prevLastPublishedBearing = lastPublishedBearing;
lastPublishedBearing = actualBearingCurrent;
actualBearing = actualBearingCurrent;
setVar("$actualBearing", actualBearingCurrent);
}
function updateBearingFromEdges(edgeName) {
var candidate = -1;
if (edgeName == "negInit" && posInit >= 0) {
candidate = (negInit + posInit) / 2;
} else if (edgeName == "negFinal" && posFinal >= 0) {
candidate = (negFinal + posFinal) / 2;
} else if (edgeName == "posInit" && negInit >= 0) {
candidate = (posInit + negInit) / 2;
} else if (edgeName == "posFinal" && negFinal >= 0) {
candidate = (posFinal + negFinal) / 2;
}
if (candidate >= 0) {
bearingCount++;
setVar("$bearingCount", bearingCount);
publishBearing(candidate);
}
}
function stopNow(reason) {
print("STOP: " + reason);
Servo.setPosition(servoPort, desiredBearing);
setVar("$sendStopNow", 1);
stopRequested = true;
}
function beginNegSweep() {
Servo.setPosition(servoPort, desiredBearing);
sleep(resetDelayMs);
resetNegEdges();
negObsTot = 0;
setVar("$negIRTot", negObsTot);
refreshAbortLimits();
state = "NEG_SEARCH_INIT";
}
function beginPosSweep() {
Servo.setPosition(servoPort, desiredBearing);
sleep(resetDelayMs);
resetPosEdges();
posObsTot = 0;
setVar("$posIRTot", posObsTot);
refreshAbortLimits();
state = "POS_SEARCH_INIT";
}
function handleRunaway() {
runawayCount++;
Servo.setPosition(servoPort, desiredBearing);
sleep(resetDelayMs);
if (runawayCount >= 3) {
stopNow("Runaway limit reached: " + runawayCount);
return;
}
if (state == "NEG_SEARCH_INIT" || state == "NEG_SEARCH_FINAL") {
beginPosSweep();
} else {
beginNegSweep();
}
}
function endOfPosSweepChecks() {
if (negObsTot > negObsMax) {
stopNow("NEG OBS total exceeded max: " + negObsTot);
return true;
}
if (posObsTot > posObsMax) {
stopNow("POS OBS total exceeded max: " + posObsTot);
return true;
}
negObsTot = 0;
posObsTot = 0;
setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);
return false;
}
function stepAndDetect(stepDelta, abortLimit, obsVarName, obsTotalRef, searchInitState, searchFinalState, initEdgeName, finalEdgeName) {
var current = readServo();
var irValue = readIR();
var reflectorDetected = (irValue >= reflectorThreshold);
if (state == searchInitState) {
if (state == "NEG_SEARCH_INIT") {
negObsTot += irValue;
setVar("$negIRTot", negObsTot);
} else {
posObsTot += irValue;
setVar("$posIRTot", posObsTot);
}
if (current >= abortLimit && !reflectorDetected) {
handleRunaway();
return;
}
if (reflectorDetected) {
if (initEdgeName == "negInit") {
negInit = current;
} else {
posInit = current;
}
updateBearingFromEdges(initEdgeName);
if (initEdgeName == "negInit") {
state = "NEG_SEARCH_FINAL";
} else {
state = "POS_SEARCH_FINAL";
}
if (initEdgeName == "posInit") {
runawayCount = 0;
}
} else {
Servo.setPosition(servoPort, current + stepDelta);
}
} else if (state == searchFinalState) {
if (state == "NEG_SEARCH_FINAL") {
negObsTot += irValue;
setVar("$negIRTot", negObsTot);
} else {
posObsTot += irValue;
setVar("$posIRTot", posObsTot);
}
if (current >= abortLimit && !reflectorDetected) {
handleRunaway();
return;
}
if (!reflectorDetected) {
if (finalEdgeName == "negFinal") {
negFinal = current;
} else {
posFinal = current;
}
updateBearingFromEdges(finalEdgeName);
if (finalEdgeName == "posFinal") {
if (endOfPosSweepChecks()) {
return;
}
beginNegSweep();
} else {
beginPosSweep();
}
} else {
Servo.setPosition(servoPort, current + stepDelta);
}
}
}
beginNegSweep();
while (true) {
if (stopRequested) {
break;
}
if (state == "NEG_SEARCH_INIT") {
stepAndDetect(
-servoStep,
negAbortLimit,
"$negIRTot",
negObsTot,
"NEG_SEARCH_INIT",
"NEG_SEARCH_FINAL",
"negInit",
"negFinal"
);
} else if (state == "NEG_SEARCH_FINAL") {
stepAndDetect(
-servoStep,
negAbortLimit,
"$negIRTot",
negObsTot,
"NEG_SEARCH_INIT",
"NEG_SEARCH_FINAL",
"negInit",
"negFinal"
);
} else if (state == "POS_SEARCH_INIT") {
stepAndDetect(
servoStep,
posAbortLimit,
"$posIRTot",
posObsTot,
"POS_SEARCH_INIT",
"POS_SEARCH_FINAL",
"posInit",
"posFinal"
);
} else if (state == "POS_SEARCH_FINAL") {
stepAndDetect(
servoStep,
posAbortLimit,
"$posIRTot",
posObsTot,
"POS_SEARCH_INIT",
"POS_SEARCH_FINAL",
"posInit",
"posFinal"
);
}
sleep(settleDelayMs);
}
The IR sensor is active-low for the reflector, so this line should be:
var reflectorDetected = (irValue <= reflectorThreshold);
Why negFinal is being missed
What is happening in REV 27.4 is that the edge logic is still too compressed, and the helper is not preserving the state transitions cleanly enough. In particular:
- negInit is being detected.
- The code then transitions too quickly into the next phase.
- negFinal can be skipped if the reflector state flips during the same sample window or if the servo step direction is not applied in the exact phase you expect.
Also, the helper as written is doing too much at once:
- reading the servo position
- reading the IR sensor
- handling runaway conditions
- detecting init/final edges
- changing state
- moving the servo
That makes it harder to reason about and makes timing-sensitive edge detection fragile.
Why that version is failing
The helper is too generic for ARC JavaScript and for this edge timing. The biggest issue is that the function tries to handle both NEG and POS cases with a single code path, but the edge transitions are not symmetrical in this logic. Since the IR behavior is clearly directional and timing-sensitive, it's better to keep the helper small and let the four states remain explicit in the robot skill script.
Thank you, Athena, for this analysis. Can you please rewrite this as REV 27.5? Please use:
- correct active-low reflector logic
- four explicit states preserved (NEG_SEARCH_INIT, NEG_SEARCH_FINAL, POS_SEARCH_INIT, POS_SEARCH_FINAL)
- no over-generic helper handling everything
- one small helper only for repeated step/IR sampling
- minimal print overhead
- clear comments for each state
Thanks, Jack

Absolutely - here’s a cleaner REV 27.5 rewrite that keeps the four explicit states, uses the correct active-low reflector logic, and limits the helper to just one small repeated sample/step action.
What changed in REV 27.5
NEG_SEARCH_INITNEG_SEARCH_FINALPOS_SEARCH_INITPOS_SEARCH_FINALREV 27.5 JavaScript
A couple of notes
updateBearingFromEdges()so it computes the midpoint only when the matching pair of edges is complete:negInit + negFinalposInit + posFinalOne important question
In your hardware setup, is the reflector sensor high when dark / low when reflective exactly as assumed here, or is it inverted by the sensor board logic? If you want, I can also produce a REV 27.6 version with:
If you'd like, I can also convert this into a more modular ARC JavaScript Robot Skill style with a small configuration block at the top for threshold, step size, and limits.
Thank you @Athena for REV 27.5.
I am having trouble verifying the proper operation of function ' UpdateBearingFromEdges'.
Here is the function UpdateBearingFromEdges:
The Problem:
After making the correct changes, candidate = (negInit + posInit) / 2 AND candidate = (negFinal + posFinal) / 2, the second 'else if' statement [else if (posInit >= 0 && posFinal >= 0)] never seems to run. The var negInit AND negFinal never stop being TRUE.
Can you troubleshoot the function 'updateBearingFromEdge' using the corrected equations for 'candidate'?
Thank you. Jack
Yes - the issue is in the edge pairing logic, not just the midpoint math.
You’re correct that the midpoint equations should be:
(negInit + posInit) / 2(negFinal + posFinal) / 2So the function should not pair
negInitwithnegFinal, orposInitwithposFinal.Why the second branch never runs
If
negInitandnegFinalnever stop being true, that usually means:updateBearingFromEdges()is being called before the correct pair has a chance to formIn other words, once
negInitandnegFinalboth get set, that firstifremains true forever unless you reset them. Then theelse iffor the positive sweep never gets a chance to run.What to change
1) Fix the midpoint pairing
Use the correct pairings:
2) Reset edge variables after publishing
This is likely the main reason the second branch never runs.
If
negInitstays valid forever, then the first condition can keep winning.You need to clear the pair once it has been used.
A better version is:
This way, once a pair is consumed, it won’t block the next pair.
3) Make sure
updateBearingFromEdges()is called at the right momentsIt should be called only when you actually set one of the edges:
negInitis capturedposInitis capturednegFinalis capturedposFinalis capturedBut if the state machine never reaches the positive states, then
posInit/posFinalwill never be set.So if the script is staying in negative sweep states, the real issue may be this:
NEG_SEARCH_INITtransitions toNEG_SEARCH_FINALNEG_SEARCH_FINALnever exits because the reflector never clearsPOS_SEARCH_INITnever beginsThat means your logic is probably not advancing out of the negative sweep soon enough.
Recommended fix for the state logic
If you want the two bearing calculations to represent the two sides of the reflector window, then the transitions should behave like this:
negInitnegFinalposInitposFinalAnd only after a pair is complete should you compute the midpoint.
Best practical version
Here’s the version I’d recommend:
Important troubleshooting note
If
negInitandnegFinalare always both set, then your script may be capturing them during the same sweep, which usually means the state transitions are not being advanced correctly.A helpful debug step is to print the state only when an edge is set:
But only do that temporarily, because you asked for minimal print overhead.
What I think is happening
Most likely:
negInitnegFinalIf you want, I can rewrite REV 27.5 with:
If you'd like, I can post the full corrected REV 27.5 script next.
Thank you @Athena. Can you please post the full corrected REV 27.5 script next? Please call it REV 27.6.
Thanks. Jack
Absolutely - here is a full corrected REV 27.6 with:
Important note
I kept the bearing midpoint logic exactly as you requested:
candidate = (negInit + posInit) / 2candidate = (negFinal + posFinal) / 2And I made sure the used edge pair is cleared immediately after publishing, so the next pair can be evaluated.
One thing to verify
Because your IR is active-low, please confirm whether the reflector threshold should truly be:
or whether your sensor is inverted again by external hardware.
If you want, I can also give you a REV 27.6 debug build that prints only:
so you can verify the timing without flooding the ARC log.
Thank you @Athena for REV 27.6. Can you correct the following in REV 27.6?
Thank you. Can you also write REV 27.6 debug build that prints only:
so, I can verify the timing without flooding the ARC log? Thank you very much. Jack
Yes - and a few of your requested changes need small syntax corrections so the script still works in ARC JavaScript.
Important fixes to your requested edits
if (sample.current = negAbortLimit ...)would assign, not compare. In JavaScript it must be:>=if you want a limit check>if you want strictly greater thannegSearchFinal/posSearchFinal, if you truly want no abort-limit checks, we’ll remove those checks entirely as requested.desiredBearingto be replaced withroundedin several functions. I’ve done that below, butroundedis now the bearing reference used for reset/recenter behavior.REV 27.6 Debug Build
This version prints only:
Notes on your requested changes
I implemented these:
negObsMax = 2000posObsMax = 2000desiredBearingreferences in reset/recenter logic changed toroundedrunawayCount = 0;added whennegInitis captureddoNegSearchFinal()doPosSearchFinal()irValue <= reflectorThresholdfor active-low detectionOne thing to confirm
You asked for:
if (sample.current > posAbortLimit && !sample.reflectorDetected)That would mean strictly greater than, but since you also asked to remove abort-limit checks in final states, I left that out entirely. If you want me to add a strict greater-than abort only in
POS_SEARCH_INIT, I can do that too.Would you like me to produce a REV 27.6.1 cleaned version that also removes any redundant variables and further reduces log noise?
Thank you @Athena for REV 27.6 Debug Build. Can you incorporate the following changes?
CHANGE:
NEG SWEEP:
detect negInit
updateBearingFromEdges() after update, negInit and posInit are placed negative
publishBearing()
detect negFinal
updateBearingFromEdges() after update, negFinal and posFinal are placed negative
publishBearing()
POS SWEEP:
detect posInit
updateBearingFromEdges() no update takes place because negInit was placed negative during NEG SWEEP
publishBearing()
detect posFinal
updateBearingFromEdges() no update takes place because negFinal was placed negative during NEG SWEEP
publishBearing()
So, in NEG SWEEP and during updateBearingFromEdges() if both negInit AND posInit are placed negative, an interruption will take place in bearing update during POS SWEEP.
So, in NEG SWEEP and during updateBearingFromEdges() if both negFinal AND posFinal are placed negative, an interruption will take place in bearing update during POS SWEEP.
Consequently, no update to var candidate and no bearingCount++. 3. For doNeglSearchInit() please include:
if sample.current is less than negAbortLimit call for handleRunaway(). 4. For doPosSearchInit() please include:
if sample.current is greater than posAbortLimit call for handleRunaway(). 5. For doNegSearchInit(), when negInit is detected, please include runawayCount = 0. 6. For doPosSearchInit(), when posInit is detected, please include runawayCount = 0. Also, can you produce a REV 27.6.1 cleaned version that also removes any redundant variables and further reduces log noise?
Thank you. Jack