<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://danleyb2.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://danleyb2.dev/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-07-20T06:57:24+00:00</updated><id>https://danleyb2.dev/feed.xml</id><title type="html">danleyb2</title><subtitle>Software developer specializing in Python, JavaScript, Docker, and system reliability. Currently building Vision AI integrations at PlateRecognizer.</subtitle><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><entry><title type="html">Running PlateRecognizer on i-PRO Edge Cameras with the ADAM Framework</title><link href="https://danleyb2.dev/visionai/embeddedai/docker/python/cpp/edgecomputing/camerasdk/2026/07/08/running-platerecognizer-on-ipro-edge-cameras.html" rel="alternate" type="text/html" title="Running PlateRecognizer on i-PRO Edge Cameras with the ADAM Framework" /><published>2026-07-08T05:10:00+00:00</published><updated>2026-07-08T05:10:00+00:00</updated><id>https://danleyb2.dev/visionai/embeddedai/docker/python/cpp/edgecomputing/camerasdk/2026/07/08/running-platerecognizer-on-ipro-edge-cameras</id><content type="html" xml:base="https://danleyb2.dev/visionai/embeddedai/docker/python/cpp/edgecomputing/camerasdk/2026/07/08/running-platerecognizer-on-ipro-edge-cameras.html"><![CDATA[<p>At PlateRecognizer, we’ve shipped detection in browsers, Docker containers, and cloud APIs. But the edge case that keeps engineers awake at 2 AM is always the same: <strong>running on hardware with no network, limited memory, and a locked-down firmware.</strong></p>

<p>Recently I worked on getting our stream pipeline running on i-PRO’s ADAM camera framework — an embedded app platform built on top of Panasonic’s Amba V5X SoC. Here’s what that looked like and what made it interesting.</p>

<h2 id="the-landscape-embedded-cameras-arent-linux-vms">The Landscape: Embedded Cameras Aren’t Linux VMs</h2>

<p>i-PRO cameras don’t run Debian. They run a real-time OS with the <strong>ADAM (Application Development and Management)</strong> framework — essentially a constrained app runtime with its own lifecycle, event model, and sandboxing. Think of it as the world’s most opinionated Docker for cameras, but without the networking stack or package manager.</p>

<p>The target hardware runs an ARM64 (aarch64) AmbaCV5XCEX SoC with ~180 MB of heap available to applications. That’s not a lot when your model weights are already in the hundreds of megabytes. The catch: i-PRO cameras come pre-installed with Python 3.7 under <code class="language-plaintext highlighter-rouge">/usr/bin/python3</code>, so you’re working with what’s there.</p>

<h2 id="architecture-overview">Architecture Overview</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────┐
│              i-PRO Camera                    │
│                                             │
│  ┌──────────────┐   ┌──────────────────┐   │
│  │   ADAM Core  │◄──►│  Platerecognizer │   │
│  │  (C/C++ API) │   │     Stream App    │   │
│  └──────┬───────┘   │                  │   │
│         │            │  main.cpp (host) │   │
│         │            │  pymain.py (logic)│   │
│         ▼            │                  │   │
│  ┌──────────────┐    └──────────────────┘   │
│  │  Python 3.7  │                           │
│  │  (bundled or │◄── appPrefs.json           │
│  │   camera-inst)│                           │
│  └──────────────┘    ┌──────────────────┐   │
│                      │  ONNX Runtime     │   │
│                      │  + OpenCV         │   │
│                      └──────────────────┘   │
└─────────────────────────────────────────────┘
</code></pre></div></div>

<p>The app splits into two layers:</p>

<h3 id="the-c-host-maincpp">The C++ Host (<code class="language-plaintext highlighter-rouge">main.cpp</code>)</h3>

<p>This is the bridge between ADAM’s event loop and your Python logic. It handles:</p>

<ul>
  <li>Camera lifecycle (start/stop/restart callbacks)</li>
  <li>App preferences passed from the i-PRO web UI (via <code class="language-plaintext highlighter-rouge">appPrefs.json</code>)</li>
  <li>Memory management for the Python GIL across threads</li>
  <li>Communication back to ADAM Core for frame data access</li>
</ul>

<p>The C++ code embeds the Python interpreter using <code class="language-plaintext highlighter-rouge">Py_Initialize()</code> and <code class="language-plaintext highlighter-rouge">PyRun_SimpleFile()</code> — it reads a Python file from disk and executes it in-process. This is simpler than building a full Python-C API bridge, and perfectly adequate when your Python module is mostly stateless processing logic.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Simplified embedding pattern from main.cpp</span>
<span class="kt">void</span> <span class="nf">initPython</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">// Dynamic PYTHONPATH based on deployment target</span>
  <span class="cp">#if defined(ADAM_TARGET_PF_ipro_ambaCV5XCAZ_linux)
</span>    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">pythonPath</span> <span class="o">=</span> <span class="s">"/app/python"</span><span class="p">;</span>
  <span class="cp">#else
</span>    <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">pythonPath</span> <span class="o">=</span> <span class="n">ADAM_GetAppDataDirPath</span><span class="p">()</span> <span class="o">+</span> <span class="s">"/../python"</span><span class="p">;</span>
  <span class="cp">#endif
</span>  
  <span class="c1">// Conditional PyEval_InitThreads for Python &lt; 3.9 compatibility</span>
  <span class="cp">#if (PY_MAJOR_VERSION == 3) &amp;&amp; (PY_MINOR_VERSION &lt; 9)
</span>    <span class="n">PyEval_InitThreads</span><span class="p">();</span>
  <span class="cp">#endif
