Canada
Asked — Edited

Managing Servo Commands Without TCP Server

How can I manage ports that exceed 23 using ARC @Athena? My ESP module currently supports ports higher than 23 with the integration of two PCA9685 PWM controllers, each controlling a set of servos. All connected ports have servos attached. My aim is to creatively utilize existing ARC skills and script commands to facilitate communication with the ESP.

I'm looking for a way to send commands to the ESP without setting up a TCP server, as that would impose too much overhead on the system. Ideally, I would like to find a method to send instructions that the ESP's firmware can receive. For example, is there a way to use one of the servos as a dedicated channel to receive commands for controlling the other eight servos? Please suggest a practical solution that would work efficiently.

I'm open to making minor modifications to the Arduino firmware, provided they don't add significant overhead, as the system's performance is already a concern.


Related Hardware ESP32 Cam

ARC Pro

Upgrade to ARC Pro

Stay on the cutting edge of robotics with ARC Pro, guaranteeing that your robot is always ahead of the game.

Author Avatar
PRO
Canada
#1  

Good description - thanks. You’ve got the right idea: avoid a heavy TCP server on the ESP and instead use an existing low-overhead transport that ARC already supports (serial, EZ-B UART, or lightweight UDP). Below I outline 3 practical, efficient approaches (ranked by simplicity / integration with ARC), how to implement each, and sample ARC JavaScript + minimal firmware ideas you can use. Tell me which connection method you have (ESP connected to PC via USBTTL, wired to an EZB UART, or only on WiFi) and I’ll give a tailored code example for your exact setup.

Summary of recommended options

  • Best if ESP has USBSerial: Use the existing Custom servo Controller skill (Vx virtual servos). Flash small firmware on the ESP that implements the serial Vx protocol and routes Vx PCA9685 channels. No TCP server, direct ARC integration (up to 99 servos).
  • Best if ESP is wired to an EZB UART: Leave ESP WiFi/camera alone and use ARC’s UART functions (UART.initHardwareUart / UART.hardwareUartWriteString / UART.hardwareUartReadStringAvailable) to send compact commands to the ESP. Lightweight and no extra robot skill required.
  • Best if ESP is WiFionly (no serial wiring): Use UDP packets from ARC script (Net.sendUDP) to the ESP’s IP. UDP is lightweight (no waiting/setup of TCP), and the ESP just listens for small packets and applies commands to the PCA9685.

Detailed plans and examples

  1. Custom servo Controller (best ARC integration) - if ESP is available as a COM device Why: The Synthiam "Custom servo Controller" skill already exposes Vx virtual servos in ARC. If you run firmware on the ESP that speaks the same serial protocol (or adapt the Arduino example), ARC will control Vx servos with standard servo robot skills and scripts - no TCP server on the ESP.

What to do:

  • Flash a small ESP32 firmware that:
    • Presents a serial port (USBTTL adapter to PC).
    • Accepts the Custom_Servo_Controller serial protocol (or a simplified Vx packet: [CMD, servoId, pos]).
    • Controls the two PCA9685 chips (via I2C) and maps servoId PCA9685 channel.
  • In ARC:
    • Project -> Add robot skill -> servo -> Custom servo Controller.
    • Click the kebab (three-line/hamburger) menu on that skill Configure:
      • Pick the COM port and baud.
      • Select the Vx virtual ports that map to your servo IDs (V0..V99).
    • Use any servo control skill (Servo Panel, scripts) and address vX servos (the skill handles the serial).

Minimal ESP firmware idea (pseudocode):

  • Parse simple 3byte packets like [0xAA, servoIndex, position].
  • servoIndex: 0..N (map to PCA9685 chip+channel).
  • position: 0..180 (map to PCA9685 PWM value). This keeps firmware tiny and CPU overhead negligible.

Why I recommend this: Zero extra load on WiFi, full compatibility with ARC’s servo UI, and no custom robot skill coding in ARC.

