Drive a 3D Printed Robot Arm with a PS3 Controller: ESP32, Bluetooth, No PC (OmArm One Part 1.5)

A DualShock 3 controller held in one hand with the 3D printed OmArm One robot arm behind it holding an orange cube
Two thumbs, six joints and a gripper. The controller talks straight to the ESP32 inside the base: no computer, no phone and no WiFi network in the loop.

OmArm One Series Part 1: Build, statics and payload · Part 1.5: Game controller (this post) · Part 2: Digital twin (URDF in RViz and Gazebo) (coming soon) · Part 3: MoveIt 2 on real hardware (coming soon) · Part 4: Computer vision pick and place (coming soon) · Part 5: AI: grasping by name, by voice (coming soon)

This is a bonus chapter between the build and the digital twin. You need a finished OmArm One from Part 1 (or any arm with the same ESP32 + PCA9685 electronics) and a Bluetooth game controller. There is no new wiring.

In Part 1 the arm got a web app. The ESP32 in the base brings up its own WiFi network, you open a page on your phone, and seven sliders move the six joints and the gripper. That is the right tool for setting the arm up: you see numbers, you can type a pose, you can tell the firmware where the arm stands after a power cut. It is the wrong tool for driving. A slider is a position: you drag it, the joint goes there, as fast or as jerkily as your finger went. You watch the screen instead of the arm, one thumb does one joint at a time, and a phone that goes to sleep takes the arm’s controls with it.

A game controller fixes all of that at once. A stick is a velocity: push a little and the joint turns slowly, push more and it turns faster, let go and it brakes. Two thumbs move six joints together, the triggers work the gripper, your eyes stay on the arm. And because the controller talks to the ESP32 directly over Bluetooth, nothing else has to be switched on: no router, no laptop, no app. Pair once, press PS, drive.

The Part 1 web app open on a phone next to a DualShock 3 controller held in one hand
Part 1’s sliders and Part 1.5’s controller. The web app stays for setup; the controller is for driving.
Diagram comparing the WiFi web app path and the Bluetooth game controller path to the same motion core, with what each one is good for
Both paths end in the same motion core from Part 1, and both firmwares keep the same position record in flash. You switch between them with one upload.

The controller in the photos is a PlayStation 3 DualShock 3, because I had one in a drawer. The firmware does not care: PS4, PS5, Xbox, Switch Pro and most third-party pads work with the same code and the same mapping. The one thing the DualShock 3 needs that the others do not is a small pairing step, and that gets its own section.


At a glance

What you’ll build: A second firmware for the ESP32 in your arm. Flash it, pair a controller, and drive all six joints and the gripper with the sticks, the D-pad and the shoulder buttons. Four speed levels plus an analog gas pedal, a home button, a sequence of up to 16 poses that you record with one tap and play in a loop, and an emergency stop that latches. Motion is velocity-controlled with a dead zone, an expo curve and a fixed braking rate, so the arm moves like a machine, not like a toy.

What you’ll learn: Why a stick has to command speed rather than position, and how a speed becomes something a servo without feedback can follow safely. Which controllers can talk to an ESP32 at all, and why the ESP32-S3 in your drawer cannot help here. How to pair a DualShock 3, the one pad that does not pair like the others. And what the firmware refuses to do, and why.

Time required: One evening. Ten minutes to install the board package and flash, ten to pair, the rest is driving.

Total cost: 0 € if you own a controller. A used DualShock 3 is 10 to 20 € second-hand; make sure it is a real one (section 1), clones do not pair.

Difficulty: Easy, if Part 1 is done.

Safety: The arm moves faster under a stick than it ever did under a slider. The PS button freezes it instantly, the first seconds after every start are slow, and the firmware will not move at all until it knows where the arm is. Keep the servo power switch from Part 1 within reach of your free hand.

What you need: a finished OmArm One from Part 1, or any arm on the same ESP32 DevKit and PCA9685 electronics. A Bluetooth game controller from the table in section 1. A USB cable and the Arduino IDE. For a DualShock 3, also a PC for the one-time pairing step. No new parts, no new wiring.

Files you’ll need: the OmArmOne_Gamepad firmware and, optionally, the mapper tool. Both are in the GitHub repository linked at the end, and in the complete package, which now carries this part next to Part 1 and includes a printable PDF of this guide. No new hardware either.


