Community project

Underwater ROV Thruster Controller

Smruti Sourav Sahoo

Published July 30, 2026

ESP327 components6 assembly steps
Remix this project
Photo of Underwater ROV Thruster Controller

This guide builds a three-axis underwater ROV thruster controller powered by an ESP32 microcontroller. The system combines vectored thrust control via three 30A ESCs, real-time depth and distance sensing from a Ping sonar module, and AI-powered object detection through a HuskyLens camera—all coordinated through a single command interface.

Readers will receive a complete wiring diagram showing power distribution from the 3S LiPo battery through the buck converter to logic and thruster circuits, a full parts list with watertight housing specifications, Arduino firmware with UART communication handlers for both sonar and camera sensors, and step-by-step assembly instructions including electrical checkout and pressure testing procedures.

Wiring diagram

Interactive · read-only
Wiring diagram for Underwater ROV Thruster Controller

Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.

Parts list

Bill of materials
ComponentQtyNotes
30A ESC (Electronic Speed Controller)30 A, bidirectional1A 30A Electronic Speed Controller (ESC) used to drive brushless (BLDC) outrunner or inrunner motors. On the motor side, three wires connect to the three phases of the brushless motor. On the power side, two wires (VCC and GND) connect to a LiPo battery (typically 2S–4S). A three-wire signal harness (signal, +5V, GND) interfaces with the microcontroller and provides a Battery Eliminator Circuit (BEC) that outputs regulated 5V to optionally power the Arduino. Control is achieved via a standard 50 Hz PWM signal with pulse widths ranging from 1000 µs (minimum/stop) to 2000 µs (maximum speed), identical to servo motor control. The ESC requires an arming sequence at startup (throttle at minimum) and supports calibration of high/low throttle endpoints.
30A ESC (Electronic Speed Controller)30 A, bidirectional1A 30A Electronic Speed Controller (ESC) used to drive brushless (BLDC) outrunner or inrunner motors. On the motor side, three wires connect to the three phases of the brushless motor. On the power side, two wires (VCC and GND) connect to a LiPo battery (typically 2S–4S). A three-wire signal harness (signal, +5V, GND) interfaces with the microcontroller and provides a Battery Eliminator Circuit (BEC) that outputs regulated 5V to optionally power the Arduino. Control is achieved via a standard 50 Hz PWM signal with pulse widths ranging from 1000 µs (minimum/stop) to 2000 µs (maximum speed), identical to servo motor control. The ESC requires an arming sequence at startup (throttle at minimum) and supports calibration of high/low throttle endpoints.
30A ESC (Electronic Speed Controller)30 A, bidirectional1A 30A Electronic Speed Controller (ESC) used to drive brushless (BLDC) outrunner or inrunner motors. On the motor side, three wires connect to the three phases of the brushless motor. On the power side, two wires (VCC and GND) connect to a LiPo battery (typically 2S–4S). A three-wire signal harness (signal, +5V, GND) interfaces with the microcontroller and provides a Battery Eliminator Circuit (BEC) that outputs regulated 5V to optionally power the Arduino. Control is achieved via a standard 50 Hz PWM signal with pulse widths ranging from 1000 µs (minimum/stop) to 2000 µs (maximum speed), identical to servo motor control. The ESC requires an arming sequence at startup (throttle at minimum) and supports calibration of high/low throttle endpoints.
Blue Robotics Ping Sonar (Ping1D underwater echosounder)Underwater 1D sonar1Pressure-rated single-beam underwater echosounder. TTL UART uses 3.3 V logic and reports target distance/confidence using Ping Protocol.
MP1584 Buck Converter5 V, 3 A1Adjustable buck (step-down) DC-DC converter, 4.5-28 V in -> 0.8-20 V out, ~3 A. Configured to 5 V to step a 9 V / 12 V supply or battery pack down to the board's 5 V rail.
3S 11.1 V LiPo propulsion battery with balance lead3S 11.1 V, 5000 mAh minimum1High-discharge 3-cell LiPo pack for the three ROV thrusters and logic regulator. Use a capacity and continuous-discharge rating appropriate for the selected thrusters.
HuskyLens K210 AI Vision Sensor (SEN0305)Object-recognition AI camera1Self-contained AI vision camera used in Object Recognition mode. Learns target objects on its built-in screen and reports recognized object IDs, bounding-box position, and size to the ESP32 over UART. Mount it in a pressure-rated, optically clear camera housing for underwater use.

