Hi @Athena,
Below are the files IR_TRACKERv4.1.ino, IR_TRACKERv4.1.h, and IR_TRACKERv4.1.cpp, plus the compilation warning I get when attempting to compile. Also included is the ARC UART script I use on the PC side (ARC Script robot skill + COM Connection Robot Skill).
For context: this is a Teensy 4.1 firmware sketch that uses Serial8 (TX8 = pin 35, RX8 = pin 34) to communicate over a PC COM port (e.g., COM3) with ARC. The Teensy is connected directly to the PC via USB serial (COM port), and ARC on the PC opens that COM port with a Script robot skill (not via an EZB). I’m asking two things:
- Can you rewrite the Teensy code to correct the compilation warning shown below?
- Can you make a v4.2 that cleans things up by splitting the parser into a dedicated packet handler and providing a proper ARC project setup (Script skill + COM Connection Robot Skill)? If possible, please include the Script skill code and a description of the ARC Robot Skills configuration.
IR_TRACKERv4.1.ino
#include "IR_TRACKERv4.1.h"
void setup() {
setupTracker();
}
void loop() {
loopTracker();
}
IR_TRACKERv4.1.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;
// Packet format:
// $BEARING,123*hh
// $COUNT,18*hh
// $STOP*hh
extern servo trackingServo;
extern TrackerState state;
extern int desiredBearing;
extern int roundedBearing;
extern int actualBearing;
extern int bearing;
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();
IR_TRACKERv4.1.cpp
#include "IR_TRACKERv4.1.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 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 lastDebugMs = 0;
static int lastActualBearing = -1;
static int lastPublishedBearing = -1;
static int prevLastPublishedBearing = -1;
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;
}
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);
}
static uint8_t simpleChecksum(const String& payload) {
uint8_t sum = 0;
for (unsigned int i = 0; i < payload.length(); i++) {
sum ^= (uint8_t)payload[i];
}
return sum;
}
static String toHex2(uint8_t value) {
const char* hex = "0123456789ABCDEF";
String out;
out += hex[(value >> 4) & 0x0F];
out += hex[value & 0x0F];
return out;
}
static bool validatePacket(const String& packet, String& payloadOut) {
if (packet.length() < 5) return false;
if (packet.charAt(0) != '$') return false;
int star = packet.lastIndexOf('*');
if (star < 0 || star + 2 >= packet.length()) return false;
String payload = packet.substring(1, star);
String checksumText = packet.substring(star + 1, star + 3);
uint8_t received = (uint8_t)strtol(checksumText.c_str(), nullptr, 16);
uint8_t computed = simpleChecksum(payload);
if (received != computed) return false;
payloadOut = payload;
return true;
}
void sendToEZB(const String& msg) {
uint8_t chk = simpleChecksum(msg);
Serial8.print('$');
Serial8.print(msg);
Serial8.print('*');
Serial8.print(toHex2(chk));
Serial8.print('\n');
}
void sendBearingUpdate() {
sendToEZB("BEARING," + String(actualBearing));
}
void sendBearingCount() {
sendToEZB("COUNT," + String(bearingCount));
}
void publishBearing(int candidate) {
roundedBearing = candidate;
if (roundedBearing == lastActualBearing) {
if (roundedBearing != lastPublishedBearing && roundedBearing != prevLastPublishedBearing) {
prevLastPublishedBearing = lastPublishedBearing;
lastPublishedBearing = roundedBearing;
actualBearing = roundedBearing;
Serial.print("actualBearing: ");
Serial.println(actualBearing);
sendBearingUpdate();
}
} else {
lastActualBearing = roundedBearing;
}
}
void updateBearingFromInitEdges() {
if (negInit >= 0 && posInit >= 0) {
int candidate = (negInit + posInit) / 2;
bearingCount++;
publishBearing(candidate);
Serial.print("bearingCount: ");
Serial.println(bearingCount);
}
}
void updateBearingFromFinalEdges() {
if (negFinal >= 0 && posFinal >= 0) {
int candidate = (negFinal + posFinal) / 2;
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 < 0) {
handleRunaway();
return;
}
if (reflectorDetected) {
negFinal = current;
updateBearingFromFinalEdges();
state = POS_SEARCH_INIT;
} 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();
state = NEG_SEARCH_INIT;
} 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;
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 (current > posAbortLimit) {
handleRunaway();
return;
}
if (reflectorDetected) {
posFinal = current;
updateBearingFromFinalEdges();
state = NEG_SEARCH_INIT;
} else {
int next = current + STEP_DELTA;
setServoBearing(next);
delay(SETTLE_DELAY_MS);
}
}
void handleSerialCommand(const String& cmd) {
String s = cmd;
s.trim();
s.toUpperCase();
if (s == "START") {
startRequested = true;
stopRequested = false;
Serial.println("START OK");
return;
}
if (s == "STOP") {
stopNow("Serial STOP");
return;
}
if (s.startsWith("BEARING,")) {
int value = s.substring(8).toInt();
roundedBearing = clampServoAngle(value);
setServoBearing(roundedBearing);
Serial.print("BEARING SET ");
Serial.println(roundedBearing);
return;
}
if (s.startsWith("COUNT,")) {
int value = s.substring(6).toInt();
bearingCount = value;
Serial.print("COUNT SET ");
Serial.println(bearingCount);
return;
}
if (s == "BEARING?") {
sendBearingUpdate();
return;
}
if (s == "COUNT?") {
sendBearingCount();
return;
}
Serial.print("Unknown cmd: ");
Serial.println(s);
}
void pollSerial() {
static bool inPacket = false;
static String packet;
static unsigned long packetStartMs = 0;
while (Serial8.available() > 0) {
char c = (char)Serial8.read();
if (!inPacket) {
if (c == '$') {
inPacket = true;
packet = "$";
packetStartMs = millis();
}
continue;
}
if (millis() - packetStartMs > 200) {
inPacket = false;
packet = "";
Serial.println("Packet timeout/reset");
continue;
}
packet += c;
if (c == '\n') {
String payload;
if (validatePacket(packet, payload)) {
handleSerialCommand(payload);
} else {
Serial.print("Bad packet: ");
Serial.println(packet);
}
packet = "";
inPacket = false;
}
if (packet.length() > 100) {
packet = "";
inPacket = false;
Serial.println("Packet overflow/reset");
}
}
}
void processTrackerState() {
if (stopRequested || !startRequested) {
return;
}
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 (millis() - lastDebugMs >= 1000) {
lastDebugMs = millis();
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);
startRequested = true;
beginNegSweep();
Serial.println("IR_TRACKERv4 READY");
Serial.println("Teensy Serial8 active on TX8=pin 35, RX8=pin 34");
}
void loopTracker() {
pollSerial();
processTrackerState();
}
Compilation warning
C:\Users\jackl\OneDrive\Documents\Arduino\IR_TRACKERv4.1\IR_TRACKERv4.1.cpp: In function 'bool validatePacket(const String&, String&)':
C:\Users\jackl\OneDrive\Documents\Arduino\IR_TRACKERv4.1\IR_TRACKERv4.1.cpp:97:28: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
97 | if (star < 0 || star + 2 >= packet.length()) return false;
| ~~~~~~~~~^~~~~~~~~~~~~~~~~~
ARC-side example (ARC Script skill, COM connection on COM3)
// Here’s a simple ARC JavaScript example for sending and reading packets.
var port = "COM3"; // change to your Teensy COM port
COM.open(port, 115200);
function checksumXor(payload) {
var sum = 0;
for (var i = 0; i < payload.length; i++) {
sum ^= payload.charCodeAt(i);
}
return sum;
}
function hex2(value) {
var s = value.toString(16).toUpperCase();
if (s.length < 2) s = "0" + s;
return s;
}
function sendPacket(payload) {
var chk = hex2(checksumXor(payload));
var packet = "$" + payload + "*" + chk + "\n";
COM.writeString(port, packet);
print("TX: " + packet);
}
function readPackets() {
while (COM.available(port) > 0) {
var line = COM.readLine(port);
if (line == null || line == "") continue;
line = line.trim();
print("RX: " + line);
if (line.charAt(0) != "$") continue;
var star = line.indexOf("*");
if (star < 0) continue;
var payload = line.substring(1, star);
var parts = payload.split(",");
if (parts[0] == "BEARING") {
print("Bearing: " + parts[1]);
} else if (parts[0] == "COUNT") {
print("Count: " + parts[1]);
} else if (parts[0] == "STOP") {
print("Tracker stopped");
}
}
}
sendPacket("START");
sendPacket("BEARING,123");
sendPacket("COUNT,18");
while (true) {
readPackets();
sleep(50);
}
Questions / requests
- Can you rewrite the Teensy code to correct the compilation warning shown above?
- Can you create v4.2 that further cleans up the code by splitting the parser into a dedicated packet handler and providing a proper ARC project setup (Script skill + COM Connection Robot Skill) so ARC can send/receive the packet format ($PAYLOAD*hh)? If possible, please include the Script skill code and a description of the ARC Robot Skills configuration.
Thanks for v4.1 - I think we're getting closer to a final product.
Thank you, Jack