</span>  
  <span class="n">Py_Initialize</span><span class="p">();</span>
  <span class="n">s_pAdamModule</span> <span class="o">=</span> <span class="n">PyImport_ImportModule</span><span class="p">(</span><span class="s">"libAdamApiPython"</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">executePython</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">string</span> <span class="n">pyFile</span> <span class="o">=</span> <span class="cm">/* resolved path */</span> <span class="o">+</span> <span class="s">"pymain.py"</span><span class="p">;</span>
  
  <span class="kt">FILE</span><span class="o">*</span> <span class="n">pFp</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">pyFile</span><span class="p">.</span><span class="n">c_str</span><span class="p">(),</span> <span class="s">"r"</span><span class="p">);</span>
  <span class="n">PyGILStateLock</span> <span class="n">_lock</span><span class="p">;</span> <span class="c1">// RAII GIL management</span>
  <span class="kt">int</span> <span class="n">ret</span> <span class="o">=</span> <span class="n">PyRun_SimpleFile</span><span class="p">(</span><span class="n">pFp</span><span class="p">,</span> <span class="n">pyFile</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">PyErr_Print</span><span class="p">();</span>
    <span class="n">ADAM_DEBUG_PRINT</span><span class="p">(</span><span class="n">ADAM_LV_ERR</span><span class="p">,</span> <span class="s">"Python execution failed</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">PyGILStateLock</code> class is critical here — it’s a RAII guard that ensures the GIL is acquired before any Python C API calls and released when the scope exits. Without it, you get intermittent crashes that are nearly impossible to reproduce in testing.</p>

<h3 id="the-python-runtime-pymainpy">The Python Runtime (<code class="language-plaintext highlighter-rouge">pymain.py</code>)</h3>

<p>The actual plate recognition logic lives in Python. This is where the ONNX model runs, frames get processed through OpenCV, and results get formatted for the ADAM event system. Since i-PRO’s environment provides <code class="language-plaintext highlighter-rouge">libAdamApiPython</code>, you get access to:</p>

<ul>
  <li>Camera frame data streams</li>
  <li>App preferences (license key, token, log level) from <code class="language-plaintext highlighter-rouge">appPrefs.json</code></li>
  <li>HTTP networking to PlateRecognizer’s API (for cloud mode) or local detection models</li>
</ul>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"preference"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"prefName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LICENSE_KEY"</span><span class="p">,</span><span class="w"> </span><span class="nl">"prefType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"String"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"prefName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TOKEN"</span><span class="p">,</span><span class="w">      </span><span class="nl">"prefType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"String"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"prefName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOGGING"</span><span class="p">,</span><span class="w">    </span><span class="nl">"prefType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Enumeration"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enumerationList"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"10"</span><span class="p">,</span><span class="s2">"20"</span><span class="p">,</span><span class="s2">"30"</span><span class="p">,</span><span class="s2">"40"</span><span class="p">,</span><span class="s2">"50"</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>These preferences are what the camera’s web UI shows to end-users — no code change needed between a dev install and production deployment.</p>

<h2 id="building-two-paths-same-result">Building: Two Paths, Same Result</h2>

<p>The project ships two Dockerfiles for different build scenarios:</p>

<h3 id="local-build-dockerfileext">Local Build (<code class="language-plaintext highlighter-rouge">Dockerfile.ext</code>)</h3>

<p>For development and testing without i-PRO’s cloud toolchain:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ARG</span><span class="s"> CADAMBUILDBASE_PATH</span>
<span class="k">ARG</span><span class="s"> CADAMAPPBASE_PATH</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">${CADAMBUILDBASE_PATH}</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build-env</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> . ./</span>
<span class="k">WORKDIR</span><span class="s"> /iprosdk</span>
<span class="k">RUN </span><span class="nb">chmod</span> +x setup_env.sh
<span class="k">RUN </span>/bin/bash <span class="nt">-c</span> <span class="s2">"source setup_env.sh ambaCV5XCEXinternal &amp;&amp; </span><span class="se">\
</span><span class="s2">    cd /app &amp;&amp; </span><span class="se">\
</span><span class="s2">    make clean &amp;&amp; </span><span class="se">\
</span><span class="s2">    make"</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">${CADAMAPPBASE_PATH}</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">aplbase</span>
<span class="k">RUN </span>useradd <span class="nt">-ms</span> /bin/bash moduleuser
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> --from=build-env /app/ /app/</span>
<span class="k">USER</span><span class="s"> moduleuser</span>
</code></pre></div></div>

<p>This uses the camera SDK’s cross-compilation toolchain to produce a <code class="language-plaintext highlighter-rouge">.ext</code> package — i-PRO’s app bundle format. The Makefile is ADAM-specific and auto-generates build rules based on <code class="language-plaintext highlighter-rouge">TARGET_FOR_ADAM</code>.</p>

<h3 id="azure-iot-build-dockerfileazureiot">Azure IoT Build (<code class="language-plaintext highlighter-rouge">Dockerfile.azureIoT</code>)</h3>

<p>For production builds, i-PRO hosts the SDK in their private ACR:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">iprocamsdk.azurecr.io/sdk/containeradam/env/cdamenv:1.0.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build-env</span>

<span class="k">RUN </span><span class="nb">mv</span> /iprosdk/lib/aarch64-linux-gnu_CV5XCAZ/libForPython3.7/<span class="k">*</span> <span class="se">\
</span>    /iprosdk/lib/aarch64-linux-gnu_CV5XCAZ

<span class="k">RUN </span>/bin/bash <span class="nt">-c</span> <span class="s2">"source setup_env.sh ambaCV5XCAZipro &amp;&amp; </span><span class="se">\
</span><span class="s2">    cd /app &amp;&amp; make clean &amp;&amp; make"</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">iprocamsdk.azurecr.io/sdk/containeradam/env/cdamappbase:1.0.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">aplbase</span>
<span class="k">COPY</span><span class="s"> --from=build-env /app/ /app/</span>
<span class="k">CMD</span><span class="s"> ["/usr/share/lib/cadamClient"]</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">libForPython3.7</code> symlink dance is a known workaround — the SDK’s library layout changes between versions and the Python module loader needs to find <code class="language-plaintext highlighter-rouge">.so</code> files in a specific path.</p>

<h2 id="configuration-at-runtime">Configuration at Runtime</h2>

<p>The app’s configuration file tells ADAM everything about the binary:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>APPLICATION     platerecognizerStream
APPVERSION      V1.2
ROMSIZE         1055920
RAMSIZE         300000
CPURATE         90
FUNCID          00002178
</code></pre></div></div>

<ul>
  <li><strong>RAMSIZE</strong> (300 KB) is the temporary allocation <em>on top of</em> the heap — not total memory</li>
  <li><strong>CPURATE</strong> (90%) tells ADAM this app needs aggressive scheduling; higher values mean more CPU budget but less sharing with other apps on the same camera</li>
  <li><strong>FUNCID</strong> is a licensing marker used by i-PRO’s app store for version tracking</li>
</ul>

<h2 id="the-installer-a-python-cli-youll-actually-use">The Installer: A Python CLI You’ll Actually Use</h2>

<p>Since these cameras don’t have <code class="language-plaintext highlighter-rouge">pip</code> or a package manager, we built a small installer that talks to the camera’s ADAM CGI endpoints over HTTP Digest auth:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># installer.py uses requests-toolbelt for multipart uploads
# with tqdm progress bars — because watching a 15MB upload on a slow link
# deserves better than blank terminal output
</span>
<span class="k">def</span> <span class="nf">upload_adam_app</span><span class="p">(</span><span class="n">ext_file_path</span><span class="p">):</span>
    <span class="n">file_size</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">getsize</span><span class="p">(</span><span class="n">ext_file_path</span><span class="p">)</span>
    <span class="n">progress_bar</span> <span class="o">=</span> <span class="n">tqdm</span><span class="p">.</span><span class="nf">tqdm</span><span class="p">(</span><span class="n">desc</span><span class="o">=</span><span class="sa">f</span><span class="sh">"</span><span class="s">Uploading [</span><span class="si">{</span><span class="n">ext_file_path</span><span class="si">}</span><span class="s">]</span><span class="sh">"</span><span class="p">,</span>
                             <span class="n">total</span><span class="o">=</span><span class="n">file_size</span><span class="p">,</span> <span class="n">unit</span><span class="o">=</span><span class="sh">"</span><span class="s">B</span><span class="sh">"</span><span class="p">,</span> 
                             <span class="n">unit_scale</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">unit_divisor</span><span class="o">=</span><span class="mi">1024</span><span class="p">)</span>
    
    <span class="k">def</span> <span class="nf">progress_callback</span><span class="p">(</span><span class="n">monitor</span><span class="p">):</span>
        <span class="n">progress_bar</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="n">monitor</span><span class="p">.</span><span class="n">bytes_read</span> <span class="o">-</span> <span class="n">progress_bar</span><span class="p">.</span><span class="n">n</span><span class="p">)</span>
    
    <span class="n">encoder</span> <span class="o">=</span> <span class="nc">MultipartEncoder</span><span class="p">(</span><span class="n">fields</span><span class="o">=</span><span class="p">{</span>
        <span class="sh">"</span><span class="s">methodName</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">installApplication</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">applicationPackage</span><span class="sh">"</span><span class="p">:</span> <span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="n">ext_file_path</span><span class="p">),</span> <span class="n">fp</span><span class="p">,</span> 
                               <span class="sh">"</span><span class="s">application/octet-stream</span><span class="sh">"</span><span class="p">),</span>
    <span class="p">})</span>
    <span class="n">monitor</span> <span class="o">=</span> <span class="nc">MultipartEncoderMonitor</span><span class="p">(</span><span class="n">encoder</span><span class="p">,</span> <span class="n">progress_callback</span><span class="p">)</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">monitor</span><span class="p">,</span> <span class="n">auth</span><span class="o">=</span><span class="nc">HTTPDigestAuth</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="k">pass</span><span class="p">))</span>
</code></pre></div></div>

<p>It handles the full lifecycle: list → stop → uninstall → upload → start. No manual SSH needed.</p>

<h2 id="what-made-this-hard">What Made This Hard</h2>

<h3 id="1-python-gil--real-time-scheduling">1. Python GIL + Real-Time Scheduling</h3>

<p>The ADAM framework runs the app’s main thread in a real-time event loop. Embedding Python means that loop competes with <code class="language-plaintext highlighter-rouge">PyEval_InitThreads()</code> for CPU time. The solution was using <code class="language-plaintext highlighter-rouge">Py_UNBLOCK_THREADS</code> before the ADAM event dispatch and <code class="language-plaintext highlighter-rouge">Py_BLOCK_THREADS</code> after — effectively telling the interpreter “this is safe to suspend.”</p>

<h3 id="2-memory-is-everything">2. Memory Is Everything</h3>

<p>The V5XCEX SoC has 1GB total RAM, but the camera firmware, video encoding, and network stack consume most of it. Your app gets ~180 MB heap plus a small scratch buffer. This means:</p>

<ul>
  <li>No lazy model loading — everything initializes before the first frame</li>
  <li>OpenCV uses <code class="language-plaintext highlighter-rouge">cv2.IMREAD_GRAYSCALE</code> where possible to halve frame buffers</li>
  <li>ONNX models use the execution provider optimized for ARM NEON (not CUDA)</li>
</ul>

<h3 id="3-cross-compilation-is-a-black-box">3. Cross-Compilation Is a Black Box</h3>

<p>i-PRO’s toolchain isn’t standard Yocto or Buildroot — it’s vendor-supplied and opaque. You get <code class="language-plaintext highlighter-rouge">setup_env.sh</code> which sets compiler paths, sysroot, and flags. Breaking the build pipeline for new SDK versions is an annual maintenance task.</p>

<h2 id="debugging">Debugging</h2>

<p>i-PRO ships two tools worth knowing about:</p>

<ul>
  <li><strong>Resource Monitor</strong> — Docker container browser, live tail of container logs, and per-container resource usage.</li>
  <li><strong>Adam Operations UI</strong> (Chrome extension) — start, stop, uninstall apps; useful when the web UI is unresponsive.</li>
</ul>

<p>Remember: no SSH to the camera by default. No <code class="language-plaintext highlighter-rouge">gdb</code> attached to the process. The only debug output goes through ADAM’s logging infrastructure (<code class="language-plaintext highlighter-rouge">ADAM_DEBUG_PRINT</code>) which surfaces in the i-PRO web UI at a configurable log level — and even then, it’s line-buffered.</p>

<h2 id="installation-walkthrough">Installation Walkthrough</h2>

<p>Here’s what deployment looks like in practice:</p>

<p><strong>Supported models:</strong> CV52-series cameras with Docker capability — WV-X15300<em>, WV-X15500</em>, WV-X22300<em>, WV-X22500</em> and the X25600/X22600/WV-X15700/WV-X22700/WV-X25700 lines. The older CV2/CV22/CV25m (ambaCV2X) chips are <strong>not</strong> supported.</p>

<p><strong>Before you start:</strong></p>
<ul>
  <li>Upgrade to the latest firmware first — extract the <code class="language-plaintext highlighter-rouge">.img</code> file from i-PRO’s documentation database and install via Setup &gt; Maintenance &gt; Upgrade. Stream needs Docker support that only ships in recent firmwares.</li>
  <li>Enable “Ext. software mode” in the camera UI, then mount <code class="language-plaintext highlighter-rouge">/mnt/sda/adamapp</code> → <code class="language-plaintext highlighter-rouge">/user-data</code> for SD card storage. The flash has a write-cycle limit; without an SD card you’ll kill the camera’s NAND over time.</li>
  <li>You need ~700 MB free (Stream + models) and internet access from the camera for license validation.</li>
</ul>

<p><strong>Installing:</strong>
The camera runs two AdamApps: the <strong>Docker extension module</strong> (prerequisite, installed from i-PRO’s docs first), then the <strong>Stream <code class="language-plaintext highlighter-rouge">.ext</code></strong> bundle itself. You can use the camera web UI (Setup &gt; Application &gt; Extensions) or the Adam Operations UI Chrome extension. If the camera throws a ROM error during install, just retry — it works.</p>

<p><strong>Configuring detection:</strong>
Everything goes through the Plate Recognizer dashboard — add your camera with an RTSP URL from these patterns:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rtsp://&lt;cam_ip&gt;:554/mediainput/h264/stream_1
rtsp://&lt;user:pass&gt;@&lt;cam_ip&gt;:554/mediainput/h264/stream_1
# stream_2, stream_3, stream_4 for multi-stream cameras
</code></pre></div></div>
<p>Recommended feed: 1280×720 at 15fps, H.264.</p>

<p>For detection results, enable webhooks in the Plate Recognizer account and turn on <strong>caching</strong> if the camera’s internet is spotty — it queues results locally and flushes when connectivity returns.</p>

<p>The biggest win: <strong>the Python code is identical to what runs on Docker.</strong> Only the C++ host layer changes — the embedding glue that translates ADAM events into Python calls. Everything from frame processing through API submission stays in one <code class="language-plaintext highlighter-rouge">pymain.py</code>.</p>

<h2 id="open-questions">Open Questions</h2>

<ul>
  <li>Can we replace the Python host with a Rust binary that spawns Python as needed, reducing memory pressure?</li>
  <li>The ADAM framework doesn’t expose GPU acceleration — is there a path to NPU inference on newer i-PRO models?</li>
  <li>Would container-based deployment (<code class="language-plaintext highlighter-rouge">Dockerfile.ext</code>) eventually replace <code class="language-plaintext highlighter-rouge">.ext</code> packages for app distribution?</li>
</ul>

<p>For now, it runs. And running on edge cameras that have never seen Docker is still one of the more satisfying engineering victories I’ve had this year.</p>

<hr />

<p><em>This work is part of the <a href="https://github.com/parkpow/deep-license-plate-recognition">deep-license-plate-recognition</a> project at PlateRecognizer — specifically the <code class="language-plaintext highlighter-rouge">stream/ipro-adam-app</code> module for on-camera deployment.</em></p>]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="VisionAI" /><category term="EmbeddedAI" /><category term="Docker" /><category term="Python" /><category term="Cpp" /><category term="EdgeComputing" /><category term="CameraSDK" /><summary type="html"><![CDATA[At PlateRecognizer, we’ve shipped detection in browsers, Docker containers, and cloud APIs. But the edge case that keeps engineers awake at 2 AM is always the same: running on hardware with no network, limited memory, and a locked-down firmware.]]></summary></entry><entry><title type="html">SPKI Pinning in Python Requests</title><link href="https://danleyb2.dev/python/security/ssl/tls/cryptography/2026/07/08/spki-pinning-in-python-requests.html" rel="alternate" type="text/html" title="SPKI Pinning in Python Requests" /><published>2026-07-08T02:32:00+00:00</published><updated>2026-07-08T02:32:00+00:00</updated><id>https://danleyb2.dev/python/security/ssl/tls/cryptography/2026/07/08/spki-pinning-in-python-requests</id><content type="html" xml:base="https://danleyb2.dev/python/security/ssl/tls/cryptography/2026/07/08/spki-pinning-in-python-requests.html"><![CDATA[<p>Certificate pinning has long been the gold standard for preventing man-in-the-middle attacks in client applications. Most ecosystems support fingerprint-based cert pinning, but the industry is shifting toward <strong>SPKI (Subject Public Key Info) pinning</strong> — which pins the underlying public key rather than the certificate itself. This means rotating your CA won’t break your clients, while still defending against a rogue CA issuing an untrusted certificate.</p>

