<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Iot on Guillaume Delré</title><link>https://guillaumedelre.github.io/categories/iot/</link><description>Recent content in Iot on Guillaume Delré</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 17 Nov 2019 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/categories/iot/index.xml" rel="self" type="application/rss+xml"/><item><title>From a €10 sensor to a Home Assistant dashboard with a Raspberry Pi and MQTT</title><link>https://guillaumedelre.github.io/2019/11/17/from-a-10-sensor-to-a-home-assistant-dashboard-with-a-raspberry-pi-and-mqtt/</link><pubDate>Sun, 17 Nov 2019 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2019/11/17/from-a-10-sensor-to-a-home-assistant-dashboard-with-a-raspberry-pi-and-mqtt/</guid><description>A €10 BME280 sensor, a Raspberry Pi, and an MQTT broker: building a room climate monitor that feeds Home Assistant.</description><content:encoded><![CDATA[<p>The question was simple: what&rsquo;s the temperature and humidity in my home office right now? Not the weather outside, not a city average — the actual conditions in the room where I spend most of my day. Opening a weather app for that felt wrong.</p>
<p>A Raspberry Pi was already running on the shelf. A BME280 sensor costs around €10. This should have been a weekend project.</p>
<p>It mostly was, except for the part where I assumed reading a temperature sensor meant reading a register.</p>
<h2 id="four-wires-and-a-chip">Four wires and a chip</h2>
<p>The Bosch BME280 measures temperature, humidity, and atmospheric pressure over I²C. Four wires to the Raspberry Pi GPIO pins, enable I²C in <code>raspi-config</code>, and the sensor shows up at address <code>0x77</code> on the bus:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>i2cdetect -y <span style="color:#ae81ff">1</span>
</span></span></code></pre></div><p>That&rsquo;s the easy part. The catch is what happens next.</p>
<h2 id="you-dont-just-read-the-temperature">You don&rsquo;t just read the temperature</h2>
<p>The BME280 doesn&rsquo;t hand you <code>21.5°C</code>. It gives you raw ADC values: 20-bit integers that mean absolutely nothing by themselves. To get an actual temperature, you have to:</p>
<ol>
<li>Read the calibration coefficients Bosch burned into the chip&rsquo;s EEPROM at the factory (registers <code>0x88</code>, <code>0xA1</code>, <code>0xE1</code>)</li>
<li>Apply Bosch&rsquo;s compensation formulas: double-precision floating point arithmetic that uses those coefficients to turn raw values into real measurements</li>
<li>Wait for the measurement to finish by polling the status register</li>
</ol>
<p>The temperature compensation alone takes the raw value, applies a quadratic correction with three calibration constants, and spits out a value in hundredths of degrees Celsius. Pressure depends on the corrected temperature. Humidity depends on both.</p>
<p>It&rsquo;s all straight from the <a href="https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf" target="_blank" rel="noopener noreferrer">Bosch datasheet</a>, nothing clever. But it does mean the driver isn&rsquo;t a five-liner. It&rsquo;s implementing a spec, not importing a library.</p>
<h2 id="making-it-network-accessible">Making it network-accessible</h2>
<p>Once the driver worked, the next question was how to get those values into Home Assistant. The simplest path: a Flask API with two endpoints.</p>
<p><code>GET /bme280</code> returns the current reading as JSON. <code>GET /bme280/publish</code> reads the sensor and pushes the three values to an MQTT broker. A cron job on the Pi calls the publish endpoint every few minutes, and Home Assistant picks up the values in real time.</p>
<p>The MQTT discovery mechanism made the Home Assistant side almost frictionless. One <code>mosquitto_pub</code> command per sensor type — publishing a JSON config payload to the right topic — and the entities appear automatically in the UI. No <code>configuration.yaml</code> editing, no restart required.</p>
<pre tabindex="0"><code>BME280  ──I²C──►  bme280.py  ──►  Flask API  ──MQTT──►  Home Assistant
</code></pre><p>The full setup guide is <a href="https://github.com/guillaumedelre/bme280" target="_blank" rel="noopener noreferrer">in the repo</a>.</p>
<h2 id="what-i-didnt-expect">What I didn&rsquo;t expect</h2>
<p><strong>The Bosch calibration is non-negotiable.</strong> I started by reading the raw temperature register directly and scaling it naively. The result was numbers that looked almost plausible and were completely wrong. The compensation algorithm isn&rsquo;t optional decoration, it&rsquo;s what makes the output mean anything.</p>
<p><strong>Polling beats events here.</strong> The sensor doesn&rsquo;t push data, you ask it for a reading. A cron job every minute is all you need for room monitoring. Real-time streaming would be overkill and would probably wear out the sensor faster.</p>
<p><strong>MQTT discovery is underrated.</strong> Manually declaring sensors in <code>configuration.yaml</code> works, but auto-discovery just feels right. Publish a config payload once, and Home Assistant takes it from there. Adding a new sensor type later takes about thirty seconds.</p>
<p>The room is now 21.4°C and 47% humidity. I know this without opening anything.</p>
<h2 id="a-note-on-the-official-bosch-sensorapi">A note on the official Bosch SensorAPI</h2>
<p>While writing the driver I peeked at the <a href="https://github.com/boschsensortec/BME280_SensorAPI" target="_blank" rel="noopener noreferrer">official Bosch SensorAPI</a> for reference. Two things caught my attention.</p>
<p>The Linux userspace example doesn&rsquo;t actually work on a Raspberry Pi out of the box. Several contributors tripped over the same bug independently: <code>ioctl</code> is called before <code>dev_addr</code> is assigned, so the I²C device address never gets set properly. The fix is obvious once you see it, and multiple PRs documented it, but they sat open for years. Some still are.</p>
<p>Then there&rsquo;s <a href="https://github.com/boschsensortec/BME280_SensorAPI/pull/94" target="_blank" rel="noopener noreferrer">PR #94</a> (still open as of early 2025), reporting undefined behavior in <code>bme280_get_sensor_mode()</code>: the left operand of a bitwise <code>&amp;</code> is an uninitialized variable, caught by static analysis.</p>
<p>The chip itself is great. But manufacturer reference code is a starting point, not gospel. Implementing the compensation algorithm straight from the datasheet meant I understood every line of it. When a reading looks weird, there&rsquo;s no mystery C library to blame.</p>
<div style="border: 1px solid #e8e8e8; padding: 16px; margin-top: 2em; border-radius: 3px;">
  <img src="https://cdn.simpleicons.org/github" width="20" style="vertical-align: middle; margin-right: 8px;" />
  <strong><a href="https://github.com/guillaumedelre/bme280" target="_blank" rel="noopener noreferrer">guillaumedelre/bme280</a></strong>
  <p style="margin: 8px 0 0; color: #828282; font-size: 14px;">Python driver for the BME280 sensor — temperature, humidity, and pressure over I²C, with MQTT publishing and Home Assistant integration.</p>
