@Athena,
Please review the JavaScript code below.
Context:
- This script (IR_TRACKER_v20) runs on the EZ-B v4 (EZBv4) firmware as a JavaScript script that is part of my ARC project.
- The hardware (servo and IR sensor) is connected through the EZB (not directly to the PC). The servo is on port 17 and the IR sensor is read from ADC channel 2.
- The code treats IR <= 15 as a reflector "hit" and IR >= 16 as an obstruction. It updates values back to ARC via setVar() and uses the EZB servo and ADC APIs.
- Ultimately I want to transpose this JavaScript to C++ to run on a Teensy 4.1. For the review, please keep portability and timing/ADC/servo considerations in mind, but do not implement the port yet.
Requested review items:
- Check the overall construction and organization for efficiency (loops, variable usage, control flow).
- If changes are needed, please update the code while preserving the stated objectives and behavior.
- Keep in mind the eventual goal of porting this logic to C++ for a Teensy 4.1.
Thanks, Jack
var negIR = 0;
var posIR = 0;
var negIRTot = 0;
var posIRTot = 0;
setVar("$posIRTot", posIRTot);
setVar("$negIRTot", negIRTot);
var negHit = 0;
var posHit = 0;
var negInit = 90;
var negFinal = 90;
var lastNegInit = negInit;
var lastNegFinal = negFinal;
var posInit = 90;
var posFinal = 90;
var lastPosInit = posInit;
var lastPosFinal = posFinal;
var servoStart = 90;
var actualBearing = 90;
var desiredBearing = 90;
var actualBearingCurrent = 90;
var lastActualBearingCurrent = 90;
var lastAB = 90;
var prevLastAB = 90;
var bearingCount = 0;
var equalCount = 0;
var sleepVal = 20;
/* IR_TRACKER_v20 operates a single servo and IR sensor to track a single reflector across the room.
The servo swings back and forth, equal degrees from the desiredBearing. It collects IR readings
that meet the criteria of a reflector (IR <= 15), and those that do not (IR >= 16). IR >= 16 are
considered obstructions. When an IR reading exceeds an obstruction threshold, the robot will stop,
run a separate script to precisely locate the bearing and distance to the offending obstruction,
maneuver around it, and continue on its path relative to the reflector.
*/
// INITIAL PRINT
setVar("$sendActualBearingNow", 1);
print(" AC===actualBearingCurrent: " + actualBearingCurrent);
// Only called from the main loop when enough criteria are met to possibly change actualBearingCurrent.
function courseDetermination() {
// bearingCount > 5 keeps actualBearingCurrent from being computed until both
// negScanInit/negScanFinal and posScanInit/posScanFinal have run once.
if (bearingCount > 5) {
// lastActualBearing (lastAB) and prevLastActualBearing (prevLastAB) are to prevent
// unnecessary cycling back and forth from 'close' actualBearingCurrent.
if (actualBearingCurrent != lastAB) {
if (actualBearingCurrent != prevLastAB) {
print(" AC===actualBearingCurrent: " + actualBearingCurrent + " prevLastAB: " + prevLastAB);
prevLastAB = lastAB;
lastAB = actualBearingCurrent;
}
}
}
}
// Part 1 of a 2-part sequence for swinging the servo in a Neg direction (Right).
// Called at the beginning of the swing Right (starts at posScanInit position). Ends at first IR <= 15.
function negScanInit() {
servoStart = Servo.getPosition(17, 0);
for (a = 0; a < 30; a++) {
negIR = ADC.get(2, 0);
sleep(20); // previously 10
if (negIR <= 15) {
negInit = Servo.getPosition(17, 0);
print(" negInit: " + negInit);
negHit = 1;
actualBearingCurrent = Math.round(negInit + ((posInit - negInit) / 2));
if (actualBearingCurrent == lastActualBearingCurrent) {
equalCount++;
} else {
equalCount = 0;
}
bearingCount++;
setVar("$bearingCount", bearingCount);
setVar("$posIRTot", posIRTot);
posIRTot = 0;
lastActualBearingCurrent = actualBearingCurrent;
return;
} else {
if (Servo.getPosition(17, 0) >= desiredBearing && Servo.getPosition(17, 0) < (posFinal + 5)) {
posIRTot = posIRTot + negIR;
} else if (Servo.getPosition(17, 0) < desiredBearing && Servo.getPosition(17, 0) > (negFinal - 5)) {
negIRTot = negIRTot + negIR;
}
}
Servo.decrement(17, 1);
}
// servoStart only sequences if no IR <= 15 is detected.
Servo.setPosition(17, servoStart);
sleep(50);
sleepVal = 10;
print("neg-servoStart: " + servoStart);
}
// Part 2 of a 2-part sequence swinging the servo in a Neg direction (Right).
// Called only after part 1 noted a negHit (IR <= 15). Continues until IR >= 16.
// The servo resets to negInit (to save time) before continuing to posScanInit.
function negScanFinal() {
for (aa = 0; aa < 30; aa++) {
negIR = ADC.get(2, 0);
sleep(20); // previously 10
if (negIR >= 16) {
negIRTot = negIRTot + negIR;
negFinal = Servo.getPosition(17, 0);
print(" negFinal: " + negFinal);
actualBearingCurrent = Math.round(negFinal + ((posFinal - negFinal) / 2));
bearingCount++;
setVar("$bearingCount", bearingCount);
Servo.setPosition(17, negInit);
posIR = 0;
sleep(10);
return;
}
Servo.decrement(17, 1);
}
}
// Part 1 of a 2-part sequence for swinging the servo in a Pos direction (Left).
// Called at the beginning of the swing Left (starts at negScanInit position). Ends at first IR <= 15.
function posScanInit() {
servoStart = Servo.getPosition(17, 0);
for (b = 0; b < 30; b++) {
posIR = ADC.get(2, 0);
sleep(sleepVal); // previously 10
if (posIR <= 15) {
posInit = Servo.getPosition(17, 0);
print(" posInit: " + posInit);
posHit = 1;
actualBearingCurrent = Math.round(posInit - ((posInit - negInit) / 2));
bearingCount++;
setVar("$bearingCount", bearingCount);
setVar("$negIRTot", negIRTot);
negIRTot = 0;
sleepVal = 20;
return;
} else {
if (Servo.getPosition(17, 0) < desiredBearing && Servo.getPosition(17, 0) > (negFinal - 5)) {
negIRTot = negIRTot + posIR;
} else if (Servo.getPosition(17, 0) >= desiredBearing && Servo.getPosition(17, 0) < (posFinal + 5)) {
posIRTot = posIRTot + posIR;
}
}
Servo.increment(17, 1);
}
// servoStart only sequences if no IR <= 15 is detected.
Servo.setPosition(17, servoStart);
sleep(50);
sleepVal = 10;
print("pos-servoStart: " + servoStart);
}
// Part 2 of a 2-part sequence swinging the servo in a Pos direction (Left).
// Called only after part 1 noted a posHit (IR <= 15). Continues until IR >= 16.
// The servo resets to posInit (to save time) before continuing to negScanInit.
function posScanFinal() {
for (bb = 0; bb < 30; bb++) {
posIR = ADC.get(2, 0);
sleep(20); // previously 10
if (posIR >= 16) {
posIRTot = posIRTot + posIR;
posFinal = Servo.getPosition(17, 0);
print(" posFinal: " + posFinal);
actualBearingCurrent = Math.round(posFinal - ((posFinal - negFinal) / 2));
bearingCount++;
setVar("$bearingCount", bearingCount);
Servo.setPosition(17, posInit);
negIR = 0;
sleep(10);
return;
}
Servo.increment(17, 1);
}
}
// Main loop
// courseDetermination() is called only after certain criteria are met to avoid unnecessary calls.
while (true) {
negScanInit();
if (negInit > lastNegInit && negFinal != lastNegFinal) {
courseDetermination();
}
if (negHit == 1) {
negHit = 0;
negScanFinal();
if (negFinal > lastNegFinal && negInit != lastNegInit) {
courseDetermination();
}
lastNegInit = negInit;
lastNegFinal = negFinal;
}
posScanInit();
if (posInit < lastPosInit && posFinal != lastPosFinal) {
courseDetermination();
}
if (posHit == 1) {
posHit = 0;
posScanFinal();
if (posFinal < lastPosFinal && posInit != lastPosInit) {
courseDetermination();
}
lastPosInit = posInit;
lastPosFinal = posFinal;
}
}