Assembly

6 steps
  1. Disconnect propulsion power

    Remove the 3S battery connection and keep all propellers removed while adding the camera wiring.

    • Tip: This prevents an accidental motor start while working on the electronics.
    • Do not work on the wiring with the propulsion battery connected.
  2. Mount the AI camera in a watertight optical housing

    Secure huskylens_camera behind a flat, clear optical window in the ROV enclosure. Keep the window clean, scratch-free, and close to the lens; point the camera forward and avoid trapping air bubbles in front of it.

    • Tip: A flat window is preferable to a curved dome for a simple forward-looking setup.
    • Tip: Use a pressure-rated enclosure and cable gland appropriate for the intended depth.
    • HuskyLens itself is not waterproof. Water ingress will damage it.
    • Object recognition is affected by underwater turbidity, colour loss, reflections, and low light; add external ROV lighting if necessary.
  3. Power the camera from the regulated 5 V logic rail

    Connect huskylens_camera VCC to the 5 V VOUT of logic_buck and connect huskylens_camera GND to the common logic ground. The buck regulator must have sufficient spare output current for the ESP32, sonar, and camera.

    • Tip: Use short power leads and a solid common-ground connection.
    • Tip: The existing 5 V / 3 A regulator is adequate for this low-power camera alongside the controller electronics when correctly wired.
    • Never connect the 3S battery directly to the camera.
  4. Connect the UART data pair

    Cross the UART wires: connect huskylens_camera TX to ESP32 GPIO32 and huskylens_camera RX to ESP32 GPIO33. Leave the existing Ping sonar on GPIO16 and GPIO17 unchanged.

    • Tip: Keep the TX/RX data leads away from high-current ESC and thruster power wires.
    • Tip: Use twisted pairs or shielded cable for long internal runs in a noisy ROV enclosure.
    • A shared ground is required for UART communication.
    • Do not swap the camera and sonar UART pins.
  5. Set the camera’s local recognition mode

    Before closing the enclosure, power the 5 V logic rail and use the HuskyLens built-in screen and controls to select Object Recognition mode. Teach each desired target; the camera assigns it an object ID.

    • Tip: Train using targets under lighting and water conditions similar to the actual mission.
    • Tip: The firmware sends each detected target as OBJECT id,x,y,width,height over USB serial.
    • Do not rely on camera detection alone for navigation, collision avoidance, or safety-critical control.
  6. Complete the ROV electrical and watertightness check

    Verify the three ESC signal connections, Ping sonar wiring, camera wiring, and every common ground before reconnecting the 3S battery. Perform a dry electronics check with propellers removed, then pressure/leak-test the sealed enclosure before water use.

    • Tip: Use the firmware STATUS command to see CAMERA=READY or CAMERA=OFFLINE.
    • Tip: Use the Deploy button in Schematik to compile and flash the controller.
    • Fit the existing propulsion fuse/disconnect as specified in the ROV build.
    • Never place hands near installed propellers while the battery is connected.

Pin assignments