1. Which controller do you have?

This decides whether you pair in ten seconds or spend an hour on it, so it comes first. Turn the controller over and read the label.

ControllerHow to tellWorks?Pairing
PlayStation 2 DualShock 2grey plug on a cable, no USB port, no LEDsNohas no Bluetooth at all
PlayStation 3 DualShock 3 / Sixaxismodel CECHZC2E (EU), CECHZC2U (US), CECHZC2J (JP) on the back label; mini-USB port, four LEDsYes (the one in the photos)the ESP32’s address is written into the pad once, section 4
PlayStation 4 DualShock 4CUH-ZCT1 / ZCT2, light bar, micro-USBYeshold Share + PS until the bar flashes
PlayStation 5 DualSenseCFI-ZCT1, USB-CYeshold Create + PS until the bar flashes
Xbox One S / Series X|Smodel 1708 or 1914, Bluetooth logo near the pair buttonYeshold the pair button on the top edge
Xbox One (2013, model 1537 / 1697)no Bluetooth logoNouses Xbox Wireless, not Bluetooth
Nintendo Switch Pro / Joy-ConPro: model HAC-013, USB-C, sync button on the top edgeYeshold the sync button until the LEDs run
8BitDo and other third partycheck the box for “Switch” or “Xbox” modemostlystart in Switch mode (X + Start) or Xbox mode
Front view of a genuine Sony DualShock 3 controller showing the PS button, both analog sticks and the four face buttons
A real DualShock 3. The model number is on the label on the back; a pad that looks like this but carries no Sony model number is a clone, and clones are the single most common reason pairing fails: most of them skip the part of the Bluetooth handshake the ESP32 needs.

Two more checks on a DualShock 3 before you start. The battery must hold a charge; a pad that only works on the cable has a dead battery, and there is no cable mode here. And it must not be paired to a PS3 that is switched on in the same room, because the console will grab it first.


2. How it works, and why the ESP32 gets its own firmware

A game controller is a Bluetooth HID device, the same class of thing as a wireless keyboard. To talk to one, the ESP32 needs a Bluetooth stack that speaks HID and knows the quirks of each controller family. Writing that yourself is months of work. Bluepad32 is an open-source project that has done it: it supports every controller in the table above and presents all of them to your sketch as one abstract layout: two sticks, two triggers, four face buttons, two shoulder buttons, a D-pad and three system buttons. Your code never sees a PlayStation or an Xbox. It sees “right stick X” and “button A”.

Block diagram of the controller, the ESP32 Bluetooth stack, the 100 Hz control step, the 250 Hz servo task, flash storage and the PCA9685 driver
The whole chain. Bluepad32 receives the pad’s reports, a 100 Hz control step turns them into velocities and enforces the safety rules, and the 250 Hz servo task from Part 1 drives the PCA9685. The position record and the sequence live in flash.

Bluepad32 swaps the ESP32’s Bluetooth stack for its own, which is why it ships as an Arduino board package rather than a library you could add from the Library Manager. Two constraints come with it.

It needs a classic ESP32. PlayStation controllers use Bluetooth Classic, not Bluetooth Low Energy. The original ESP32 (WROOM, WROVER, the DevKit from Part 1) has both. The ESP32-S2 has no Bluetooth; the S3 and C3 have BLE only, enough for an Xbox Series pad but not for any PlayStation pad. If your arm has the Part 1 electronics, you are fine.

It gets its own firmware. The Bluetooth radio and the WiFi radio share one antenna. They can coexist, but coexistence means time-slicing, and a servo task that has to wait for the radio is exactly the jitter Part 1 went to some trouble to remove. So the gamepad firmware has no WiFi and no web server at all. Same wiring, same motion.h, same servo_map.h, same stored position, one radio.

That gives the arm three firmwares, and it is worth knowing which is which:

FirmwareRadioTalks toUse it for
OmArmOne_LittleFS (Part 1)WiFi access pointbrowser on phone or PCsetup, typing poses, declaring the position
OmArmOne_Gamepad (this part)Bluetoothgame controllerdriving by hand, recording and looping a sequence
omarm_one_ros2_serial (Part 2)none, USB cableROS 2 on a PCdigital twin, MoveIt

