Troubleshooting a phone-eyed RC robot: the walls we hit, and how we got past them
Audience: anyone converting a hobby RC vehicle into an autonomy testbed: ExpressLRS radio, ArduPilot flight controller, Jetson companion computer, phone as a sensor. Roughly our build.
How to read the tags: [measured] we ran it, method stated · [reported] someone identifiable measured it, on their hardware · [claim] a vendor said so and nobody checked · [estimate] reasoned, never measured · [none found] we looked and found nothing, which is itself a result.
Every entry is something that actually cost us time. Nothing here is hypothetical.
1 · ExpressLRS won't bind
Your receiver is powered through an ESC, and power-cycle bind does nothing
Symptom. The receiver blinks steadily, never enters bind mode, and pressing Bind on the transmitter does nothing. Link quality stays at zero.
What worked. Stop cycling the ESC's switch. Unplug the receiver's servo lead and reseat it: that cuts power at the receiver itself. Three cycles, then leave it seated, then press Bind. Bound on the first attempt after doing this. [measured]: RadioMaster ER5C-i on a Traxxas XL-5, 2026-09-05.
Why we think it happens. The ESC's capacitors likely hold the receiver's supply up through the switch, so it never sees clean interruptions to count. [estimate]: we did not put a scope on the rail and did not measure how long the 6 V line stays up. The fix is verified; the explanation is not.
Each power-cycle has to be brief
Roughly one second powered, then off. More than about two seconds and ExpressLRS resets its counter, so a slow, careful sequence fails where a brisk one works. [measured], consistent with documented ELRS behaviour.
You're reading the wrong version number
On an EdgeTX radio, SYS → Version is EdgeTX, the radio's operating system. It has nothing
to do with binding. The number that matters is the ExpressLRS module firmware, at the bottom
of SYS → Tools → ExpressLRS. [measured]

The bottom of the ELRS Lua screen. [Bind] sits directly above the version line 3.3.1 ISM2G4,
which is both the firmware version that governs binding and the regulatory domain. (The
transmitter's binding UID, shown beside it, is redacted here: it is the bind key.)
Checking FCC vs LBT
Read it off the same ELRS screen, not the box. ISM2G4 is the non-LBT domain (full power);
an EU/LBT build reads CE2G4. [measured]
Versions don't have to match exactly
ExpressLRS breaks over-the-air compatibility across a minor version change (3.3 → 3.4), not a patch. Our TX on 3.3.1 bound to a receiver on 3.3.0 with no flashing. [measured]
Still no link — check these three before reflashing anything
| Setting | Value that works |
|---|---|
| Model Match | Off. A mismatch here silently prevents a link with no error anywhere |
| External RF | OFF when using an internal module |
| Internal RF Mode | CRSF |
The 0/250-style figure in the Lua screen header is link quality. Zero means no packets at
all, which points at binding rather than configuration. [measured]

A known-good ELRS configuration, with 0/250 in the header: link quality zero, meaning no
packets at all. Note Model Match: Off; a mismatch there prevents a link with no error anywhere.

Internal RF in CRSF mode, External RF OFF. Both screens are the same Model Setup page, scrolled.
Last resort: talk to the receiver directly
Leave the receiver powered with no bound transmitter for ~60 seconds. It gives up and
broadcasts its own WiFi access point; connect and browse to 10.0.0.1. That page reports the
receiver's real firmware version, whether a binding phrase is set (a phrase makes traditional
binding impossible, which is the other common cause of exactly these symptoms), and it offers a
flash button. If no access point appears after two minutes, the receiver isn't booting. [claim],
vendor-documented behaviour; we did not need to use it.
Don't trust port labels when swapping receivers
Traxxas receivers carry three channel ports plus a battery port, which reads at a glance as a duplicate channel. Map by what the wire physically connects to (steering servo to CH1, ESC to CH2), not by matching printed numbers between two different manufacturers. Both the Traxxas TQ #6519 and the RadioMaster ER5C-i use black for negative, so plugs transfer without adapting. [measured]

