EZB Connect By Name icon EZB Connect By Name — Discover EZB WiFi controllers by name, auto-bind connections, avoid IP management; live discovery and multi-EZB support for ARC. Try it →
Asked — Edited
Resolved Resolved by Athena!
1 Participant
Translate REV 28 to C++ Teensy 4.1

Translate REV 28 To C++ Teensy 4.1

Hi @Athena,

Attached below is REV 28 - a JavaScript script currently running on an EZBv4 (an EZB-side script). I need to translate this to C++ so it will run on a Teensy 4.1. I plan to use the Arduino IDE to further refine the C++ code.

For context:

  • The current script uses EZB/ARC-style calls such as Servo.getPosition, Servo.setPosition, ADC.get, setVar, print, and sleep.
  • In ARC this runs as code on the EZBv4 hardware (an EZB-side script within an ARC project). The Teensy implementation would run on the Teensy microcontroller and will need equivalent access to the IR sensor (ADC2) and the servo (d17). I will map those to the appropriate Teensy ADC and PWM pins in the Arduino code.
  • I’m asking for REV 28 to be reviewed and rewritten in C++ (Arduino-style) so it can be uploaded to a Teensy 4.1. Please preserve the logic and behavior of the original script while converting EZB/ARC API calls to appropriate Arduino/Teensy equivalents (e.g., analogRead/analogWrite or PWM library calls, delay instead of sleep, and local/global variable handling rather than setVar).

Can you please review REV 28 and rewrite it in C++? Thank you.

Jack

// REV 28
// Active-low IR reflector tracker
// Tightened four-state flow with reduced duplicate motion logic

// -------------------- CONFIG --------------------
var servoPort = d17;
var irPort = ADC2;

var desiredBearing = 90;
var roundedBearing = 90;

var actualBearing = 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 = 2000;
var posObsMax = 2000;

var bearingCount = 0;
var reflectorThreshold = 15; // active-low reflector detect: irValue <= threshold

var stepDelta = 1;
var resetDelayMs = 20;
var settleDelayMs = 30;

var runawayCount = 0;
var runawayMargin = 20;
var negAbortLimit = roundedBearing - runawayMargin;
var posAbortLimit = roundedBearing + runawayMargin;

var state = "NEG_SEARCH_INIT";
var stopRequested = false;

setVar("$desiredBearing", desiredBearing);
setVar("$actualBearing", actualBearing);
setVar("$bearingCount", bearingCount);
setVar("$negIRTot", negObsTot);
setVar("$posIRTot", posObsTot);

print("IR tracker Rev28 started.");

// -------------------- HELPERS --------------------

function readIR() {
  return ADC.get(irPort, 0);
}

function readServo() {
  return Servo.getPosition(servoPort, 0);
}

function publishBearing(newBearing) {

  roundedBearing = Math.round(newBearing);
  //This second instance allows considering it for a published actual bearing
  if (roundedBearing == lastActualBearing) {
  //First test: Was it the last published actual bearing?
  if (roundedBearing != lastPublishedBearing) {
  //Second test: Was it the previous to last published actual bearing?
  if (roundedBearing != prevLastPublishedBearing) {
  prevLastPublishedBearing = lastPublishedBearing; 
  lastPublishedBearing = roundedBearing;
  actualBearing = roundedBearing;
  //It then becomes the global actual bearing
  setVar("$actualBearing", actualBearing);
  print("                                   actualBearing:  " + actualBearing);
  } 
  }
  }else {// The first instance before considering it for a published actual bearing
  lastActualBearing = roundedBearing;
  }
}

function updateBearingFromInitEdges() {
 var candidate = -1;

 if (negInit >= 0 && posInit >= 0) {
  // print("negInit:  " + negInit + "    posInit:  " + posInit);
    candidate = (negInit + posInit) / 2;
}
  //print("candidate:  " + candidate);
  if (candidate >= 0) {
    bearingCount++;
  //  print("bearingCount:  " + bearingCount);
    setVar("$bearingCount", bearingCount);
    publishBearing(candidate);
  }

}