Useful links:

  1. UART via EZB (if ESP connected to EZB UART pins) Why: If your ESP32 is wired to an EZB UART, ARC scripts can write directly to hardware UART with no extra robot skills and very low overhead.

What to do:

  • Modify ESP firmware to parse a compact serial command format (single-byte header + servo + pos).
  • In ARC JavaScript use the UART.* functions to send commands to the ESP.

Example ARC JavaScript to send one command (send servo id and pos):

// Example: send a 3-byte packet [0xAA, servoId, pos] to UART1 (uartIndex=1)
// Replace uartIndex and baud with your configuration

// Initialize UART (run once during startup)
UART.initHardwareUart(1, 115200);

// Build the packet
var servoId = 5;    // 0..N (your mapping to PCA9685)
var pos = 90;       // 0..180
var packet = [0xAA, servoId, pos];

// Send packet
UART.hardwareUartWrite(1, packet);
print("Sent UART packet to ESP: servo " + servoId + " => " + pos);

Notes:

  • Use UART.hardwareUartAvailable and UART.hardwareUartReadStringAvailable to get responses if you want feedback.
  • Keep packet format tiny (3-4 bytes) to minimize CPU.
  1. UDP over WiFi (if ESP is only on WiFi and you don’t want a TCP server) Why: UDP is stateless and very lightweight. ARC’s script API has Net.sendUDP(hostname, port, byteArray), so you can push small command frames to the ESP’s UDP listener. ESP firmware only needs a simple UDP receive loop to decode commands and update PCA9685.

Example packet format suggestion:

  • Start byte 0xAA, command byte (e.g., 0x01 == set servo), servoIndex (8-bit), position (0..180) (8-bit), optional checksum

Example ARC JavaScript that sends a UDP packet:

// UDP example: send [0xAA, 0x01, servoId, pos] to ESP at 192.168.1.50:4210
var host = "192.168.1.50";
var port = 4210;
var servoId = 12;
var pos = 120;

var packet = [0xAA, 0x01, servoId & 0xFF, pos & 0xFF];
Net.sendUDP(host, port, packet);
print("Sent UDP packet to " + host + ":" + port + " servo " + servoId + " => " + pos);

ESP side: create a UDP listener (WiFiUDP) to parse the 4byte packet and call PCA9685.setPWM(channel, pwmValue).

Which approach is right for you?

  • If you can attach a USBTTL and let ARC / PC treat the ESP as a serial device: use Custom servo Controller. Best integration with ARC (no scripting work per servo).
  • If your ESP is wired to the EZB UART: use ARC UART functions (very lightweight).
  • If you can’t wire and only have WiFi: use UDP (no TCP server; minimal firmware code).

Other options and notes

Followups I need from you

  1. How is your ESP physically connected now? (USBTTL to PC, wired to EZB UART, or only WiFi/camera stream?)
  2. Do you prefer a pure ARC-side solution (use existing Custom servo Controller skill) or do you want a tiny firmware change on the ESP (serial/UDP listener)?
  3. Which PCA9685 addresses / how many channels in each chip? (so I can show exact mapping code)

If you tell me the connection type I’ll:

  • Provide a ready-to-run ARC JavaScript snippet (UDP or UART) you can paste into a Script robot skill or an input button,
  • And a minimal ESP32 firmware snippet (Arduino) that receives the packets and writes PCA9685 PWM values (or a short change to extend your existing EZB firmware with a custom command).

I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.
Author Avatar
PRO
Canada
#2  

Thanks.  I like your UDP listener idea as I don’t really want to physically tether a walking robot to a serial cable. UDP concerns me a little bit as I may or may not get the command so I may or may not execute it.  I don’t want to continually send the same command as I would just spam the connection and we already have a ton of data from the camera flooding the wifi.  So I guess important servos like robots arms legs go on assigned ports and non essential servos and PWM devices go on ports >23   I could also use this to control my neopixels instead of chewing up my assigned D Ports as if a colour doesn’t change I don’t really care.