Board wiring reference
PinConnectionType
EXTesc_left VCC3S 11.1 V LiPo propulsion battery with balance lead BAT+power
EXTesc_left GND3S 11.1 V LiPo propulsion battery with balance lead BAT-ground
EXTesc_right VCC3S 11.1 V LiPo propulsion battery with balance lead BAT+power
EXTesc_right GND3S 11.1 V LiPo propulsion battery with balance lead BAT-ground
EXTesc_vertical VCC3S 11.1 V LiPo propulsion battery with balance lead BAT+power
EXTesc_vertical GND3S 11.1 V LiPo propulsion battery with balance lead BAT-ground
EXTlogic_buck VIN3S 11.1 V LiPo propulsion battery with balance lead BAT+power
EXTlogic_buck GND3S 11.1 V LiPo propulsion battery with balance lead BAT-ground
VINlogic_buck VOUTpower
GPIO 25esc_left PWMpwm
GPIO 26esc_right PWMpwm
GPIO 27esc_vertical PWMpwm
VINping_sonar VINpower
GNDping_sonar GNDground
GPIO 16ping_sonar TXuart
GPIO 17ping_sonar RXuart
VINhuskylens_camera VCCpower
GNDhuskylens_camera GNDground
GPIO 32huskylens_camera TXuart
GPIO 33huskylens_camera RXuart

Firmware

ESP32
main.cppDeploy to device
#include <Arduino.h>
#include <ESP32Servo.h>
#include <HUSKYLENS.h>


// Forward declarations
int throttleToPulse(int throttle);
void writeThrusters(int left, int right, int vertical);
void stopThrusters();
void printHelp();
void processCommand(String line);
void readConsole();
void forwardSonarFrames();
void reportCameraObjects();

constexpr int ESC_LEFT_PIN = 25;
constexpr int ESC_RIGHT_PIN = 26;
constexpr int ESC_VERTICAL_PIN = 27;
constexpr int SONAR_RX_PIN = 16;
constexpr int SONAR_TX_PIN = 17;
constexpr int CAMERA_RX_PIN = 32;
constexpr int CAMERA_TX_PIN = 33;
constexpr int PWM_NEUTRAL_US = 1500;
constexpr int PWM_MIN_US = 1100;
constexpr int PWM_MAX_US = 1900;
constexpr uint32_t COMMAND_TIMEOUT_MS = 500;
constexpr uint32_t STATUS_INTERVAL_MS = 250;
constexpr uint32_t CAMERA_POLL_INTERVAL_MS = 100;

Servo leftEsc;
Servo rightEsc;
Servo verticalEsc;
HardwareSerial sonarSerial(2);
HardwareSerial cameraSerial(1);
HUSKYLENS huskylens;
int leftCommand = 0;
int rightCommand = 0;
int verticalCommand = 0;
bool cameraReady = false;
uint32_t lastCommandMs = 0;
uint32_t lastStatusMs = 0;
uint32_t lastCameraPollMs = 0;
String commandLine;

int throttleToPulse(int throttle) { return map(constrain(throttle, -100, 100), -100, 100, PWM_MIN_US, PWM_MAX_US); }
void writeThrusters(int left, int right, int vertical) { leftEsc.writeMicroseconds(throttleToPulse(left)); rightEsc.writeMicroseconds(throttleToPulse(right)); verticalEsc.writeMicroseconds(throttleToPulse(vertical)); }
void stopThrusters() { leftCommand = 0; rightCommand = 0; verticalCommand = 0; writeThrusters(0, 0, 0); }

void printHelp() {
  Serial.println("Commands: T left right vertical (-100..100), STOP, STATUS, HELP");
  Serial.println("Vision: set HuskyLens to Object Recognition and teach object IDs on its screen.");
  Serial.println("Detection output: OBJECT id,x,y,width,height.");
}

void processCommand(String line) {
  line.trim(); line.toUpperCase();
  if (line == "STOP") { stopThrusters(); lastCommandMs = millis(); Serial.println("ACK STOP"); return; }
  if (line == "HELP") { printHelp(); return; }
  if (line == "STATUS") { Serial.printf("STATUS L=%d R=%d V=%d CAMERA=%s\n", leftCommand, rightCommand, verticalCommand, cameraReady ? "READY" : "OFFLINE"); return; }
  int l, r, v;
  if (sscanf(line.c_str(), "T %d %d %d", &l, &r, &v) == 3) {
    leftCommand = constrain(l, -100, 100); rightCommand = constrain(r, -100, 100); verticalCommand = constrain(v, -100, 100);
    writeThrusters(leftCommand, rightCommand, verticalCommand); lastCommandMs = millis();
    Serial.printf("ACK T %d %d %d\n", leftCommand, rightCommand, verticalCommand);
  } else Serial.println("ERR. Type HELP.");
}