function updateBearingFromFinalEdges() {
  var candidate = -1;
  
   if (negFinal >= 0 && posFinal >= 0) {
   // print(" negFinal:  " + negFinal + "    posFinal:  " + posFinal);
    candidate = (negFinal + posFinal) / 2;
}
   //print(" candidate:  " + candidate);
  if (candidate >= 0) {
    bearingCount++;
  //  print("bearingCount:  " + bearingCount);
    setVar("$bearingCount", bearingCount);
    publishBearing(candidate);
  }
}

function stopNow(reason) {
  print("STOP: " + reason);
  settleDelayMs;
  Servo.setPosition(servoPort, roundedBearing);
  sleep(resetDelayMs);
  setVar("$sendStopNow", 1);
  stopRequested = true;
}

function beginNegSweep() {
  settleDelayMs;
  Servo.setPosition(servoPort, roundedBearing);
  sleep(resetDelayMs);
  //print("RESservo:  " + Servo.getPosition(servoPort,0));
  negInit = -1;
  negFinal = -1;
  negObsTot = 0;
  negAbortLimit = roundedBearing - runawayMargin;
  state = "NEG_SEARCH_INIT";
}

function beginPosSweep() {
  settleDelayMs;
  Servo.setPosition(servoPort, roundedBearing);
  sleep(resetDelayMs);
// print("RES--servo:  " + Servo.getPosition(servoPort,0));
  posInit = -1;
  posFinal = -1; 
  posObsTot = 0;
  posAbortLimit = roundedBearing + runawayMargin; 
  state = "POS_SEARCH_INIT";
}

function handleRunaway() {
  runawayCount++;
  print("runawayCount:  " + runawayCount);
  settleDelayMs;
  Servo.setPosition(servoPort, roundedBearing);
  sleep(resetDelayMs);
  print("RES--servo:  " + Servo.getPosition(servoPort,0));
  
 if (state == "NEG_SEARCH_INIT" ) { 
    beginPosSweep();
    print("POS SWEEP");
  } else {
    beginNegSweep();
    print("NEG SWEEP");
  } 
  
  if (runawayCount >= 3) {
    stopNow("Runaway limit reached: " + runawayCount);
    return;
  }

}

// -------------------- STATE HANDLERS --------------------

// NEG_SEARCH_INIT
function doNegSearchInit() {
  var current = readServo();
  var irValue = readIR();
  var reflectorDetected = (irValue <= reflectorThreshold);
  negObsTot += irValue;
  if (current < negAbortLimit) {    
    handleRunaway();
    return;
  }

  //print("servo( NEG In ):  " + Servo.getPosition(servoPort,0) + "    irValue:  " + irValue);
  if (reflectorDetected) {
    negInit = current;
  //print("negInit:  " + negInit);
    runawayCount = 0;
    updateBearingFromInitEdges();
    state = "NEG_SEARCH_FINAL";
  } else {
    Servo.setPosition(servoPort, current - stepDelta);
    settleDelayMs;
  }
}

// NEG_SEARCH_FINAL
function doNegSearchFinal() {
  var current = readServo();
  var irValue = readIR();
  var reflectorDetected = (irValue <= reflectorThreshold);

   negObsTot += irValue;
   //print("servo( NEG Fi ):  " + Servo.getPosition(servoPort,0) + "    irValue:  " + irValue);
  if (!reflectorDetected) {
    negFinal = current;
   //print("negFinal:  " + negFinal);
    updateBearingFromFinalEdges();
    beginPosSweep();
  } else {
    Servo.setPosition(servoPort, current - stepDelta);
    settleDelayMs;
  }
}

// POS_SEARCH_INIT
function doPosSearchInit() {
  var current = readServo();
  var irValue = readIR();
  var reflectorDetected = (irValue <= reflectorThreshold);
  posObsTot += irValue;
  if (current > posAbortLimit) {  
    handleRunaway();
    return;
  }

//print("servo( POS In ):  " + Servo.getPosition(servoPort,0)+ "    irValue:  " + irValue);
  if (reflectorDetected) {
    posInit = current;
   //print("posInit:  " + posInit);
    runawayCount = 0;
    updateBearingFromInitEdges();
    state = "POS_SEARCH_FINAL";
  } else {
    Servo.setPosition(servoPort, current + stepDelta);
    settleDelayMs;
  }
}

// POS_SEARCH_FINAL
function doPosSearchFinal() {
  var current = readServo();
  var irValue = readIR();
  var reflectorDetected = (irValue <= reflectorThreshold);
  posObsTot += irValue;
  //print("servo( POS Fi ):  " + Servo.getPosition(servoPort,0)+ "    irValue:  " + irValue);
  if (!reflectorDetected) {
    posFinal = current;
   //print("posFinal:  " + posFinal);
   updateBearingFromFinalEdges();
    if (negObsTot > negObsMax) { 
      setVar("$negIRTot", negObsTot);
      stopNow("NEG OBS total exceeded max: " + negObsTot);
      return;
    }
    if (posObsTot > posObsMax) {
    setVar("$posIRTot", posObsTot);
    stopNow("POS OBS total exceeded max: " + posObsTot);
    return;
    }
    negObsTot = 0;
    posObsTot = 0;
    beginNegSweep();
  } else {
    Servo.setPosition(servoPort, current + stepDelta);
    settleDelayMs;
  }
}

// -------------------- MAIN LOOP --------------------
beginNegSweep();
settleDelayMs;
while (true) {
  if (stopRequested) {
    break;
  }

  if (state == "NEG_SEARCH_INIT") {
    doNegSearchInit();
  } else if (state == "NEG_SEARCH_FINAL") {
    doNegSearchFinal();
  } else if (state == "POS_SEARCH_INIT") {
    doPosSearchInit();
  } else if (state == "POS_SEARCH_FINAL") {
    doPosSearchFinal();
  }

 sleep(5);
}


Related Hardware (view all EZB hardware)
EZ-B V4 by EZ-Robot
EZ-B v4 with Synthiam ARCx controls servos, sensors, camera and audio over Wi-Fi or USB serial, no firmware needed.
Wi-Fi / USB
Servos 24
Camera
Audio
UART 3
I2C
ADC 8
Digital 24

Related Robot Skills (view all robot skills)
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
Python Native by Synthiam Inc.
Run real CPython in ARC with full package support, camera access, ARC APIs, and bidirectional robot control.

ARC Pro

Upgrade to ARC Pro

Discover the limitless potential of robot programming with Synthiam ARC Pro – where innovation and creativity meet seamlessly.

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#9  

Absolutely - and yes, I can clarify how the files work together.

1) doNegSearchInit() change

You want this line:

bool reflectorDetected = (irNorm > IR_THRESHOLD);

changed to:

bool reflectorDetected = (irNorm < IR_THRESHOLD);

If your sensor is active-low, that makes sense and matches the rest of your logic.


2) UART pin mapping review

Your pin plan is valid:

Teensy 4.1

  • TX8 = pin 35
  • RX8 = pin 34

EZ-B v4

  • TX2 = D18
  • RX2 = D19

That is the correct crossed-serial style wiring direction:

  • Teensy TX8 (35) EZ-B RX2 (D19)
  • Teensy RX8 (34) EZ-B TX2 (D18)
  • GND GND

Since you mentioned the Arduino Mega uses EZ-B UART TX0/RX0, that is fine too - just keep each UART pair dedicated and matched.


3) ARC JavaScript snippet for UART START / STOP / BEARING / COUNT

Here is a simple ARC JavaScript example you can use with the EZ-B v4 UART skill or from a Script skill in ARC.

// ARC JavaScript UART example for EZ-B v4
// Assumes UART2 is used for Teensy communication on D18/D19

var uartIndex = 2;
var baud = 115200;

// Initialize UART2
UART.initHardwareUart(uartIndex, baud);

function sendCommand(cmd) {
  UART.hardwareUartWriteString(uartIndex, cmd + "\n");
  print("Sent: " + cmd);
}

function readAvailable() {
  var data = UART.hardwareUartReadStringAvailable(uartIndex);
  if (data != null && data.length > 0) {
    print("Received: " + data);
  }
}

