Wired and Wireless SO-ARM101 Teleoperation with XIAO ESP32-C3 and micro-ROS
This tutorial is a community contribution by @linao681. Thanks for sharing this project with the Seeed Studio community!
Introduction
This tutorial documents two supported ways to control an SO-ARM101 follower from an SO-ARM101 leader:
- Wired leader + wireless follower: the leader uses a standard USB bus-servo driver board.
- Wireless leader + wireless follower: each arm uses a Seeed Studio XIAO ESP32-C3 Bus Servo Adapter.
In both modes, the follower communicates with ROS 2 through micro-ROS over Wi-Fi UDP. The wireless leader XIAO reads the six leader actuator positions and publishes read-only state; it never writes position commands to the leader arm.
The implementation provides:
- feedback from all six follower joints on
/joint_states; - commands for all six follower joints on
/joint_command; - LeRobot leader-to-follower joint mapping;
- a startup handshake that prevents an unexpected position jump;
- calibration, joint-limit, command-step, and bus-health checks;
- sequence checks, feedback watchdogs, safe hold-last-command behavior, and session recovery;
- runtime Agent discovery from the computer's current Wi-Fi address, so a hotspot address change does not require reflashing;
- a wired leader fallback for diagnosis and operation when the wireless leader is unavailable;
- automatic preflight checks and one-command teleoperation startup.
The complete source code is available in the soarm101-drone-teleop repository.
This project was developed as a ground-tested prototype for a future drone-mounted SO-ARM101 demonstration. This guide covers only the robotic arm communication and teleoperation link. It does not provide a flight-certified control or safety system.
System Architecture
SO-ARM101 leader
├─ wireless: leader XIAO ── Wi-Fi / micro-ROS ──┐
└─ wired: USB bus-servo driver ────────────────┤
▼
Ubuntu 22.04 PC
├─ LeRobot reads the leader
├─ ROS 2 Humble bridge publishes /joint_command
└─ micro-ROS Agent, UDP port 8888
│
│ 2.4 GHz Wi-Fi LAN
▼
follower XIAO ESP32-C3 Bus Servo Adapter
├─ micro-ROS publishes /joint_states
└─ 1 Mbps UART Sync Read/Write
│
▼
SO-ARM101 follower, 6 × STS3215
The PC and XIAO must be connected to the same local network. A phone hotspot or a dedicated 2.4 GHz access point can be used for a demonstration.
Hardware
- 1 × SO-ARM101 leader
- 1 × SO-ARM101 follower
- 1 × standard USB bus-servo driver board for leader calibration and wired fallback
- 1 × XIAO ESP32-C3 Bus Servo Adapter for the follower
- 1 × additional XIAO ESP32-C3 Bus Servo Adapter for wireless leader mode
- 2 × correctly rated arm power supplies
- 1 × Ubuntu 22.04 computer
- 1 × 2.4 GHz Wi-Fi network
- USB cables for calibration and firmware flashing
This reference firmware was tested with the standard 5 V SO-ARM101 follower using six STS3215 servos with model number 777.
- Disconnect servo power before changing any servo cable.
- Use the voltage specified for your exact SO-ARM101 version. Do not connect a 12 V supply to a 5 V arm.
- USB does not supply enough power for the servos.
- Perform the first test on a stable workbench with a clear emergency power disconnect.
- If testing near a drone, remove the propellers.
Software Requirements
The tested host configuration is:
- Ubuntu 22.04
- ROS 2 Humble
- LeRobot with Feetech support
- micro-ROS Agent
- Python 3.12
- PlatformIO
Install the micro-ROS Agent and PlatformIO if they are not already available:
sudo snap install micro-ros-agent
python3 -m pip install --user platformio
Follow the SO-ARM100/101 LeRobot guide to install LeRobot and configure the servo IDs before continuing.
Step 1: Clone the Project
git clone https://github.com/linao681/soarm101-drone-teleop.git
cd soarm101-drone-teleop
The important project paths are:
firmware/xiao_soarm/ PlatformIO firmware for the wireless follower
firmware/xiao_soarm_leader/ PlatformIO firmware for the wireless leader
tools/wireless_teleoperate.py ROS 2 and LeRobot teleoperation bridge
tools/soarm_agent_discovery.py Agent discovery service for both XIAOs
start_soarm_demo.sh network, Agent, arm, and topic preflight checks
cali/ leader and follower calibration files
The repository includes separate PlatformIO firmware projects for the wireless follower and wireless leader. The projects include the required ESP32-C3 micro-ROS libraries, so a normal user does not need to cross-compile micro-ROS.
Step 2: Calibrate Both Arms
Calibrate the follower first with a standard USB bus-servo driver. Replace /dev/ttyACM0 with the correct port:
python -m lerobot.scripts.lerobot_calibrate \
--robot.type=so101_follower \
--robot.port=/dev/ttyACM0 \
--robot.id=follower_recal \
--robot.calibration_dir="$PWD/cali"
Then connect and calibrate the leader:
python -m lerobot.scripts.lerobot_calibrate \
--teleop.type=so101_leader \
--teleop.port=/dev/ttyACM0 \
--teleop.id=leader_recal \
--teleop.calibration_dir="$PWD/cali"
This creates:
cali/follower_recal.json
cali/leader_recal.json
Calibration values are specific to one physical arm. Do not control another follower with the calibration values included as an example in the repository.
Copy the follower calibration into the firmware
The follower XIAO validates the servo EEPROM before enabling torque. Open:
firmware/xiao_soarm/src/servo_bus.cpp
Replace these three arrays with the values from your cali/follower_recal.json:
constexpr int16_t kHomingOffsets[kJointCount] = {
/* homing_offset for joints 1 to 6 */
};
constexpr int16_t kRangeMin[kJointCount] = {
/* range_min for joints 1 to 6 */
};
constexpr int16_t kRangeMax[kJointCount] = {
/* range_max for joints 1 to 6 */
};
The expected order is:
shoulder_pan, shoulder_lift, elbow_flex,
wrist_flex, wrist_roll, gripper
The following command prints the three arrays in the correct order:
python3 - <<'PY'
import json
joints = [
"shoulder_pan", "shoulder_lift", "elbow_flex",
"wrist_flex", "wrist_roll", "gripper",
]
with open("cali/follower_recal.json", encoding="utf-8") as calibration_file:
calibration = json.load(calibration_file)
for key in ("homing_offset", "range_min", "range_max"):
print(key, [calibration[joint][key] for joint in joints])
PY
Step 3: Configure Wi-Fi and Agent discovery
Connect the Ubuntu computer and both XIAOs to the same 2.4 GHz Wi-Fi network. The XIAO firmware stores only the SSID and password. The computer's current Agent address is announced at runtime by the launcher and discovery service; do not hard-code the computer IP into a public firmware file.
ip -4 address
Enter the follower firmware directory and create the private configuration file:
cd firmware/xiao_soarm
cp src/wifi_config.example.h src/wifi_config.h
For wireless leader mode, repeat the same step under firmware/xiao_soarm_leader.
Edit src/wifi_config.h:
#pragma once
const char* WIFI_SSID = "YOUR_2G4_WIFI_SSID";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
For the leader firmware, use the corresponding #define WIFI_SSID and #define WIFI_PASS placeholders in firmware/xiao_soarm_leader/src/wifi_config.example.h.
wifi_config.h is ignored by Git and must never be committed to a public repository. The current launcher uses Agent discovery to announce the computer's current Wi-Fi address, so a hotspot IP change normally does not require reflashing either XIAO.
The ESP32-C3 uses 2.4 GHz Wi-Fi. If a phone hotspot supports both bands, select the compatibility or 2.4 GHz mode.
Step 4: Build and Flash the XIAOs
Connect one XIAO to the computer through USB at a time. Run the native tests and the ESP32-C3 build before uploading:
(cd firmware/xiao_soarm && \
pio test -e native && \
pio run -e seeed_xiao_esp32c3)
(cd firmware/xiao_soarm_leader && \
pio test -e native && \
pio run -e seeed_xiao_esp32c3)
After the tests and builds pass, upload the selected firmware:
cd firmware/xiao_soarm
pio run -e seeed_xiao_esp32c3 --target upload
pio device monitor -b 115200
For wireless leader mode, run the upload and monitor commands from firmware/xiao_soarm_leader instead.
Power the follower arm with its external supply. A successful startup contains messages similar to:
Servo Ping mask: 0x3f (expected 0x3f)
Servo calibration match: YES
IP: 192.168.x.x RSSI: -xx
Waiting for micro-ROS Agent...
0x3f means all six servo IDs responded. If the calibration does not match, the firmware still reports state but rejects motion commands.
After flashing, the USB cable is required only for serial monitoring. Keep the corresponding arm's external servo power connected. The leader firmware disables torque and publishes position/state only; the follower firmware is the component that executes position commands.
Step 5: Start the micro-ROS Agent and preflight
From the project root, the launcher can start the Agent and discovery service:
source /opt/ros/humble/setup.bash
./start_soarm_demo.sh --leader wireless --check
The launcher starts the Agent and discovery service, then checks the required ROS 2 topics. When the XIAOs discover the Agent, their serial monitors should report:
micro-ROS ready
The ROS 2 interfaces are:
| Topic | Message type | Direction | Nominal rate |
|---|---|---|---|
/joint_states | sensor_msgs/msg/JointState | follower to PC | 20 Hz |
/joint_command | sensor_msgs/msg/JointState | PC to follower | up to 30 Hz |
Verify the feedback:
source /opt/ros/humble/setup.bash
ros2 topic echo /joint_states --once
ros2 topic hz /joint_states
Do not send arbitrary joint values before completing the current-pose startup handshake.
Step 6: Run Wired or Dual-Wireless Teleoperation
Choose one of the two leader inputs. For wired mode, connect the leader to the computer through its normal USB bus-servo driver. For wireless mode, power the leader with its external supply and leave its XIAO connected to the configured Wi-Fi network.
Find its stable serial path:
ls -l /dev/serial/by-id/
From the project root, export the local configuration:
export SOARM_WIFI_SSID="YOUR_2G4_WIFI_SSID"
export SOARM_PYTHON="$(command -v python)"
First run the non-moving preflight check:
./start_soarm_demo.sh --leader wireless --check
It verifies:
- the Wi-Fi SSID and Agent IP;
- the selected leader input (
wirelesschecks/leader/raw_stateand/leader/status;wiredchecks the USB adapter when starting); - the leader and follower calibration files;
- the micro-ROS Agent;
- live follower feedback on
/joint_states; - that another teleoperation process is not using the same leader bus.
If all checks pass, start teleoperation:
./start_soarm_demo.sh --leader wireless
# Wired leader fallback:
./start_soarm_demo.sh --leader wired
The bridge reads the initial follower pose and repeatedly publishes the same pose before enabling torque. It then uses relative mapping, so the follower starts where it is and follows changes made to the selected leader. If the wireless session recovers, the bridge blends toward the leader's current pose before resuming normal commands. Press Ctrl+C to stop; the follower holds its last commanded position.
Stopping the bridge or losing commands does not release torque. The follower holds its last commanded position. Disconnect servo power for an emergency stop.
Safety Mechanisms
The reference implementation includes several checks intended to make a demonstration more predictable:
- Servo identity check: all six IDs and model numbers must match.
- EEPROM calibration check: homing offsets and limits must match the follower calibration compiled into the firmware.
- Current-pose handshake: the first command must be within
0.05 radof the measured pose. - Joint soft limits: every command must stay inside the calibrated range.
- Per-command step limit: after arming, a target cannot change by more than
0.25 radin one command. - Feedback watchdog: the PC bridge stops publishing if follower feedback is older than
0.5 s. - Wi-Fi recovery: the XIAO restarts cleanly if Wi-Fi cannot recover within 10 seconds.
These software checks supplement but do not replace a physical emergency stop.
Troubleshooting
The XIAO stays at Waiting for micro-ROS Agent
- Confirm that the computer and XIAO are on the same LAN.
- Confirm that the launcher reports the current Wi-Fi IPv4 address and starts Agent discovery.
- Confirm that the Agent is using UDP port
8888. - Check whether the hotspot enables client isolation.
- If a firewall is active, allow UDP port
8888.
servo_mask is not 0x3f
One or more servos did not answer:
- disconnect power and inspect the three-wire bus cables;
- verify that every servo has a unique ID from 1 to 6;
- verify the power supply voltage and current rating;
- keep the servo bus at the configured 1 Mbps baud rate.
The firmware reports calib:0
The servo EEPROM does not match the values compiled into servo_bus.cpp. Reconnect the follower through the USB driver board, recalibrate it, update the three firmware arrays, and flash the XIAO again.
Wi-Fi disconnects during motion
- move the access point closer;
- place the external antenna away from the servo power wires and metal parts;
- use a dedicated 2.4 GHz network for the demonstration;
- watch the RSSI value in the serial diagnostic output;
- compare the result with the servos powered off to identify possible power or electromagnetic interference.
The wireless leader is not ready
- confirm that the leader XIAO publishes both
/leader/raw_stateand/leader/status; - confirm that all six leader servos respond and torque is disabled;
- confirm that
cali/leader_recal.jsonmatches the physical leader; - use
./start_soarm_demo.sh --leader wiredas a temporary fallback.
A joint direction or range is incorrect
Recalibrate both arms and confirm the joint order in both JSON files. Also verify that the follower arrays in servo_bus.cpp came from the same physical follower currently connected to the XIAO.
Validation scope
The project has been exercised in the reference setup with:
- all six follower servos were detected (
servo_mask=0x3f); /joint_stateswas published at approximately 20 Hz;- the leader bridge published commands at 30 Hz;
- all six joints followed together over a phone hotspot;
- the XIAO continued operating without its USB data cable after flashing and external arm power was connected.
The dual-wireless path and the wired fallback are both supported by the current launcher. A reference wireless endurance run was completed on 2026-09-20 and lasted 783 seconds (about 13 minutes). The run recorded zero command timeouts, zero command rejections, and zero leader recoveries; feedback averaged 19.87 Hz, command-acknowledgement latency was 66.0 ms at P50, and the minimum observed RSSI was -65 dBm. These figures describe one reference run, not a guarantee for every Wi-Fi environment.
References
- Project source code
- Getting Started with SO-ARM100 and SO-ARM101 in LeRobot
- Getting Started with the XIAO Bus Servo Adapter
- micro-ROS
- ROS 2 Humble
- LeRobot
This contribution documents an independently developed integration. LeRobot, ROS 2, micro-ROS, PlatformIO, and the servo library remain subject to their respective licenses.