All three read and write the same position record in flash. Upload the gamepad firmware over the web-app firmware and the arm knows where it is; upload the web-app firmware back and it still knows.


3. Install and flash

1. Add the board package. Arduino IDE → File → Preferences → Additional boards manager URLs. Keep whatever is already in the list (the plain ESP32 URL from Part 1 stays) and add a second line:

https://raw.githubusercontent.com/ricardoquesada/esp32-arduino-lib-builder/master/bluepad32_files/package_esp32_bluepad32_index.json
Arduino IDE preferences dialog showing the Espressif ESP32 and the Bluepad32 additional boards manager URLs on two separate lines
One URL per line. The Espressif one is from Part 1, the Bluepad32 one is new.

Then Tools → Board → Boards Manager, search Bluepad32, install Bluepad32 ESP32 (4.1.0 is the version this post was built with). It takes a few minutes.

2. Board and port. Tools → Board → Bluepad32 ESP32 → ESP32 Dev Module, or use the board selector at the top of the window and pick the entry marked esp32_bluepad32, not the plain esp32 one with the same name. Leave the partition scheme on its default. Guides that tell you to switch to “Huge APP” for Bluepad32 are not wrong, but this sketch does not need it: it compiles to about 758 kB, 57 % of the default 1.2 MB app partition. If you add a lot of your own code later and the upload fails with “sketch too big”, switch to Huge APP (3MB No OTA) then; this firmware uses no file system, so nothing is lost.

Arduino IDE board and port selector with ESP32 Dev Module under esp32_bluepad32 highlighted
Two boards share the name. The one you want says esp32_bluepad32 next to it.

3. Library. Adafruit PWM Servo Driver from the Library Manager. You have it from Part 1. Nothing else; Bluepad32 comes with the board package.

4. Open the sketch. firmware/OmArmOne_Gamepad/OmArmOne_Gamepad.ino from the repository. The IDE treats the folder as one program: open the .ino, the headers appear as tabs, one Upload builds all of it.

Code map of the seven files in the sketch folder showing which one you edit, which are tested on the PC, and what the tools folder contains
You edit one file, gamepad_config.h. The headers that do the thinking are compiled and tested on your PC by tests/run_all.sh before they go anywhere near a servo.

5. Check the channel line. Same rule as Part 1: SERVO_CHANNELS[] = {0, 3, 4, 7, 8, 11} must match your wiring to the PCA9685. If it does in the web-app firmware, it does here.

Wiring schematic showing the ESP32 DevKit connected over I2C to a PCA9685 servo driver with a separate switched servo power supply
The electronics are the Part 1 electronics, unchanged. The Bluetooth radio is already on the ESP32.

6. Switch the servo power off, close any open serial monitor, upload, then open the serial monitor at 115200. (An open serial monitor is the usual reason for “Failed to connect to ESP32: No serial data received”; if it still happens, hold the BOOT button on the board while the IDE says Connecting.) You will see this:

Arduino IDE serial monitor showing the OmArm One gamepad firmware boot log with the ESP32 Bluetooth address line highlighted
The boot log. restored ... nothing moved is the good case. BT address is the number a DualShock 3 needs in the next section, and only a DualShock 3. Serial commands lists what you can type into the monitor.

Three lines deserve a second look. mapping: default ... (v2) tells you which gamepad_config.h is running, so you always know whether an edit made it onto the board. restored live position ... nothing moved means the arm’s position survived the upload, exactly as in Part 1, and the servos were re-commanded to where they already stand. If instead you see The servos lost power, the firmware refuses to move until you confirm the position with the controller (section 6).


4. Pairing, step by step

Flow diagram: read the ESP32 Bluetooth address, plug the pad in over USB, write the address, unplug and press PS; other pads use pairing mode
Five steps for a DualShock 3, one for everything else.

DualShock 3