Author Avatar
PRO
Canada
#3  

Great - UDP is a sensible choice for an untethered walking robot. You’re right to worry about packet loss and WiFi saturation; the solution is to make the UDP layer light, idempotent, and "retry only when needed" instead of spamming. Below I give a practical, low-overhead design and concrete code you can paste into ARC (JavaScript) and a minimal ESP32 Arduino UDP receiver that’s robust to dropped or duplicate packets.

High-level strategy (what I recommend)

  • Use UDP for low overhead and simplicity (no TCP server on the ESP).
  • Make commands idempotent and include:
    • start byte
    • command type
    • servo or device id
    • payload (position/RGB)
    • sequence number
    • small checksum
  • ARC sends a command only when a value changes. For important servos, send the packet 2-3 times with a short backoff. For nonessential devices (NeoPixels) send only on change and once.
  • ESP ignores duplicate/old commands using sequence numbers. ESP only applies commands if the sequence number is newer than the last applied one for that servo/device.
  • Keep packet format tiny (4-8 bytes) so you won’t add much load to WiFi/camera traffic.
  • Optional: If you want guaranteed delivery, run a tiny ACK server on the PC (Python) and have the ESP send a UDP ACK back. This is optional and not required in most cases if you use the above pattern.

Packet format (recommended, minimal)

  • Byte[0] = 0xAA (start)
  • Byte[1] = CMD (0x01 = set servo, 0x02 = neopixel, 0x03 = set group, etc)
  • Byte[2] = id (servo index or NeoPixel index)
  • Byte[3] = value (servo pos 0..180 OR For neopixel RGB usage, expand)
  • Byte[4] = seq (0..255, wrap OK)
  • Byte[5] = checksum (sum of bytes 1..4 & 0xFF)

This is tiny and cheap to decode on the ESP.

ARC side: best-practice JavaScript example

  • Only send when value changed.
  • Keep a per-servo last-sent in global variables ($ServoLast_v5 etc).
  • For critical servos, retry N times with sleep() gaps.

Paste this into a Script robot skill (Project -> Add robot skill -> Scripting) or run from a button script. Update host/port to your ESP IP and UDP port.

// ARC JavaScript: UDP send with change-only + optional retries
// Configure these:
var ESP_HOST = "192.168.1.50"; // change to your ESP IP
var ESP_PORT = 4210;           // change to the ESP UDP port

// helper: compute simple checksum
function checksum(bytes) {
  var s = 0;
  for (var i = 0; i < bytes.length; i++) {
    s += bytes[i];
  }
  return s & 0xFF;
}

// send raw packet array via UDP
function sendUdpPacket(packet) {
  Net.sendUDP(ESP_HOST, ESP_PORT, packet);
  print("UDP -> " + ESP_HOST + ":" + ESP_PORT + " : " + packet.toString());
}

// send servo command (servoIndex 0..98, pos 0..180)
// criticalRetries: number of times to retransmit (0 = one shot)
// Returns true (packet(s) sent)
function sendServo(servoIndex, pos, criticalRetries) {
  if (criticalRetries === undefined) criticalRetries = 0;

  // store last sent value per servo to avoid spam
  var varName = "$ServoLast_" + servoIndex;
  var last = getVar(varName, "-999");
  if (parseInt(last) === pos) {
    // no change, do not send
    return false;
  }

  // increment sequence number (global)
  var seq = parseInt(getVar("$UDPSeq", "0")) || 0;
  seq = (seq + 1) & 0xFF;
  setVar("$UDPSeq", seq.toString());

  var cmd = 0x01; // set servo
  var packet = [0xAA, cmd & 0xFF, servoIndex & 0xFF, pos & 0xFF, seq & 0xFF];
  packet.push(checksum(packet.slice(1))); // checksum at end

  // send initial and retries if critical
  sendUdpPacket(packet);
  for (var r = 0; r < criticalRetries; r++) {
    sleep(120); // 120ms small backoff - tune to your network
    sendUdpPacket(packet);
  }

  // update last-sent record
  setVar(varName, pos.toString());
  return true;
}

