The AY-Robots SO-100 hub page showing the arm and its specifications, the reference arm for the USB and serial troubleshooting steps in this guide
TroubleshootingSO-100USB SerialLeRobotLinux

USB and Serial Troubleshooting for Robot Arms

AY-Robots ResearchAugust 23, 202617 min read

Serial bridge chips, udev rules, unbind and rebind, and the triage order that separates a driver problem from a dead servo on an SO-100 class arm in five minutes.

An arm that has stopped answering looks identical in every log: a timeout. The process cannot reach motor 1, the teleoperation loop stalls, and the arm freezes or sags. Nothing in that message says whether the USB bridge chip fell off the bus, whether a background daemon opened the port first, or whether one servo stopped talking. Three problems, three fixes, and exactly one of them involves a screwdriver.

This is the triage order for SO-100 class arms, written for Linux because that is where the arms end up. Where macOS differs it is called out. If you are still assembling, start at the SO-100 getting started guide and come back here the first time it goes quiet.

What you need to know

  • The node name is half a diagnosis: /dev/ttyUSB* means a driver such as ch341 claimed the chip, /dev/ttyACM* means the CDC-ACM path. LeRobot documents SO-101 boards as /dev/ttyACM0.
  • A port that opens but never answers is not a driver problem. open() succeeding proves the kernel and the bridge are fine.
  • On a stock Ubuntu desktop, brltty and ModemManager are the two daemons that quietly take the port from you.
  • tty numbers are assigned in bind order, not by socket. A udev rule keyed on the port path or the chip serial number is the fix.
  • Writing a device id to /sys/bus/usb/drivers/usb/unbind and then bind re-enumerates a wedged bridge without touching the cable.
  • Latency is a separate failure: the FTDI latency timer defaults to 16 ms and camera streams outrank your bulk serial traffic.

The chain between your Python process and a servo horn

Seven layers sit between a call to bus.read('Present_Position', 'shoulder_pan') and a horn moving, and each fails with its own symptom. Most wasted debugging time comes from applying a fix from one layer to a fault in another: reinstalling drivers when the jumper is wrong, or resoldering a cable when a screen reader has the port.

LayerWhat it isHow it failsWhat you actually see
USB host controller and hubThe port and any hub in betweenPort power, bad cable, a hub browning out under loadAbsent from lsusb entirely
USB-serial bridge chipCH340, CH343, CP2102 or FT232Wedges, stops answering control transfersListed by lsusb, no node, or writes stop landing
Kernel driverch341, cdc_acm, cp210x, ftdi_sioAnother driver binds it firstAn attach, then a disconnect a second later
tty device node/dev/ttyACM0 or /dev/ttyUSB0Number moved after replug, wrong groupNo such file or directory, or Permission denied
Userspace claimpyserial opening the nodeAnother process holds itDevice or resource busy on open
Half-duplex servo busOne TTL line shared by every servo, 1 MbpsWrong baud, wrong jumper channel, servo power offPort opens cleanly, no id answers
The servoFeetech STS3215 on SO-100 and SO-101Duplicate id, dead 3-pin cable, overload cutoutFive ids answer, one does not

Work down that table in order. Do not skip a row because you are sure about it. Arm not detected and servo not responding are separate pages here for exactly that reason: the boundary is whether a device node exists.

Read the node name first: ttyACM or ttyUSB

Linux exposes USB serial adapters through two unrelated paths. The usb-serial framework covers chips needing a vendor-specific protocol, uses major number 188 and names them /dev/ttyUSB0 upward. The CDC-ACM class driver covers chips that describe themselves as standard USB communications devices and names them /dev/ttyACM0 upward. Which one you get is a property of the chip, not of your distribution.

Bridge chipLinux driverNodeWorth knowing
CH340 / CH341in-kernel ch341/dev/ttyUSB*id_table: 1a86:5523, 1a86:7522, 1a86:7523, 2184:0057, 4348:5523, 9986:7523. Source comment: supported range is 46 to 3000000 bps.
CH343 / CH9102cdc_acm (enumerates as CDC)/dev/ttyACM*Enumerates as a standard CDC device, so no driver install on Linux or a Raspberry Pi.
CP2102 / CP2104cp210x/dev/ttyUSB*Silicon Labs part, common on nearby ESP32 boards.
FT232R / FT232Hftdi_sio/dev/ttyUSB*The only family with a tunable latency timer in sysfs.
PL2303pl2303/dev/ttyUSB*In-kernel driver, no install needed. Clone chips are common at this price point.
bash
# 1. is the bridge on the bus at all?
lsusb
lsusb -t                 # tree view: shows which driver claimed which interface