A DualShock 3 does not search for a host. It has one host address burned into it, the PlayStation it last met, and it connects to that address and nothing else. To make it connect to the arm, you overwrite that address with the ESP32’s, once, over USB.

  1. Read the ESP32’s address from the boot log (BT address: ...), or type BTADDR into the serial monitor and it prints the line again.
  2. Plug the DualShock 3 into your PC with a mini-USB cable. Windows will not show a game controller for it. That is normal: there is no driver for a bare DualShock 3, and the pairing tool does not need one.
  3. Write the address into the pad.
    Windows: SixaxisPairTool. Run it as administrator. It shows the pad’s current master address; type the ESP32 address into the Change Master field and click Update. That is the whole job. The tool’s author no longer distributes it, so you will find it on download mirrors; check the installer with VirusTotal before running it (0.3.1 is the last version). It talks to the pad through raw HID reports, which is also why Windows does not have to recognise the pad at all.
    Linux / macOS: the Bluepad32 repository has tools/sixaxispairer. Build it (make sixaxispairer, needs libhidapi-dev) and run sudo ./sixaxispairer A4:F0:0F:69:A0:06 with your own address.
  4. Unplug the pad. Press the PS button. The four LEDs blink for up to ten seconds, then the serial monitor prints controller connected - identifying... and, a moment later, controller ready: DualShock 3, followed by stick rest: LX 52 LY 8 ..., the firmware’s measurement of where your sticks sit when nobody touches them. LED 1 stays on.
Screenshot of the Sixaxis pairing tool on Windows showing the ESP32 Bluetooth address in the Current Master and Change Master fields
One field, one click. After Update, Current Master shows the ESP32’s address too. The pad remembers it until you write another one.
Screen with the Arduino sketch open, the serial monitor showing the boot log, and the DualShock 3 pairing tool open beside it
The whole pairing desk in one screen: the address in the serial monitor on the left, the tool that writes it on the right.

From then on the pad reconnects on a PS press every time the arm is powered. If you ever want it back on a PlayStation, plug it into the console with the cable once; the console rewrites the address.

Every other controller

Put the pad in pairing mode (table in section 1), and wait. Bluepad32 accepts new controllers whenever none is connected. The LEDs settle, the serial monitor prints the model name, done. The pairing is stored on the ESP32, so next time you only press the PS, Xbox or Home button.

If a pad refuses to pair after it once worked, type FORGET into the serial monitor. That clears the stored keys on the ESP32 side, and you pair again from scratch.


5. The controls

Top view of a game controller with every stick, D-pad direction, trigger and face button labelled with the robot arm joint or function it controls
The default mapping. Right thumb: the big joints, base and shoulder. Left thumb: elbow and forearm roll. Wrist on the D-pad, gripper on R1/R2, speed on L1/L2, poses on the face buttons.

There is no button to hold before the arm moves. Push a stick and the joint turns; let go and it brakes to a stop. What keeps that safe is in section 9; the short version is that the firmware knows where each stick rests, needs a deliberate push before it believes you, refuses to move before the position is known, and freezes on PS or on a lost connection.

Hands holding a game controller with both thumbs on the analog sticks and the index fingers on the triggers, the 3D printed robot arm in the background
The driving grip. Thumbs on the sticks, the left index finger on L2 for a burst of speed, the right on R2 for the gripper. You look at the arm, not at the pad.
InputFunction
Right stick left/rightJ1 base, up to 70 deg/s
Right stick forward/backJ2 shoulder, up to 50 deg/s
Left stick forward/backJ3 elbow, up to 60 deg/s
Left stick left/rightJ4 forearm roll, up to 60 deg/s
D-pad up/downJ5 wrist pitch, 60 deg/s
D-pad left/rightJ6 tool roll, 90 deg/s
R1 / R2gripper open / close (R2 is analog: half pull is half speed)
L1 (click)speed level 1 → 2 → 3 → 4 → 1: 25, 45, 70, 100 % of each joint’s maximum. The player LEDs show the level. Every boot, confirm and resume starts at level 1.
L2 (analog)gas pedal: squeeze it and the speed rises from the current level towards 100 %, release and it drops back
SquareHOME: every joint moves together to the resting pose from Part 1 (J1 0, J2 45, J3 45, J4 0, J5 0, J6 0, gripper 61). Also stops a running sequence first.
Circlesave the current pose as the next step of the sequence (rumble, the LEDs flash the count). Up to 16 poses, kept in flash.
Triangleplay the sequence in a loop: first pose to last, 0.6 s pause between, then again from the first. Triangle again stops it.
Cross, hold 2 sdelete the whole sequence (double rumble). A short tap does nothing, on purpose.
Startstop a running sequence or HOME move where it is
PSEMERGENCY STOP. Freezes every joint where it is and latches. The servos keep holding; cutting their signal would let the arm drop.
Selectresume after a stop. Speed back to level 1, the next four seconds of motion are slow again.
Select + Start, hold 2 sconfirm the position after a power cycle, section 6

