<?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/fr/categories/iot/</link><description>Recent content in Iot on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Sun, 17 Nov 2019 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/categories/iot/index.xml" rel="self" type="application/rss+xml"/><item><title>D'un capteur à 10€ à un tableau de bord Home Assistant avec Raspberry Pi et MQTT</title><link>https://guillaumedelre.github.io/fr/2019/11/17/dun-capteur-%C3%A0-10-%C3%A0-un-tableau-de-bord-home-assistant-avec-raspberry-pi-et-mqtt/</link><pubDate>Sun, 17 Nov 2019 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2019/11/17/dun-capteur-%C3%A0-10-%C3%A0-un-tableau-de-bord-home-assistant-avec-raspberry-pi-et-mqtt/</guid><description>Un capteur BME280 à 10€, un Raspberry Pi, et un broker MQTT : construire un moniteur de climat de pièce qui alimente Home Assistant.</description><content:encoded><![CDATA[<p>La question était simple : quelle est la température et l&rsquo;humidité dans mon bureau à domicile en ce moment ? Pas la météo dehors, pas une moyenne de ville — les conditions réelles dans la pièce où je passe la majeure partie de ma journée. Ouvrir une application météo pour ça semblait mal.</p>
<p>Un Raspberry Pi tournait déjà sur l&rsquo;étagère. Un capteur BME280 coûte environ 10€. Ça aurait dû être un projet de week-end.</p>
<p>C&rsquo;était globalement le cas, à l&rsquo;exception de la partie où j&rsquo;ai supposé que lire un capteur de température signifiait lire un registre.</p>
<h2 id="quatre-fils-et-une-puce">Quatre fils et une puce</h2>
<p>Le Bosch BME280 mesure la température, l&rsquo;humidité et la pression atmosphérique par I²C. Quatre fils vers les pins GPIO du Raspberry Pi, activer l&rsquo;I²C dans <code>raspi-config</code>, et le capteur apparaît à l&rsquo;adresse <code>0x77</code> sur le 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>C&rsquo;est la partie facile. Le piège, c&rsquo;est ce qui se passe ensuite.</p>
<h2 id="on-ne-lit-pas-juste-la-température">On ne lit pas juste la température</h2>
<p>Le BME280 ne vous donne pas <code>21,5°C</code>. Il vous donne des valeurs ADC brutes : des entiers 20 bits qui ne signifient absolument rien par eux-mêmes. Pour obtenir une vraie température, il faut :</p>
<ol>
<li>Lire les coefficients de calibration que Bosch a gravés dans l&rsquo;EEPROM de la puce à l&rsquo;usine (registres <code>0x88</code>, <code>0xA1</code>, <code>0xE1</code>)</li>
<li>Appliquer les formules de compensation Bosch : de l&rsquo;arithmétique en virgule flottante double précision qui utilise ces coefficients pour transformer les valeurs brutes en vraies mesures</li>
<li>Attendre que la mesure soit terminée en scrutant le registre de statut</li>
</ol>
<p>La compensation de température seule prend la valeur brute, applique une correction quadratique avec trois constantes de calibration, et crache une valeur en centièmes de degrés Celsius. La pression dépend de la température corrigée. L&rsquo;humidité dépend des deux.</p>
<p>Tout est directement tiré de la <a href="https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf" target="_blank" rel="noopener noreferrer">datasheet Bosch</a>, rien d&rsquo;inventé. Mais ça signifie que le driver n&rsquo;est pas un programme de cinq lignes. C&rsquo;est implémenter une spec, pas importer une bibliothèque.</p>
<h2 id="le-rendre-accessible-par-le-réseau">Le rendre accessible par le réseau</h2>
<p>Une fois le driver fonctionnel, la question suivante était de savoir comment amener ces valeurs dans Home Assistant. Le chemin le plus simple : une API Flask avec deux endpoints.</p>
<p><code>GET /bme280</code> retourne la lecture courante en JSON. <code>GET /bme280/publish</code> lit le capteur et pousse les trois valeurs vers un broker MQTT. Un cron job sur le Pi appelle l&rsquo;endpoint publish toutes les quelques minutes, et Home Assistant récupère les valeurs en temps réel.</p>
<p>Le mécanisme de découverte MQTT a rendu la partie Home Assistant presque sans friction. Une commande <code>mosquitto_pub</code> par type de capteur — publier un payload JSON de config vers le bon topic — et les entités apparaissent automatiquement dans l&rsquo;UI. Pas d&rsquo;édition de <code>configuration.yaml</code>, pas de redémarrage requis.</p>
<pre tabindex="0"><code>BME280  ──I²C──►  bme280.py  ──►  Flask API  ──MQTT──►  Home Assistant
</code></pre><p>Le guide d&rsquo;installation complet est <a href="https://github.com/guillaumedelre/bme280" target="_blank" rel="noopener noreferrer">dans le repo</a>.</p>
<h2 id="ce-à-quoi-je-ne-mattendais-pas">Ce à quoi je ne m&rsquo;attendais pas</h2>
<p><strong>La calibration Bosch n&rsquo;est pas négociable.</strong> J&rsquo;ai commencé par lire le registre de température brute directement et le scaler naïvement. Le résultat était des nombres qui avaient l&rsquo;air presque plausibles et qui étaient complètement faux. L&rsquo;algorithme de compensation n&rsquo;est pas une décoration optionnelle, c&rsquo;est ce qui rend la sortie significative.</p>
<p><strong>Le polling bat les événements ici.</strong> Le capteur ne pousse pas de données, on lui demande une lecture. Un cron job toutes les minutes est tout ce dont on a besoin pour surveiller une pièce. Le streaming en temps réel serait excessif et userait probablement le capteur plus vite.</p>
<p><strong>La découverte MQTT est sous-estimée.</strong> Déclarer manuellement les capteurs dans <code>configuration.yaml</code> fonctionne, mais l&rsquo;auto-découverte semble simplement juste. Publier un payload de config une fois, et Home Assistant s&rsquo;en occupe. Ajouter un nouveau type de capteur plus tard prend environ trente secondes.</p>
<p>La pièce est maintenant à 21,4°C et 47% d&rsquo;humidité. Je le sais sans rien ouvrir.</p>
<h2 id="une-note-sur-le-sensorapi-officiel-bosch">Une note sur le SensorAPI officiel Bosch</h2>
<p>En écrivant le driver, j&rsquo;ai jeté un œil au <a href="https://github.com/boschsensortec/BME280_SensorAPI" target="_blank" rel="noopener noreferrer">SensorAPI officiel Bosch</a> pour référence. Deux choses ont retenu mon attention.</p>
<p>L&rsquo;exemple userspace Linux ne fonctionne pas vraiment sur Raspberry Pi sans modifications : <code>ioctl</code> est appelé avant que <code>dev_addr</code> soit assigné, donc l&rsquo;adresse du périphérique I²C n&rsquo;est jamais correctement définie. Le correctif est évident une fois qu&rsquo;on le voit, et plusieurs contributeurs ont buté sur le même bug indépendamment, mais ils attendaient en PR depuis des années. Certains attendent encore.</p>
<p>Il y a aussi la <a href="https://github.com/boschsensortec/BME280_SensorAPI/pull/94" target="_blank" rel="noopener noreferrer">PR #94</a> (toujours ouverte début 2025), signalant un comportement indéfini dans <code>bme280_get_sensor_mode()</code> : l&rsquo;opérande gauche d&rsquo;un <code>&amp;</code> bit à bit est une variable non initialisée, détecté par analyse statique.</p>
<p>La puce elle-même est excellente. Mais le code de référence du fabricant est un point de départ, pas un évangile. Implémenter l&rsquo;algorithme de compensation directement depuis la datasheet signifiait que je comprenais chaque ligne. Quand une lecture paraît bizarre, il n&rsquo;y a pas de mystérieuse bibliothèque C à blâmer.</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;">Driver Python pour le capteur BME280 — température, humidité et pression par I²C, avec publication MQTT et intégration Home Assistant.</p>
</div>
]]></content:encoded></item><item><title>Contrôler un lance-missiles USB en HTTP avec FastAPI et Docker</title><link>https://guillaumedelre.github.io/fr/2017/02/21/contr%C3%B4ler-un-lance-missiles-usb-en-http-avec-fastapi-et-docker/</link><pubDate>Tue, 21 Feb 2017 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2017/02/21/contr%C3%B4ler-un-lance-missiles-usb-en-http-avec-fastapi-et-docker/</guid><description>Comment on a branché un lance-missiles USB en mousse sur le pipeline CI — et ce que Docker, udev et WSL2 avaient à dire là-dessus.</description><content:encoded><![CDATA[<p>La règle était simple : celui qui casse le build CI offre le café à l&rsquo;équipe. Ça a marché un moment. Puis quelqu&rsquo;un a proposé qu&rsquo;on ait un retour plus immédiat. Quelque chose de physique. Quelque chose qui tire.</p>
<p>Un <a href="http://www.dreamcheeky.com/thunder-missile-launcher" target="_blank" rel="noopener noreferrer">Dream Cheeky Thunder</a> a atterri sur un bureau peu après. Quatre missiles en mousse, un câble USB, et un consensus d&rsquo;équipe très clair : le brancher au cluster, le câbler au pipeline de build, et laisser le CI décider qui mérite une volée.</p>
<p>Le lanceur devait répondre à des appels HTTP depuis n&rsquo;importe où sur le réseau. Sans driver, sans GUI, sans visée manuelle. Juste un endpoint qui le fait tirer dans la direction du bureau du coupable.</p>
<p>Voilà l&rsquo;histoire de <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="pas-de-sdk-pas-de-docs-pas-de-problème">Pas de SDK, pas de docs, pas de problème</h2>
<p>Dream Cheeky n&rsquo;a jamais publié de spec de protocole. Le lanceur parle USB HID brut, et le seul point de départ était un script Python vendorisé de 2012 qui traînait dans des fils de forum. Vendor ID <code>0x2123</code>, product ID <code>0x1010</code>, et une poignée d&rsquo;octets de contrôle que quelqu&rsquo;un avait rétro-ingénié des années auparavant.</p>
<p>C&rsquo;était suffisant. Le protocole est simple : envoyer une séquence d&rsquo;octets pour bouger les moteurs, en envoyer une autre pour tirer. La partie délicate : le lanceur n&rsquo;a aucun retour de position. Pas d&rsquo;encodeurs, pas de fins de course en dehors des butées physiques aux extrémités. On le pilote à l&rsquo;aveugle.</p>
<h2 id="du-usb-au-http">Du USB au HTTP</h2>
<p>Le pipeline CI devait déclencher le lanceur par le réseau. Un script local ne suffisait pas — le lanceur devait être accessible depuis n&rsquo;importe quelle machine du cluster, y compris le serveur de build. Donc : une API REST.</p>
<p>FastAPI était le choix évident. Le flux de ciblage côté CI se résume à trois appels HTTP :</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 vers une position connue</span>
</span></span><span style="display:flex;"><span>curl -X POST http://localhost:8000/yaw/20    <span style="color:#75715e"># rotation vers le bureau du coupable</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>L&rsquo;appel <code>/park</code> est plus important qu&rsquo;il n&rsquo;y paraît. Puisque le lanceur n&rsquo;a pas de retour de position, le serveur estime l&rsquo;angle courant en suivant le temps de rotation des moteurs. Cette estimation dérive. Un choc sur le hardware, une commande interrompue, ou simplement l&rsquo;imprécision du tracking temporel — tout s&rsquo;accumule. Le parking pousse les deux moteurs contre les butées physiques en balayage complet, ce qui garantit l&rsquo;alignement quelle que soit la représentation interne du serveur. Sans ça, la visée est une approximation.</p>
<p>La référence complète de l&rsquo;API est <a href="https://github.com/guillaumedelre/dream-cheeky-thunder/blob/develop/docs/api.md" target="_blank" rel="noopener noreferrer">dans le repo</a>. Il y a aussi une UI web si vous préférez cliquer plutôt que <code>curl</code>.</p>
<h2 id="docker-ne-connaît-pas-lusb">Docker ne connaît pas l&rsquo;USB</h2>
<p>Faire tourner ça dans un conteneur Docker sur le cluster, c&rsquo;est là que les choses ont commencé à devenir intéressantes : les conteneurs ne voient pas les périphériques USB par défaut.</p>
<p>Le mount <code>devices</code> dans <code>compose.yaml</code> expose le bus USB au conteneur :</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>Pas suffisant. Première exécution : <code>USBError: [Errno 13] Access denied</code>. Le nœud de device est bien là dans le conteneur, mais il hérite des permissions du host, et sur le host seul root peut l&rsquo;ouvrir par défaut.</p>
<p>La solution : une règle udev. Déposer un fichier dans <code>/etc/udev/rules.d/</code>, et le kernel applique le bon groupe et les bonnes permissions quand le device se branche. Après ça, l&rsquo;utilisateur du conteneur peut l&rsquo;ouvrir sans privilèges élevés. La règle est fournie avec le projet, les instructions d&rsquo;installation sont <a href="https://github.com/guillaumedelre/dream-cheeky-thunder/blob/develop/docs/setup-linux.md" target="_blank" rel="noopener noreferrer">dans la doc</a>.</p>
<h2 id="wsl2-a-rendu-ça-intéressant">WSL2 a rendu ça intéressant</h2>
<p>La moitié de l&rsquo;équipe tourne sous Windows avec Docker Desktop sur WSL2. C&rsquo;est là que ça devient créatif.</p>
<p>WSL2 n&rsquo;a pas accès aux périphériques USB par défaut : le kernel Windows les détient, et le mount <code>devices</code> seul ne fait rien parce que WSL2 ne voit simplement pas le hardware. La solution est <a href="https://github.com/dorssel/usbipd-win" target="_blank" rel="noopener noreferrer">usbipd-win</a>, qui transfère le périphérique USB de Windows vers le kernel WSL2 par IP. Une fois ça fait, le chemin Linux fonctionne à l&rsquo;identique : règle udev, mount <code>devices</code>, terminé.</p>
<p>L&rsquo;attachement ne survit pas aux redémarrages, cependant. usbipd v4+ a ajouté un mécanisme de policy qui automatise la reconnexion, ce qui a mis fin au mystère du &ldquo;ça marchait hier&rdquo; qui nous agaçait depuis des jours.</p>
<h2 id="ce-qui-nous-a-vraiment-surpris">Ce qui nous a vraiment surpris</h2>
<p><strong>Le positionnement temporel fonctionne suffisamment bien.</strong> Sans encodeurs, on s&rsquo;attendait à ce que le tracking d&rsquo;angle soit quasi-inutilisable. En pratique, le parking avant chaque séquence le maintenait assez précis pour viser un bureau spécifique de manière fiable. Pas au millimètre, mais la précision missile en mousse, ça convient.</p>
<p><strong>Le mount <code>devices</code> est nécessaire mais pas suffisant.</strong> L&rsquo;erreur de permission était déroutante précisément parce que le device était clairement visible dans le conteneur. La règle udev est la partie que la plupart des tutoriels passent discrètement sous silence.</p>
<p><strong>La règle café n&rsquo;a plus jamais été la même après ça.</strong> Une fois le lanceur câblé au pipeline, les builds cassés sont devenus beaucoup plus motivants à corriger.</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 — contrôle HTTP pour le lance-missiles USB Dream Cheeky Thunder. Pull requests bienvenus, surtout si vous avez une meilleure approche de calibration d'angle.</p>
</div>
]]></content:encoded></item></channel></rss>