@Athena - thank you for REV 26. I needed to switch the resetNegEdges() and resetPosEdges() calls so the logic in updateBearingFromEdges() would run properly. I also made a few timing adjustments and rewrote the publishBearing() function. It's running well overall.
However, negObsTot and posObsTot are not totaling as they should. Can you review the REV 26 script I am using (below) and rewrite it as REV 27, making sure negObsTot and posObsTot sum IR readings as they are taken?
Context: This appears to be EZ-Script running on an EZB (servo on d17 and IR on ADC2). The script is intended to run as part of an ARC project and interfaces with the EZB hardware (Servo and ADC). It also uses setVar() to publish globals back to ARC. Please keep the implementation consistent with ARC/EZB scripting and robot skill usage.
Criteria for summing IR values:
- Summing IR values for negObsTot should begin when the NEG sweep begins searching for negInit.
- Summing IR values for posObsTot should begin when the POS sweep begins searching for posInit.
- Checking negObsTot against negObsMax and posObsTot against posObsMax should be done once, at the end of each POS sweep. After that check, both negObsTot and posObsTot should be zeroed.
I do not want any additional changes beyond this right now. If you see a clear simplification that improves sensor/servo speed and keeps signal processing uncluttered, feel free to implement it. Sensor/servo speed and minimal signal processing are my top priorities. After REV 27, I plan to start a new thread and port this to C++ for a Teensy 4.1.
Thanks,
Jack
REV 26
var servoPort = d17;
var irPort = ADC2;
var desiredBearing = 90;
var actualBearing = 90;
var actualBearingCurrent = 90;
var lastActualBearingCurrent = 90;
var lastPublishedBearing = 90;
var prevLastPublishedBearing = 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 resetDelayMs = 50;
var settleDelayMs = 10;
var sweepDirection = "NEG";
var state = "MOVE";
var runawayMargin = 20;
var negAbortLimit = desiredBearing - runawayMargin;
var posAbortLimit = desiredBearing + runawayMargin;
print("IR tracker Rev26 corrected started.");
print("desiredBearing: " + desiredBearing);
print("negAbortLimit: " + negAbortLimit);
print("posAbortLimit: " + posAbortLimit);
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 refreshAbortLimits() {
negAbortLimit = desiredBearing - runawayMargin;
posAbortLimit = desiredBearing + runawayMargin;
}
function publishBearing(newBearing) {
actualBearing = Math.round(newBearing);
actualBearingCurrent = actualBearing;
// This second instance allows considering it for a published actual bearing
if (actualBearingCurrent == lastActualBearingCurrent) {
// First test: Was it the last published actual bearing?
if (actualBearingCurrent != lastPublishedBearing) {
// Second test: Was it the previous to last published actual bearing?
if (actualBearingCurrent != prevLastPublishedBearing) {
prevLastPublishedBearing = lastPublishedBearing;
lastPublishedBearing = actualBearingCurrent;
// It then becomes the global actual bearing
setVar("$actualBearing", actualBearingCurrent);
print(" actualBearingCurrent: " + actualBearingCurrent);
}
}
} else { // The first instance before considering it for a published actual bearing
lastActualBearingCurrent = 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 abortSweep(reason) {
print("ABORT: " + reason);
Servo.setPosition(servoPort, desiredBearing);
setVar("$sendStopNow", 1);
state = "ABORT";
}
function resetNegEdges() {
negInit = -1;
negFinal = -1;
}
function resetPosEdges() {
posInit = -1;
posFinal = -1;
}
function scanStep() {
var current = readServo();
var irValue = readIR();
var reflectorDetected = (irValue <= reflectorThreshold);
if (state == "ABORT") {
return;
}
// -----------------------------
// NEG sweep
// -----------------------------
if (sweepDirection == "NEG") {
// NEG runaway protection:
// If negInit is never found, stop before running too far left.
if (current <= negAbortLimit && negInit < 0) {
abortSweep("NEG sweep runaway protection triggered at " + current);
return;
}
// First reflector detection on NEG sweep
if (reflectorDetected && negInit < 0) {
negInit = current;
// print("negInit found at " + negInit);
updateBearingFromEdges("negInit");
}
// Reflector exit on NEG sweep
else if (!reflectorDetected && negInit >= 0 && negFinal < 0) {
negFinal = current;
// print("negFinal found at " + negFinal);
updateBearingFromEdges("negFinal");
// NEG sweep is complete. Check total, then prepare for POS.
if (negObsTot >= negObsMax) {
abortSweep("NEG IR total exceeded max: " + negObsTot);
return;
}
negObsTot = 0;
setVar("$negIRTot", negObsTot);
// Do NOT clear pos edges yet if you still need them for current bearing logic.
// Clear NEG edges only after NEG sweep is complete and bearing update has run.
resetPosEdges();
Servo.setPosition(servoPort, negInit);
sweepDirection = "POS";
sleep(resetDelayMs);
return;
}
// Keep moving NEG only while we have not completed the NEG edge pair
if (current > 0 && negFinal < 0) {
Servo.decrement(servoPort, servoStep, 0);
} else if (negFinal >= 0) {
// Safety: once NEG final is found, force the direction change.
sweepDirection = "POS";
} else {
sweepDirection = "POS";
}
return;
}
// -----------------------------
// POS sweep
// -----------------------------
if (sweepDirection == "POS") {
// POS runaway protection:
// If posInit is never found, stop before running too far right.
if (current >= posAbortLimit && posInit < 0) {
abortSweep("POS sweep runaway protection triggered at " + current);
return;
}
// First reflector detection on POS sweep
if (reflectorDetected && posInit < 0) {
posInit = current;
// print("posInit found at " + posInit);
updateBearingFromEdges("posInit");
}
// Reflector exit on POS sweep
else if (!reflectorDetected && posInit >= 0 && posFinal < 0) {
posFinal = current;
// print("posFinal found at " + posFinal);
updateBearingFromEdges("posFinal");
// POS sweep is complete. Check total, then prepare for NEG.
if (posObsTot >= posObsMax) {
abortSweep("POS IR total exceeded max: " + posObsTot);
return;
}
posObsTot = 0;
setVar("$posIRTot", posObsTot);
// Clear POS edges only after POS sweep is complete and bearing update has run.
resetNegEdges();
Servo.setPosition(servoPort, posInit);
sweepDirection = "NEG";
sleep(resetDelayMs);
return;
}
// Keep moving POS only while we have not completed the POS edge pair
if (current < 180 && posFinal < 0) {
Servo.increment(servoPort, servoStep, 0);
} else if (posFinal >= 0) {
// Safety: once POS final is found, force the direction change.
sweepDirection = "NEG";
} else {
sweepDirection = "NEG";
}
return;
}
}
while (true) {
scanStep();
sleep(settleDelayMs);
}

Yes - that’s a good direction. The repeated
step + read + detect + runawaypattern is exactly what should be factored out, and keeping the four explicit states will make the behavior much easier to maintain and later port to C++.Below is a REV 27.4 cleanup that:
NEG_SEARCH_INITNEG_SEARCH_FINALPOS_SEARCH_INITPOS_SEARCH_FINALREV 27.4 JavaScript
Notes
stepAndDetect(...)One thing to double-check
In your pasted comments, a few variables were renamed or implied differently:
lastActualBearingprevLastPublishedBearingactualBearingCandidateI kept the cleaner REV 27.4 version aligned with the working logic we were building. If you want, I can do a REV 27.5 final cleanup that:
stepAndDetect()accept the current state as a single parameterIf you’d like, I can also turn this into a Teensy-ready pseudo-C++ structure next so the later port is straightforward.
Thank you @Athena for REV 27.4. When I run it, the servo drives full positive. As I look at the code, the following appears wrong:
After making the above change, it stopped with the servo runaways. Now however, after negInit prints, negFinal is missed, and then only posInit prints followed by posFinal.
Can you troubleshoot Rev 27.4 for me? Also, can you comment on the logic in function stepAndDetect(stepDelta, abortLimit, obsVarName, obsTotalRef, searchInitState, searchFinalState, initEdgeName, finalEdgeName). This is new to me and I need understanding.
Thank you. Jack
Yes - that makes sense.
If your sensor is active-low for the reflector, then this line should indeed be:
That would explain the full-positive drive behavior. Good catch.
Why
negFinalis being missedWhat is happening in
REV 27.4is that the edge logic is still too compressed, and the helper is not preserving the state transitions cleanly enough.In particular:
negInitgets foundnegFinalcan be skipped if the reflector state flips during the same sample window or if the step direction is not being applied in the exact phase you expectAlso, the helper as written is doing too much at once:
That makes it harder to reason about.
What
stepAndDetect(...)was intended to doHere’s the idea in plain language:
stepDelta= how far and in what direction to move the servo each loopabortLimit= boundary where runaway protection should triggersearchInitState= the state where the code is looking for the first edgeNEG_SEARCH_INITsearchFinalState= the state where the code is looking for the second edgeNEG_SEARCH_FINALinitEdgeName/finalEdgeName= which edge variable to updatenegInit,negFinal,posInit,posFinalSo conceptually:
Why that version is failing
The helper is too generic for ARC JavaScript and for your edge timing.
The biggest issue is that the function tries to handle both NEG and POS cases with a single path, but the edge transitions are not symmetrical in your logic. Since your IR behavior is clearly directional and timing-sensitive, it’s better to keep the helper small, and let the four states remain explicit.
Recommended fix
I suggest a REV 27.5 that keeps the four explicit states, but uses one tiny helper only for the repeated "read IR, determine reflector, step servo" behavior.
That will make it easier to see exactly where:
negInitnegFinalposInitposFinalare set.
It will also avoid the state confusion from the previous helper.
Clean understanding of the state flow
For your project, the logic should be:
NEG_SEARCH_INIT
negInitNEG_SEARCH_FINALNEG_SEARCH_FINAL
negFinalPOS_SEARCH_INITPOS_SEARCH_INIT
posInitPOS_SEARCH_FINALPOS_SEARCH_FINAL
posFinalNEG_SEARCH_INITMy recommendation
I can rewrite this as REV 27.5 with:
That should make it much easier to understand and much less likely to miss
negFinal.If you want, I can provide that next in a clean ARC JavaScript block.
Thank you @Athena for this analysis. Can you please rewrite this as REV 27.5? Please use:
Thanks. 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:
Thank you. Jack