Vague, but exciting…

Troubleshooting a phone-eyed RC robot: the walls we hit, and how we got past them

· Colin Rooney

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]

MT12 ExpressLRS Lua screen, bottom: the Bind entry above a version line reading 3.3.1 ISM2G4

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]

MT12 ExpressLRS Lua config screen showing packet rate 250Hz, telemetry ratio, switch mode, Model Match off, TX power, and a 0/250 link-quality reading in the header

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.

MT12 Model Setup page showing Internal RF mode CRSF 1.87M at 250Hz over channels 1-16 MT12 Model Setup page showing External RF mode set to OFF

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]

Traxxas TQ 6519 receiver mounted in the truck, showing its row of ports and a label reading 6V 15mA

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.

RadioMaster ER5C-i receiver, showing channel labels CH1 through CH5, a BOOT button, an EXT-V two-pin header, and five shrouded three-pin headers

The ER5C-i. Five shrouded 3-pin headers labelled CH1CH5, 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.

iOS prompt: the Wi-Fi network ExpressLRS RX is not connected to the Internet, with options Use Without Internet, Use Other Network, Dismiss

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

The iOS Captive Wi-Fi mini browser showing the ExpressLRS page with OPTIONS WIFI MODEL UPDATE tabs

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.

A page at 10.0.0.1 showing Xfinity home-network content instead of the ExpressLRS interface

Same address, wrong device. The phone had dropped back to the house network.

What the page should look like

ExpressLRS web UI in Safari: blue header reading RadioMaster ER5C-i 2.4GHz PWM RX, Firmware Rev 3.3.1, and four tabs OPTIONS WIFI MODEL UPDATE

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.

Mode dropdown on output row 1 listing 50Hz through 400Hz, 10KHzDuty and On/Off, with no Serial option

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.

Mode dropdown on output row 2 showing the same list plus Serial TX at the bottom, checked

Row 2's dropdown. Identical list plus Serial TX, and row 3 has already greyed itself out as the paired Serial RX.

Then

  1. 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.
  2. SAVE. You want "Configuration updated."
  3. UART baud on the OPTIONS tab. It already read 420000 for us. Check before changing.

ExpressLRS Set Configuration dialog reading Configuration updated, with Serial Protocol CRSF visible behind it

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

ExpressLRS Runtime Options showing the binding UID field (redacted), WiFi auto-on interval 60, and UART baud 420000

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:

[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:


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:

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.