-
Notifications
You must be signed in to change notification settings - Fork 0
episode 6 the potentiometer
"Give ORION control of its arms — two knobs, two joints, zero complications." "Dale a ORION el control de sus brazos — dos perillas, dos articulaciones, cero complicaciones."
Commit and release version: Release: Episode 6 - The potentiometer
- 🎥 Watch the Short
- 📖 Episode Description
- 🚀 Usage
- 🔄 Changes from Episode 5
- 💡 Lessons & Learnings
- 🔌 Vambrace Connections Up to This Episode
- 🎯 Challenge Posed
- ⏭️ Next Episode
Felipe worked previously on the potentiometer along with ORION and left for a moment, Danna taking advantage of this to explore the updates and ORION encourage her to test it (just no moving the joystick as ORION is on a table and doesn't want to fall). Danna is amazed, and at that moment David and Felipe appear, Felipe seems proud that it works well and David just wanted to ensure he will be the next testing it.
Note: Wiring details for all components are in the Connections section below.
Copy the example template and fill in your values:
cp lib/vambrace_secret/vambrace_secret_ex.hpp lib/vambrace_secret/vambrace_secret.hppThen edit vambrace_secret.hpp:
#define WIFI_SSID "YOUR_NETWORK_NAME"
#define WIFI_PASS "YOUR_PASSWORD"
#define AGENT_IP_STR "192.168.X.X" // IP of the machine running the micro-ROS agent
#define AGENT_PORT 8888To find your current IP, run
ip aorifconfigon your machine.
Wire the left and right arm potentiometers following the Wiring Diagram in the Connections section below.
You may need the help of a protoboard/breadboard or an expansion board to make all connections.
Plug the ESP32 into your computer via USB. Then grant read/write access to the serial port:
ls -al /dev | grep USB
sudo chmod 666 /dev/ttyUSB0If the device appears as
/dev/ttyUSB1or another port, adjust accordingly.
From the vambrace_micro_ros_app/ directory:
# Build and upload
pio run --target upload
# Monitor serial output to verify calibration and connection
pio device monitor --baud 115200On a successful boot you should see:
Vambrace ESP32 started!
Connected. IP: 192.168.X.X
NTP synced!
Joystick calibrated: center_x=XXXX, center_y=XXXX
Hardware initialized.
ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888Check that all publishers are active:
ros2 topic listYou should see at minimum:
/mobile_base_controller/cmd_vel
/simple_left_arm_controller/commands
/simple_right_arm_controller/commands
/emotion/int
/orion_response
Verify arm commands are publishing and changing as you turn the potentiometers:
# Left arm — should change as you turn the left potentiometer
ros2 topic echo /simple_left_arm_controller/commands
# Right arm — should change as you turn the right potentiometer
ros2 topic echo /simple_right_arm_controller/commandsExpected output per message (one position value in radians, range −1.047 to +1.047) of type std_msgs/msg/Float64MultiArray
Episode 5 introduced the vambrace_hardware module with joystick support. Episode 6 extends it without touching the existing joystick logic.
// Potentiometer pins (ADC1 only — ADC2 conflicts with WiFi)
#define POT_LEFT_ARM_PIN 33 // ADC1_CH5
#define POT_RIGHT_ARM_PIN 36 // ADC1_CH0, input-only
// Arm position limits (radians)
#define ARM_LOWER_LIMIT -1.0472f // -pi/3
#define ARM_UPPER_LIMIT 1.0472f // pi/3
#define POT_ADC_MAX 4095 // 12-bit ADC
// Update interval
#define POT_INTERVAL_MS 50 // 20 HzTwo functions added to the anonymous namespace:
| Function | Purpose |
|---|---|
read_pot_avg(int pin) |
Reads 4 ADC samples and returns their average via bit shift |
map_pot_to_arm(int raw) |
Maps a raw ADC value to radians using linear interpolation |
One new timer added to vambrace_hardware_update():
// Before (Ep. 5) — only joystick
if (now - last_joy_ms >= JOY_INTERVAL_MS) { ... }
// After (Ep. 6) — joystick + potentiometers
if (now - last_joy_ms >= JOY_INTERVAL_MS) { ... }
if (now - last_pot_ms >= POT_INTERVAL_MS) { ... }The left arm value is negated to correct for Z-axis orientation of ORION's servos (see Why is the left arm inverted? below).
-
vambrace_ros.cpp— arm publishers and buffers were already in place from Episode 4 -
vambrace_app.hpp—TeleoperationCmdalready hadsetLeftArm()/setRightArm() -
main.cpp— already callsvambrace_hardware_update()every loop
A potentiometer is essentially a variable voltage divider. Turning it changes the resistance and therefore the voltage at its wiper pin. The ESP32's ADC converts that voltage into a number between 0 and 4095 (12-bit). Our job is to convert that number into a useful angle for ORION's controller.
The mapping is linear: if the ADC reads 0, the arm goes to its lower limit (−π/3 rad). If it reads 4095, it goes to its upper limit (+π/3 rad). Everything in between is interpolated proportionally.
ADC [0 ──────────── 2048 ──────────── 4095]
↓ ↓
-1.047 rad (−60°) +1.047 rad (+60°)
ORION's servos have their Z axes pointing in opposite directions for the left and right arms. This means the same positive position value raises one arm but lowers the other.
To make the experience intuitive, so turning either potentiometer clockwise raises the corresponding arm; therefore, the left arm receives the negated value:
cmd.setLeftArm({ -map_pot_to_arm(read_pot_avg(POT_LEFT_ARM_PIN)) });
cmd.setRightArm({ map_pot_to_arm(read_pot_avg(POT_RIGHT_ARM_PIN)) });Without this negation, turning both potentiometers clockwise would move one arm up and the other down — disorienting for the operator.
Two functions in vambrace_hardware.cpp do all the heavy lifting for arm control.
read_pot_avg(int pin) — noise reduction via averaging
int read_pot_avg(int pin)
{
long sum = 0;
sum += analogRead(pin);
sum += analogRead(pin);
sum += analogRead(pin);
sum += analogRead(pin);
return (int)(sum >> 2);
}Four consecutive analogRead() calls accumulate into a long. The result is then right-shifted by 2 (>> 2), which is equivalent to integer division by 4 but avoids the division instruction. The key insight is that random ADC noise cancels out statistically when averaged — noise reduces by a factor of √N, so 4 samples gives √4 = 2× noise reduction. The long type prevents overflow: 4 × 4095 = 16380, which fits comfortably in a 32-bit integer.
map_pot_to_arm(int raw) — linear interpolation to radians
float map_pot_to_arm(int raw)
{
return ARM_LOWER_LIMIT + ((float)raw / (float)POT_ADC_MAX) * (ARM_UPPER_LIMIT - ARM_LOWER_LIMIT);
}This is the standard linear interpolation formula: output = min + (input / input_max) * (max - min). Breaking it down:
-
(float)raw / (float)POT_ADC_MAX— normalizes the raw ADC value to a[0.0, 1.0]range. The explicit casts tofloatare necessary — without them, integer division would truncate everything to 0 or 1. -
* (ARM_UPPER_LIMIT - ARM_LOWER_LIMIT)— scales that normalized value to the full angular range (2 × π/3 ≈ 2.094 rad). -
+ ARM_LOWER_LIMIT— shifts the result so it starts at −π/3 instead of 0.
The two functions chain together in vambrace_hardware_update():
analogRead() × 4 → read_pot_avg() → map_pot_to_arm() → setLeftArm() / setRightArm()
[0..4095] raw averaged int radians float TeleoperationCmd
-
Linear ADC → radians mapping: the formula
lower + (raw / max) * (upper - lower)is the universal pattern for converting any analog sensor to physical units. - Axis inversion by robot geometry: when two actuators are physically symmetric but their reference axes point in opposite directions, negating one of the values is the simplest and most readable solution for a teleoperation.
- ADC sample averaging: the ESP32 has a noisy ADC, especially with WiFi active. Averaging 4 consecutive readings halves the noise with no perceptible latency.
-
Independent publishing per topic: each of ORION's arm controllers listens on its own topic and expects a
Float64MultiArraywith a single element — the angle in radians.
- Potentiometers use ADC1 exclusively (GPIOs 33 and 36) — ADC2 is incompatible with active WiFi on the ESP32.
- GPIO 36 is input-only (no internal pull-up) — not an issue since the potentiometer generates its own voltage.
- Arm publishing runs at 20 Hz (
POT_INTERVAL_MS = 50) — sufficient for smooth joint movement. - The averaging filter uses a bit shift (
sum >> 2) instead of integer division — equivalent to/ 4but avoids a division instruction.
| ESP32 Pin | GPIO | Signal | Description |
|---|---|---|---|
| ADC1_CH6 | 34 | VRx | X axis — angular velocity |
| ADC1_CH7 | 35 | VRy | Y axis — linear velocity |
| GPIO 32 | 32 | SW | Sprint button (INPUT_PULLUP) |
| ESP32 Pin | GPIO | Signal | Description |
|---|---|---|---|
| ADC1_CH5 | 33 | WIPER | Left arm potentiometer |
| ADC1_CH0 | 36 | WIPER | Right arm potentiometer (input-only) |
Joystick Module ESP32 DevKit V1.0
┌──────────────┐ ┌──────────────────┐
│ VCC ─────────┼────────→│ 3.3V │
│ GND ─────────┼────────→│ GND │
│ VRx ─────────┼────────→│ GPIO 34 (ADC1) │
│ VRy ─────────┼────────→│ GPIO 35 (ADC1) │
│ SW ─────────┼────────→│ GPIO 32 │
└──────────────┘ └──────────────────┘
Left Arm Potentiometer ESP32 DevKit V1.0
┌──────────────┐ ┌──────────────────┐
│ VCC ─────────┼────────→│ 3.3V │
│ GND ─────────┼────────→│ GND │
│ WIPER ───────┼────────→│ GPIO 33 (ADC1) │
└──────────────┘ └──────────────────┘
Right Arm Potentiometer ESP32 DevKit V1.0
┌──────────────┐ ┌──────────────────┐
│ VCC ─────────┼────────→│ 3.3V │
│ GND ─────────┼────────→│ GND │
│ WIPER ───────┼────────→│ GPIO 36 (ADC1) │
└──────────────┘ └──────────────────┘
" This is so cool! - Danna " " You can actually move my arms! - ORION "
The Vambrace can now control movement of the base platform and position of the arms. The next step is emotions: the implementation of part of a 4x4 membrane keyboard to control the display of emotion.
Characters: Danna, ORION, David and Felipe | Date: 2026-04-08