// Example commands
sendCommand("START");
sleep(200);

sendCommand("BEARING,123");
sleep(200);

sendCommand("COUNT,18");
sleep(200);

sendCommand("STOP");
sleep(200);

// Poll for any response
readAvailable();

If you want a loop to continuously read responses

var uartIndex = 2;
var baud = 115200;

UART.initHardwareUart(uartIndex, baud);

function sendCommand(cmd) {
  UART.hardwareUartWriteString(uartIndex, cmd + "\n");
  print("Sent: " + cmd);
}

sendCommand("START");

while (true) {
  var rx = UART.hardwareUartReadStringAvailable(uartIndex);
  if (rx != null && rx.length > 0) {
    print("RX: " + rx);
  }

  sleep(100);
}

4) Do you upload each file separately?

No - you do not upload .h, .cpp, and .ino separately as separate programs.

How Arduino/Teensy IDE uses them

  • .ino = the main sketch file
  • .h = header file with declarations
  • .cpp = implementation file with the code

They all belong to the same project folder.

Typical workflow

  1. Create a folder named something like:
    • IR_TRACKERv3
  2. Put these files in it:
    • IR_TRACKERv3.ino
    • IR_TRACKERv3.h
    • IR_TRACKERv3.cpp
  3. Open the .ino file in Arduino IDE
  4. The IDE automatically compiles the .cpp and .h files with it
  5. Upload the sketch, not each file separately

5) Does .ino and .h prepare .cpp to run?

Yes - in an Arduino/Teensy project:

  • .ino is the entry point
  • .h tells the compiler what functions/variables exist
  • .cpp contains the actual logic

So:

  • .h = declarations
  • .cpp = code
  • .ino = setup/loop and project start

They work together as one compiled firmware image.


6) Important note about your current UART plan

If you want Teensy UART8, make sure your code is using the matching serial port object:

  • On Teensy, Serial1, Serial2, etc. map to specific pins.
  • TX8/RX8 is not a standard Arduino-style name unless you are specifically using a Teensy board definition or a library wrapper that exposes it that way.

On Teensy 4.1, you’ll likely need to use:

  • Serial8 for pin 35/34

So in the Teensy code, your UART should probably be:

Serial8.begin(115200);

and then:

Serial8.print("START\n");

If you want, I can update the full IR_TRACKERv3.cpp to use Serial8 explicitly for those pins.


7) Recommendation

I’d suggest this setup:

  • Teensy 4.1
    • Serial8 on pins 35 TX / 34 RX
  • EZ-B v4
    • UART2 on D18 TX / D19 RX
  • Shared baud rate:
    • 115200

If you want, I can do the next step and provide:

  1. the updated IR_TRACKERv3.cpp using Serial8
  2. the matching ARC JavaScript for send/receive
  3. a wiring table for Teensy EZ-B UART connections

If you'd like, I can rewrite the full Teensy file now with Serial8 integrated.

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. Thank you for the explanations. Can you please do the next step and provide the following?

  1. the updated IR_TRACKERv3.cpp using Serial8
  2. the matching ARC JavaScript for send/receive
  3. a wiring table for Teensy EZ-B UART connections

Can you rewrite the full Teensy file now with Serial8 integrated? Thanks. Jack

Author Avatar
PRO
Canada
Twitter YouTube GitHub
#11  

Yes - here’s the next step with Serial8 integrated for the Teensy 4.1, plus the ARC JavaScript and a wiring table.

1) Updated IR_TRACKERv3.cpp using Serial8

This version uses:

  • Teensy UART: Serial8 on TX8 pin 35 and RX8 pin 34
  • EZ-B v4 UART: UART2 on D18 / D19
  • newline-delimited text messages
  • your requested IR threshold logic in doNegSearchInit()
#include "IR_TRACKERv3.h"

Servo trackingServo;
TrackerState state = NEG_SEARCH_INIT;

int desiredBearing = DESIRED_BEARING;
int roundedBearing = DESIRED_BEARING;
int actualBearing = DESIRED_BEARING;
int bearing = DESIRED_BEARING;

int lastActualBearing = -1;
int lastPublishedBearing = -1;
int prevLastPublishedBearing = -1;

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

long negObsTot = 0;
long posObsTot = 0;

int bearingCount = 0;
int runawayCount = 0;

int negAbortLimit = 0;
int posAbortLimit = 0;

bool stopRequested = false;
bool startRequested = false;

static int irSamples[IR_SMOOTH_SAMPLES];
static int irSampleIndex = 0;
static bool irInitialized = false;

static String serialLine;
static unsigned long lastStepMs = 0;
static unsigned long lastDebugMs = 0;

static int clampAngle(int angle) {
  return constrain(angle, SERVO_MIN_ANGLE, SERVO_MAX_ANGLE);
}

int clampServoAngle(int angle) {
  return clampAngle(angle);
}

void setServoBearing(int angle) {
  bearing = clampAngle(angle);
  trackingServo.write(bearing);
}

int readServo() {
  return bearing;
}

int normalizeIRTo255(int raw) {
  raw = constrain(raw, 0, ADC_MAX_RAW);
  return map(raw, 0, ADC_MAX_RAW, 0, 255);
}

int readIRSmooth() {
  int raw = analogRead(IR_PIN);

  if (!irInitialized) {
    for (int i = 0; i < IR_SMOOTH_SAMPLES; i++) {
      irSamples[i] = raw;
    }
    irInitialized = true;
  } else {
    irSamples[irSampleIndex] = raw;
    irSampleIndex = (irSampleIndex + 1) % IR_SMOOTH_SAMPLES;
  }

  long sum = 0;
  for (int i = 0; i < IR_SMOOTH_SAMPLES; i++) {
    sum += irSamples[i];
  }

  return (int)(sum / IR_SMOOTH_SAMPLES);
}

void sendToEZB(const String& msg) {
  Serial8.print(msg);
  Serial8.print('\n');
  Serial.print("TX->EZB: ");
  Serial.println(msg);
}

void sendBearingUpdate() {
  sendToEZB("BEARING," + String(actualBearing));
}

void sendBearingCount() {
  sendToEZB("COUNT," + String(bearingCount));
}

void publishBearing(int candidate) {
  roundedBearing = round(candidate);

  if (roundedBearing == lastActualBearing) {
    if (roundedBearing != lastPublishedBearing) {
      if (roundedBearing != prevLastPublishedBearing) {
        prevLastPublishedBearing = lastPublishedBearing;
        lastPublishedBearing = roundedBearing;
        actualBearing = roundedBearing;

        Serial.print("actualBearing: ");
        Serial.println(actualBearing);

        sendBearingUpdate();
      }
    }
  } else {
    lastActualBearing = roundedBearing;
  }
}

void updateBearingFromInitEdges() {
  int candidate = -1;
  if (negInit >= 0 && posInit >= 0) {
    candidate = (negInit + posInit) / 2;
  }

  if (candidate >= 0) {
    bearingCount++;
    publishBearing(candidate);
    Serial.print("bearingCount: ");
    Serial.println(bearingCount);
  }
}

void updateBearingFromFinalEdges() {
  int candidate = -1;
  if (negFinal >= 0 && posFinal >= 0) {
    candidate = (negFinal + posFinal) / 2;
  }

  if (candidate >= 0) {
    bearingCount++;
    publishBearing(candidate);
    Serial.print("bearingCount: ");
    Serial.println(bearingCount);
  }
}

void stopNow(const char* reason) {
  Serial.print("STOP: ");
  Serial.println(reason);

  setServoBearing(roundedBearing);
  delay(RESET_DELAY_MS);

  sendToEZB("STOP");
  sendBearingCount();

  stopRequested = true;
  state = TRACKER_STOPPED;
}

void beginNegSweep() {
  delay(SETTLE_DELAY_MS);
  setServoBearing(roundedBearing);
  delay(RESET_DELAY_MS);

  if (negObsTot > NEG_OBS_MAX) {
    Serial.print("NEG OBS total exceeded max: ");
    Serial.println(negObsTot);
    stopNow("NEG OBS total exceeded max");
    return;
  }

  if (posObsTot > POS_OBS_MAX) {
    Serial.print("POS OBS total exceeded max: ");
    Serial.println(posObsTot);
    stopNow("POS OBS total exceeded max");
    return;
  }

  negObsTot = 0;
  posObsTot = 0;

  negInit = -1;
  negFinal = -1;
  negAbortLimit = roundedBearing - RUNAWAY_MARGIN;

  state = NEG_SEARCH_INIT;
}

void beginPosSweep() {
  delay(SETTLE_DELAY_MS);
  setServoBearing(roundedBearing);
  delay(RESET_DELAY_MS);

  posInit = -1;
  posFinal = -1;
  posAbortLimit = roundedBearing + RUNAWAY_MARGIN;

  state = POS_SEARCH_INIT;
}

void handleRunaway() {
  runawayCount++;
  Serial.print("runawayCount: ");
  Serial.println(runawayCount);

  delay(SETTLE_DELAY_MS);
  setServoBearing(roundedBearing);
  delay(RESET_DELAY_MS);

  if (state == NEG_SEARCH_INIT) {
    beginPosSweep();
    Serial.println("POS SWEEP");
  } else {
    beginNegSweep();
    Serial.println("NEG SWEEP");
  }

  if (runawayCount >= 3) {
    stopNow("Runaway limit reached");
    return;
  }
}

void doNegSearchInit() {
  int current = readServo();
  int irRaw = readIRSmooth();
  int irNorm = normalizeIRTo255(irRaw);
  bool reflectorDetected = (irNorm < IR_THRESHOLD);

  if (current < negAbortLimit) {
    handleRunaway();
    return;
  }

  if (reflectorDetected) {
    negInit = current;
    runawayCount = 0;
    updateBearingFromInitEdges();
    state = NEG_SEARCH_FINAL;
  } else {
    int next = current - STEP_DELTA;
    setServoBearing(next);
    delay(SETTLE_DELAY_MS);
  }
}

void doNegSearchFinal() {
  int current = readServo();
  int irRaw = readIRSmooth();
  int irNorm = normalizeIRTo255(irRaw);
  bool reflectorDetected = (irNorm < IR_THRESHOLD);

  if (reflectorDetected) {
    negFinal = current;
    updateBearingFromFinalEdges();
    beginPosSweep();
  } else {
    int next = current - STEP_DELTA;
    setServoBearing(next);
    delay(SETTLE_DELAY_MS);
  }
}

void doPosSearchInit() {
  int current = readServo();
  int irRaw = readIRSmooth();
  int irNorm = normalizeIRTo255(irRaw);
  bool reflectorDetected = (irNorm < IR_THRESHOLD);

  if (current > posAbortLimit) {
    handleRunaway();
    return;
  }

  if (reflectorDetected) {
    posInit = current;
    runawayCount = 0;
    updateBearingFromInitEdges();
    state = POS_SEARCH_FINAL;
  } else {
    int next = current + STEP_DELTA;
    setServoBearing(next);
    delay(SETTLE_DELAY_MS);
  }
}

void doPosSearchFinal() {
  int current = readServo();
  int irRaw = readIRSmooth();
  int irNorm = normalizeIRTo255(irRaw);
  bool reflectorDetected = (irNorm < IR_THRESHOLD);

  if (reflectorDetected) {
    posFinal = current;
    updateBearingFromFinalEdges();
    beginNegSweep();
  } else {
    int next = current + STEP_DELTA;
    setServoBearing(next);
    delay(SETTLE_DELAY_MS);
  }
}

void handleSerialCommand(const String& cmd) {
  String trimmed = cmd;
  trimmed.trim();
  trimmed.toUpperCase();

  if (trimmed == CMD_START) {
    startRequested = true;
    stopRequested = false;
    sendToEZB("ACK_START");
    return;
  }

  if (trimmed == CMD_STOP) {
    stopRequested = true;
    sendBearingCount();
    sendToEZB("ACK_STOP");
    return;
  }

  if (trimmed == "BEARING?") {
    sendBearingUpdate();
    return;
  }

  if (trimmed == "COUNT?") {
    sendBearingCount();
    return;
  }
}

void pollSerial() {
  while (Serial8.available() > 0) {
    char c = (char)Serial8.read();

    if (c == '\n' || c == '\r') {
      if (serialLine.length() > 0) {
        handleSerialCommand(serialLine);
        serialLine = "";
      }
    } else {
      serialLine += c;
    }
  }
}

void processTrackerState() {
  if (stopRequested) {
    return;
  }

  if (!startRequested) {
    return;
  }

  unsigned long now = millis();

  switch (state) {
    case NEG_SEARCH_INIT:
      doNegSearchInit();
      break;
    case NEG_SEARCH_FINAL:
      doNegSearchFinal();
      break;
    case POS_SEARCH_INIT:
      doPosSearchInit();
      break;
    case POS_SEARCH_FINAL:
      doPosSearchFinal();
      break;
    case TRACKER_STOPPED:
    default:
      return;
  }

  if (now - lastDebugMs >= 1000) {
    lastDebugMs = now;
    Serial.print("state=");
    Serial.print((int)state);
    Serial.print(" bearing=");
    Serial.print(bearing);
    Serial.print(" actualBearing=");
    Serial.print(actualBearing);
    Serial.print(" negObsTot=");
    Serial.print(negObsTot);
    Serial.print(" posObsTot=");
    Serial.println(posObsTot);
  }
}

void setupTracker() {
  Serial.begin(EZB_BAUD);
  Serial8.begin(EZB_BAUD);

  delay(1000);
  analogReadResolution(12);

  pinMode(IR_PIN, INPUT);
  trackingServo.attach(SERVO_PIN, SERVO_MIN_PULSE, SERVO_MAX_PULSE);

  actualBearing = DESIRED_BEARING;
  roundedBearing = DESIRED_BEARING;
  bearing = DESIRED_BEARING;

  setServoBearing(roundedBearing);
  beginNegSweep();

  Serial.println("IR_TRACKERv3 READY");
  Serial.println("Teensy Serial8 active on TX8=pin 35, RX8=pin 34");
  Serial.println("Commands: START, STOP, BEARING?, COUNT?");
}

void loopTracker() {
  pollSerial();
  processTrackerState();
}

2) IR_TRACKERv3.ino

#include "IR_TRACKERv3.h"

void setup() {
  setupTracker();
}

void loop() {
  loopTracker();
}

3) IR_TRACKERv3.h

#pragma once
#include <Arduino.h>
#include <Servo.h>

enum TrackerState {
  NEG_SEARCH_INIT,
  NEG_SEARCH_FINAL,
  POS_SEARCH_INIT,
  POS_SEARCH_FINAL,
  TRACKER_STOPPED
};

// -------------------- PINS --------------------
static const uint8_t SERVO_PIN = 29;
static const uint8_t IR_PIN = A14;

// Teensy 4.1 UART8 pins:
// TX8 = pin 35
// RX8 = pin 34

// -------------------- servo CALIBRATION --------------------
static const int SERVO_MIN_ANGLE = 5;
static const int SERVO_MAX_ANGLE = 175;
static const int SERVO_MIN_PULSE = 500;
static const int SERVO_MAX_PULSE = 2500;

// -------------------- BEHAVIOR --------------------
static const int DESIRED_BEARING = 90;
static const int STEP_DELTA = 1;
static const int RUNAWAY_MARGIN = 20;
static const int RESET_DELAY_MS = 20;
static const int SETTLE_DELAY_MS = 30;

// -------------------- IR --------------------
static const int IR_THRESHOLD = 15;
static const int IR_SMOOTH_SAMPLES = 8;
static const int ADC_MAX_RAW = 4095;

// -------------------- LIMITS --------------------
static const long NEG_OBS_MAX = 2000;
static const long POS_OBS_MAX = 2000;

// -------------------- UART --------------------
static const unsigned long EZB_BAUD = 115200;

// -------------------- COMMANDS --------------------
static const char* CMD_START = "START";
static const char* CMD_STOP = "STOP";
static const char* CMD_BEARING = "BEARING";
static const char* CMD_COUNT = "COUNT";