The per-joint speeds are not arbitrary. The shoulder carries the whole arm and gets the lowest limit; the tool roll carries only a gripper and gets the highest. Each number is MAP_Jx_VMAX in gamepad_config.h, and section 8 shows how to change them.

Diagram of the four speed levels on L1 shown on the player LEDs, the analog gas pedal formula on L2, and the slow window after every start
Level and pedal multiply all joint speeds together. At level 2 with L2 half pressed, every joint runs at 45 % + 0.5 × 55 % = 72.5 % of its maximum.

The sequence. Part 1’s web app could record poses too, but they lived in the browser and were gone after a reload. This one is written to the ESP32’s flash with a checksum: switch the arm off, switch it on, the sequence is still there (sequence: 3 pose(s) stored in the boot log). Drive to a pose, tap Circle. Drive to the next, tap Circle. Triangle plays them in a loop until you stop it, or until you touch a stick, the D-pad or the gripper: your thumb always wins, and the arm stays exactly where it was interrupted.

Three frames of the 3D printed robot arm working through a recorded pose sequence with the controller lying on the table
Three poses recorded with Circle, played in a loop with Triangle. The pad is on the table; the arm is doing its rounds on its own.
State machine diagram of the sequence player showing Circle to record, Triangle to play and loop, and Cross held to delete, with what interrupts playback
Playback uses the same coordinated move as HOME, so all joints start and arrive together. A pose is only accepted if every joint is a sane number inside the limits, and a store that fails its checksum is discarded rather than trusted.

6. After a power cycle: the confirm gesture

Part 1 explained why an arm without position feedback must not move after its servos lost power: the stored position may be wrong, because gravity or a hand may have moved the arm while the servos were limp. The web app shows a warning and a confirm button. The controller has no screen, so the rule becomes a gesture: look at the arm, check it still stands where it was, then hold Select and Start together for two seconds. The pad rumbles, the servos energise at the stored position, nothing moves.

State diagram showing the restored, lost power and unknown position paths, the ready state with its slow window, moving, latched stop and resume
Every path out of a reset is answered explicitly. The boot log tells you which one you are on.

If the arm was moved while unpowered, do not confirm; flash the web-app firmware and declare the real position with its SETPOS dialog instead. On a brand-new ESP32 with no stored position at all, the same gesture adopts the home pose, so move the arm to that pose by hand before you hold the buttons.


7. Why the motion is smoother than the sliders

Part 1 gave the arm two kinds of motion: a coordinated point-to-point move with a quintic profile (velocity and acceleration zero at both ends, every joint arriving together), and a jog follower for the sliders that chases a moving target with limited acceleration and brakes early enough that it can never overshoot. The controller uses both. HOME and the sequence are coordinated moves. The sticks drive the jog follower, but they drive it differently from a slider, and the difference is the whole point of this section.

Diagram of seven stages: raw stick, rest tracking, dead zone, expo curve, speed scale, velocity filter and jog follower, with joint speeds and braking distances
Seven stages between the thumb and the servo, 100 times a second.

A slider sends a position. Drag it 20 degrees and the follower runs the joint 20 degrees and stops. A stick sends a fraction between −1 and +1, and the firmware turns that fraction into a velocity: full stick means “turn this joint at its maximum speed”, half stick means half of it, centre means stop. The level and the gas pedal from section 5 scale it, and three more things happen to it on the way.

Rest position and dead zone. A stick that is not touched rarely reads exactly zero, and a worn DualShock 3 is worse: mine rests 10 % off centre, and not at the same spot twice. So the firmware measures where each stick rests when the pad connects, and keeps measuring while the stick is idle, slowly enough that a stick you hold at a small deliberate deflection is never mistaken for “rest”. On top of that sits a ±12 % dead zone, rescaled so the first millimetre past it starts at zero speed rather than jumping to 12 %. An untouched pad never makes a joint creep, and a stick that has wandered never blocks a button.

Expo curve. Thumbs are precise near the centre and coarse at the edge. The command is blended between linear and cubic: 0.5·x³ + 0.5·x. At half stick the joint runs at 31 % of its maximum instead of 50 %; at full stick it still reaches 100 %. The first third of travel becomes a fine-positioning zone.