# 2. what did the kernel decide to do with it?
sudo dmesg -w            # leave running, then unplug and replug the arm

# 3. which node appeared, and what are the stable aliases?
ls -l /dev/ttyACM* /dev/ttyUSB* 2>/dev/null
ls -l /dev/serial/by-id/ /dev/serial/by-path/
The three commands to run before touching anything else.

On a healthy CH340 you want a dmesg pair like ch341-uart converter detected followed by ch341-uart converter now attached to ttyUSB0. A CDC device gives a line naming ttyACM0. An attach followed by a disconnect a second later means the chip is fine and userspace is taking it: skip to the traps.

On macOS, use the cu node

The same board appears twice: /dev/tty.usbmodem* and /dev/cu.usbmodem*. The tty variant waits for carrier detect before open() returns, which for a servo adapter can mean never. The cu variant opens immediately. lerobot-find-port globs /dev/tty*, so it always hands you the tty name. If a connect hangs with no error, swap the prefix to cu.

Five minute triage: driver problem or servo problem

The useful boundary: can a process open the port. If it can, the kernel, the host cable and the bridge chip all work, and everything after that is the servo bus.

  1. 1
    Confirm the bridge enumerates

    If the chip is not in lsusb, no driver work helps. Try a different cable first: charge-only USB cables look identical to good ones and are the most common cause of a device that never appears.

    bash
    lsusb | grep -iE '1a86|0403|10c4|067b'
    lsusb -t     # look for the Driver= field on the interface line
  2. 2
    Confirm a node exists and nobody else holds it

    Wrong group is a permissions problem. Busy means another process got there first.

    bash
    ls -l /dev/ttyACM0
    stat -c '%G' /dev/ttyACM0        # which group owns it
    fuser -v /dev/ttyACM0            # who has it open (or: lsof /dev/ttyACM0)
  3. 3
    Open the port with nothing but pyserial

    Take LeRobot out of the picture. If this succeeds, every remaining failure is on the servo side.

    python
    import serial
    s = serial.Serial("/dev/ttyACM0", 1_000_000, timeout=1)
    print("opened:", s.is_open, s.name)
    s.close()
  4. 4
    Ask the bus which motors exist, at every baud rate

    LeRobot ships a scanner that broadcast-pings at each supported baud and reports every id that answered. The list runs 4800 through 1000000, and 1000000 is the Feetech default.

    python
    from lerobot.motors.feetech import FeetechMotorsBus
    
    # returns {baudrate: [motor ids that answered]}
    print(FeetechMotorsBus.scan_port("/dev/ttyACM0"))
  5. 5
    Read the error text literally

    LeRobot and the Feetech SDK use distinct strings for distinct faults. Worth memorising.

    text
    Could not connect on port '/dev/ttyACM0'.   -> kernel refused: node, permission, or busy
    [TxRxResult] There is no status packet!     -> nobody answered: baud, power, wiring, id
    [TxRxResult] Incorrect status packet!       -> something answered but the framing was wrong
    [TxRxResult] Port is in use!                -> the SDK already has the port open elsewhere
The AY-Robots fix index listing failure modes such as arm not detected, servo not responding and joint stops early, each linking to its own page
The failure-mode index on /fix. The split between arm not detected and servo not responding is the same boundary as step 3: whether a process can open the port.

The traps that eat a day

brltty takes your CH340

A braille display uses the same CH340 chip as half the hobby robotics world, so brltty ships a udev rule matching USB id 1a86:7523 and claims the device on sight. Ubuntu bug 1990357 records exactly that against brltty 6.4-4ubuntu3 on Ubuntu 22.04.1 and Linux Mint 21: lsusb lists the chip as 1a86:7523, nothing appears in /dev, and the device comes back as /dev/ttyUSB0 once brltty is removed. The report is still Confirmed.

bash
# is brltty even here?
dpkg -l | grep -i brltty
systemctl status brltty-udev.service

# surgical: find the rule matching your chip and comment that one line out.
# on Ubuntu the generated file is 85-brltty.rules; the ids are lowercase hex
grep -n "1a86" /usr/lib/udev/rules.d/85-brltty.rules