Thanks, Jack - I reviewed the script with ARC JavaScript, timing/ADC/servo behavior, and your future Teensy 4.1 C++ port in mind.
High-level review
Your code has a clear intent:
desiredBearingADC2IR = 16as the reflection targetIR >= 16as obstruction behaviorThat said, there are a few issues that will hurt:
Main concerns I see
1) Repeated
Servo.getPosition()calls inside tight loopsYou call
Servo.getPosition(17, 0)many times inside conditions like:That is inefficient and also makes timing less deterministic. In ARC, that means multiple API calls every loop iteration. For a Teensy port, this would translate into unnecessary repeated reads too.
Recommendation: read servo position once per loop iteration and store it in a local variable.
2) Possible bug: assignment instead of comparison
I see this pattern in the pasted code:
and similarly:
If that is really in the live script, it is a bug because it assigns
16instead of comparing. It should almost certainly be:Same for
posIR.3) Loop variables are not declared
You use:
Those should be declared with
varin ARC JavaScript, or better yet removed if they are not actually needed.In ARC JavaScript, undeclared variables can lead to accidental globals and hard-to-track behavior. For a C++ port, this would be a bad habit to carry forward.
4) The
forloops don’t appear to be doing useful workIn the current code, the
forloops are not really acting as bounded loops in a meaningful way. The logic appears to be:That is more like a state machine than a
forloop.Recommendation: use
whileloops or a state-based approach instead of dummyforcounters.5) Excessive duplicated logic
negScanInit,negScanFinal,posScanInit, andposScanFinalare very similar.This is a good candidate for refactoring into shared helper logic:
That will make the code easier to port to C++ later.
6)
sleep(10)after servo repositioning may not guarantee physical completionIf the servo is driven through ARC,
Servo.setPosition()is non-blocking. Asleep(10)is usually too short to guarantee the servo has physically reached the new location.If the intent is only to "release" the scan quickly, that’s fine. But if you expect stable position before the next scan, you should allow more time or use a movement/position wait strategy.
For Teensy C++, you’ll want a similar concept: command position, then either:
7)
actualBearingCurrentlogic could be simplifiedThis section:
works as a duplicate suppression mechanism, but it’s a bit indirect.
A simpler and more portable approach is to compare against the last published value and only update when changed.
Portability notes for Teensy C++
For the eventual C++ port, the biggest structural improvement would be to separate the logic into:
That makes the same code easier to:
Timing/ADC considerations
ARC ADC values are
0-255; Teensy ADC may be0-4095unless you scale it.Servo timing on a Teensy will likely be more deterministic than ARC, but you’ll still want to avoid rapid oscillation and repeated reads in a single loop.
If
IR = 16is a special threshold, consider defining constants like:REFLECTOR_THRESHOLD = 16OBSTRUCTION_THRESHOLD = 16Even if they are the same value now, naming them separately helps if the meaning diverges later.
Cleaned-up ARC JavaScript version
Below is a version that keeps your intent and behavior, but improves structure and reduces repeated API calls.
I kept the logic close to your original design rather than redesigning it into a full state machine.
Important caveats about that cleanup
I preserved your general structure, but a couple things still deserve attention:
A)
negInit,posInit,negFinal,posFinalare all initialized to90That means your scan bounds may not actually move unless something outside this snippet updates them.
If those values are supposed to be dynamic, I’d want to see where they get changed.
B)
Servo.setPosition(servoPort, negInit)assumes the servo should jump back immediatelyThat matches your stated goal, but it can affect scan repeatability if the servo hasn’t physically arrived yet.
C) The threshold names should probably be clarified
If
IR = 16means "reflector found" andIR >= 16means "obstruction," those are overlapping meanings. It may be intentional, but it reads like two different rules sharing the same number.My recommendation for the next step
Before porting to C++, I’d strongly suggest converting this into a small explicit state machine. That would make the Teensy version much cleaner and more reliable.
A good C++-friendly breakdown would be something like:
SCAN_NEG_INITSCAN_NEG_FINALSCAN_POS_INITSCAN_POS_FINALOBSTRUCTION_HANDLINGThat approach will be much easier to port than the current nested loop style.
Questions for you
A couple of details would help me give you a more precise review:
negInit,negFinal,posInit, andposFinalupdated elsewhere, or are they fixed at 90 in this script?IR == 16andIR >= 16really both be treated as separate conditions, or did you meanIR > 16for obstruction?If you want, I can next help you do a logic-only refactor into a Teensy-friendly state machine while still keeping it in ARC JavaScript for validation first.
@Athena. I am thrilled with what you have done thus far. Please feel free to update any and all of my code, as I wish the result to be the simplest and most efficient possible. The construction of my submitted code was simply my best effort. It runs and proves the strategy used for reflector tracking and obstacle logging. It is my hope that you can interpret from it my intent and update it. By keeping it JavaScript during this phase I can easily test and evaluate the revisions. Teensy code can come later.
Things to clarify moving forward:
Please accomplish your recommendations for the next step
Before porting to C++, I’d strongly suggest converting this into a small explicit state machine. That would make the Teensy version much cleaner and more reliable. A good C++-friendly breakdown would be something like:
SCAN_NEG_INITSCAN_NEG_FINALSCAN_POS_INITSCAN_POS_FINALOBSTRUCTION_HANDLINGThat approach will be much easier to port than the current nested loop style.
Questions you asked
A couple of details would help me give you a more precise review:
negInit,negFinal,posInit, andposFinalupdated elsewhere, or are they fixed at 90 in this script? Answer: Fixed at 90 for now, global variable will update later.IR == 16andIR >= 16really both be treated as separate conditions, or did you meanIR > 16for obstruction? Answer: IR<=15 equals Reflector. All other IR values equal Obstructions.If you want, I can next help you do a logic-only refactor into a Teensy-friendly state machine while still keeping it in ARC JavaScript for validation first. Yes, please do this.
Thanks. Jack
Absolutely - given your clarified intent, I’d recommend turning this into a small explicit state machine now, while keeping it in ARC JavaScript for easy validation before any Teensy C++ port.
Below is a simplified refactor that keeps the same goals:
desiredBearing = 90for nowI also removed a lot of repeated variable noise and redundant reads/calls.
Key simplifications made
What changed
Servo.getPosition()andADC.get()callsWhat I assumed
negInit,negFinal,posInit, andposFinalare fixed at90for nowdesiredBearingis90Refactored ARC JavaScript
Important note about the scan flow
Your original script had a fairly custom pattern with
negHitandposHit. In the refactor above, I made the flow more linear and easier to reason about.If you want the scan to behave more like your original intent:
We can refine the state transitions to follow this exact pattern:
That is probably the most straightforward version for a future Teensy port.
Even cleaner version: simpler 4-state cycle
If your goal is "simplest and most efficient possible," this is the version I’d actually recommend as the next step. It is cleaner than mirroring the old hit flags.
States
SCAN_NEGSCAN_POSCOMPUTERESETIf you want, I can rewrite the code into that even cleaner cycle next.
Why this is better for the Teensy later
This structure will port nicely because on Teensy you’ll want:
switch(state)loopServoandADCmillis()That makes the eventual C++ version much safer and more deterministic.
One ARC-specific suggestion
Since you’re working in ARC JavaScript first, this kind of refactor is exactly the kind of thing the ARC AI Assistant can help iterate on directly in the script editor. It can rewrite, simplify, and reorganize code while keeping the ARC command structure intact:
https://synthiam.com/Products/AI-Assistant
My recommendation
If you’d like, I can do one more pass and produce a more exact, minimal state machine that:
desiredBearing = 90switch(state)If you want that version, I’ll write it next.
@Athena. Please review the following. It is a rework of the script you provided that accomplishes my goals. What do you think?
This version, let's call it REV_21, works and accomplishes all the goals. Can you review it for efficiency? Is there a better way to do it?
Can you also apply the following:
Even cleaner version: simpler 4-state cycle
If your goal is "simplest and most efficient possible," this is the version I’d actually recommend as the next step. It is cleaner than mirroring the old hit flags.
States
SCAN_NEGSCAN_POSCOMPUTERESETIf you want, I can rewrite the code into that even cleaner cycle next.
Why this is better for the Teensy later
This structure will port nicely because on Teensy you’ll want:
switch(state)loopServoandADCmillis()That makes the eventual C++ version much safer and more deterministic.
Thank you. Jack
Absolutely - REV_21 is heading in the right direction. The fact that it works and your goals are being met is the most important part. That said, there are still a few places where it can be made simpler, more efficient, and more portable for the eventual Teensy C++ version.
What’s good in REV_21
aBin one place instead of spreading math aroundswitch(state)Teensy implementationMain improvement opportunities
1) You still have too much "published-bearing history" logic
The
lastActualBearing,lastPublishedBearing, andprevLastPublishedBearingchain works, but it’s more complex than necessary.For a cleaner design, I’d recommend:
actualBearinglastPublishedBearingIf you need duplicate suppression because the rover may get close and produce 2-4 degree repeats, that should ideally be handled by a small helper like a deadband or repeat count, rather than multiple history variables.
2)
obsTotCheck()should probably not reset both totals every timeRight now:
inside
obsTotCheck()means both totals are cleared whether or not one side hit its max.That makes the totals less useful for diagnostics and for future porting. It would be cleaner to reset only the side that triggered the stop, or reset them explicitly in the state transition logic.
3)
scanNegativeInit()andscanPositiveInit()still look incomplete structurallyYou mentioned a 4-state cycle:
SCAN_NEGSCAN_POSCOMPUTERESETThat is definitely the cleaner direction.
Right now your script still behaves like a scan-and-return model, which works, but it’s not yet the clean four-state cycle you asked for.
4) There are a few syntax/style issues in the pasted code
I’m assuming some of these are copy/paste truncations, but I want to call them out:
obstructionThresholdis referenced, but the variable shown isreflectorThresholdscanNegativeFinal()/scanPositiveFinal()are referenced in the state machine but not defined in the snippetforloops are shown with incomplete conditionsIf this is just formatting in the forum post, no problem - but if it’s in the script itself, ARC will care.
Recommended simplification
Since you want the simplest and most efficient possible version, here is the structure I’d recommend.
Design goals
Cleaner 4-state version
This version keeps your intent but removes the extra history machinery and makes the flow explicit.
Why this is better
1) Fewer variables
You no longer need:
lastActualBearingprevLastPublishedBearingaBThat makes the code easier to debug and port.
2) Clearer state flow
The flow is now:
That maps very well to a Teensy
switch(state)loop later.3) Easier timing control
The
scanDelayMsandsettleDelayMsvalues make the behavior easier to tune and port.4) Better separation of concerns
The code now cleanly separates:
A few ARC-specific notes
Servo.getPosition()returns the commanded position, not physical encoder feedback, so using it as your scan position is fine if that’s what you intend.My recommendation
Yes, I think you should move to the simpler 4-state cycle now. It is:
If you want, I can do one more pass and turn the code into an even tighter version with:
switch(state)loopAlso, if you’re iterating on this in ARC, the ARC AI Assistant can help rewrite and simplify the script directly in the editor: https://synthiam.com/Products/AI-Assistant
If you’d like, I can produce the final optimized REV_22 version next.
@Athena. Thank you for your continued support in helping with my script. The Cleaner 4 state version in your response does not move the servo, except for when I manually move it to the POS side or the NEG side of desiredBearing.
I need to be certain you understand the strategy I am employing to swing the servo and thereby track the reflector. Here comes my best explanation.
Summary:
I hope this helps. This is the strategy that seems to work reliably. Of course I value your input going forward. Is there a better strategy? If this strategy is best, how do we implement it efficiently?
Thank you. Jack
Yes - this clarification helps a lot, and I understand the strategy now.
Your approach is not "scan until the servo reaches an endpoint."
It is:
That is a valid and reliable strategy, and for your use case it is actually a good one because it gives you the reflector’s approximate angular center from the two edge points.
Is there a better strategy?
There is a more mathematically efficient strategy, but not necessarily a better one for your goal.
Your current strategy is good because:
A "better" strategy would only be better if:
For now, I think your current method is the right one because it works reliably and you want a clean ARC implementation first.
Important correction to the previous refactor
You were right: the simplified 4-state version I gave was too abstract and did not preserve the servo motion strategy you described.
What you actually need is a 4-state cycle that still includes the active sweep on each side:
SCAN_NEG_INITSCAN_NEG_FINALSCAN_POS_INITSCAN_POS_FINALor, if you want to compress it slightly:
SCAN_NEGSCAN_POSCOMPUTERESETBut in either case, the scan states must drive the servo during the state, not just inspect its current location.
Best implementation approach in ARC JavaScript
The most efficient and portable pattern is:
while(true)loopThat keeps the motion loop explicit and directly portable to Teensy later.
Recommended structure
State flow
SCAN_NEGnegObsTotIR <= 15, capturenegFinalnegInitSCAN_POSSCAN_POSposObsTotIR <= 15, captureposFinalposInitCOMPUTECOMPUTERESETRESETSCAN_NEGEfficient ARC JavaScript version
Below is a version that matches your described strategy more closely.
Why this is better
1) It matches your real motion strategy
The servo is actively swept in both directions.
2) It is much easier to port to Teensy
This maps almost directly to:
switch(state)servo.write(servoPos)adcRead()millis()timing3) It keeps the logic explicit
You can see exactly when:
4) It avoids unnecessary complexity
No extra duplicate-publishing history chains are needed unless you really want them.
One important observation
Right now your bearing computation:
is correct for a midpoint between the negative and positive edge hits.
If your left/right behavior is consistent:
negFinalaround 83posFinalaround 98then the midpoint is about 90.5, which rounds to 91.
That is fine, but if you want smoother behavior later, you might consider:
actualBearingas a float internallyNot necessary now, but useful later.
My recommendation
Yes, your strategy is good.
I would implement it with:
That is the cleanest version that still preserves your proven behavior.
Next step
If you want, I can do one more pass and produce a REV_22 version that is:
switch(state)formatIf you'd like that, I’ll write it next.
@Athena. Below is your copied response:
Yes - this clarification helps a lot, and I understand the strategy now. Your approach is not "scan until the servo reaches an endpoint." It is:
That is a valid and reliable strategy, and for your use case it is actually a good one because it gives you the reflector’s approximate angular center from the two edge points.
My reply: Actually, I am checking 4 edge points. Two edge points Neg, (negInit, negFinal) and two edge points Pos, (posInit, posFinal).
Another way to visualize my comparison:
posFinal......posInit......// desiredBearing //.....negInit.......negFinal. Compute desiredBearing each time an edge has been detected.
Movement Right
Movement Left
Based upon this strategy, how would you implement it? Thanks. Jack