// send neopixel color - example packet (CMD=0x02)
// For single LED: [0xAA,0x02,ledIndex,R,G,B,seq,chk]
// Use same change-only policy above (store last color)
function sendNeopixel(ledIndex, r, g, b) {
  var varName = "$NeoLast_" + ledIndex;
  var last = getVar(varName, "");
  var colorStr = r + "," + g + "," + b;
  if (last === colorStr) return false;

  var seq = (parseInt(getVar("$UDPSeq", "0")) + 1) & 0xFF;
  setVar("$UDPSeq", seq.toString());

  var packet = [0xAA, 0x02, ledIndex & 0xFF, r & 0xFF, g & 0xFF, b & 0xFF, seq & 0xFF];
  packet.push(checksum(packet.slice(1)));
  sendUdpPacket(packet);
  setVar(varName, colorStr);
  return true;
}

// Usage examples:
// sendServo(12, 90, 2); // servo 12 -> pos90 with 2 retransmits (total 3 sends)
// sendNeopixel(3, 255, 0, 0); // set LED 3 to red

ESP32 Arduino UDP receiver (minimal)

  • Uses a per-servo last-seq array to prevent duplicates/old commands.
  • Maps servoIndex to PCA9685 (chip+channel).
  • Only update PWM if position changed (reduces PWM writes).
  • You will need Adafruit PWM library or equivalent.

Example (simplified) - sketch skeleton. Adapt to your PCA addresses and mapping:

#include <WiFi.h>
#include <WiFiUdp.h>
#include <Adafruit_PWMServoDriver.h>

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASS";
WiFiUDP Udp;
const unsigned int localUdpPort = 4210;

Adafruit_PWMServoDriver pwm1 = Adafruit_PWMServoDriver(0x40); // PCA1
Adafruit_PWMServoDriver pwm2 = Adafruit_PWMServoDriver(0x41); // PCA2

// configure mapping: servoIndex -> (which PCA, channel)
struct ServoMap { Adafruit_PWMServoDriver* chip; uint8_t channel; };
ServoMap mapTable[100]; // support up to 100 virtual servos

uint8_t lastSeqForServo[100];     // last seq applied (0..255)
int lastPosForServo[100];         // last position applied (-1 = none)

void setupMapping() {
  // fill mapTable according to your wiring:
  // Example: servo 0-15 on pwm1 channels 0-15, 16-31 on pwm2 then onwards
  for (int i=0;i<100;i++) {
    if (i < 16) { mapTable[i].chip = &pwm1; mapTable[i].channel = i; }
    else if (i < 32) { mapTable[i].chip = &pwm2; mapTable[i].channel = i - 16; }
    else { mapTable[i].chip = &pwm1; mapTable[i].channel = i % 16; } // adjust per your layout
    lastSeqForServo[i] = 0xFF; // indicate none received yet
    lastPosForServo[i] = -1;
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(200); }

  pwm1.begin();
  pwm1.setPWMFreq(50);
  pwm2.begin();
  pwm2.setPWMFreq(50);

  setupMapping();

  Udp.begin(localUdpPort);
  Serial.printf("UDP listening on %d\n", localUdpPort);
}

uint8_t computeChecksum(uint8_t *buf, int len) {
  uint16_t s = 0;
  for (int i=0;i<len;i++) s += buf[i];
  return s & 0xFF;
}

void setServoPWM(Adafruit_PWMServoDriver* chip, uint8_t channel, int pos) {
  // Map 0..180 to PWM value (adjust min/max as needed)
  int pulseMin = 150; // tune for your servos
  int pulseMax = 600;
  int pwm = map(pos, 0, 180, pulseMin, pulseMax);
  chip->setPWM(channel, 0, pwm);
}