The stock Traxxas TQ #6519. The port block carries three channels plus a battery port — at a
glance it reads as a duplicate channel. Its own label states 6V / 15mA, which is the supply the
replacement receiver will see.

The ER5C-i. Five shrouded 3-pin headers labelled CH1–CH5, a BOOT button, and a separate
2-pin EXT-V header — that last one is an external voltage telemetry input, not a channel.
The TQ #6519's own label reads 6 V, comfortably inside the ER5C-i's stated 4.5–8.4 V input range [claim], so no level shifting is needed on this combination.
1b · Getting CRSF out of an ExpressLRS PWM receiver
Your flight controller can't read individual PWM channels: it wants one serial stream. On a PWM receiver you get that by repurposing an output pin. Everything below is [measured] on a RadioMaster ER5C-i, firmware 3.3.1 ISM2G4, 2026-09-05.
First: reaching the receiver's web page at all
The receiver hosts a config page on its own WiFi. Four things cost us most of an hour.
The transmitter must be OFF. If the radio is on, the receiver stays linked and never starts WiFi. Turn it off and leave it off.
It takes minutes, not the 60 seconds the setting implies. The "WiFi auto on interval" reads 60, but the network took several minutes to appear. Don't conclude it failed at ninety seconds.
Better: command it into WiFi mode from the radio. SYS → Tools → ExpressLRS → WiFi Connectivity has an option to enable WiFi on the receiver. That's far more reliable than
waiting for a timeout that fires only when the receiver gives up looking.
Network ExpressLRS RX, password expresslrs.
🔴 Escape iOS's captive-portal browser. Connecting pops a cut-down "Captive Wi-Fi" window.
The page renders, but the tabs don't respond — you can look and not touch. Close it, choose
Use Without Internet when iOS asks, then load 10.0.0.1 in Safari proper.

Choose Use Without Internet. "Dismiss" or "Use Other Network" drops you off the receiver.

The captive window. It looks right and the tabs do nothing. This is the trap.
🔴 And 10.0.0.1 is also a very common home-router address. The moment the phone falls back
to house WiFi, the same URL quietly serves your gateway instead of the receiver — for us, an
Xfinity page. Check the page header says ExpressLRS before you trust anything on it.

Same address, wrong device. The phone had dropped back to the house network.
What the page should look like

Correct page. Four tabs: OPTIONS · WIFI · MODEL · UPDATE. Serial output lives under MODEL.
🔴 Serial TX is only offered on ONE output row
This is the thing that cost the most time. The receiver's own help text says "Serial (for MCU
pins 1 and 3 only)" — that is internal MCU numbering, not the output numbers in the table.
Going by it sends you to the wrong row, where the Mode dropdown simply ends at On/Off and you
conclude the firmware doesn't support serial.

Row 1's Mode dropdown. Ends at On/Off. No serial — and nothing tells you why.
On the ER3Ci / ER4 / ER5 series, it is output 2. Set output 2 → Serial TX and output 3
becomes Serial RX automatically. [reported]: Oscar Liang,
confirmed [measured] on our own ER5C-i.

Row 2's dropdown. Identical list plus Serial TX, and row 3 has already greyed itself out
as the paired Serial RX.
Then
- Serial Protocol: appears below the output table only after a pin is set to Serial TX, which is why you can't find it beforehand. Set it to CRSF.
- SAVE. You want "Configuration updated."
- UART baud on the OPTIONS tab. It already read 420000 for us. Check before changing.

What success looks like, with Serial Protocol: CRSF visible behind the dialog.