Yes - keep them separate.
That’s the better long-term choice because:
Recommended ARC layout
ARC robot skills
EZ-B Connection skill
Script skill: Teensy UART Manager
Script skill: Mega UART Manager
1) Final polished Teensy v4.2.1
Below is a cleaned-up version with:
$PAYLOAD*HHPacketHandler.hPacketHandler.cppIR_TRACKERv4.2.1.hIR_TRACKERv4.2.1.cppIR_TRACKERv4.2.1.ino2) ARC Script skill for Teensy UART2
This version uses:
d18= TXd19= RXUART_INDEX = 2for the Teensy link3) Should the Teensy and Mega scripts be merged?
No, keep them separate.
For your rover, separate scripts are better because:
If you want, you can share a few helper ideas later, but I would still run them as two Script skills in ARC.
4) Simple project layout diagram
ARC skill layout
5) Important wiring note
Since you said:
Your wiring should be:
And because Teensy is 3.3V logic, make sure the EZ-B UART signaling is safe for that link.
If you want, I can do one more pass and make the Mega script match the same style as the Teensy script so both UART managers in ARC look consistent.
@Athena. Can you please do one more pass and make the Mega script match the same style as the Teensy script so both UART managers in ARC look consistent?
Thank you. Jack
Absolutely - that’s a good cleanup step.
A consistent style across both ARC Script skills will make the project much easier to maintain and debug.
What I’ll do
I’ll align the Mega UART manager to the same pattern as the Teensy script:
UART_INDEXUART_BAUDbuildPacket()sendPacket()readIncoming()requestHello()requestStatus()That way both scripts behave the same way in ARC, even though the underlying packet formats are different.
Mega UART Manager v1.0 (matching style)
This version keeps your binary packet protocol intact, but gives it the same clean structure as the Teensy manager.
Notes on this Mega version
A couple of important points:
HELLO,READY, orSTATUS, the ARC script still works - those calls are just logical placeholders until you add the matching responses on the Mega side.If you want, I can also rewrite the Mega firmware to support the same
HELLO / READY / STATUS? / ACK / NAKpattern so both controllers behave identically.2) How I would structure the ARC project
ARC skills
EZ-B Connection
Script: Mega UART Manager
Script: Teensy UART Manager
This is the cleanest and easiest-to-debug layout.
3) Simple project layout diagram
Teensy wiring
My recommendation
Keep the scripts separate.
That gives you:
The style should be consistent, but the scripts should remain separate.
If you want, I can do the next pass and give you:
HELLO / READY / STATUS? / ACK / NAKThank you @Athena.