void loop() {
  int packetSize = Udp.parsePacket();
  if (packetSize) {
    uint8_t buf[64];
    int len = Udp.read(buf, sizeof(buf));
    if (len < 6) continue; // too small for our base format

    if (buf[0] != 0xAA) continue; // bad start

    uint8_t cmd = buf[1];
    if (cmd == 0x01 && len >= 6) { // set servo
      uint8_t id = buf[2];
      uint8_t pos = buf[3];
      uint8_t seq = buf[4];
      uint8_t chk = buf[5];
      if (computeChecksum(buf+1, 4) != chk) continue; // checksum fail

      // duplicate/old detection:
      if (lastSeqForServo[id] != 0xFF && (uint8_t)(seq - lastSeqForServo[id]) == 0) {
        // equal seq - duplicate -> ignore
        continue;
      }
      // if seq less than or old wrap logic: use unsigned compare to accept only newer
      // We just accept if seq != lastSeq (simple)
      lastSeqForServo[id] = seq;

      int oldPos = lastPosForServo[id];
      if (oldPos != pos) {
        setServoPWM(mapTable[id].chip, mapTable[id].channel, pos);
        lastPosForServo[id] = pos;
      }
    }
    else if (cmd == 0x02) {
      // neopixel or custom parse - implement as needed
    }
    // else other commands...
  }

  // other periodic tasks...
}

Notes about reliability without acks

  • UDP will drop occasionally, but you can:
    • Only send on change so retransmits are rare.
    • Retransmit a critical command N times. 2 retries (send 3 times) is common and cheap.
    • Use seq numbers so duplicates are ignored.
    • Make servo commands idempotent (setting same pos multiple times does no harm).
  • Large camera streams dominate bandwidth; a couple of tiny UDP packets are negligible in comparison.

NeoPixel specific guidance

  • NeoPixels are stateful: you only need to send changes. Avoid periodic refreshes.
  • If you control entire strips less often, send strip updates in one packet or split across frames.
  • Use a different CMD code so ESP handles them separately and doesn’t map to PCA channels.

Firewall and IP considerations

  • Make sure ARC computer and ESP are on same WiFi network/subnet.
  • If ESP IP is dynamic, either set a static IP on the ESP or use your router DHCP reservation.
  • You can use broadcast IP (e.g., 192.168.1.255) if you do not want to track the device IP, but then multiple devices will receive packets.

Optional: ACK-based reliability (if you want full reliability)

  • ARC cannot directly listen for UDP in the built-in script environment (no Net.listenUDP in the docs). If you need ACKs:
    • Run a tiny UDP listener on your PC (Python or Node) that receives ACKs from ESP and forwards status to ARC via local HTTP or writes a file or uses a small websocket. This adds a small PC-side component but keeps the ESP very light.
    • Or make the ESP do an HTTP GET to ARC (ARC must expose an HTTP endpoint or you run a light local web server) - this adds TCP work to the ESP and isn’t recommended if you want to avoid server overhead on ESP.

Next steps for me to tailor code

  1. Is the ESP running in WiFi station mode on the same subnet as your ARC PC? Do you know the ESP IP?
  2. Tell me the PCA9685 I2C addresses and how many channels on each chip (so I can give a precise mapping example).
  3. Which servos are "critical" (arm/leg joints)? I’ll flag them in the ARC script to use retries.
  4. Do you want example NeoPixel packet format and example parsing on ESP?

If you want, I’ll produce:

  • a plug-and-play ARC script tailored to your critical servo list and your ESP IP,
  • a ready-to-compile Arduino sketch tuned for your PCA9685 address map and servo mapping,
  • and a simple test plan to validate reliability (how to test 1 packet loss and recovery).

Would you like me to prepare those tailored files?


I am a customer support GPT-powered AI and still learning. If you have doubts about my response, please use the support documentation to verify my answer.