Velocity filter. A thumb can flick a stick from centre to full in a few milliseconds. The commanded velocity passes through an 80 ms low-pass, so a flick becomes a ramp. The follower already limits acceleration; the filter limits how fast the acceleration itself changes, which is the difference between a machine that glides and one that clunks.

Now the part that makes it safe. The follower from Part 1 runs toward its target at the fastest speed from which it can still stop inside the remaining distance: v_stop = √(2·a·d). To make a joint cruise at velocity v, the target simply moves at v, and the follower settles exactly one stopping distance behind it: d = v²/(2a). With the shipped acceleration of 720 deg/s² and the base at 70 deg/s that is 3.4 degrees. Release the stick, and the target is parked at that stopping distance in front of the joint. The follower decelerates at 720 deg/s² and comes to rest right there. It cannot overshoot, because the target never asked it to go further, and it cannot bounce, because the follower never reverses.

Plot of joint velocity, position and jog target over time showing full stick at 0.1 seconds and release at 0.7 seconds with no overshoot
Computed with the exact jog_control.h and motion.h that go on the ESP32, at the firmware’s own 100 Hz control and 250 Hz servo rates. The joint reaches 90 % of its 70 deg/s in 0.28 s, holds it within 1 %, and after release comes to rest in 0.25 s and 5.7 degrees: 3.4 of braking plus what the 80 ms filter lets through. Nothing in the curve goes backwards.

That braking distance is the number to keep in mind when you drive near a wall or a person: at full speed the base needs about six degrees to come to rest, which at 300 mm radius is 30 mm of gripper travel after you let go. At speed level 1 the base runs at 17 deg/s and that shrinks to a few millimetres.

The sticks and the coordinated moves do not fight. While HOME or the sequence is running and no stick is touched, the follower leaves the move alone. Push a stick clearly (past 30 % of travel, which drift never reaches) and the move is cancelled where it is.

All of this is tested on the PC before it goes anywhere near a servo: firmware/tests/run_all.sh compiles the real headers with g++ and runs them against the motion core at the real rates (the five programs and what each proves are in the code map in section 3). The run ends in all tests passed; if you change a header, run it before you flash.


8. Changing the mapping: by hand or with the mapper tool

Everything about the controls lives in gamepad_config.h. It is short, and it is meant to be edited.

Diagram of the config header showing the five lines that define a joint, the single line of a function, the input names available and the feel block
A joint takes an axis, or a button pair, or both; a function takes one input. INVERT flips a direction, VMAX sets the speed at level 4.

Want the elbow on the left stick and the shoulder on the right? Swap two lines. Want a deadman after all? Set MAP_DEADMAN to a button and nothing moves unless it is held. Want the sequence to run once instead of looping? SEQ_LOOP 0. Each change is one upload away, the boot log prints the profile name and version so you know it arrived, and the first four seconds after it are slow on purpose.

The mapper tool does the same thing with a controller in your hand instead of a text editor. It is a small Python program in tools/gamepad-mapper/:

pip install pygame pyserial
python omarm_gamepad_mapper.py
Screenshot of the Python gamepad mapper tool showing joints, functions and feel settings with a Learn button next to every binding
Plug a controller into the PC, click Learn next to a function, press the button. The tool records the abstract name, warns about double bindings and wrong kinds of input, and writes gamepad_config.h straight into the sketch folder. Shown on Linux.

It finds the controller and shows every stick and button live, so you know a pad works before you go near the ESP32. It learns bindings by touch. On its ESP32 tab it reads the BT address line from the arm over USB, with a button that copies it for the pairing tool. And “Reset to defaults” gives you back exactly the mapping this post describes; check_defaults.py in the same folder proves it, define by define.

The tool works on any controller the PC sees as a game controller, because the normalised layout SDL uses on the PC is the layout Bluepad32 uses on the ESP32. Learn a binding on an Xbox pad at the desk, flash, drive with a DualSense. One exception: Windows has no driver for a bare DualShock 3, so the tool cannot see that one pad. Pick its inputs from the lists instead, or skip the tool altogether: the shipped header already is the DualShock 3 layout, and pairing does not need the tool either.


9. The safety layer

