
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 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.
| Controller | How to tell | Works? | Pairing |
|---|---|---|---|
| PlayStation 2 DualShock 2 | grey plug on a cable, no USB port, no LEDs | No | has no Bluetooth at all |
| PlayStation 3 DualShock 3 / Sixaxis | model CECHZC2E (EU), CECHZC2U (US), CECHZC2J (JP) on the back label; mini-USB port, four LEDs | Yes (the one in the photos) | the ESP32’s address is written into the pad once, section 4 |
| PlayStation 4 DualShock 4 | CUH-ZCT1 / ZCT2, light bar, micro-USB | Yes | hold Share + PS until the bar flashes |
| PlayStation 5 DualSense | CFI-ZCT1, USB-C | Yes | hold Create + PS until the bar flashes |
| Xbox One S / Series X|S | model 1708 or 1914, Bluetooth logo near the pair button | Yes | hold the pair button on the top edge |
| Xbox One (2013, model 1537 / 1697) | no Bluetooth logo | No | uses Xbox Wireless, not Bluetooth |
| Nintendo Switch Pro / Joy-Con | Pro: model HAC-013, USB-C, sync button on the top edge | Yes | hold the sync button until the LEDs run |
| 8BitDo and other third party | check the box for “Switch” or “Xbox” mode | mostly | start in Switch mode (X + Start) or Xbox mode |

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”.

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:
| Firmware | Radio | Talks to | Use it for |
|---|---|---|---|
OmArmOne_LittleFS (Part 1) | WiFi access point | browser on phone or PC | setup, typing poses, declaring the position |
OmArmOne_Gamepad (this part) | Bluetooth | game controller | driving by hand, recording and looping a sequence |
omarm_one_ros2_serial (Part 2) | none, USB cable | ROS 2 on a PC | digital 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

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.

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.

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.

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:

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

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.
- Read the ESP32’s address from the boot log (
BT address: ...), or typeBTADDRinto the serial monitor and it prints the line again. - 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.
- 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 hastools/sixaxispairer. Build it (make sixaxispairer, needslibhidapi-dev) and runsudo ./sixaxispairer A4:F0:0F:69:A0:06with your own address. - 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 bystick rest: LX 52 LY 8 ..., the firmware’s measurement of where your sticks sit when nobody touches them. LED 1 stays on.


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

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.

| Input | Function |
|---|---|
| Right stick left/right | J1 base, up to 70 deg/s |
| Right stick forward/back | J2 shoulder, up to 50 deg/s |
| Left stick forward/back | J3 elbow, up to 60 deg/s |
| Left stick left/right | J4 forearm roll, up to 60 deg/s |
| D-pad up/down | J5 wrist pitch, 60 deg/s |
| D-pad left/right | J6 tool roll, 90 deg/s |
| R1 / R2 | gripper 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 |
| Square | HOME: 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. |
| Circle | save the current pose as the next step of the sequence (rumble, the LEDs flash the count). Up to 16 poses, kept in flash. |
| Triangle | play 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 s | delete the whole sequence (double rumble). A short tap does nothing, on purpose. |
| Start | stop a running sequence or HOME move where it is |
| PS | EMERGENCY STOP. Freezes every joint where it is and latches. The servos keep holding; cutting their signal would let the arm drop. |
| Select | resume after a stop. Speed back to level 1, the next four seconds of motion are slow again. |
| Select + Start, hold 2 s | confirm 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.

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.


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.

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.

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.

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.

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

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.

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:
- Boot and read the log.
restored ... nothing movedor a confirm prompt. Nothing else is acceptable;POSITION UNKNOWNon an arm that was set up in Part 1 means the flash was erased, so declare the position with the web-app firmware first. - Pair.
controller ready: DualShock 3in the log, thenstick rest: .... LED 1 on: speed level 1. - Confirm if asked. Look at the arm, then Select + Start for two seconds.
position adopted. - 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.
- Right stick a little to the right. The base turns slowly to the right, and the log says
moving. If it turns left, flipMAP_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. - Let go mid-move. The joint brakes to a stop; a moment later the log says
idle. - 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.
- 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. - 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.
- Square. The arm returns to the resting pose, all joints together, in about two seconds.
- 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
| Symptom | Cause | Fix |
|---|---|---|
| Upload fails: “No serial data received” | the serial monitor holds the port, or auto-reset did not fire | close 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 partition | Tools → Partition Scheme → Huge APP (3MB No OTA); this firmware uses no file system, nothing is lost |
Bluepad32.h: No such file | sketch compiled for the plain ESP32 board | Tools → 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-only | use 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 connects | wrong or unwritten master address, or a clone | re-read BTADDR, write it again; check the back label for a Sony model number |
| DualShock 3 connects to something else | a PS3 nearby is on | switch the console off |
Pad connected but nothing moves, no moving in the log | position not confirmed, or stop latched | type 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 way | direction of that axis | flip MAP_Jx_INVERT |
| Sticks move the wrong joints | swapped axes, or the pad reports a different layout | run the mapper tool’s Live inputs tab and rebind |
| A joint creeps with the stick untouched | rest 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 nothing | a stick is not centred | the log says HOME ignored: let go of the sticks first with the values; WATCH shows them live |
| Motion feels laggy | filter too long | lower VELOCITY_FILTER_S to 0.05; below 0.03 the ramp is gone |
| Motion feels twitchy | acceleration too high | lower JOG_ACCEL to 480 |
late servo ticks in the log | Bluetooth stack starving the servo task | should not happen on a classic ESP32; report it with the log |
| Pad worked, now refuses to pair | stale keys on the ESP32 | type 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
- Gamepad firmware, mapper tool and tests (MIT): github.com/OmArTronics/OmArm-One →
firmware/OmArmOne_Gamepad/,tools/gamepad-mapper/ - Bluepad32, the Bluetooth controller stack: bluepad32.readthedocs.io
- The arm itself, STLs and build guide: OmArm One complete package in the shop · Part 1 build guide