<p>Here’s how to implement SPKI pinning using <code class="language-plaintext highlighter-rouge">requests</code> and <code class="language-plaintext highlighter-rouge">urllib3</code>.</p>

<h2 id="why-spki-over-certificate-fingerprint">Why SPKI over Certificate Fingerprint?</h2>

<p>A cert fingerprint breaks every time the certificate renews. The public key stays the same. With SPKI pinning you get:</p>

<ul>
  <li><strong>Rotatable certificates</strong> without client updates</li>
  <li><strong>Same attack surface</strong> as cert pinning — still prevents rogue CAs</li>
  <li><strong>Future-proofing</strong> if you ever move to a different CA or HSM</li>
</ul>

<h2 id="the-implementation">The Implementation</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">hashlib</span>
<span class="kn">from</span> <span class="n">typing</span> <span class="kn">import</span> <span class="n">Any</span>

<span class="kn">import</span> <span class="n">requests</span>
<span class="kn">import</span> <span class="n">urllib3</span>
<span class="kn">from</span> <span class="n">cryptography</span> <span class="kn">import</span> <span class="n">x509</span>
<span class="kn">from</span> <span class="n">cryptography.hazmat.primitives</span> <span class="kn">import</span> <span class="n">serialization</span>
<span class="kn">from</span> <span class="n">requests.adapters</span> <span class="kn">import</span> <span class="n">HTTPAdapter</span>
<span class="kn">from</span> <span class="n">urllib3.connection</span> <span class="kn">import</span> <span class="n">HTTPSConnection</span>
<span class="kn">from</span> <span class="n">urllib3.connectionpool</span> <span class="kn">import</span> <span class="n">HTTPSConnectionPool</span>
<span class="kn">from</span> <span class="n">urllib3.poolmanager</span> <span class="kn">import</span> <span class="n">PoolManager</span><span class="p">,</span> <span class="n">ProxyManager</span>


