<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>Jeroen Van Goey</title>
<link>https://jeroen.vangoey.be/blog.html</link>
<atom:link href="https://jeroen.vangoey.be/blog.xml" rel="self" type="application/rss+xml"/>
<description>Notes on building ML systems for biology — and the occasional side quest.</description>
<image>
<url>https://jeroen.vangoey.be/images/og-image.jpg</url>
<title>Jeroen Van Goey</title>
<link>https://jeroen.vangoey.be/blog.html</link>
</image>
<generator>quarto-1.9.38</generator>
<lastBuildDate>Sun, 28 Jun 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Teaching an RC car to drive itself</title>
  <dc:creator>Jeroen Van Goey</dc:creator>
  <link>https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/</link>
  <description><![CDATA[ 





<p>This is a warts-and-all build log of getting a <a href="https://docs.donkeycar.com/">DonkeyCar</a> running on a Jetson Nano in an afternoon. Almost nothing went smoothly, and that’s exactly why it’s worth writing down — every roadblock below cost real time, and the fix is the kind of thing you only find by reading kernel logs.</p>
<p>The full parts list — and where this whole project is going — lives on the <a href="../../projects/self-driving-toy-car.html#the-hardware">project overview</a>. In short: a Jetson Nano 4 GB drives an RC chassis through a PCA9685 PWM board, with an IMX219 CSI camera as the only sensor and a training PC doing the heavy lifting off-board.</p>
<section id="why-the-software-stack-is-frozen-in-2021" class="level2">
<h2 class="anchored" data-anchor-id="why-the-software-stack-is-frozen-in-2021">Why the software stack is frozen in 2021</h2>
<p>The original Jetson Nano is <strong>end-of-life</strong>. NVIDIA’s last image for it is <strong><span class="term-tip" tabindex="0" aria-label="NVIDIA's bundled OS image for Jetson boards (Linux plus GPU drivers and CUDA), shipped as one package." data-tip="NVIDIA's bundled OS image for Jetson boards (Linux plus GPU drivers and CUDA), shipped as one package.">JetPack</span> 4.5.1 / 4.6.x</strong> (Ubuntu 18.04, <span class="term-tip" tabindex="0" aria-label="NVIDIA's platform for running general-purpose computation on the GPU." data-tip="NVIDIA's platform for running general-purpose computation on the GPU.">CUDA</span> 10.2, <strong>Python 3.6</strong>). JetPack 5/6 dropped Nano support entirely. The whole stack is vertically welded together — the GPU driver, CUDA, cuDNN and the OS ship as one <span class="term-tip" tabindex="0" aria-label="The vendor bundle of kernel, drivers and firmware that makes a specific board boot." data-tip="The vendor bundle of kernel, drivers and firmware that makes a specific board boot.">Board Support Package</span> — so you can’t just <code>apt upgrade</code> your way to a modern Python or TensorFlow. That forces:</p>
<ul>
<li><strong>JetPack 4.5.1</strong>, Python <strong>3.6</strong>, TensorFlow <strong>2.3.x</strong> (NVIDIA’s special Jetson wheels)</li>
<li><strong>DonkeyCar 4.5.x</strong> (the last line that runs on Python 3.6)</li>
</ul>
<p>So the first lesson: on this board you don’t <em>choose</em> old versions, the chip’s ceiling chooses them for you.</p>
</section>
<section id="roadblock-1-ssh-too-many-authentication-failures" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-1-ssh-too-many-authentication-failures">Roadblock 1 — SSH: “Too many authentication failures”</h2>
<p>First contact over SSH died before a password prompt even appeared:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ssh jeroen@192.168.0.5</span>
<span id="cb1-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Received</span> disconnect from 192.168.0.5 port 22:2: Too many authentication failures</span>
<span id="cb1-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Disconnected</span> from 192.168.0.5 port 22</span></code></pre></div></div>
<p>This is <strong>not</strong> a wrong password. The client offers every key in your agent, and each rejected key counts against the server’s <code>MaxAuthTries</code> (default 6) — so it hits the limit before reaching password auth. The fix is to stop offering keys:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> IdentitiesOnly=yes <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> PubkeyAuthentication=no <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb2-2">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> PreferredAuthentications=password jeroen@192.168.0.5</span></code></pre></div></div>
<p>The durable fix is a dedicated key plus an SSH config entry. Generate one key just for the car:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">ssh-keygen</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> ed25519 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> ~/.ssh/id_ed25519_donkeycar <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-N</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-C</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jeroen@donkeycar-nano"</span></span></code></pre></div></div>
<p><code>ssh-copy-id</code> then hit the <em>same</em> “too many failures” wall (it offers all 6 keys before installing the new one), so it needs the same flags:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">ssh-copy-id</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> IdentitiesOnly=yes <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> PubkeyAuthentication=no <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb4-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-o</span> PreferredAuthentications=password <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-i</span> ~/.ssh/id_ed25519_donkeycar.pub jeroen@192.168.0.5</span></code></pre></div></div>
</section>
<section id="roadblock-2-the-terminal-type-the-nano-had-never-heard-of" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-2-the-terminal-type-the-nano-had-never-heard-of">Roadblock 2 — the terminal type the Nano had never heard of</h2>
<p><code>top</code>, <code>nano</code>, anything full-screen, failed instantly:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> top</span>
<span id="cb5-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'xterm-ghostty'</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">:</span> unknown terminal type.</span></code></pre></div></div>
<p>Ubuntu 18.04’s <span class="term-tip" tabindex="0" aria-label="The database that tells programs how to draw on a given terminal type." data-tip="The database that tells programs how to draw on a given terminal type.">terminfo</span> database predates the <a href="https://ghostty.org/">Ghostty</a> terminal. Tell the Nano to use a <code>TERM</code> it knows:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'export TERM=xterm-256color'</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&gt;</span> ~/.bashrc</span></code></pre></div></div>
</section>
<section id="roadblock-3-apt-lock-on-first-boot" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-3-apt-lock-on-first-boot">Roadblock 3 — apt lock on first boot</h2>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> sudo apt-get upgrade <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span>
<span id="cb7-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">E:</span> Could not get lock /var/lib/dpkg/lock-frontend <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-</span> open <span class="er" style="color: #AD0000;
background-color: null;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">11:</span> Resource temporarily unavailable<span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span></span></code></pre></div></div>
<p><code>unattended-upgrades</code> grabs the dpkg lock at boot. Don’t force it — wait it out:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">while</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> fuser /var/lib/dpkg/lock-frontend <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span>/dev/null <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;&amp;</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">do</span></span>
<span id="cb8-2">  <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">echo</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"apt is busy, waiting..."</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sleep</span> 5</span>
<span id="cb8-3"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">done</span></span>
<span id="cb8-4"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt-get update <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> apt-get upgrade <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span></span></code></pre></div></div>
<p>The DonkeyCar dependencies install fine as one command (<code>apt-get</code> happily takes a long list), and <code>grpcio</code> then <strong>compiles from source</strong> because there’s no <span class="term-tip" tabindex="0" aria-label="The 64-bit ARM CPU architecture the Jetson uses, so x86 PC binaries do not run on it." data-tip="The 64-bit ARM CPU architecture the Jetson uses, so x86 PC binaries do not run on it.">aarch64</span> / Python 3.6 wheel — expect 10–25 minutes of a spinner that looks frozen but isn’t. That’s <code>gcc</code> working, not a hang.</p>
</section>
<section id="calibration-finding-the-pca9685-then-its-pwm-limits" class="level2">
<h2 class="anchored" data-anchor-id="calibration-finding-the-pca9685-then-its-pwm-limits">Calibration: finding the PCA9685, then its PWM limits</h2>
<p>The steering servo and ESC are driven by a PCA9685 over I²C. First confirm the kernel sees it:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> sudo i2cdetect <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-y</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-r</span> 1</span>
<span id="cb9-2">     <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">0</span>  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f</span>
<span id="cb9-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">40:</span> 40 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span></span>
<span id="cb9-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">70:</span> 70 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--</span></span></code></pre></div></div>
<p><code>40</code> is the PCA9685’s control address (and <code>70</code> its all-call). The first scan came up <strong>empty</strong> — a wiring fault; reseating the I²C leads brought it back. With the board visible, the DonkeyCar default pins already matched the hardware:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1">DRIVE_TRAIN_TYPE <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PWM_STEERING_THROTTLE"</span></span>
<span id="cb10-2">PWM_STEERING_THROTTLE <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb10-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PWM_STEERING_PIN"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PCA9685.1:40.1"</span>,   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># steering servo → channel 1</span></span>
<span id="cb10-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PWM_THROTTLE_PIN"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PCA9685.1:40.0"</span>,   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ESC → channel 0</span></span>
<span id="cb10-5">}</span></code></pre></div></div>
<p>To calibrate interactively over SSH I wrote a tiny helper that sets a single PWM pulse using DonkeyCar’s <em>own</em> PCA9685 driver (so it behaves exactly like the real car), and exploits the fact that the chip <strong>holds</strong> the last value after the script exits:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># cal_set.py</span></span>
<span id="cb11-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> sys</span>
<span id="cb11-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> donkeycar.parts.actuator <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> PCA9685</span>
<span id="cb11-4">ch, pulse <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(sys.argv[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]), <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>(sys.argv[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>])</span>
<span id="cb11-5">PCA9685(ch, address<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bn" style="color: #AD0000;
background-color: null;
font-style: inherit;">0x40</span>, busnum<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).set_pulse(pulse)</span>
<span id="cb11-6"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"channel </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%d</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> -&gt; pulse </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%d</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%</span> (ch, pulse))</span></code></pre></div></div>
<section id="the-its-not-moving-detour" class="level3">
<h3 class="anchored" data-anchor-id="the-its-not-moving-detour">The “it’s not moving” detour</h3>
<p>The servo wouldn’t budge — but the I²C writes <em>succeeded</em>. That combination (commands accepted, nothing moves) is the textbook sign that the <strong>V+ servo-power rail is dead</strong>. On a standard build that rail is fed by the <strong>ESC’s <span class="term-tip" tabindex="0" aria-label="Battery Eliminator Circuit — a 5–6 V regulator inside the ESC that powers the servo rail." data-tip="Battery Eliminator Circuit — a 5–6 V regulator inside the ESC that powers the servo rail.">BEC</span></strong>, which needs the <strong>drive battery connected and the ESC switched on</strong>. With no battery, the chip happily sends PWM into an unpowered servo. Battery in, ESC on, and the sweep worked.</p>
<p>Then it’s just binary-searching the limits, with a human watching the wheels:</p>
<table class="caption-top table">
<colgroup>
<col style="width: 33%">
<col style="width: 33%">
<col style="width: 33%">
</colgroup>
<thead>
<tr class="header">
<th>Setting</th>
<th>Value</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>STEERING_LEFT_PWM</code></td>
<td>460</td>
<td>full left (no servo strain)</td>
</tr>
<tr class="even">
<td><code>STEERING_RIGHT_PWM</code></td>
<td>290</td>
<td>full right</td>
</tr>
<tr class="odd">
<td><code>THROTTLE_FORWARD_PWM</code></td>
<td>430</td>
<td>gentle max forward (capped from 500)</td>
</tr>
<tr class="even">
<td><code>THROTTLE_STOPPED_PWM</code></td>
<td>370</td>
<td>ESC armed, motor stopped</td>
</tr>
<tr class="odd">
<td><code>THROTTLE_REVERSE_PWM</code></td>
<td>320</td>
<td>gentle reverse (needs a brake→neutral→reverse “double-tap”)</td>
</tr>
</tbody>
</table>
<p>Validated by loading it back through DonkeyCar’s own config loader:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> python <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-c</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'import donkeycar as dk; d=dk.load_config("myconfig.py").PWM_STEERING_THROTTLE; \</span></span>
<span id="cb12-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">             print(d["STEERING_LEFT_PWM"], d["THROTTLE_FORWARD_PWM"])'</span></span>
<span id="cb12-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">460</span> 430</span></code></pre></div></div>
</section>
</section>
<section id="roadblock-4-the-controller-the-kernel-refuses" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-4-the-controller-the-kernel-refuses">Roadblock 4 — the controller the kernel refuses</h2>
<p>I’d bought an “off-brand Xbox controller.” Plugged in, <code>dmesg</code> told a different story:</p>
<pre class="text"><code>usb 1-2.3: New USB device found, idVendor=054c, idProduct=05c4
usb 1-2.3: Product: Wireless controller, Manufacturer: Sony Computer Entertainment
sony 0003:054C:05C4.0001: failed to retrieve feature report 0x81 with the DualShock 4 MAC address
sony: probe of 0003:054C:05C4.0001 failed with error -110</code></pre>
<p>It’s not an Xbox pad at all — it’s a <strong>counterfeit DualShock 4</strong>. It <em>spoofs</em> a genuine DS4’s USB ID (<code>054c:05c4</code>) but doesn’t implement <strong><span class="term-tip" tabindex="0" aria-label="A specific USB-HID query the kernel sends a DualShock 4 to read its Bluetooth MAC address; counterfeit pads do not implement it." data-tip="A specific USB-HID query the kernel sends a DualShock 4 to read its Bluetooth MAC address; counterfeit pads do not implement it.">feature report 0x81</span></strong> (the MAC address) that the kernel’s <code>hid-sony</code> driver demands. The probe fails with <code>-110</code> (timeout), so no <code>/dev/input/js0</code> is ever created — <code>jstest</code> just gets:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> jstest /dev/input/js0</span>
<span id="cb14-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jstest:</span> No such file or directory</span></code></pre></div></div>
<p>The obvious workaround — force it onto the generic HID driver — is <strong>blocked on this kernel</strong>. <code>hid-sony</code> is <em>built in</em> (<code>CONFIG_HID_SONY=y</code>), so it can’t be unloaded or blacklisted, and a manual bind to <code>hid-generic</code> is refused:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb15" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb15-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> echo <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-n</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"0003:054C:05C4.0003"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> tee /sys/bus/hid/drivers/hid-generic/bind</span>
<span id="cb15-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">bind:</span> FAILED   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the HID core won't let hid-generic claim a device a special driver owns</span></span></code></pre></div></div>
<p><strong>Verdict:</strong> a counterfeit DS4 cannot work on this kernel without rebuilding it without <code>CONFIG_HID_SONY</code>. The lesson — buy a <em>genuine</em> Sony DS4, a real Xbox pad (the <code>xpad</code> driver is far more forgiving), or a Logitech F710. For now, the <strong>web controller</strong> (below) needs no gamepad at all.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/images/finished.jpg" class="img-fluid figure-img" alt="Two people smiling and holding up the assembled DonkeyCar and a black DualShock-style controller; a blue &quot;DOUBLESHOCK&quot; retail box sits on the cluttered desk in front of them"></p>
<figcaption>Holding up the car and the controller that started the fight — note the giveaway “DOUBLESHOCK” box on the desk, the counterfeit DualShock 4 the kernel refused.</figcaption>
</figure>
</div>
</section>
<section id="roadblock-5-the-camera-that-wasnt-sending-data" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-5-the-camera-that-wasnt-sending-data">Roadblock 5 — the camera that wasn’t sending data</h2>
<p>The CSI camera was the longest hunt. The sensor’s I²C control channel worked — the driver bound and <code>/dev/video0</code> existed — but every capture <strong>segfaulted</strong>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb16" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb16-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> gst-launch-1.0 nvarguscamerasrc num-buffers=1 ! <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb16-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'video/x-raw(memory:NVMM),width=1280,height=720'</span> ! nvjpegenc ! filesink location=/tmp/camtest.jpg</span>
<span id="cb16-3"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">(</span><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Argus</span><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">)</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">Error</span> EndOfFile: ... Caught SIGSEGV</span>
<span id="cb16-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ls <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span> /tmp/camtest.jpg</span>
<span id="cb16-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">-rw-rw-r--</span> 1 jeroen jeroen 0 ...   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># zero bytes</span></span></code></pre></div></div>
<p>The smoking gun was in <code>dmesg</code> and the Argus daemon log:</p>
<pre class="text"><code>isp 54680000.isp:     TIMEOUT     10000        # ISP waited for frames, got none
nvargus-daemon: CSI_DEBUG_COUNTER_2_0 = 0x00000000   # ZERO bytes received on the CSI bus</code></pre>
<p>So: <strong>I²C fine, but zero data on the <span class="term-tip" tabindex="0" aria-label="The high-speed differential signalling standard used on the CSI camera ribbon's data lanes." data-tip="The high-speed differential signalling standard used on the CSI camera ribbon's data lanes.">MIPI</span>/CSI lanes.</strong> A reboot later it got <em>worse</em> — even the I²C read started failing:</p>
<pre class="text"><code>imx219 6-0010: imx219_board_setup: error during i2c read probe (-110)
imx219: probe of 6-0010 failed with error -110</code></pre>
<p>That the symptom <em>changed between boots</em> is the tell: this is an <strong>intermittent physical connection</strong>, not software. A useful piece of logic narrowed it down — I²C and the high-speed data lanes share the same <span class="term-tip" tabindex="0" aria-label="Flat Flexible Cable (FFC) — the thin flat camera cable; its conductors can crack while still looking fine." data-tip="Flat Flexible Cable (FFC) — the thin flat camera cable; its conductors can crack while still looking fine.">ribbon</span>, so the fact I²C <em>ever</em> worked proved the <strong>orientation was correct</strong>; only the contact was unreliable.</p>
<p>The fix was a careful, full reseat of the ribbon at <strong>both</strong> ends with the latches firmly closed. Immediately:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb19" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb19-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> dmesg <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> imx219</span>
<span id="cb19-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">imx219</span> 6-0010: tegracam sensor driver:imx219_v2.0.6</span>
<span id="cb19-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">vi</span> 54080000.vi: subdev imx219 6-0010 bound          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># I²C bind clean, no -110</span></span>
<span id="cb19-4"></span>
<span id="cb19-5"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> gst-launch-1.0 nvarguscamerasrc num-buffers=1 ! <span class="dt" style="color: #AD0000;
background-color: null;
font-style: inherit;">\</span></span>
<span id="cb19-6">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'video/x-raw(memory:NVMM),width=1280,height=720'</span> ! nvjpegenc ! filesink location=/tmp/camtest.jpg</span>
<span id="cb19-7"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">GST_ARGUS:</span> Done Success</span>
<span id="cb19-8"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ls <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span> /tmp/camtest.jpg</span>
<span id="cb19-9"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">-rw-rw-r--</span> 1 jeroen jeroen 51866 /tmp/camtest.jpg   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># a real 51 KB frame</span></span></code></pre></div></div>
<p>A genuine 51 KB JPEG of the room — the camera works. <strong>Moral:</strong> “the ribbon looks fine” means nothing; CSI ribbons fail intermittently on the data lanes while I²C still limps along.</p>
<p>While the camera was down, <code>CAMERA_TYPE = "MOCK"</code> let everything else (web UI, actuators) be tested with blank frames — a useful way to keep moving. Once fixed, back to <code>CSIC</code>.</p>
</section>
<section id="roadblock-6-power-gremlins-three-of-them" class="level2">
<h2 class="anchored" data-anchor-id="roadblock-6-power-gremlins-three-of-them">Roadblock 6 — power gremlins (three of them)</h2>
<p><strong>(a) Reboots under load.</strong> Running <code>manage.py drive</code> made the Nano vanish from the network and reboot. The reflex is “out of memory,” but <code>free -h</code> showed a 4 GB board with <strong>~8 GB of swap</strong> and only 359 MB used — <em>not</em> <span class="term-tip" tabindex="0" aria-label="Out Of Memory — when the system exhausts RAM (and swap) and the kernel starts killing processes." data-tip="Out Of Memory — when the system exhausts RAM (and swap) and the kernel starts killing processes.">OOM</span>. It was a <strong>power transient</strong>: the Nano was in <span class="term-tip" tabindex="0" aria-label="The Jetson's maximum-performance power mode (all cores, highest clocks, highest current draw)." data-tip="The Jetson's maximum-performance power mode (all cores, highest clocks, highest current draw).">MAXN</span> mode pulling current spikes a marginal supply couldn’t hold. A solid 5 V/4 A barrel supply plus 5 W mode fixed it:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb20" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb20-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sudo</span> nvpmodel <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-m</span> 1     <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 5 W mode (MAXN is -m 0) — lower current draw</span></span></code></pre></div></div>
<p><strong>(b) <span class="term-tip" tabindex="0" aria-label="USB Power Delivery — a protocol that negotiates higher voltages (9–20 V) over USB-C." data-tip="USB Power Delivery — a protocol that negotiates higher voltages (9–20 V) over USB-C.">USB-PD</span> over-voltage — the scary one.</strong> The power bank’s USB-<strong>C</strong> port is <strong>USB-PD</strong>: it negotiates 9–20 V. Fed into the Nano’s 5 V barrel jack via a USB-C-to-DC cable, the green LED lit for ~10 s, then died as the bank ramped voltage — risking a fried board. <strong>Rule: only ever use the power bank’s plain USB-A (5 V) output for the barrel jack.</strong> The Nano survived; it was lucky.</p>
<p><strong>(c) <span class="term-tip" tabindex="0" aria-label="The protocol a router uses to hand out IP addresses automatically — which is why the Nano's IP can change." data-tip="The protocol a router uses to hand out IP addresses automatically — which is why the Nano's IP can change.">DHCP</span> musical chairs.</strong> After all the reboots, <code>ssh nano</code> suddenly connected to… nothing useful:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb21" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb21-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ping 192.168.0.5</span>
<span id="cb21-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">64</span> bytes from 192.168.0.5: ttl=64 time=0.026 ms   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># loopback speed — that's *this PC*</span></span>
<span id="cb21-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ip <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-4</span> addr show <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> 192.168.0.5</span>
<span id="cb21-4"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">inet</span> 192.168.0.5/24 ... wlp0s20f3                 <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the laptop grabbed the Nano's old IP</span></span></code></pre></div></div>
<p>DHCP had reassigned <code>192.168.0.5</code> to the laptop; the Nano moved to <code>.4</code>. The fix is to stop hard-coding the IP and use <span class="term-tip" tabindex="0" aria-label="Multicast DNS — lets you reach a device by a .local name without knowing its IP address." data-tip="Multicast DNS — lets you reach a device by a .local name without knowing its IP address.">mDNS</span>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb22" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb22-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> getent hosts donkeycar.local</span>
<span id="cb22-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">192.168.0.4</span>     donkeycar.local</span></code></pre></div></div>
<p>…and bake it into <code>~/.ssh/config</code> so it follows the Nano forever:</p>
<pre><code>Host nano
    HostName donkeycar.local
    User jeroen
    IdentityFile ~/.ssh/id_ed25519_donkeycar
    IdentitiesOnly yes
    StrictHostKeyChecking accept-new</code></pre>
</section>
<section id="driving-it-the-web-controller" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="driving-it-the-web-controller">Driving it: the web controller</h2>
<p>No gamepad needed — DonkeyCar serves a browser UI:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb24" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb24-1"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">cd</span> ~/mycar <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">&amp;&amp;</span> <span class="ex" style="color: null;
background-color: null;
font-style: inherit;">python</span> manage.py drive</span>
<span id="cb24-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># → http://donkeycar.local:8887/drive</span></span></code></pre></div></div>
<p>One gotcha cost ten minutes: after a Nano reboot, the <strong>page open on the phone was a stale tab</strong> pointing at the dead server’s <span class="term-tip" tabindex="0" aria-label="A persistent two-way browser-to-server connection — how the web UI streams your joystick input to the car in real time." data-tip="A persistent two-way browser-to-server connection — how the web UI streams your joystick input to the car in real time.">WebSocket</span>. The clue was server-side:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb25" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb25-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> ss <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-tn</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> :8887 <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">|</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grep</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span> 127.0.0.1</span>
<span id="cb25-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (empty) — the phone wasn't actually connected</span></span></code></pre></div></div>
<p>A hard refresh re-established the <code>101 GET /wsDrive</code> WebSocket upgrade and the wheels responded.</p>
<div class="page-columns page-full">
<video controls="" preload="none" poster="images/drive-poster.jpg" width="1280" height="720" style="width:100%;height:auto;border-radius:6px;" class="page-columns page-full">
<source src="images/drive.mp4" type="video/mp4">
<p>Your browser doesn’t support embedded video. <a href="images/drive.mp4">Download the clip</a>. </p>
<p>:::</p>
<p>The payoff: driving it untethered across the floor from a phone — no gamepad, no tether, just the Nano and the web controller.</p>
<section id="version-control-on-the-edge" class="level2 page-columns page-full">
<h2 class="anchored" data-anchor-id="version-control-on-the-edge">Version control on the edge</h2>
<p>The calibration and camera config live in a fork (<code>BioGeek/mycar</code>). GitHub no longer accepts passwords, so it’s a Personal Access Token cached once:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb26" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb26-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> config credential.helper store   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># then enter the PAT once on first push</span></span>
<span id="cb26-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> checkout <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-b</span> jeroen_nano          <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the repo was in detached HEAD at tag 4.5.1</span></span>
<span id="cb26-3"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">git</span> push <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span> fork jeroen_nano</span></code></pre></div></div>
<div class="quarto-figure quarto-figure-center page-columns page-full">
<figure class="figure page-columns page-full">
<p class="page-columns page-full"><img src="https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/images/workbench-night.jpg" class="img-fluid figure-img column-body" alt="A person working at a laptop late at night at a wooden dining table cluttered with the DonkeyCar, a breadboard, components and a DualShock controller"></p>
<figcaption>Most of this build log happened here — a long evening of breadboards, kernel logs and reseated cables.</figcaption>
</figure>
</div>
</section>
<section id="whats-next" class="level2">
<h2 class="anchored" data-anchor-id="whats-next">What’s next</h2>
<p>The camera works, the car drives, and it runs untethered. The next milestone is the real point: <strong>record laps → train a <span class="term-tip" tabindex="0" aria-label="Training a model to imitate recorded human actions — here, mapping camera frames to the steering/throttle you commanded." data-tip="Training a model to imitate recorded human actions — here, mapping camera frames to the steering/throttle you commanded.">behavioural-cloning</span> model on the RTX 4070 → hand control to the autopilot</strong>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb27" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb27-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">python</span> manage.py drive              <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># record "tubs" of (image, steering, throttle)</span></span>
<span id="cb27-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">donkey</span> train <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--tub</span> data <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--model</span> models/pilot.h5   <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># on the PC, not the Nano</span></span>
<span id="cb27-3"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">python</span> manage.py drive <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--model</span> models/pilot.h5</span></code></pre></div></div>
<p>…and before training anything, <em>look at the data</em> — the steering-angle balance decides where the car will fail long before the model architecture does. But that’s the next post.</p>
</section>
<section id="the-transferable-lessons" class="level2">
<h2 class="anchored" data-anchor-id="the-transferable-lessons">The transferable lessons</h2>
<ul>
<li><strong>Read the kernel log first.</strong> <code>dmesg</code> named every real fault here — the counterfeit controller (<code>probe failed -110</code>), the dead CSI lanes (<code>CSI_DEBUG_COUNTER = 0</code>), the power resets — while the surface symptoms lied.</li>
<li><strong>Distinguish “accepted” from “effective.”</strong> I²C writes succeeded into an unpowered servo; the camera bound but sent no data. <em>Acknowledged ≠ working.</em></li>
<li><strong>On EOL hardware, the chip sets your software ceiling</strong>, not your preferences.</li>
<li><strong>Buy genuine peripherals.</strong> A counterfeit DS4 turned into an unwinnable fight with a built-in kernel driver.</li>
</ul>


<!-- -->

</section>
</video></div>
</section>

 ]]></description>
  <category>robotics</category>
  <category>edge ml</category>
  <category>jetson</category>
  <category>side project</category>
  <guid>https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/</guid>
  <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
  <media:content url="https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/images/finished.jpg" medium="image" type="image/jpeg"/>
</item>
</channel>
</rss>