# blunt: stop udev from starting brltty at all
sudo systemctl mask brltty-udev.service
sudo systemctl stop brltty-udev.service
sudo udevadm control --reload
Prefer the surgical fix if anyone on the machine actually uses a braille display.
The tell is the timing, not the message

This failure produces no error. The chip attaches, goes away, and every later command reports the less helpful No such file or directory. If ls /dev/ttyUSB* shows the node for a second after replug and then it vanishes, stop reading driver documentation and look at brltty.

ModemManager probes anything that looks like a port

ModemManager opens new serial ports to check for a modem behind them. The probe holds the port for a few seconds after every plug event, so the first lerobot-calibrate after connecting fails and the same command twenty seconds later works. Two details decide whether the fix takes: the property goes on the usb subsystem, not tty, because a device-level tag belongs on the device, and the ModemManager documentation asks for a 78-mm- or 79-mm- filename, because its own candidate rules are the 80-mm- file.

bash
# /etc/udev/rules.d/79-mm-lerobot-ignore.rules
# the 79- prefix is not cosmetic: rules files from every directory are sorted
# together by filename, and ModemManager's candidate rules are the 80-mm- file
ACTION!="add|change|move|bind", GOTO="mm_lerobot_end"
# substitute the ids you actually saw in lsusb
SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", ENV{ID_MM_DEVICE_IGNORE}="1"
LABEL="mm_lerobot_end"

# then
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=usb

# confirm the property landed where ModemManager will read it
udevadm info -p /sys/class/tty/ttyACM0 | grep ID_MM
ID_MM_DEVICE_IGNORE ignores the whole device; ID_MM_PORT_IGNORE ignores a single port.

Permissions, and why chmod 666 keeps coming back

Serial nodes are group-owned: dialout on Debian and Ubuntu, uucp on Arch. The LeRobot SO-101 guide suggests sudo chmod 666 /dev/ttyACM0, which works until the next replug, because udev recreates the node with the default mode. Group membership is durable and needs a fresh login.

bash
ls -l /dev/ttyACM0
# crw-rw---- 1 root dialout 166, 0 Aug 24 09:14 /dev/ttyACM0

sudo usermod -aG "$(stat -c '%G' /dev/ttyACM0)" "$USER"
# log out and back in, or start a subshell with the new group:
newgrp dialout

# verify without replugging anything
id -nG | tr ' ' '\n' | grep -E 'dialout|uucp'

The jumper that is not a software problem at all

Jumpers on A means the port opens and nothing answers

The LeRobot SO-101 guide states it plainly: on a Waveshare controller board, set the two jumpers on the B channel (USB). Waveshare's Bus Servo Adapter (A) wiki explains the other position: A routes the bus to the UART header pins for a host like a Pi Zero or an ESP32, B routes it to the USB connector. On A the bridge still enumerates, you still get /dev/ttyACM0, the port still opens, and not one servo answers.

The second trap is the power rail. The SO-100 and SO-101 run Feetech STS3215 bus servos at 7.4 V. Koch v1.1 uses Dynamixel XL330 and XL430 parts on 5 V and 12 V rails, and LeKiwi is a 7.4 V arm on a 12 V base, the layout that invites the wrong barrel jack. Feeding 12 V into STS3215 servos destroys them, and the same Waveshare adapter wiki says the input voltage must match the servo voltage.

udev rules: stop chasing /dev/ttyACM0

tty numbers are handed out in bind order, which follows probe timing. With a leader-follower pair the leader is ttyACM0 today and ttyACM1 tomorrow, and hardcoded numbers mean driving the wrong arm at least once. A rule in the udev rule language fixes the name.