<span class="n">PINNED_KEYS</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">btwo.danleyb2.dev</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
        <span class="sh">"</span><span class="s">b41ccc8c89282902cc4dbcbfff32323e8ffb098b0afb707f5935e2d75c7ce11a</span><span class="sh">"</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="step-1-the-connection-class">Step 1: The Connection Class</h3>

<p><code class="language-plaintext highlighter-rouge">urllib3</code> lets us swap in a custom connection class. We override <code class="language-plaintext highlighter-rouge">connect()</code> to extract the peer certificate after the TLS handshake, pull its public key, and compare it against our pin:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PinnedHTTPSConnection</span><span class="p">(</span><span class="n">HTTPSConnection</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">connect</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">connect</span><span class="p">()</span>
        <span class="n">hostname</span> <span class="o">=</span> <span class="nf">getattr</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">sock</span><span class="p">,</span> <span class="sh">"</span><span class="s">server_hostname</span><span class="sh">"</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span> <span class="ow">or</span> <span class="n">self</span><span class="p">.</span><span class="n">host</span>

        <span class="n">cert_der</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">sock</span><span class="p">.</span><span class="nf">getpeercert</span><span class="p">(</span><span class="n">binary_form</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
        <span class="n">cert</span> <span class="o">=</span> <span class="n">x509</span><span class="p">.</span><span class="nf">load_der_x509_certificate</span><span class="p">(</span><span class="n">cert_der</span><span class="p">)</span>
        <span class="n">spki</span> <span class="o">=</span> <span class="n">cert</span><span class="p">.</span><span class="nf">public_key</span><span class="p">().</span><span class="nf">public_bytes</span><span class="p">(</span>
            <span class="n">serialization</span><span class="p">.</span><span class="n">Encoding</span><span class="p">.</span><span class="n">DER</span><span class="p">,</span>
            <span class="n">serialization</span><span class="p">.</span><span class="n">PublicFormat</span><span class="p">.</span><span class="n">SubjectPublicKeyInfo</span>
        <span class="p">)</span>
        <span class="n">actual</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span><span class="n">spki</span><span class="p">).</span><span class="nf">hexdigest</span><span class="p">()</span>

        <span class="n">expected</span> <span class="o">=</span> <span class="n">PINNED_KEYS</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">hostname</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">expected</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">sock</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
            <span class="k">raise</span> <span class="n">requests</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="nc">SSLError</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">No SPKI pin configured: </span><span class="si">{</span><span class="n">hostname</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span>

        <span class="k">if</span> <span class="nf">isinstance</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
            <span class="n">expected</span> <span class="o">=</span> <span class="p">{</span><span class="n">expected</span><span class="p">}</span>

        <span class="k">if</span> <span class="n">actual</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">expected</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">sock</span><span class="p">.</span><span class="nf">close</span><span class="p">()</span>
            <span class="k">raise</span> <span class="n">requests</span><span class="p">.</span><span class="n">exceptions</span><span class="p">.</span><span class="nc">SSLError</span><span class="p">(</span>
                <span class="sa">f</span><span class="sh">"</span><span class="s">SPKI pin mismatch for </span><span class="si">{</span><span class="n">hostname</span><span class="si">}</span><span class="s"> (sha256=</span><span class="si">{</span><span class="n">actual</span><span class="si">}</span><span class="s">)</span><span class="sh">"</span>
            <span class="p">)</span>
</code></pre></div></div>

<h3 id="step-2-pool-classes">Step 2: Pool Classes</h3>

<p>We wire the custom connection into urllib3’s pool hierarchy:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PinnedHTTPSConnectionPool</span><span class="p">(</span><span class="n">HTTPSConnectionPool</span><span class="p">):</span>
    <span class="n">ConnectionCls</span> <span class="o">=</span> <span class="n">PinnedHTTPSConnection</span>


<span class="k">class</span> <span class="nc">PinnedPoolManager</span><span class="p">(</span><span class="n">PoolManager</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">pool_classes_by_scheme</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">http</span><span class="sh">"</span><span class="p">:</span> <span class="n">urllib3</span><span class="p">.</span><span class="n">connectionpool</span><span class="p">.</span><span class="n">HTTPConnectionPool</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">https</span><span class="sh">"</span><span class="p">:</span> <span class="n">PinnedHTTPSConnectionPool</span><span class="p">,</span>
        <span class="p">}</span>


<span class="k">class</span> <span class="nc">PinnedProxyManager</span><span class="p">(</span><span class="n">ProxyManager</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
        <span class="nf">super</span><span class="p">().</span><span class="nf">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">pool_classes_by_scheme</span> <span class="o">=</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">http</span><span class="sh">"</span><span class="p">:</span> <span class="n">urllib3</span><span class="p">.</span><span class="n">connectionpool</span><span class="p">.</span><span class="n">HTTPConnectionPool</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">https</span><span class="sh">"</span><span class="p">:</span> <span class="n">PinnedHTTPSConnectionPool</span><span class="p">,</span>
        <span class="p">}</span>
