Virtual Reality Robot icon Virtual Reality Robot — Stream robot camera to VR and control servos with headset pitch/yaw; supports SteamVR and Cardboard (iOS/Android) for immersive teleoperation Try it →
Asked — Edited
Resolved Resolved by Athena!
1 Participant
IR Tracker Code Review

IR Tracker Code Review

@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:

  1. Check the overall construction and organization for efficiency (loops, variable usage, control flow).
  2. If changes are needed, please update the code while preserving the stated objectives and behavior.
  3. 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;
  }
}


Related Hardware (view all EZB hardware)
Roli Rover by EZ-Robot
Roli Rover with Synthiam ARCx: drive with live video, control arms, follow faces, patrol, and program with Blockly or Python
Wi-Fi / USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 24

Related Robot Skills (view all robot skills)
Movement Script by Synthiam
Trigger direction-specific scripts when ARC movement panels change; assign scripts per direction/stop, access direction and speed (JS/Python/EZ).
Script by Synthiam
Multi-language ARC Script: build, run and debug Blockly, JavaScript, EZ-Script or Python with Intellisense, run/save/load and Roboscratch support

ARC Pro

Upgrade to ARC Pro

Subscribe to ARC Pro, and your robot will become a canvas for your imagination, limited only by your creativity.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#9  

Yes - that clarification changes the logic in an important way.

You’re not doing a simple two-edge midpoint. You’re doing a four-edge relationship and updating desiredBearing from whichever pair of edges is currently valid.

What I understand your strategy to be

You are detecting and maintaining these four edge positions:

  • negInit
  • negFinal
  • posInit
  • posFinal

And then using them to compute the reflector center from different combinations depending on scan direction and which edges are most recently valid.

So the reflector center can be derived from:

  • (negInit + posInit) / 2
  • (negFinal + posFinal) / 2
  • (posInit + negInit) / 2
  • (posFinal + negFinal) / 2

That means the real model is:

  • capture edges
  • keep the latest valid edge pair
  • recompute desiredBearing whenever a new edge is found
  • move the servo relative to the updated desiredBearing

That’s a more complete and useful strategy than the simplified midpoint approach I described earlier.


Best implementation approach

I would implement this as a 4-state edge capture loop with a small edge-memory model:

States

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL

Core idea

Each time an edge is detected:

  1. store that edge position
  2. update the most recent opposite edge pair if available
  3. compute desiredBearing
  4. publish it to ARC
  5. transition to the next scan state

Cleaner logic model

Instead of keeping many overlapping formulas in the scan code, use a single helper that recomputes bearing whenever enough edge data exists.

For example:

  • if negInit and posInit are valid, compute from those
  • if negFinal and posFinal are valid, compute from those
  • if you want, prefer the newest pair or the pair that matches the current scan direction

In ARC JavaScript, I’d keep it simple:

var negInit = -1;
var negFinal = -1;
var posInit = -1;
var posFinal = -1;

var desiredBearing = 90;
var actualBearing = 90;
var bearingCount = 0;

var state = "SCAN_NEG_INIT";

Then make one function to update the bearing:

function updateDesiredBearing() {
  if (negInit >= 0 && posInit >= 0) {
    actualBearing = Math.round((negInit + posInit) / 2);
  }

  if (negFinal >= 0 && posFinal >= 0) {
    actualBearing = Math.round((negFinal + posFinal) / 2);
  }

  desiredBearing = actualBearing;
  setVar("$desiredBearing", desiredBearing);
  setVar("$actualBearing", actualBearing);
  print("desiredBearing: " + desiredBearing);
}

That is the simplest version.


But your logic sounds a bit more directional than that

Because you mentioned:

  • when moving right, compare negInit with posInit
  • when moving left, compare posInit with negInit
  • and also use negFinal with posFinal

That suggests you may want separate edge pairs depending on current motion direction.

In that case, I would implement it like this:

  • while scanning NEG/right, detect and store negInit or negFinal
  • while scanning POS/left, detect and store posInit or posFinal
  • once a complementary edge exists, calculate the midpoint and update desiredBearing

Practical implementation recommendation

The most efficient structure is:

1. Scan NEG until IR <= 15

  • capture edge into negInit or negFinal depending on whether it is the first or second NEG edge in the current cycle
  • move to POS scan

2. Scan POS until IR <= 15

  • capture edge into posInit or posFinal
  • compute desiredBearing
  • move back to NEG scan

3. Use a small edge-state tracker

This avoids trying to infer everything from one or two variables.


A better way to code it

Instead of thinking in terms of "four separate formulas," I’d think in terms of:

  • edge slot
  • scan direction
  • current bearing estimate

Example concept

var edgeStage = 0;
// 0 = need negInit
// 1 = need posInit
// 2 = need negFinal
// 3 = need posFinal

Then:

  • detect edge
  • store in the correct slot
  • when a matching pair exists, compute midpoint
  • update desiredBearing
  • continue scanning

That is the cleanest and most portable design for Teensy later.


My recommendation

Yes, I think your 4-edge method is the right one if your goal is to keep refining the bearing as the robot scans.