KeyWhat the man page saysExample
SUBSYSTEMMatch the subsystem of the event deviceSUBSYSTEM=="tty"
KERNELMatch the name of the event deviceKERNEL=="ttyACM[0-9]*"
ATTRS{...}Search the devpath upwards for a device with matching sysfs attribute valuesATTRS{idVendor}=="1a86"
KERNELSSearch the devpath upwards for a matching device nameKERNELS=="1-1.2" (on USB that name is the physical port path)
SYMLINKThe name of a symlink targeting the nodeSYMLINK+="robot_follower"
MODE, GROUPThe permissions for the device nodeMODE="0660", GROUP="dialout"
ENV{key}Match against a device property valueENV{ID_MM_DEVICE_IGNORE}="1"
== vs = vs +=Compare for equality; assign; add to a key holding a listSYMLINK+= appends, SYMLINK= replaces
  1. 1
    Walk the device tree and pick a discriminator

    The walk prints one block per ancestor device. You want a value unique to this arm and stable across reboots: ATTRS{serial} if the chip reports one, otherwise the parent kernel name, which encodes the socket.

    bash
    udevadm info --name=/dev/ttyACM0 --attribute-walk | head -60
    
    # the short version of the same question
    udevadm info --name=/dev/ttyACM0 --query=property | grep -E 'ID_VENDOR_ID|ID_MODEL_ID|ID_SERIAL|ID_PATH'
  2. 2
    Write the rule

    Put it in /etc/udev/rules.d/, but know what that buys you. Rules files from every directory are sorted and processed together in lexicographic order by filename, so a 99- file runs last wherever it lives. The /etc/ precedence applies only to a file with the same name as one under /usr/lib/: that replaces a packaged rule, it does not outrank one.

    bash
    # /etc/udev/rules.d/99-lerobot-arms.rules
    
    # by serial number: survives moving the cable to any socket,
    # but only works if the bridge chip actually reports one
    SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{serial}=="58FD017123", \
      SYMLINK+="robot_follower", MODE="0660", GROUP="dialout"
    
    # by physical port: works with chips that report no serial at all,
    # but breaks the moment you move the plug
    SUBSYSTEM=="tty", KERNELS=="1-1.2", \
      SYMLINK+="robot_leader", MODE="0660", GROUP="dialout"
  3. 3
    Reload, trigger, verify

    Reloading alone changes nothing for devices already plugged in. The trigger replays the events, so the rule applies without a replug.

    bash
    sudo udevadm control --reload
    sudo udevadm trigger --subsystem-match=tty
    ls -l /dev/robot_*
    
    # if the symlink does not appear, watch the events live
    udevadm monitor --udev --property --subsystem-match=tty
  4. 4
    Use the stable name everywhere

    Once the symlink exists, no LeRobot command needs a number again.

    bash
    lerobot-calibrate \
        --robot.type=so101_follower \
        --robot.port=/dev/robot_follower \
        --robot.id=my_follower_arm
systemd already built you two of these

The stock 60-serial.rules creates /dev/serial/by-id/ and /dev/serial/by-path/, but only for nodes matching ttyUSB[0-9]*|ttyACM[0-9]*, and it skips the by-id link when ID_SERIAL is empty. That is why two identical adapters can produce one by-id entry: the chips report no serial, so udev cannot tell them apart. by-path comes from topology and always exists.

Unbind and rebind: resetting a wedged bridge without touching the cable

Sometimes the chip is present, the node is present, and writes go nowhere: the bridge has wedged. Manual driver binding through sysfs has existed since kernel 2.6.13-rc3 and gives two levels of reset short of pulling the cable.

bash
PORT=/dev/ttyACM0
TTY=$(basename "$PORT")

# derive the ids instead of guessing them
IFACE=$(basename "$(readlink -f /sys/class/tty/$TTY/device)")
DRV=$(basename "$(readlink -f /sys/class/tty/$TTY/device/driver)")
DEV=${IFACE%%:*}
echo "interface=$IFACE driver=$DRV device=$DEV"
# e.g. interface=1-1.4:1.0 driver=cdc_acm device=1-1.4

# level 1: drop only the interface the tty hangs off, leave the device enumerated
echo -n "$IFACE" | sudo tee /sys/bus/usb/drivers/$DRV/unbind
echo -n "$IFACE" | sudo tee /sys/bus/usb/drivers/$DRV/bind

# level 2: re-enumerate the whole device, equivalent to a replug
echo -n "$DEV" | sudo tee /sys/bus/usb/drivers/usb/unbind
sleep 2
echo -n "$DEV" | sudo tee /sys/bus/usb/drivers/usb/bind
Reading the interface and the driver out of sysfs beats guessing them: cdc_acm on a CDC board, ch341 on a CH340 one.

Level 2 re-runs udev, so symlinks and permissions come back as configured. That is why the udev rule comes before the rebind script. Level 3 cuts port power with uhubctl, with one caveat: on Raspberry Pi the ports are ganged. On the B+, 2B and 3B, ports 2 to 5 are controlled by port 2, and on the Pi 5 all four form one group. Cutting power to the arm cuts power to the cameras too.

Automating a rebind on timeout
Advantages
  • Recovers a wedged bridge in about two seconds with no physical access.
  • Re-runs udev on level 2, so symlinks, group ownership and ModemManager exclusions come back.
  • Scriptable: N consecutive receive timeouts triggers one rebind, then a reconnect.
  • Costs nothing to try and cannot damage the servos.