extern servo trackingServo;
extern TrackerState state;

extern int desiredBearing;
extern int roundedBearing;
extern int actualBearing;
extern int bearing;

extern int lastActualBearing;
extern int lastPublishedBearing;
extern int prevLastPublishedBearing;

extern int negInit;
extern int negFinal;
extern int posInit;
extern int posFinal;

extern long negObsTot;
extern long posObsTot;

extern int bearingCount;
extern int runawayCount;

extern int negAbortLimit;
extern int posAbortLimit;

extern bool stopRequested;
extern bool startRequested;

void setupTracker();
void loopTracker();

void beginNegSweep();
void beginPosSweep();

void doNegSearchInit();
void doNegSearchFinal();
void doPosSearchInit();
void doPosSearchFinal();

void handleSerialCommand(const String& cmd);
void pollSerial();

void sendToEZB(const String& msg);
void sendBearingUpdate();
void sendBearingCount();

void stopNow(const char* reason);

int normalizeIRTo255(int raw);
int readIRSmooth();

int clampServoAngle(int angle);
void setServoBearing(int angle);
int readServo();

void publishBearing(int candidate);
void updateBearingFromInitEdges();
void updateBearingFromFinalEdges();
void handleRunaway();
void processTrackerState();

4) ARC JavaScript for UART send/receive

This is for the EZ-B v4 side in ARC using UART2 on D18/D19.

// ARC JavaScript - UART2 with Teensy 4.1
// EZ-B UART2: TX D18, RX D19
// Teensy UART8: TX pin 35, RX pin 34

var uartIndex = 2;
var baud = 115200;

UART.initHardwareUart(uartIndex, baud);

function sendCommand(cmd) {
  UART.hardwareUartWriteString(uartIndex, cmd + "\n");
  print("TX: " + cmd);
}

function readAvailable() {
  var data = UART.hardwareUartReadStringAvailable(uartIndex);
  if (data != null && data.length > 0) {
    print("RX: " + data);
  }
}

sendCommand("START");
sleep(200);

sendCommand("BEARING,123");
sleep(200);

sendCommand("COUNT,18");
sleep(200);

sendCommand("STOP");
sleep(200);

while (true) {
  readAvailable();
  sleep(100);
}

Optional ARC control snippet

If you want to send a command once:

UART.hardwareUartWriteString(2, "START\n");

5) UART wiring table

Teensy 4.1 to EZ-B v4

Function Teensy 4.1 EZ-B v4
Teensy TX Pin 35 / Serial8 TX D19 / UART2 RX
Teensy RX Pin 34 / Serial8 RX D18 / UART2 TX
Ground GND GND

Important

  • TX goes to RX
  • RX goes to TX
  • Ground must be shared

6) How to use the sketches

Yes - these are one Teensy project, not separate uploads.

You do this:

  1. Create a folder named IR_TRACKERv3
  2. Put these three files inside it:
    • IR_TRACKERv3.ino
    • IR_TRACKERv3.h
    • IR_TRACKERv3.cpp
  3. Open IR_TRACKERv3.ino in Arduino IDE
  4. Compile and upload to the Teensy

How they work together

  • .ino = startup entry points: setup() and loop()
  • .h = declarations and shared constants
  • .cpp = the actual code

So yes, the .ino and .h file prepare the .cpp to compile as one project.


7) One important compatibility note

Your ARC JavaScript can send:

  • START
  • STOP
  • BEARING,123
  • COUNT,18

But the current Teensy code is mainly written to receive control commands and send status updates like:

  • ACK_START
  • ACK_STOP
  • BEARING,xxx
  • COUNT,xxx
  • STOP

If you want, I can do one more refinement and make the Teensy parser fully handle inbound:

  • BEARING,123
  • COUNT,18

as explicit commands too.


If you'd like, I can next produce a cleaner V4 of IR_TRACKERv3 that adds:

  • explicit parsing of BEARING,123 and COUNT,18
  • a simple checksum packet format
  • and a more robust non-blocking UART state machine

If you want that, I can generate it right away.

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.
📚 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.