</code></pre></div></div>

<h3 id="step-3-the-adapter">Step 3: The Adapter</h3>

<p>The adapter bridges urllib3’s pools with the requests <code class="language-plaintext highlighter-rouge">Session</code> interface:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">PinnedHTTPAdapter</span><span class="p">(</span><span class="n">HTTPAdapter</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">init_poolmanager</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">connections</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">maxsize</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">block</span><span class="p">:</span> <span class="nb">bool</span> <span class="o">=</span> <span class="bp">False</span><span class="p">,</span> <span class="o">**</span><span class="n">pool_kwargs</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_pool_connections</span> <span class="o">=</span> <span class="n">connections</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_pool_maxsize</span> <span class="o">=</span> <span class="n">maxsize</span>
        <span class="n">self</span><span class="p">.</span><span class="n">_pool_block</span> <span class="o">=</span> <span class="n">block</span>
        <span class="n">self</span><span class="p">.</span><span class="n">poolmanager</span> <span class="o">=</span> <span class="nc">PinnedPoolManager</span><span class="p">(</span>
            <span class="n">num_pools</span><span class="o">=</span><span class="n">connections</span><span class="p">,</span> <span class="n">maxsize</span><span class="o">=</span><span class="n">maxsize</span><span class="p">,</span> <span class="n">block</span><span class="o">=</span><span class="n">block</span><span class="p">,</span> <span class="o">**</span><span class="n">pool_kwargs</span>
        <span class="p">)</span>

    <span class="k">def</span> <span class="nf">proxy_manager_for</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">proxy</span><span class="p">,</span> <span class="o">**</span><span class="n">proxy_kwargs</span><span class="p">):</span>
        <span class="n">proxy_str</span> <span class="o">=</span> <span class="n">proxy</span><span class="p">.</span><span class="n">url</span> <span class="k">if</span> <span class="nf">hasattr</span><span class="p">(</span><span class="n">proxy</span><span class="p">,</span> <span class="sh">'</span><span class="s">url</span><span class="sh">'</span><span class="p">)</span> <span class="k">else</span> <span class="n">proxy</span>
        <span class="k">if</span> <span class="n">proxy_str</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">self</span><span class="p">.</span><span class="n">proxy_manager</span><span class="p">:</span>
            <span class="n">manager</span> <span class="o">=</span> <span class="nc">PinnedProxyManager</span><span class="p">(</span>
                <span class="n">proxy_url</span><span class="o">=</span><span class="n">proxy_str</span><span class="p">,</span>
                <span class="n">num_pools</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_pool_connections</span><span class="p">,</span>
                <span class="n">maxsize</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_pool_maxsize</span><span class="p">,</span>
                <span class="n">block</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_pool_block</span><span class="p">,</span>
                <span class="o">**</span><span class="n">proxy_kwargs</span>
            <span class="p">)</span>
            <span class="n">self</span><span class="p">.</span><span class="n">proxy_manager</span><span class="p">[</span><span class="n">proxy_str</span><span class="p">]</span> <span class="o">=</span> <span class="n">manager</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">proxy_manager</span><span class="p">[</span><span class="n">proxy_str</span><span class="p">]</span>
</code></pre></div></div>

<h3 id="step-4-usage">Step 4: Usage</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">'</span><span class="s">__main__</span><span class="sh">'</span><span class="p">:</span>
    <span class="n">session</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nc">Session</span><span class="p">()</span>
    <span class="n">session</span><span class="p">.</span><span class="nf">mount</span><span class="p">(</span><span class="sh">"</span><span class="s">https://</span><span class="sh">"</span><span class="p">,</span> <span class="nc">PinnedHTTPAdapter</span><span class="p">())</span>

    <span class="c1"># Optional — blocks proxies that do TLS inspection (MITM)
</span>    <span class="n">session</span><span class="p">.</span><span class="n">proxies</span><span class="p">.</span><span class="nf">update</span><span class="p">({</span>
        <span class="sh">"</span><span class="s">http</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">http://0.0.0.0:8087</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">https</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">http://0.0.0.0:8087</span><span class="sh">"</span><span class="p">,</span>
    <span class="p">})</span>

    <span class="n">res</span> <span class="o">=</span> <span class="n">session</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">https://btwo.danleyb2.dev/api/v1/secure</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">res</span><span class="p">.</span><span class="nf">raise_for_status</span><span class="p">()</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">res</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="getting-your-spki-pin">Getting Your SPKI Pin</h2>

<p>You can extract the pin from any server that’s already running:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="n">ssl</span><span class="p">,</span> <span class="n">hashlib</span>
<span class="kn">from</span> <span class="n">cryptography</span> <span class="kn">import</span> <span class="n">x509</span>

<span class="n">context</span> <span class="o">=</span> <span class="n">ssl</span><span class="p">.</span><span class="nf">create_default_context</span><span class="p">()</span>
<span class="n">conn</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="nf">wrap_socket</span><span class="p">(</span><span class="n">ssl</span><span class="p">.</span><span class="nf">socket</span><span class="p">(),</span> <span class="n">server_hostname</span><span class="o">=</span><span class="sh">"</span><span class="s">btwo.danleyb2.dev</span><span class="sh">"</span><span class="p">)</span>
<span class="n">conn</span><span class="p">.</span><span class="nf">connect</span><span class="p">()</span>

<span class="n">spki</span> <span class="o">=</span> <span class="n">conn</span><span class="p">.</span><span class="nf">getpeercert</span><span class="p">(</span><span class="n">binary_form</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">cert</span> <span class="o">=</span> <span class="n">x509</span><span class="p">.</span><span class="nf">load_der_x509_certificate</span><span class="p">(</span><span class="n">spki</span><span class="p">)</span>
<span class="n">pin</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="nf">sha256</span><span class="p">(</span>
    <span class="n">cert</span><span class="p">.</span><span class="nf">public_key</span><span class="p">().</span><span class="nf">public_bytes</span><span class="p">(</span>
        <span class="n">serialization</span><span class="p">.</span><span class="n">Encoding</span><span class="p">.</span><span class="n">DER</span><span class="p">,</span>
        <span class="n">serialization</span><span class="p">.</span><span class="n">PublicFormat</span><span class="p">.</span><span class="n">SubjectPublicKeyInfo</span>
    <span class="p">)</span>
<span class="p">).</span><span class="nf">hexdigest</span><span class="p">()</span>
<span class="nf">print</span><span class="p">(</span><span class="n">pin</span><span class="p">)</span>  <span class="c1"># your sha256 SPKI fingerprint
</span></code></pre></div></div>

<h2 id="caveats">Caveats</h2>

<ul>
  <li>This pins to the leaf cert’s public key, not the full chain. If you have intermediate CAs with different keys, that doesn’t matter — only the server’s key matters for pinning.</li>
  <li><strong>Never skip the pin in production.</strong> The code above raises on mismatch, which is correct. A <code class="language-plaintext highlighter-rouge">verify=False</code> approach defeats the entire purpose.</li>
  <li>You’ll want a fallback pin in your config so you can rotate without downtime — just add more entries to <code class="language-plaintext highlighter-rouge">PINNED_KEYS[hostname]</code>.</li>
</ul>

<h2 id="dependencies">Dependencies</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cryptography &gt;= 41.0.0
requests &gt;= 2.31.0
urllib3 &gt;= 2.0.0
</code></pre></div></div>

<p>The full working example is in my <a href="https://github.com/danleyb2">spki-pinning</a> repo. Questions or improvements? Drop a comment below.</p>]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="Python" /><category term="Security" /><category term="SSL/TLS" /><category term="Cryptography" /><summary type="html"><![CDATA[Certificate pinning has long been the gold standard for preventing man-in-the-middle attacks in client applications. Most ecosystems support fingerprint-based cert pinning, but the industry is shifting toward SPKI (Subject Public Key Info) pinning — which pins the underlying public key rather than the certificate itself. This means rotating your CA won’t break your clients, while still defending against a rogue CA issuing an untrusted certificate.]]></summary></entry><entry><title type="html">My Journey as a Developer</title><link href="https://danleyb2.dev/career/development/fullstack/2026/06/24/fullstack-journey.html" rel="alternate" type="text/html" title="My Journey as a Developer" /><published>2026-06-24T12:00:00+00:00</published><updated>2026-06-24T12:00:00+00:00</updated><id>https://danleyb2.dev/career/development/fullstack/2026/06/24/fullstack-journey</id><content type="html" xml:base="https://danleyb2.dev/career/development/fullstack/2026/06/24/fullstack-journey.html"><![CDATA[<h2 id="learning-and-hobby-projects-2014-2016">Learning and Hobby Projects (2014-2016)</h2>

<p>While taking a <strong>Bachelor’s in Electrical and Electronics Engineering</strong> from The Technical University of Kenya (graduating 2018), I realized something: <em>I loved building things far more than analyzing circuits.</em>
The coursework gave me a technical foundation into Microprocessor systems, programming and simulation, internet database programming.</p>

<p>I pivoted. Hard. In the years leading up to graduation, what followed were marathon coding sessions on hobby projects in Java 7 and Python 2.7.</p>

<h2 id="interintel-technologies-20162020">InterIntel Technologies (2016–2020)</h2>

<p>My first real dive into professional development started at <strong>InterIntel Technologies</strong> in Nairobi as an Implementations Lead. The role was wide-open — there wasn’t one stack, so I learned them all.</p>

<ul>
  <li>Built and maintained a progressive web application that processed payloads to render dynamic interfaces (the frontend to the company’s site generator, the core of InterIntel)</li>
  <li>Developed <strong>Native Android apps</strong> alongside Polymer-based web apps, both backed by Django DRF APIs running on CentOS 7</li>
  <li>Led the transition across two major versions of the frontend framework, and eventually drove the migration away from deprecated frameworks entirely — no framework at all, just vanilla</li>
  <li>Spearheaded the company’s tech stack setup on a new client’s on-premise datacenter (nginx, EMV processing, USSD systems)</li>
  <li>Pushed the team’s first Android app to the Google Play Store</li>
  <li>Championed better DevOps workflows: Git branching models, CI pipelines, documentation standards</li>
</ul>

<p>The stack? <strong>Java, Python/Django, JavaScript, Polymer, Android, nginx, CentOS 7, USSD protocols, SQL.</strong> A true full-stack playground.</p>

<h2 id="parkpow--plate-recognizer-2020present">ParkPow / Plate Recognizer (2020–Present)</h2>

<p>A contract role turned into something bigger — joining <strong>ParkPow</strong> (now part of Plate Recognizer) in California as a Software Quality Assurance Tester, working with Vision AI models and building integrations around them.</p>

<p>This is where I got deep into:</p>
<ul>
  <li><strong>Computer Vision &amp; AI</strong>: Working with license plate recognition models (ALPR/ANPR), creating APIs and integrations</li>
  <li><strong>Docker &amp; DevOps</strong>: Built a <a href="https://github.com/danleyb2/py-installer-to-docker-desktop-extension">PyInstaller-to-Docker extension</a> for Docker Desktop, bridging desktop apps into the container ecosystem</li>
  <li><strong>Integration development</strong>: Building sites, scripts, and APIs around vision AI models</li>
  <li>Contributing to the <a href="https://github.com/parkpow/deep-license-plate-recognition/">deep-license-plate-recognition</a> codebase</li>
</ul>

<h2 id="freelance--parallel-work-2017present">Freelance &amp; Parallel Work (2017–Present)</h2>

<p>Running parallel to ParkPow, I’ve been a freelance developer taking on projects end-to-end:</p>

<ul>
  <li><strong><a href="https://salesleadgen.com">Salesleadgen</a></strong> — A leads generation web application I built and still maintain. Built with Python/Django, it scrapes LinkedIn and Sales Navigator data for lead prospecting.</li>
  <li><strong><a href="https://github.com/danleyb2/">AMZ Top 16</a></strong> — Tools for tracking Amazon best seller rankings across products</li>
  <li><strong>Inventory Tracker</strong> — A Discord bot that monitors price drops on German shoe retailers and alerts users via Discord</li>
  <li><strong>ABS R Android</strong> — An Android app tracking Amazon BSR (Best Seller Rank) for specific products</li>
  <li><strong>EAD (Email Attachments Downloader)</strong> — An Android app that periodically downloads email attachments to a chosen directory</li>
  <li><strong>Tumblr Poster</strong> — A Java Spring Boot desktop app with Selenium WebDriver to automate posting across multiple Tumblr blogs</li>
</ul>

<h2 id="open-source-contributions">Open Source Contributions</h2>

<p>I believe in giving back. Here are some of my notable open-source projects:</p>

<table>
  <thead>
    <tr>
      <th>Project</th>
      <th>Stack</th>
      <th>Stars</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://github.com/danleyb2/Instagram-API">Instagram-API</a></td>
      <td>Python</td>
      <td>⭐ 120 (as of June 2026)</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/alpr-anpr-android">alpr-anpr-android</a></td>
      <td>Java</td>
      <td>⭐ 3 (as of June 2026)</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/ussds">ussds</a></td>
      <td>Python/Django</td>
      <td>USSD utility for finding shortcodes</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/django-pde">django-pde</a></td>
      <td>Python</td>
      <td>Django packages dev environment</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/mitmproxy-docker">mitmproxy-docker</a></td>
      <td>Python</td>
      <td>Dockerized mitmproxy</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/rich-text-editor">rich-text-editor</a></td>
      <td>LitElement/WebComponents</td>
      <td>Lightweight WYSIWYG editor</td>
    </tr>
    <tr>
      <td><a href="https://github.com/danleyb2/passmark-cpu">passmark-cpu</a></td>
      <td>Docker</td>
      <td>CPU performance testing container</td>
    </tr>
  </tbody>
</table>

<p>And I’ve contributed to projects like Home Assistant integrations (EcoFlow Cloud, KPLC), Music Assistant documentation, Chiaki (PlayStation remote play client), and many others.</p>

<h2 id="what-i-build-with-today">What I Build With Today</h2>

<p>After nearly a decade in the trenches, these are my core tools and technologies:</p>

<ul>
  <li><strong>Languages</strong>: Python, JavaScript/TypeScript, Java, PHP</li>
  <li><strong>Backend</strong>: Django, DRF, Express.js, Node.js</li>
  <li><strong>Frontend</strong>: LitElement/WebComponents, Polymer, Vuetify, Materialize</li>
  <li><strong>Mobile</strong>: Android (Java/Kotlin)</li>
  <li><strong>DevOps</strong>: Docker, nginx, CentOS 7, CI/CD pipelines, Git workflows</li>
  <li><strong>AI/ML</strong>: Computer Vision (ALPR), Vision AI model integrations</li>
  <li><strong>Databases</strong>: PostgreSQL, MySQL, MongoDB, Realm DB</li>
  <li><strong>Infrastructure</strong>: Linux, Kubernetes, Helm charts, Proxmox, Home Assistant</li>