Trade-offs
  • Needs root. On a locked-down machine you are back to pulling the cable.
  • Does not power-cycle the chip. A genuinely stuck bridge needs uhubctl or a real replug.
  • The path 1-1.4 identifies a socket, not a device. Move the cable and the script rebinds the wrong thing.
  • It hides the cause. A rebind every twenty minutes means a cable, hub or power problem.

When it really is the servo

Everything past a successful open is the half-duplex TTL bus, where the two Feetech SDK strings from step 5 earn their keep. There is no status packet means nothing replied within the timeout: power, baud or wiring. Incorrect status packet means something replied with wrong framing, which points at a baud mismatch or electrical noise, not at absence. Opposite fixes.

What you seeMost likely layerNext thing to run
lsusb does not list the bridgeCable, hub or port powerSwap the cable first, then lsusb -t
lsusb lists it, no node appearsAnother driver claimed itdmesg | tail -30, then check brltty
Permission denied on the nodeGroup membershipls -l on the node, then usermod -aG
Port opens, no id answers at any baudJumper channel, servo power, wiringFeetechMotorsBus.scan_port()
Ids answer at 115200 but not 1000000Motors set up for another projectRe-run lerobot-setup-motors
Five ids answer, one is silentThat servo, its 3-pin cable, or a duplicate idMove the known-good cable to the silent joint
Ids answer, positions are nonsenseCalibration, not connectivitylerobot-calibrate for that arm id
Works, then dies under loadRail sag or the servo overload cutoutMeasure the rail while the arm lifts

The last row is worth doing by hand: move a known-good 3-pin cable to the silent joint. If the joint answers, the cable was the fault; if not, the servo is. Both outcomes have a page, joint stops early and arm twitches then sags. A missing camera is a different subsystem again, at camera not detected.

The AY-Robots SO-100 hub page showing the arm, its specifications and links to setup, data collection and imitation learning guides
The SO-100 hub page. The arm runs Feetech STS3215 bus servos at 7.4 V, the number that decides whether a wrong power supply is an inconvenience or a replacement order.

The latency layer nobody measures

A link can be connected and still be too slow for a control loop. Three places add delay before a line of your code runs, and none report an error.

SourceDefaultHow to checkWhen it matters
FTDI latency timer16 ms (ftdi_sio sets priv->latency = 16 when it cannot read the device value)cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timerFTDI only. A status packet is a few bytes, so 16 ms of buffering dwarfs the transaction
USB autosuspendpower/control is 'on' for non-hubs, autosuspend_delay_ms is 2000cat /sys/bus/usb/devices/1-1.4/power/controlPower tools and some distro profiles flip it to 'auto', adding a resume cost after an idle gap
Camera bandwidthOn a high speed bus, 80 percent of the frame is reserved for periodic transfers (kernel USB docs)Move a camera to a different root hub and re-testBridges use bulk transfers and get the leftovers. Two cameras plus an arm on one controller is where jitter starts
Where this platform stops helping, plainly

Inference has to sit next to the servos for anything fast. The control loop here is 20 ms per action step for ACT, 152 ms for GR00T N1.7, 165 ms for GR00T N1.5, 245 ms for SmolVLA and 485 ms for Pi0.5. Public-internet round trips on top turn a working policy into a hesitant one: remote inference is viable for slow pick-and-place, not for fast reactive motion, and no serial tuning changes that. See inference latency.

On an FTDI adapter one write removes the buffering. It does not survive a replug, so it belongs in your udev rule file or your startup script. CH340, CH343 and CDC-ACM devices have no equivalent knob.

bash
# FTDI only: drop the receive buffering from 16 ms to 1 ms
cat /sys/bus/usb-serial/devices/ttyUSB0/latency_timer
echo 1 | sudo tee /sys/bus/usb-serial/devices/ttyUSB0/latency_timer

# keep the device awake regardless of power policy
echo on | sudo tee /sys/bus/usb/devices/1-1.4/power/control

# if you need to see the actual traffic, usbmon is the tcpdump of USB
sudo modprobe usbmon
ls /sys/kernel/debug/usb/usbmon
sudo cat /sys/kernel/debug/usb/usbmon/1u > /tmp/bus1.mon

Two ways to get an arm answering again