</div>
]]></content:encoded></item><item><title>Controlling a USB missile launcher over HTTP with FastAPI and Docker</title><link>https://guillaumedelre.github.io/2017/02/21/controlling-a-usb-missile-launcher-over-http-with-fastapi-and-docker/</link><pubDate>Tue, 21 Feb 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/2017/02/21/controlling-a-usb-missile-launcher-over-http-with-fastapi-and-docker/</guid><description>How we wired a USB foam missile launcher to the CI pipeline — and what Docker, udev, and WSL2 had to say about it.</description><content:encoded><![CDATA[<p>The rule was simple: whoever breaks the CI build owes the team a coffee. It worked fine for a while. Then someone suggested we needed something with more immediate feedback. Something physical. Something that fires.</p>
<p>A <a href="http://www.dreamcheeky.com/thunder-missile-launcher" target="_blank" rel="noopener noreferrer">Dream Cheeky Thunder</a> appeared on a desk shortly after. Four foam missiles, a USB cable, and a very clear team consensus: hook it to the cluster, wire it to the build pipeline, and let the CI decide who deserves a volley.</p>
<p>The launcher needed to respond to HTTP calls from anywhere on the network. No driver, no GUI, no manual aiming. Just an endpoint that makes it shoot in the direction of the guilty party&rsquo;s desk.</p>
<p>This is the story of <a href="https://github.com/guillaumedelre/dream-cheeky-thunder" target="_blank" rel="noopener noreferrer">dream-cheeky-thunder</a>.</p>
<p><img alt="Dream Cheeky Thunder" loading="lazy" src="https://raw.githubusercontent.com/guillaumedelre/dream-cheeky-thunder/develop/docs/Dream-Cheeky-Thunder.jpg"></p>
<h2 id="no-sdk-no-docs-no-problem">No SDK, no docs, no problem</h2>
<p>Dream Cheeky never published a protocol spec. The launcher speaks raw USB HID, and the only starting point was a vendored Python script from 2012 floating around in forum threads. Vendor ID <code>0x2123</code>, product ID <code>0x1010</code>, and a handful of control bytes that someone had reverse engineered years before.</p>
<p>That was enough. The protocol is simple: send a byte sequence to move the motors, send another to fire. The tricky part is that the launcher has no position feedback. No encoders, no limit switches beyond the physical hard stops at the extremes. You drive it blind.</p>
<h2 id="from-usb-to-http">From USB to HTTP</h2>
<p>The CI pipeline needed to trigger the launcher over the network. A local script wasn&rsquo;t going to cut it — the launcher had to be reachable from any machine on the cluster, including the build server. So: a REST API.</p>
<p>FastAPI was the obvious choice. The targeting flow from the CI side ends up being three HTTP calls:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -X POST http://localhost:8000/park      <span style="color:#75715e"># reset to known position</span>
</span></span><span style="display:flex;"><span>curl -X POST http://localhost:8000/yaw/20    <span style="color:#75715e"># rotate toward guilty desk</span>
</span></span><span style="display:flex;"><span>curl -X POST <span style="color:#e6db74">&#34;http://localhost:8000/fire?shots=2&#34;</span>
</span></span></code></pre></div><p>The <code>/park</code> call matters more than it looks. Since the launcher has no position feedback, the server estimates the current angle by tracking how long the motors have been running. That estimate drifts. Bumping the hardware, interrupting a command, or just the imprecision of time-based tracking — they all accumulate. Parking drives both motors against the physical hard stops at full sweep, which guarantees alignment regardless of what the server thinks it knows. Skip it, and your aim is a guess.</p>
<p>The full API reference is <a href="https://github.com/guillaumedelre/dream-cheeky-thunder/blob/develop/docs/api.md" target="_blank" rel="noopener noreferrer">in the repo</a>. There&rsquo;s also a web UI if you prefer clicking over <code>curl</code>.</p>
<h2 id="docker-knows-nothing-about-usb">Docker knows nothing about USB</h2>
<p>Running this in a Docker container on the cluster was where the fun really started: containers don&rsquo;t see USB devices by default.</p>
<p>The <code>devices</code> mount in <code>compose.yaml</code> exposes the USB bus to the container:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">devices</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#ae81ff">/dev/bus/usb:/dev/bus/usb</span>
</span></span></code></pre></div><p>Not enough. First run came back with <code>USBError: [Errno 13] Access denied</code>. The device node is there inside the container, but it inherits permissions from the host, and on the host only root can open it by default.</p>
<p>The fix is a udev rule. Drop one file into <code>/etc/udev/rules.d/</code>, and the kernel sets the right group and permissions when the device plugs in. After that, the container user can open it without needing elevated privileges. The rule ships with the project, setup instructions are <a href="https://github.com/guillaumedelre/dream-cheeky-thunder/blob/develop/docs/setup-linux.md" target="_blank" rel="noopener noreferrer">in the docs</a>.</p>
<h2 id="wsl2-made-it-interesting">WSL2 made it interesting</h2>
<p>Half the team runs Windows with Docker Desktop on WSL2. That&rsquo;s where things got creative.</p>
<p>WSL2 has no access to USB devices by default: the Windows kernel holds them, and the <code>devices</code> mount alone does nothing because WSL2 simply doesn&rsquo;t see the hardware. The fix is <a href="https://github.com/dorssel/usbipd-win" target="_blank" rel="noopener noreferrer">usbipd-win</a>, which forwards the USB device from Windows into the WSL2 kernel over IP. Once that&rsquo;s done, the Linux path works exactly the same: udev rule, <code>devices</code> mount, done.</p>
<p>The attachment doesn&rsquo;t survive reboots, though. usbipd v4+ added a policy mechanism that automates reconnection, which killed the &ldquo;it worked yesterday&rdquo; mystery that had been annoying us for days.</p>
<h2 id="what-actually-surprised-us">What actually surprised us</h2>
<p><strong>Time-based positioning works well enough.</strong> No encoders meant we went in expecting the angle tracking to be basically useless. Turns out, parking before every sequence kept it accurate enough to reliably aim at a specific desk. Not millimeter precision, but foam missile precision is fine.</p>
<p><strong>The <code>devices</code> mount is necessary but not sufficient.</strong> The permission error was confusing precisely because the device was clearly visible inside the container. The udev rule is the bit most tutorials quietly skip.</p>
<p><strong>The coffee rule was never the same after this.</strong> Once the launcher was wired to the pipeline, broken builds suddenly became a lot more motivating to fix.</p>
<div style="border: 1px solid #e8e8e8; padding: 16px; margin-top: 2em; border-radius: 3px;">
  <img src="https://cdn.simpleicons.org/github" width="20" style="vertical-align: middle; margin-right: 8px;" />
  <strong><a href="https://github.com/guillaumedelre/dream-cheeky-thunder" target="_blank" rel="noopener noreferrer">guillaumedelre/dream-cheeky-thunder</a></strong>
  <p style="margin: 8px 0 0; color: #828282; font-size: 14px;">FastAPI + Docker + PyUSB — HTTP control for the Dream Cheeky Thunder USB missile launcher. Pull requests welcome, especially if you have a better angle calibration approach.</p>
</div>
]]></content:encoded></item></channel></rss>