void readConsole() {
  while (Serial.available()) {
    char c = static_cast<char>(Serial.read());
    if (c == '\n' || c == '\r') { if (commandLine.length()) { processCommand(commandLine); commandLine = ""; } }
    else if (commandLine.length() < 80) commandLine += c;
  }
}

void forwardSonarFrames() {
  static uint8_t frame[128]; static size_t length = 0;
  while (sonarSerial.available()) {
    uint8_t b = static_cast<uint8_t>(sonarSerial.read());
    if (length < sizeof(frame)) frame[length++] = b; else length = 0;
    if (length >= 4 && frame[0] == 0x42 && frame[1] == 0x52) {
      uint16_t payloadLength = static_cast<uint16_t>(frame[2]) | (static_cast<uint16_t>(frame[3]) << 8);
      size_t frameLength = static_cast<size_t>(payloadLength) + 8;
      if (frameLength <= sizeof(frame) && length == frameLength) {
        Serial.print("PING_RAW "); for (size_t i = 0; i < length; ++i) Serial.printf("%02X", frame[i]); Serial.println(); length = 0;
      } else if (frameLength > sizeof(frame)) length = 0;
    } else if (length >= 2) { frame[0] = frame[length - 1]; length = 1; }
  }
}

void reportCameraObjects() {
  if (!cameraReady || millis() - lastCameraPollMs < CAMERA_POLL_INTERVAL_MS) return;
  lastCameraPollMs = millis();
  if (!huskylens.request()) { Serial.println("CAMERA_ERROR request failed"); cameraReady = false; return; }
  if (!huskylens.isLearned()) { Serial.println("CAMERA_NOT_LEARNED"); return; }
  if (!huskylens.available()) { Serial.println("NO_OBJECT"); return; }
  do {
    HUSKYLENSResult result = huskylens.read();
    if (result.command == COMMAND_RETURN_BLOCK) Serial.printf("OBJECT %d,%d,%d,%d,%d\n", result.ID, result.xCenter, result.yCenter, result.width, result.height);
  } while (huskylens.available());
}

void setup() {
  Serial.begin(115200);
  sonarSerial.begin(115200, SERIAL_8N1, SONAR_RX_PIN, SONAR_TX_PIN);
  cameraSerial.begin(9600, SERIAL_8N1, CAMERA_RX_PIN, CAMERA_TX_PIN);
  leftEsc.setPeriodHertz(50); rightEsc.setPeriodHertz(50); verticalEsc.setPeriodHertz(50);
  leftEsc.attach(ESC_LEFT_PIN, 1000, 2000); rightEsc.attach(ESC_RIGHT_PIN, 1000, 2000); verticalEsc.attach(ESC_VERTICAL_PIN, 1000, 2000);
  stopThrusters(); delay(2500);
  cameraReady = huskylens.begin(cameraSerial);
  lastCommandMs = millis();
  Serial.println("ROV controller ready; all thrusters neutral.");
  Serial.printf("HuskyLens camera: %s. Set it to Object Recognition and teach target IDs.\n", cameraReady ? "READY" : "OFFLINE");
  printHelp();
}

void loop() {
  readConsole(); forwardSonarFrames(); reportCameraObjects();
  if (millis() - lastCommandMs > COMMAND_TIMEOUT_MS && (leftCommand || rightCommand || verticalCommand)) { stopThrusters(); Serial.println("FAILSAFE: command timeout; thrusters neutral."); }
  if (millis() - lastStatusMs >= STATUS_INTERVAL_MS) { lastStatusMs = millis(); Serial.printf("STATUS L=%d R=%d V=%d CAMERA=%s\n", leftCommand, rightCommand, verticalCommand, cameraReady ? "READY" : "OFFLINE"); }
}

“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik