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

Yes - the cleanest fix is to make
updateBearingFromEdges()consume one pair at a time and then reset only that pair, rather than permanently blocking later POS sweep updates.Also, I corrected the naming to
roundedBearingeverywhere relevant, and I added the runaway reset on bothnegInitandposInitcapture as requested.One note before the code:
doNeglSearchInit()looks like a typo, so I assumed you meantdoNegSearchInit()sample.current posAbortLimit; in JavaScript that must be a comparison, so I used:doNegSearchInit(), I kept that there exactly as a safety gateREV 27.6.1 - cleaned version
What changed in this cleaned version
roundedBearingupdateBearingFromEdges()now:negInit + posInitnegFinal + posFinalrunawayCount = 0;is added when:negInitis detectedposInitis detectedOne thing I want to confirm
In
doNegSearchInit(), you asked for:I used:
because that is the valid JavaScript comparison and preserves your safety intent.
If you want, I can produce a REV 27.6.2 that further simplifies the state handlers by removing the duplicate
stepAndSample()motion logic and making the sweep transitions even tighter.Thank you @Athena for REV 27.6.1 .
Please call the next revision as REV 28. Please do the following:
NEG SWEEP:
POS SWEEP:
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. 3. For doNegSearchInit() 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().
Can you produce a REV 28 that further simplifies the state handlers by removing the duplicate
stepAndSample()motion logic and making the sweep transitions even tighter? Thanks. JackAbsolutely - here’s a REV 28 that incorporates your requested changes and further tightens the sweep handlers.
What changed in REV 28
publishBearing()now usesroundedBearingconsistentlyupdateBearingFromEdges()no longer clears both members of a pair in a way that can interfere with the next sweepdoNegSearchInit()includes the posAbortLimit runaway checkstepAndSample()motion logic has been removedREV 28
One clarification
Your note about not clearing both
negInitandposInittogether is exactly the kind of timing issue the ARC Script skill can be sensitive to, so I preserved the one-pair-at-a-time consumption behavior inupdateBearingFromEdges().If you want, I can make a REV 28.1 that adds a single debug print per state transition only so you can verify the exact sequence without cluttering the ARC log.
Also, if you'd like to refine the script further, the ARC AI Assistant can help rewrite and simplify this directly inside the Script skill editor: https://synthiam.com/Products/AI-Assistant
Thank you @Athena. I will be starting a new thread to get my final REV translated to C++ for the Teensy 4.1. Jack