Part 1’s rules all still apply: no motion before the position is known, a stored position that survived a reset is trusted and one that did not is confirmed by a human, the first move after a boot is slow, an I2C failure disables the output. The controller adds six more, and each one answers a specific way a wireless link, a worn stick or a screenless device can go wrong.

Diagram pairing six risks with the rule that answers each: stick drift, lost link, undoable stop, backwards joint, arm moved while limp, silent refusal
Six risks, six rules. None of them replaces the switch in the servo power line.

A stick has to mean it. There is no hold-to-move button, so the firmware has to tell a push from a wobble on its own: rest tracking, a 12 % dead zone, and only a deflection past 30 % is deliberate enough to interrupt a running move. If you prefer a deadman, one line in gamepad_config.h gives you one.

Disconnect. Batteries run out and Bluetooth has a range. If the pad drops while a stick is held, the last command would otherwise stand forever. On a disconnect the arm freezes where it is and latches; reconnect the pad, press PS, then Select to continue.

Latched stop. The PS button fires on the press, not on the release, freezes the arm and refuses every command until Select is pressed. A stop that could be undone by the same stick that caused the problem is not a stop.

Slow start. The first four seconds of motion after a boot, a resume or a confirm are capped at 15 % speed, whatever the level or the gas pedal says, and the level itself is reset to 1. That is the window in which you find out that a joint is mapped the wrong way round.

No motion before a human confirms. Section 6. The firmware believes you; look before you hold the buttons.

Nothing fails silently. Every button that does not do what you asked says why on the serial monitor: HOME ignored: let go of the sticks first, sequence is empty - tap Circle to save poses first, save: position unknown - confirm first. And WATCH streams every stick and button five times a second, so when something feels wrong you can see what the pad is actually sending.

And, as in Part 1: none of this replaces a switch in the servo power line. The firmware can stop commanding a servo; it cannot stop one that was told something wrong a millisecond earlier. Keep the switch where your other hand can reach it.


10. First drive: a bench checklist

Servo power off, arm in its resting pose, nothing in the gripper. Then in this order:

  1. Boot and read the log. restored ... nothing moved or a confirm prompt. Nothing else is acceptable; POSITION UNKNOWN on an arm that was set up in Part 1 means the flash was erased, so declare the position with the web-app firmware first.
  2. Pair. controller ready: DualShock 3 in the log, then stick rest: .... LED 1 on: speed level 1.
  3. Confirm if asked. Look at the arm, then Select + Start for two seconds. position adopted.
  4. Servo power on. The arm must not twitch. If it does, the stored position and the real arm disagree: power off, fix it with the web app.
  5. Right stick a little to the right. The base turns slowly to the right, and the log says moving. If it turns left, flip MAP_J1_INVERT. Check each joint the same way. The first four seconds are slow whatever you do; for another slow pass, press PS and then Select.
  6. Let go mid-move. The joint brakes to a stop; a moment later the log says idle.
  7. Squeeze L2 while turning. The base speeds up with the trigger and slows down when you release it. Click L1 twice: three LEDs, and the same stick deflection is now much faster.
  8. PS button mid-move. Everything freezes, the log says STOP (PS button) - latched. Sticks do nothing. Select releases it, and the LEDs are back to one.
  9. Switch the pad off mid-move (hold PS for ten seconds on a DualShock 3). The arm freezes and latches. Switch the pad on, press PS, then Select.
  10. Square. The arm returns to the resting pose, all joints together, in about two seconds.
  11. Drive a pose, tap Circle (rumble, LEDs flash once). Drive somewhere else, tap Circle again (two flashes). Triangle. The arm plays both poses and starts over. Nudge a stick: it stops where it is. Triangle again: it plays again. Hold Cross for two seconds: double rumble, sequence cleared.

If all eleven pass, the arm is yours to drive.


11. Troubleshooting