</ul>

<h2 id="the-pattern">The Pattern</h2>

<p>I’m also drawn to projects at the intersection of domains — automation + AI, mobile + web, DevOps + developer tooling. The interesting work lives in those overlap zones.</p>

<h2 id="whats-next">What’s Next</h2>

<p>I’m continuing to work on Plate Recognizer’s vision AI integrations and exploring where Home Assistant, automation, and AI converge. There’s a lot more to build.</p>]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="career" /><category term="development" /><category term="fullstack" /><summary type="html"><![CDATA[Learning and Hobby Projects (2014-2016)]]></summary></entry><entry><title type="html">Creating a Lightweight Rich Text Editor</title><link href="https://danleyb2.dev/lit-element/lit-html/web-components/2008/10/19/lit-rich-text-editor.html" rel="alternate" type="text/html" title="Creating a Lightweight Rich Text Editor" /><published>2008-10-19T12:18:25+00:00</published><updated>2008-10-19T12:18:25+00:00</updated><id>https://danleyb2.dev/lit-element/lit-html/web-components/2008/10/19/lit-rich-text-editor</id><content type="html" xml:base="https://danleyb2.dev/lit-element/lit-html/web-components/2008/10/19/lit-rich-text-editor.html"><![CDATA[]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="lit-element" /><category term="lit-html" /><category term="web-components" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Svg Icon</title><link href="https://danleyb2.dev/lit-element/lit-html/2008/10/19/svg-icon.html" rel="alternate" type="text/html" title="Svg Icon" /><published>2008-10-19T12:18:25+00:00</published><updated>2008-10-19T12:18:25+00:00</updated><id>https://danleyb2.dev/lit-element/lit-html/2008/10/19/svg-icon</id><content type="html" xml:base="https://danleyb2.dev/lit-element/lit-html/2008/10/19/svg-icon.html"><![CDATA[<p><code class="language-plaintext highlighter-rouge">svg-icon</code> component</p>]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="lit-element" /><category term="lit-html" /><summary type="html"><![CDATA[svg-icon component]]></summary></entry><entry><title type="html">Building Python Desktop Executables with GUIs</title><link href="https://danleyb2.dev/docker/pyinstaller/desktopgui/python/2008/06/25/docker-desktop-extension-python.html" rel="alternate" type="text/html" title="Building Python Desktop Executables with GUIs" /><published>2008-06-25T12:18:25+00:00</published><updated>2008-06-25T12:18:25+00:00</updated><id>https://danleyb2.dev/docker/pyinstaller/desktopgui/python/2008/06/25/docker-desktop-extension-python</id><content type="html" xml:base="https://danleyb2.dev/docker/pyinstaller/desktopgui/python/2008/06/25/docker-desktop-extension-python.html"><![CDATA[<p>For years, the go-to way to ship a Python desktop app has been clear: build your GUI in whatever framework makes sense, then bundle it with <strong>PyInstaller</strong> into a standalone binary. It works. But it’s also a fragile process — dependency hell, platform-specific quirks, and binaries that grow to 50MB+ for what should be a simple script.</p>

<p>Enter <strong>Docker Desktop Extensions</strong>.</p>

<p>They let you build desktop-quality GUIs using modern web tech (React, Vue, whatever), run the heavy lifting in containers, and distribute them through Docker’s official ecosystem. No compilers, no cross-platform headaches, just <code class="language-plaintext highlighter-rouge">docker compose up</code> under the hood.</p>

<h2 id="why-this-matters-for-python-tooling">Why This Matters for Python Tooling</h2>

<p>Many of the tools we build are CLI-first by nature. Plate recognition pipelines, data processing workflows, monitoring dashboards — they’re all perfectly fine from a terminal. But when you hand them to non-technical stakeholders, or try to demo them at a conference, “run this script” is not enough.</p>

<p>The traditional path:</p>
<ol>
  <li>Wrap the CLI in <strong>Gooey</strong> or <strong>tkinter</strong></li>
  <li>Install dependencies into a clean venv</li>
  <li>Run <code class="language-plaintext highlighter-rouge">pyinstaller --onefile</code> and pray</li>
  <li>Test on Windows, macOS, Linux separately</li>
  <li>Maintain three installer chains forever</li>
</ol>

<p>Here’s what that traditional path actually looks like:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># detect.py - A simple plate recognition CLI wrapped in Gooey
</span><span class="kn">from</span> <span class="n">gooey</span> <span class="kn">import</span> <span class="n">Gooey</span><span class="p">,</span> <span class="n">GooeyParser</span>
<span class="kn">import</span> <span class="n">subprocess</span>
<span class="kn">import</span> <span class="n">os</span>

<span class="nd">@Gooey</span><span class="p">(</span>
    <span class="n">program_name</span><span class="o">=</span><span class="sh">"</span><span class="s">Plate Recognizer</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="sh">"</span><span class="s">Upload an image and get license plate data back</span><span class="sh">"</span>
<span class="p">)</span>
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">parser</span> <span class="o">=</span> <span class="nc">GooeyParser</span><span class="p">()</span>
    <span class="n">parser</span><span class="p">.</span><span class="nf">add_argument</span><span class="p">(</span><span class="sh">"</span><span class="s">image</span><span class="sh">"</span><span class="p">,</span> <span class="n">widget</span><span class="o">=</span><span class="sh">"</span><span class="s">FileDialog</span><span class="sh">"</span><span class="p">,</span> <span class="nb">help</span><span class="o">=</span><span class="sh">"</span><span class="s">Input image file</span><span class="sh">"</span><span class="p">)</span>
    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="nf">parse_args</span><span class="p">()</span>

    <span class="n">result</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">(</span>
        <span class="p">[</span><span class="sh">"</span><span class="s">python</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">recognize.py</span><span class="sh">"</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">image</span><span class="p">],</span>
        <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">stdout</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="sh">"</span><span class="s">__main__</span><span class="sh">"</span><span class="p">:</span>
    <span class="nf">main</span><span class="p">()</span>
</code></pre></div></div>

<p>Then build it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>gooey pyinstaller
pyinstaller <span class="nt">--onefile</span> <span class="nt">--windowed</span> detect.py
</code></pre></div></div>

<p>That’s the whole process. One command for the GUI, one command for the binary.</p>

<p><strong>What works about this:</strong></p>
<ul>
  <li>Gooey auto-generates a window with file pickers, text outputs, etc. from your function signature</li>
  <li>PyInstaller bundles everything into a single <code class="language-plaintext highlighter-rouge">.exe</code> on Windows or a binary on macOS/Linux</li>
  <li>Works offline, no Docker required</li>
</ul>

<p><strong>What doesn’t work about this:</strong></p>
<ul>
  <li>The generated EXE is ~40-80MB even for trivial scripts (all of Python + stdlib bundled)</li>
  <li>Windows antivirus will flag it as suspicious 70% of the time</li>
  <li>macOS Gatekeeper rejects unsigned binaries instantly</li>
  <li>Linux requires matching glibc versions or it won’t run at all</li>
  <li>Every dependency update means rebuilding on every platform</li>
  <li>No built-in way to handle large models (onnx, torch, etc.) — they have to fit in your binary</li>
