<?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/writing.html</link>
<atom:link href="https://jeroen.vangoey.be/writing.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/writing.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-rc-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">
<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>
<video controls="" preload="none" poster="images/drive-poster.jpg" width="1280" height="720" style="width:100%;height:auto;border-radius:6px;">
<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>The payoff: driving it untethered across the floor from a phone, no gamepad, no tether, just the Nano and the web controller.</p>
</video></section>
<section id="version-control-on-the-edge" class="level2">
<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">
<figure class="figure">
<p><img src="https://jeroen.vangoey.be/posts/donkeycar-jetson-nano/images/workbench-night.jpg" class="img-fluid figure-img" 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>

 ]]></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>