I would implement it as:

  • a state machine
  • a single edge capture handler
  • a single bearing update function
  • a scan direction variable
  • a pairing rule for how each new edge is used

That will be:

  • easier to debug
  • easier to port to C++
  • less repetitive
  • more robust than hardcoding formulas all over the scan code

Next step

If you want, I can write the actual ARC JavaScript REV_22 using this exact 4-edge strategy, with:

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL
  • a bearing update helper
  • ARC global updates
  • cleaner structure for Teensy portability

If you’d like that, I’ll produce the code next.

I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#10  

@Athena. You wrote: ----------------------------

Practical implementation recommendation

The most efficient structure is:

1. Scan NEG until IR <= 15

  • capture edge into negInit or negFinal depending on whether it is the first or second NEG edge in the current cycle
  • move to POS scan

2. Scan POS until IR <= 15

  • capture edge into posInit or posFinal
  • compute desiredBearing
  • move back to NEG scan

----------------------------

Please understand that during a: Neg Scan, negInit must be captured and negFinal must be captured. Not or. Actual Bearing to be determined at each edge. (negInit <=15 and negFinal > 15) Pos Scan, posInit must be captured and posFinal must be captured. Not or. Actual Bearing to be determined at each edge. (posInit <=15 and posFinal > 15)

Once the reflector is detected (IR <= 15), the servo must continue past the reflector to a servo position where the reflector is not detected (IR > 15). This physical movement of the servo is required in each direction in order for the reflector to be detected when the servo returns.

Based upon this clarification:

Next step

If you want, I can write the actual ARC JavaScript REV_22 using this exact 4-edge strategy, with:

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL
  • a bearing update helper
  • ARC global updates
  • cleaner structure for Teensy portability

Thank you for your continued support. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#11  

Absolutely, Jack - that clarification is exactly what I needed.

You’re right:

  • posInit and posFinal are both required
  • negInit and negFinal are both required
  • each edge is captured only when the servo is physically scanning in that direction
  • bearing is updated at each edge, not only after a full pair is complete

That makes this a true 4-edge tracking loop, and it’s a very solid approach for ARC JavaScript and later Teensy C++.


I’d implement this as a four-state edge capture machine with one small helper that:

  1. stores the edge just detected
  2. updates the current bearing estimate
  3. publishes the result to ARC globals
  4. advances to the next scan state

States

  • SCAN_NEG_INIT
  • SCAN_NEG_FINAL
  • SCAN_POS_INIT
  • SCAN_POS_FINAL

Edge handling idea

Each state captures one edge:

  • SCAN_NEG_INIT capture negInit
  • SCAN_NEG_FINAL capture negFinal
  • SCAN_POS_INIT capture posInit
  • SCAN_POS_FINAL capture posFinal

Then, after each capture, recompute bearing using the best available edge pair.


A clean ARC JavaScript REV_22

Here’s the version I’d recommend as the next step:

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";
  }
}

Why this version is better

1) It matches your real strategy

Each edge is captured only during the correct physical sweep direction.

2) It updates bearing at each edge

That’s what you asked for, and it lets ARC track the reflector continuously.

3) It’s easy to port later

This maps very naturally to a Teensy switch(state) loop.

4) It avoids unnecessary complexity

No extra duplicate suppression chains unless they prove necessary later.


One important design note

Right now, updateBearingFromEdges() prefers the most recently available pair in this order:

  1. negFinal + posFinal
  2. negInit + posInit
  3. negInit + posFinal
  4. negFinal + posInit

If you want a different priority order, I can change that.

For example, if you want:

  • one pair used only for desiredBearing
  • another pair used only for actualBearing

we can separate those cleanly.


My recommendation

Yes - your 4-edge strategy is valid and I’d keep it.

The key is to make the implementation explicit:

  • one state per edge
  • one helper to compute the bearing
  • one place to publish ARC globals
  • one place to handle obstruction totals

That is the simplest efficient structure that still preserves your behavior.

If you want, I can do one more pass and produce a REV_23 version that:

  • adds your exact preferred bearing-pair priority
  • reduces repeated prints
  • separates desiredBearing vs actualBearing more clearly
  • is even closer to a final Teensy translation
I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#12  

@Athena. 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).

  }

Btw, the script will not run the servo if started out on desiredBearing(90). The only way I can get the servo to run is to move it off 90, and then it centers itself on 90 and the servo stops.

To review:

NEG movement identifies 2 servo positions. (negInit and negFinal) .

At negInit, actualBearing = (( negInit + posInit ) /2). At negFinal, actualBearing = ((negFinal + posFinal) /2).

POS movement identifies 2 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 to continually sweep NEG and POS?

Thank you. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#13  

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:

  • Start a new thread that picks up where we left off - fresh context, faster responses.
  • Or get unlimited interaction with me through ARC's built-in AI Assistant agent: Synthiam AI Assistant
I am a customer support AI and still learning. Please verify important answers in our support documentation. For advanced AI features, use the AI Script Agent built into ARC.
#14  

@Athena. Thank you. Jack

📚 This thread has been preserved as a helpful resource!

While new comments are no longer being accepted, the information here remains available for learning and reference. Check the most recent comment for additional details.