Everything above, on every machine that touches an arm. The commands are short; the ownership is the cost. You maintain the udev rules, daemon exclusions and rebind scripts, and re-derive them on each new Pi, laptop or container image.

bash
# lerobot 0.6.1, released 2026-08-03
pip install 'lerobot[core_scripts,feetech]'

lerobot-find-port                     # unplug when asked, it prints the node
sudo usermod -aG dialout "$USER"      # then log out and back in

# after writing /etc/udev/rules.d/99-lerobot-arms.rules
sudo udevadm control --reload
sudo udevadm trigger --subsystem-match=tty

lerobot-calibrate \
    --robot.type=so101_follower \
    --robot.port=/dev/robot_follower \
    --robot.id=my_follower_arm
  • Full control, no accounts, works offline.
  • You own the per-distro differences: dialout against uucp, brltty on Ubuntu, whatever the next image ships.
  • Deciding whether the arm itself is broken means finding a second arm.
  • Documented at the LeRobot SO-101 guide; nothing here is hidden.

A checklist you can run in ninety seconds

Save this next to your robot scripts. It answers, in order, the six questions that separate the layers.

bash
#!/usr/bin/env bash
# triage.sh - ninety second check for an SO-100 class arm on Linux
PORT="${1:-/dev/ttyACM0}"

echo "== bridge on the bus?"
lsusb | grep -iE '1a86|0403|10c4|067b' || echo "  no known USB-serial bridge in lsusb"

echo "== which driver claimed which interface?"
lsusb -t

echo "== node and permissions"
ls -l "$PORT" 2>&1

echo "== stable aliases"
ls -l /dev/serial/by-id/ /dev/serial/by-path/ 2>&1

echo "== who is holding the port?"
fuser -v "$PORT" 2>&1

echo "== known port thieves"
systemctl is-active brltty-udev.service ModemManager.service 2>&1

echo "== last twenty kernel lines"
dmesg | tail -20
Run it once when things work and keep the output. A diff against a healthy run beats any amount of guessing.

For the setup around this, the complete SO-100 setup guide covers assembly through calibration and first policy run, and the data collection guide picks up once the link stops dropping. SO-100 against SO-101 compares the two arms, and the SO-100 LeRobot page covers the software side.

The AY-Robots teleoperator page, headlined Become a Robot Operator from anywhere in the world, with a photo of the SO-100 arm operators drive
Remote operators drive real SO-100 arms over the internet. Everything in this article stands between an operator in one country and a servo in another.

One page per failure mode

Arm not detected, servo not responding, joint stops early, policy freezes mid-motion. One page each: the symptom, the cause, and the command that settles it.

Open the fix index
The port opens but no motor answers. Is the driver broken?

Almost certainly not. A successful open() means the kernel, the host cable and the bridge chip all work. Check three things in order: the controller board jumpers (the LeRobot guide requires the B channel on Waveshare boards), the servo power supply, and the baud rate. FeetechMotorsBus.scan_port() broadcast-pings from 4800 up to 1000000 and reports which ids answered where.

Do I need the WCH vendor driver on Linux?

Usually not. CH340 and CH341 have an in-kernel driver, and CH343 class parts are CDC compliant, so cdc-acm handles them with no install. The vendor driver ch343ser_linux exists for what the class driver cannot reach, and its README names those: hardware flow control and GPIO. That same README tells you to rmmod cdc-acm first, and the driver then creates nodes named /dev/ttyCH343USBx, which renames your port and breaks any config hardcoding ttyACM0.

Why does the port number change between reboots?

tty numbers are assigned in bind order, which depends on probe timing rather than on the socket. The fix is a udev rule keyed on something stable: ATTRS{serial} if your chip reports one, KERNELS on the parent USB path if it does not. systemd's stock 60-serial.rules already gives you /dev/serial/by-path/ for free, and that link exists even for chips with no serial.

Is it safe to unbind and rebind while a policy is running?

No. Every open file descriptor on that port dies, so stop the control loop first, rebind, then reconnect. The rebind is host-side and touches nothing on the servo bus, so the arm keeps whatever torque state it was last commanded into: an arm mid-motion stops where it is rather than going limp. Clear the workspace first.

Can AY-Robots fix a USB problem on my machine?

No. udev rules, driver binding and daemon conflicts are local to your operating system and you fix them there. The platform only narrows the search: /fix documents each failure mode on its own page, and /live is a known-good physical SO-100 in the browser with no signup, so you can separate a broken arm from a broken setup without owning two.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started