SymptomCauseFix
Upload fails: “No serial data received”the serial monitor holds the port, or auto-reset did not fireclose the serial monitor; if it persists, hold BOOT on the board while the IDE says Connecting
Upload fails: “sketch too big”you added code and outgrew the 1.2 MB app partitionTools → Partition Scheme → Huge APP (3MB No OTA); this firmware uses no file system, nothing is lost
Bluepad32.h: No such filesketch compiled for the plain ESP32 boardTools → Board → Bluepad32 ESP32 → ESP32 Dev Module (the entry marked esp32_bluepad32)
SixaxisPairTool says “no device found”the pad is not in USB data mode, or the cable is charge-onlyuse a cable with data lines (many cheap mini-USB cables charge only), plug the pad in before starting the tool, run the tool as administrator, and press the PS button once so the pad wakes up
DualShock 3 blinks, never connectswrong or unwritten master address, or a clonere-read BTADDR, write it again; check the back label for a Sony model number
DualShock 3 connects to something elsea PS3 nearby is onswitch the console off
Pad connected but nothing moves, no moving in the logposition not confirmed, or stop latchedtype STATUS: known=0 means confirm (Select + Start 2 s), estop=1 means Select. WATCH shows whether the sticks arrive at all
One joint moves the wrong waydirection of that axisflip MAP_Jx_INVERT
Sticks move the wrong jointsswapped axes, or the pad reports a different layoutrun the mapper tool’s Live inputs tab and rebind
A joint creeps with the stick untouchedrest tracking not settled yet, or a stick worn past 12 %let go of the sticks for three seconds, or type CENTER; if it persists raise STICK_DEADZONE to 0.15
Square or Triangle does nothinga stick is not centredthe log says HOME ignored: let go of the sticks first with the values; WATCH shows them live
Motion feels laggyfilter too longlower VELOCITY_FILTER_S to 0.05; below 0.03 the ramp is gone
Motion feels twitchyacceleration too highlower JOG_ACCEL to 480
late servo ticks in the logBluetooth stack starving the servo taskshould not happen on a classic ESP32; report it with the log
Pad worked, now refuses to pairstale keys on the ESP32type FORGET, pair again

12. FAQ

Can I keep the web app and the controller at the same time? Not in this firmware. The two radios share one antenna and the servo timing is the thing I refuse to compromise. Part 2’s serial firmware will take both a controller (through ROS 2) and the twin.

Does an ESP32-S3 or ESP32-C3 work? No, not with a PlayStation pad. Both have Bluetooth Low Energy only, and DualShock 3, DualShock 4 and DualSense all speak Bluetooth Classic. An Xbox Series pad is BLE and would work on an S3, but the arm’s electronics from Part 1 use a classic ESP32 anyway, so this only matters if you are building the arm on a different board. The ESP32-S2 has no Bluetooth at all.

Can two controllers connect? Bluepad32 allows up to four; the firmware accepts the first and ignores the rest. One arm, one driver.

Does the controller’s battery matter? Only as far as the disconnect rule: a pad that dies mid-move freezes the arm. A DualShock 3 lasts many hours on a charge and flashes its LEDs when it runs low.

Can I use the sticks for the gripper and the triggers for a joint? Yes. Any analog input can drive any joint; set MAP_J7_AXIS to a stick axis and give the joint the trigger pair as POS/NEG.

Why does level 4 with the gas pedal not go any faster? Level 4 already is each joint’s VMAX, and those are set where the servos still track the profile cleanly under load. You can raise them in gamepad_config.h; the DS3240 does about 350 deg/s unloaded, but the arm’s inertia does not.

Can the sequence run once instead of looping? SEQ_LOOP 0 in gamepad_config.h. The pause between poses is SEQ_DWELL_MS.

Will this work on OmArm Zero? The motion core is the same and the wiring is close, but Zero has five servos on different channels. Change NUM_SERVOS, the channel table and the home pose and it should; I have not tested it.


13. What comes next

The arm now drives like a machine is supposed to: velocity under the thumb, a stop under the finger, a memory of where it was, and a sequence it can repeat all afternoon. What it does not have is any idea of where it is in space. Six joint angles are not a gripper position; the maths that turns one into the other lives on the PC, in the digital twin.

Part 2 exports the CAD model from Part 1 into a URDF, brings it up in RViz, and connects it to the real arm over the USB cable with the serial firmware from the table in section 2. The same controller you just paired drives the twin on screen, and the twin drives the arm. From there: MoveIt 2 planning in Part 3, a camera in Part 4, and in Part 5 you tell the arm what to pick up.

If you build the controller setup, I would like to see your mapping. If you find a controller that does not pair, tell me the model number; the table in section 1 grows with every report.

Resources

Leave a Comment