</ul>

<p>Compare that to the Docker path: your Python tool stays in a lean container image (~300MB for heavy ML workloads due to pre-packaged runtimes), runs identically everywhere Docker is installed. For lightweight tools the container can be under 50MB.</p>

<p>The traditional installers ship <em>every</em> time you update; containers only need to pull delta layers.</p>

<p>The Docker Extensions path:</p>
<ol>
  <li>Build a web-based GUI</li>
  <li>Wrap the CLI in a container</li>
  <li>Publish to Docker Hub</li>
  <li>Users run <code class="language-plaintext highlighter-rouge">docker extension install yourname/app</code></li>
</ol>

<p>One codebase. One binary. Every platform that runs Docker.</p>

<h2 id="docker-desktop-extension-architecture">Docker Desktop Extension Architecture</h2>

<p>A Docker Desktop Extension is deceptively simple. It’s composed of two main parts:</p>

<h3 id="the-ui-companion-app">The UI (Companion App)</h3>

<p>A web app — typically React, Vue, or Svelte — that renders inside a dedicated browser window in Docker Desktop. It communicates with the backend through <strong>Docker’s extension SDK</strong>, which exposes a JavaScript API for container management, CLI execution, and state sharing.</p>

<h3 id="the-backend">The Backend</h3>

<p>A container (or <code class="language-plaintext highlighter-rouge">docker-compose</code> stack) that runs your actual tooling. For Python workloads, this is where PyInstaller binaries live, where heavy ML models are loaded, where Docker-in-Docker pipelines execute. The UI is just the window; the backend does the work.</p>

<p>The extension manifests tie them together:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"desktop"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"companion"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"start"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"docker"</span><span class="p">,</span><span class="w"> </span><span class="s2">"compose"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-f"</span><span class="p">,</span><span class="w"> </span><span class="s2">"docker-compose.yml"</span><span class="p">,</span><span class="w"> </span><span class="s2">"up"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"stop"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"docker"</span><span class="p">,</span><span class="w"> </span><span class="s2">"compose"</span><span class="p">,</span><span class="w"> </span><span class="s2">"-f"</span><span class="p">,</span><span class="w"> </span><span class="s2">"docker-compose.yml"</span><span class="p">,</span><span class="w"> </span><span class="s2">"down"</span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"windows"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"My Tool"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"startCommands"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"docker compose -f docker-compose.yml up ui"</span><span class="p">],</span><span class="w">
        </span><span class="nl">"frontend"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3000</span><span class="p">,</span><span class="w"> </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/"</span><span class="w"> </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"integration"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"cli"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"exec"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mytool"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker exec mytool-app mytool-cli"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This gives you:</p>
<ul>
  <li><strong>Auto-lifecycle</strong>: Docker starts/stops your backend when the extension launches</li>
  <li><strong>CLI injection</strong>: Your tool becomes available in any terminal as <code class="language-plaintext highlighter-rouge">mytool</code> without installing anything globally</li>
  <li><strong>State sharing</strong>: Backend writes to <code class="language-plaintext highlighter-rouge">$HOME/.docker/desktop-ext/mytool/data/</code>, accessible by the frontend via API</li>
</ul>

<h2 id="building-one-the-practical-flow">Building One: The Practical Flow</h2>

<h3 id="1-backend--containerize-your-python-tool">1. Backend — Containerize your Python tool</h3>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> python:3.12-slim</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> plate-recognition-sdk flask
<span class="k">COPY</span><span class="s"> . /app</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["python", "app.py"]</span>
</code></pre></div></div>

<p>No PyInstaller needed. The container <em>is</em> the runtime.</p>

<h3 id="2-frontend--web-ui-with-docker-sdk">2. Frontend — Web UI with Docker SDK</h3>

<p>Using the official Docker Desktop Extension SDK:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@docker/extension-api-client</span><span class="dl">'</span>

<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Client</span><span class="p">()</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nf">runDetection</span><span class="p">(</span><span class="nx">imagePath</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">docker</span><span class="p">.</span><span class="nx">exec</span><span class="p">.</span><span class="nf">run</span><span class="p">({</span>
    <span class="na">containerName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">app-backend</span><span class="dl">'</span><span class="p">,</span>
    <span class="na">cmd</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">python</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">detect.py</span><span class="dl">'</span><span class="p">,</span> <span class="nx">imagePath</span><span class="p">]</span>
  <span class="p">})</span>
  <span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">stdout</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">@docker/extension-api-client</code> package is the bridge between your React/Vue UI and Docker Desktop’s internals.</p>

<h3 id="3-extension-manifest">3. Extension Manifest</h3>

<p>Place <code class="language-plaintext highlighter-rouge">extension.toml</code> or <code class="language-plaintext highlighter-rouge">docker-extension.json</code> in the root of your project. This file declares what Docker Desktop sees when a user installs your extension — window titles, icons, CLI bindings, permissions.</p>

<h3 id="4-packaging--publishing">4. Packaging &amp; Publishing</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Build the extension bundle</span>
docker buildx build <span class="nt">-t</span> myname/myextension:0.1.0 <span class="nb">.</span>

<span class="c"># Push to Docker Hub</span>
docker push myname/myextension:0.1.0

<span class="c"># Local install for testing</span>
docker extension <span class="nb">install </span>myname/myextension:0.1.0 <span class="nt">--target</span> docker-desktop
</code></pre></div></div>

<p>Users then install it from the command line or Docker Desktop’s Extensions marketplace. No websites, no click-through installer wizards, no antivirus flags on PyInstaller binaries.</p>

<h2 id="when-this-makes-sense-and-when-it-doesnt">When This Makes Sense (and When It Doesn’t)</h2>

<p><strong>Good fit:</strong></p>
<ul>
  <li>Python tools that need a GUI for demos, stakeholder access, or non-technical users</li>
  <li>Tools with ML models, heavy dependencies, or Docker-native workflows</li>
  <li>Distribution across macOS and Windows without managing native installers</li>
  <li>Internal team tooling where Docker Desktop is already installed</li>
</ul>

<p><strong>Not a fit if:</strong></p>
<ul>
  <li>You need system-level integrations (file watchers, kernel modules)</li>
  <li>Your audience won’t have Docker Desktop installed</li>
  <li>The tool is genuinely simple enough for Gooey + PyInstaller in an afternoon</li>
</ul>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>PyInstaller isn’t going away — it works for what it does. But if you’re building Python tools that need a GUI and you’re already comfortable with containers, Docker Desktop Extensions save an enormous amount of pain. One build, every platform, real dependency isolation, and an installation process that doesn’t look like malware from 2014.</p>

<p>The advantage is ecosystem integration — your extension can appear in Docker Desktop’s sidebar, hook into container logs automatically, share state with other extensions, and get installed through the same mechanism as any other Docker tool.</p>

<p>The future of desktop Python tooling might not be native binaries at all — it might be <code class="language-plaintext highlighter-rouge">docker run</code> wearing a React face.</p>]]></content><author><name>Brian Nyaundi</name><email>contact@danleyb2.dev</email></author><category term="Docker" /><category term="PyInstaller" /><category term="DesktopGUI" /><category term="Python" /><summary type="html"><![CDATA[For years, the go-to way to ship a Python desktop app has been clear: build your GUI in whatever framework makes sense, then bundle it with PyInstaller into a standalone binary. It works. But it’s also a fragile process — dependency hell, platform-specific quirks, and binaries that grow to 50MB+ for what should be a simple script.]]></summary></entry></feed>