OPTIONS tab. UART baud 420000 is the CRSF default — likely nothing to change.
Leave the Binding Phrase field EMPTY. If the page says the UID was set by the traditional method, typing a phrase generates a new UID and breaks the bind you already have.
What you lose, and it is expected
Output 2 stops being a servo output the moment it becomes serial. If your ESC was on it, the throttle goes dead at the vehicle while still showing movement on the transmitter's channel monitor: the radio is transmitting fine; the receiver pin changed job. Throttle comes back through the flight controller. Any PWM outputs you didn't touch keep working.
2 · 🔴 ArduPilot's companion-computer failsafe is off by default, and fails silently
This is the most dangerous thing in this document. If your companion computer is sending guidance commands and it dies, you may have no failsafe at all, and nothing will tell you.
Three defaults combine:
FS_GCS_ENABLEdefaults to 0: the failsafe is disabled- MAVROS's
system_iddefaults to 1, while ArduPilot expects 255 - Rover's
gcs_failsafe_check()returns early if it has never seen a qualifying heartbeat, so a failsafe that was never armed never fires
[measured] against ArduPilot source and MAVROS defaults.
Minimum working configuration:
MAV_GCS_SYSID 255 # named SYSID_MYGCS on Rover <= 4.6
FS_GCS_ENABLE 1
FS_GCS_TIMEOUT 2
FS_TIMEOUT 1
FS_ACTION 2 # Hold
GUID_TIMEOUT 0.5
FS_THR_ENABLE 0 # only if no RC receiver is bound
plus MAVROS system_id: 255.
Verify by breaking it, not by reading parameters. Pull the companion computer's power in the middle of a command and watch the vehicle go to Hold. If you cannot make it fail on demand, you do not have a failsafe — you have a parameter list.
3 · ROS 2 on a Jetson
MAVROS below 2.15.1 is broken on Humble
Latched publishers broke on Humble and were fixed inside the 2.15.0 cycle. Below that, topics
including ~/state never publish, which presents as a bridge that connects and then does nothing.
Require ≥ 2.15.1. 2.15.1 also fixed duplicated launch namespaces, whose symptom is topics
appearing at /mavros/mavros/.... [measured] against the MAVROS changelog.
The MAVROS node exits immediately
GeographicLib datasets are mandatory, not optional: the node shuts itself down without them, which reads like a crash.
sudo ros2 run mavros install_geographiclib_datasets.sh
[measured]
Discovery is flaky on a Jetson specifically
Two things, both cheap:
- Use CycloneDDS:
export RMW_IMPLEMENTATION=rmw_cyclonedds_cpp. This is field consensus rather than official guidance [reported], but MAVROS's own 2.15.0 changelog cites FastDDS shared-memory and teardown hangs [measured]. - Pin
ROS_DOMAIN_ID. A Jetson typically has both WiFi and a USB-gadget interface up, and multicast discovery gets confused across them. [estimate]: reasoned from the interface layout, not isolated experimentally.
4 · JetPack and the Jetson itself
Your working setup breaks after an unattended upgrade
Ubuntu's automatic updates break JetPack compatibility. Disable unattended-upgrades and
apt-mark hold the NVIDIA/L4T packages before it matters, not after. [reported]: widely
reported on the Jetson forums, and hit here too.
nvv4l2decoder fails and /dev/nvhost-nvdec* doesn't exist
The device nodes being absent is not the cause. We measured nvv4l2decoder working on
JetPack 6.2.1 with those nodes absent, on an Orin Nano Super 8 GB. If you're chasing missing
device nodes, you're chasing the wrong thing; NVIDIA's own guidance for the related reports points
at the GStreamer registry cache (rm ~/.cache/gstreamer-1.0/registry.aarch64.bin, install
nvidia-l4t-gstreamer). [measured]: written up in full at
NVIDIA forums topic 382387.
Don't commit TensorRT engines
Engines are keyed to SM architecture and TensorRT/CUDA version, so a checked-in .engine is
portable to exactly one machine. Commit ONNX plus a deterministic builder script plus the
calibration cache instead. [measured]
FP16 gains nothing under TensorRT on an Orin ViT — but 3.4× in plain PyTorch
On Vision Transformers specifically, we found FP16 gives roughly 0% speedup under TensorRT on Orin: the ONNX exporter never emits a multi-head-attention pattern for TensorRT to fuse. In eager PyTorch the same precision change is a different story: 3.4× faster (18.9 ms vs 63.7 ms per batch) at no measurable accuracy cost for us. Measure your own path before assuming either way. [measured] for both numbers; [estimate] for the generalisation beyond the models we tested.
5 · Streaming an iPhone into a Jetson over USB
The phone doesn't enumerate
Run usbmuxd with --no-preflight. The default preflight step races the iOS trust pairing and
produces the "0 devices found" reports that dominate search results for this problem. [measured]
on iOS 26.6 → JetPack 6.2.1.
Don't do this on Ubuntu 20.04
Focal ships libimobiledevice linked against GnuTLS, which breaks trust pairing. Use 22.04.
[measured]
What we found when we looked for prior art
[none found] — and that is the finding, not a gap. The NVIDIA developer forums return five
usbmuxd topics forum-wide, none about iOS video, and zero for libimobiledevice. The
iPhone-on-Jetson space is empty. If you're doing this, you are close to the frontier and should
expect to write the documentation rather than read it.
Reported failure modes worth knowing about
Neither reproduced for us, but both are real reports from people doing very similar things:
- record3d #72: "variable lag in the video stream (1-3 seconds)", filed by a Dobb·E author streaming from an iPhone on a robot over USB, open since 2023. [reported]
- usbmuxd #215: "works fine for a random amount of minutes, until it dies." [reported]
We ran 27,000 frames over 30 minutes with zero stalls and zero reconnects on our own path (different app, different client), which does not refute either report — it establishes that our path does not have it. [measured], indoors on a bench; outdoor thermal behaviour is untested.
6 · Capturing data from a phone
Recordings come out corrupt
Video finalization races sharing. We lost video on 6 of 9 recordings in one session and 3 of 8 attempts in another. A fixed post-stop wait is not sufficient: we specified five seconds and still lost one in six. The recording appearing in the app's own list is the actual finalize signal; wait for that event, not a timer. [measured]
A truncated MP4 is partially recoverable by rebuilding annex-B HEVC from mdat using headers from
a known-good clip from the same app — but we could not prove frame alignment to single-frame
precision, and shipped nothing that depended on it. [measured] mechanically; alignment
[none found].
ARKit pose dies within a minute on a moving vehicle
Visual-inertial odometry on a phone is not rated for vehicles. Every session on one device died in 19–47 seconds. What fixed it was speed discipline, not weather: a newer phone, 5 fps capture, and keeping p95 speed at or below ~5 m/s gave clean full-session pose across six consecutive outdoor runs, validated by loop-closure error of 1.6–6.0 m over 465–636 m of driving. [measured]
Check what your "frame rate" actually is
Ours was decimation, not capture rate: ARKit ran at 59.976 Hz and the app kept every twelfth frame. That matters because every thermal event we had recorded happened with the sensor at full rate, so conclusions about "5 fps is sustainable" were about a much heavier load than they appeared to be. Verify against your own timestamps. [measured]
7 · Self-supervised labelling pipelines
Don't use the centre pixel's depth to locate an event
If the object isn't centred, the centre pixel reads the floor beside it and mislocates the label by roughly a metre. Use the nearest above-floor cluster instead. [measured]
A suspiciously good score is a bug report
We hit AUC 0.998 (obstacle model, Log #1) on geometry that was broken, twice. The number was the warning; what caught it both times was looking at the overlays. If you have a self-supervised label chain, your metric measures agreement with your labeller, not with the world — so build the visual check first and trust it over the score. [measured]
Contributing
If you hit something on this list and our fix didn't work, or you hit something that isn't here, send it with exact versions, exact commands and exact output: reply on the post, or email [email protected] until the code release opens a public tracker. That is the same standard we hold ourselves to when reporting upstream, and it is the difference between a report someone can act on and one they can only sympathise with.