<?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>@Olivier&#39;s</title>
    <link>https://coupelon.net/</link>
    <description>Recent content on @Olivier&#39;s</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>fr</language>
    <managingEditor>coupelon&#43;blog@gmail.com (Olivier Coupelon)</managingEditor>
    <webMaster>coupelon&#43;blog@gmail.com (Olivier Coupelon)</webMaster>
    <copyright>Olivier Coupelon (CC BY 4.0)</copyright>
    <lastBuildDate>Sun, 21 Dec 2025 23:28:23 +0100</lastBuildDate>
    <atom:link href="https://coupelon.net/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Construire un proxy RSS pour Telegram avec MTProto et GitHub Spec Kit</title>
      <link>https://coupelon.net/posts/spec-driven-dev/</link>
      <pubDate>Sun, 21 Dec 2025 23:28:23 +0100</pubDate><author>coupelon&#43;blog@gmail.com (Olivier Coupelon)</author>
      <guid>https://coupelon.net/posts/spec-driven-dev/</guid>
      <description>&lt;p&gt;Je teste depuis plusieurs mois les approches de développement « Spec Driven », qui me semblent promises à un bel avenir grâce à l&amp;rsquo;essor des IA génératives. Ici, on ne se contente pas de « prompter » un besoin pour voir son code généré : on adopte un nouveau paradigme où la spécification (re)devient la pierre angulaire du projet. On utilise ensuite l&amp;rsquo;IA pour produire les User Stories, les tâches et, enfin, le code.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Je teste depuis plusieurs mois les approches de développement « Spec Driven », qui me semblent promises à un bel avenir grâce à l&rsquo;essor des IA génératives. Ici, on ne se contente pas de « prompter » un besoin pour voir son code généré : on adopte un nouveau paradigme où la spécification (re)devient la pierre angulaire du projet. On utilise ensuite l&rsquo;IA pour produire les User Stories, les tâches et, enfin, le code.</p>
<ul>
<li><strong>L’intérêt majeur réside dans la conservation de l&rsquo;intention</strong>. On a rarement tendance à historiser ses prompts ; ici, la spécification sert de trace et de garde-fou. On revient en quelque sorte à une méthode Waterfall où la spécification prime, à la différence près que le délai pour obtenir le produit est drastiquement réduit. On passe de plusieurs mois ou années à quelques minutes ou heures pour un résultat équivalent. On peut donc itérer très facilement, directement depuis la spécification.</li>
<li><strong>Le LLM utilisé a bien sûr son importance</strong>, mais la maîtrise de la chaîne de raisonnement offre une plus grande liberté quant à son choix. C&rsquo;est donc une solution intéressante pour garder le contrôle, voire garantir sa souveraineté.</li>
<li><strong>Il existe actuellement trois grandes méthodes</strong> de développement par IA générative orientées spécifications (vu de ma fenêtre) : BMAD, OpenSpec et GitHub Spec Kit. C&rsquo;est cette dernière qui me séduit le plus, et que je vais illustrer par la suite.</li>
</ul>
<p>L&rsquo;objectif de ce projet est double : mettre en œuvre l&rsquo;approche Spec Driven Development sur un cas concret — une méthode que j&rsquo;ai déjà testée et qui me plaît beaucoup — et expérimenter l&rsquo;utilisation de <strong>MTProto</strong>. Il s&rsquo;agit de l&rsquo;API utilisateur de Telegram qui, contrairement à l&rsquo;API bot historique, permet de créer un client alternatif, à l&rsquo;instar du client officiel.</p>
<p>C&rsquo;est particulièrement utile car le client officiel permet de parcourir un canal auquel on n&rsquo;est pas abonné, ce qui m&rsquo;arrive parfois lors de ma veille techno, alors que l&rsquo;API Bot ne le permet pas. L&rsquo;idée est de pouvoir être notifié de l&rsquo;apparition d&rsquo;une news ou d&rsquo;un mot-clé sur un canal identifié, sans avoir à s&rsquo;y abonner ni à interagir.</p>
<p>Le projet consiste donc à transformer n&rsquo;importe quel canal Telegram public en flux RSS 2.0, sans abonnement, via une API FastAPI asynchrone. La librairie <a href="https://docs.telethon.dev/">Telethon</a>, qui implémente MTProto, sera utilisée à cet effet.</p>
<p>Le code source du projet avec son historique est disponible sur mon repository <a href="https://github.com/coupelon/telegram-rss">GitHub</a>.</p>
<h2 id="cadre-github-spec-kit">Cadre GitHub Spec Kit</h2>
<p>Il faut d&rsquo;abord initialiser un nouveau projet avec GitHub Spec Kit. Pour cela, une fois l&rsquo;outil installé, il suffit d&rsquo;exécuter la commande <code>specify init telegram-rss</code>.
Un prompt vous demande alors deux choses :</p>
<ul>
<li>Le choix de votre solution d&rsquo;IA générative. Je dispose de GitHub Copilot, qui sera donc utilisé ici, mais Spec Kit supporte actuellement 18 agents différents.</li>
<li>Le choix de votre terminal préféré pour l&rsquo;exécution des commandes.</li>
</ul>
<p><img src="/images/spec-kit-init.png" alt="Le résumé Spec Kit suite à init"></p>
<p>Le dossier ainsi créé peut ensuite être ouvert dans votre éditeur favori (VSCode pour ma part). La première étape consiste à définir les contraintes de constitution du projet. Le fichier <code>constitution.md</code> servira, lors de toutes les commandes ultérieures, à rappeler à l&rsquo;IA les contraintes et le contexte devant guider ses décisions. La commande :</p>





<pre tabindex="0"><code>/speckit.constitution Le programme doit être disponible sous forme d&#39;une image docker, pas besoin de Kubernetes. Pas de base de données externe, si besoin un SQLite peut suffire dans un volume qui sera éventuellement exporté, au même endroit que les paramètres Telegram éventuels. La librairie d&#39;accès à Telegram doit obligatoirement être Telethon (https://docs.telethon.dev/)</code></pre><p>permet de générer ce fichier. Elle intègre les invariants et les choix techniques sur lesquels on souhaite impérativement s&rsquo;appuyer.</p>
<h2 id="spécifications">Spécifications</h2>
<p>C&rsquo;est ici que l&rsquo;on pérennise le projet. Au lieu de demander directement à une IA de créer le code, on génère d&rsquo;abord une spécification, un plan et des tâches qui serviront de pivot à tous les développements futurs. Le framework propose des « slash commandes », des prompts augmentés ciblant les modèles les plus performants pour la tâche en cours.</p>





<pre tabindex="0"><code>/speckit.specify Cette application est un serveur exposant un flux RSS pour chaque channel telegram demandé, sans que le compte utilisateur Telegram s&#39;y abonne, en utilisant le protocole MTProto de https://docs.telethon.dev/. Le serveur récupère les flux à chaque requête, avec le schéma d&#39;url suivant : http://serveur:port/rss/channel, où channel est remplacé par le nom du channel telegram demandé.</code></pre><p>Cette commande décrit notre usage. À partir de là, le fichier <code>spec.md</code> est généré. Il doit être <strong>attentivement complété par l&rsquo;humain</strong>, car c&rsquo;est lui qui fixe le cadre de l&rsquo;implémentation.</p>
<p>À noter : chaque nouvelle spécification est isolée dans un dossier distinct afin de bien séparer les intentions de leur implémentation. C&rsquo;est propre, clair, et l&rsquo;intention initiale est parfaitement conservée pour la relecture.</p>
<p><img src="/images/spec-md.png" alt="L&rsquo;arborescence du projet terminé et le fichier de specification"></p>
<h2 id="planification-et-création-des-tâches">Planification et création des tâches</h2>
<p>Les commandes suivantes nécessitent potentiellement moins de prompting, car le besoin est déjà bien défini. Elles doivent permettre de décrire le projet avec assez de précision pour que l&rsquo;implémentation ne laisse plus de place au doute.</p>





<pre tabindex="0"><code>/speckit.plan L&#39;application doit être autonome, en python, et utiliser MTProto de https://docs.telethon.dev/</code></pre><p>La planification apporte le détail technique. Ici, rien de sorcier : nous avons déjà fourni nos contraintes. J&rsquo;enfonce le clou, même si c&rsquo;est probablement superflu.</p>
<p>Maintenant il reste à produire la liste des tâches unitaires nécessaires à l&rsquo;implémentation, avec la commande <code>/speckit.tasks</code>. Cette liste sera stockée dans <code>tasks.md</code>.
Le résultat est assez bluffant : l&rsquo;IA découpe le projet en tâches unitaires précises (T001, T002&hellip;). C&rsquo;est votre feuille de route. Vous pouvez l&rsquo;amender, mais globalement, il ne vous reste plus qu&rsquo;à piloter l&rsquo;implémentation, étape par étape.</p>
<h2 id="implémentation">Implémentation</h2>
<p>La commande <code>/speckit.implement</code>, utilisée de manière itérative, permet de dialoguer avec le LLM pour préciser certains points et générer le code. Elle coche automatiquement les tâches complétées dans <code>tasks.md</code>, vous permettant de suivre l&rsquo;avancement, tests et déploiements inclus.</p>
<p>Pendant l&rsquo;implémentation, tous les tests sont déroulés. À ce stade, je recommande l&rsquo;acceptation automatique de certaines commandes dans l&rsquo;IDE, tant les interactions sont nombreuses. Il faut trouver le juste équilibre entre contrôle humain et rapidité d&rsquo;exécution.</p>
<p>Pour cette application, les tâches ont été réparties ainsi :</p>
<ol>
<li><strong>Setup (T001-T003) :</strong> Dépendances (FastAPI, Telethon, feedgen, pytest), Dockerfile unique, arborescence <code>src/app</code> et <code>tests/</code>.</li>
<li><strong>Fondations (T004-T010) :</strong>
<ul>
<li>Logging JSON structuré.</li>
<li>Config loader avec fail-fast sur les clés API et les limites.</li>
<li>Validation des noms de canaux (regex) et modèles.</li>
<li>Wrapper Telethon (gestion de session, timeout, récupération des messages).</li>
<li>Générateur RSS (UTF-8, no-store).</li>
<li>App factory FastAPI et documentation OpenAPI.</li>
</ul>
</li>
<li><strong>US1 (T011-T013) :</strong> Endpoint <code>GET /rss/{channel}</code>, tests unitaires et intégration.</li>
<li><strong>US2 (T014-T015) :</strong> Gestion des erreurs 400/403/404 (invalide, privé ou manquant).</li>
<li><strong>US3 (T016-T017) :</strong> Gestion des timeouts (504) et rate-limits (429).</li>
<li><strong>Polish (T018-T021) :</strong> Quickstart, contrat de test, CI (lint/format) et README complet.</li>
</ol>
<h2 id="remarques-sur-le-déroulé-avec-github-spec-kit">Remarques sur le déroulé avec GitHub Spec Kit</h2>
<p>On peut relancer n&rsquo;importe quelle commande à tout moment. Attention toutefois : cela peut invalider certaines tâches déjà effectuées (ou non), et il faudra parfois rectifier le tir manuellement. Il est primordial de rester attentif aux actions du LLM. Cela représente <strong>une vraie charge mentale et nécessite une expertise réelle pour éviter que l&rsquo;IA ne s&rsquo;égare</strong>.</p>
<h2 id="erreurs-rencontrées-et-corrections">Erreurs rencontrées et corrections</h2>
<p>Voici les quelques accrocs rencontrés lors de la génération automatique. J&rsquo;ai pu guider l&rsquo;IA pour les corriger ; les solutions proposées étaient pertinentes, même si cela demande de rester vigilant :</p>
<ul>
<li><strong>Import feedgen introuvable :</strong> Le serveur lancé avec uvicorn global utilisait le mauvais interpréteur Python. Correction : utiliser python -m uvicorn &hellip; pour solliciter l&rsquo;environnement virtuel.</li>
<li><strong>RuntimeError « no current event loop » :</strong> TelethonClient était instancié de manière synchrone dans <strong>init</strong>. Correction : rendre get_service asynchrone et opter pour un lazy-init du client. C&rsquo;était un problème structurel, mais une minute de régénération a suffi à réécrire la version asynchrone. La perte de temps est dérisoire par rapport à un développement humain complet.</li>
<li><strong>Telethon non autorisé :</strong> Erreur 500 attendue sans session active. Solution : création d&rsquo;un script create_session.py pour générer la session dans le dossier /data.</li>
<li><strong>Ruff config obsolète :</strong> Le champ profile = &ldquo;black&rdquo; a été rejeté. Migration vers la nouvelle syntaxe de configuration de Ruff.</li>
<li><strong>Compatibilité Python 3.10 :</strong> datetime.UTC étant indisponible sur cette version, il a été remplacé par timezone.utc.</li>
<li><strong>Lint B904 :</strong> Ajout de from exc lors de la levée d&rsquo;exceptions HTTP pour préserver la trace.</li>
</ul>
<h2 id="résultat-final">Résultat final</h2>
<p>En seulement 2 heures, j&rsquo;ai obtenu une première application fonctionnelle répondant à mes attentes. Elle est simple et remplit parfaitement son objectif.</p>
<p>Côté qualité : les tests Ruff/Black sont au vert, 9 tests (unitaires, intégration, contrat) passent avec succès et l&rsquo;OpenAPI est conforme. Côté Ops, la génération automatique a bien intégré les essentiels (README, Compose, Quickstart). Le volume /data pour la session Telegram est opérationnel et les erreurs HTTP sont correctement mappées. J&rsquo;ai même ajouté quelques tests de sécurité en CI, configurés pour tourner quotidiennement. <strong>Un code généré qui n&rsquo;évolue plus accumule vite de la dette</strong>.</p>
<h2 id="verdict">Verdict</h2>
<p>Au-delà des spécifications, je remarque que je deviens moins strict sur la « beauté » pure du code. J&rsquo;exige un contrôle total sur les tests et les garde-fous, mais tant que le code fonctionne, son élégance m&rsquo;importe moins. C&rsquo;est une tendance qui devrait s&rsquo;accentuer : le craftsmanship va évoluer vers la maintenabilité des spécifications plutôt que celle du code lui-même, tant ce dernier peut être réécrit rapidement.</p>
<p>Attention toutefois à la charge mentale : on effectue une <em>code review</em> permanente sur un outil auquel on accorde, par définition, une confiance limitée. C&rsquo;est un exercice qui demande une concentration soutenue.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Transformer son Rapsberry Pi Zero W en souris programmable</title>
      <link>https://coupelon.net/posts/pi0-as-mouse/</link>
      <pubDate>Thu, 29 May 2025 18:25:33 +0200</pubDate><author>coupelon&#43;blog@gmail.com (Olivier Coupelon)</author>
      <guid>https://coupelon.net/posts/pi0-as-mouse/</guid>
      <description>&lt;p&gt;Je dispose d&amp;rsquo;un Rapsberry Pi Zero W v1.1 qui s&amp;rsquo;ennuie, et qui a l&amp;rsquo;intérêt de pouvoir se transformer en HID, c&amp;rsquo;est à dire de se faire passer pour un clavier ou une souris. Et justement, j&amp;rsquo;ai besoin d&amp;rsquo;automatiser des clics (pour un jeu qui demande beaucoup trop de tâches répétitives à mon goût). Seulement voilà, les devs du jeu sont malins et empêchent les clics simulés depuis l&amp;rsquo;OS. Donc on va simuler des clics de souris via le Pi comme si c&amp;rsquo;était une vraie souris !&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Je dispose d&rsquo;un Rapsberry Pi Zero W v1.1 qui s&rsquo;ennuie, et qui a l&rsquo;intérêt de pouvoir se transformer en HID, c&rsquo;est à dire de se faire passer pour un clavier ou une souris. Et justement, j&rsquo;ai besoin d&rsquo;automatiser des clics (pour un jeu qui demande beaucoup trop de tâches répétitives à mon goût). Seulement voilà, les devs du jeu sont malins et empêchent les clics simulés depuis l&rsquo;OS. Donc on va simuler des clics de souris via le Pi comme si c&rsquo;était une vraie souris !</p>
<h2 id="os">OS</h2>
<p>Niveau OS, j&rsquo;ai d&rsquo;abord testé <a href="https://p4wnp1.readthedocs.io/en/latest/">P4wnP1 A.L.O.A.</a>. C&rsquo;est un distribution qui permet entre autre de faire ça, mais bien plus encore. Malheureusement elle n&rsquo;a pas été mise à jour depuis plusieurs années, et les clics ne sont pas détectés correctement par les nouvelles versions de MacOS. Et je n&rsquo;ai pas trouvé comment passer outre ce problème.</p>
<p>On va donc repartir de la base, via l&rsquo;installation d&rsquo;une <a href="https://www.raspberrypi.com/software/operating-systems/">Raspberry Pi OS Lite (32-bit)</a>, facilitée par <a href="https://www.raspberrypi.com/software/">Raspberry Pi Imager</a> qui fait tout pour nous. Bien penser à configurer le wifi et un utilisateur avant de flasher la carte SD, de cette manière l&rsquo;image flashée est directement connectée à votre réseau et accessible une fois bootée au bout de quelques minutes en SSH.
De mon côté la version de l&rsquo;OS est <em>Raspbian GNU/Linux 12 (bookworm)</em></p>
<p>Avant le premier boot, monter la carte SD et modifier les fichiers suivants de la partition de boot :</p>
<p>Modifier la fin du fichier config.txt pour qu&rsquo;il ne contienne plus que ceci :</p>





<pre tabindex="0"><code>[cm5]

[all]
dtoverlay=dwc2</code></pre><p>Ajouter au fichier cmdline.txt, sur la ligne existante, après &lsquo;&lsquo;rootwait&rsquo;&rsquo; le paramètre suivant :</p>





<pre tabindex="0"><code>modules-load=dwc2,libcomposite</code></pre><h2 id="démarrage">Démarrage</h2>
<p>Il faut relier le pi à votre ordinateur, en prenant soin :</p>
<ul>
<li>De brancher votre cable micro-usb sur le port USB. C&rsquo;est important, c&rsquo;est le seul à pouvoir emmettre les signaux pour le HID. L&rsquo;autre port, PWR, ne fonctionnera pas.</li>
<li>Il ne faut pas utiliser un cable OTG. Un cable classique, par exemple micro-usb vers USB-A fera l&rsquo;affaire. Sur le mac il faut éventuellement un adapteur USB-A vers ESB-C, n&rsquo;importe quel hub fera l&rsquo;affaire.</li>
<li>Le cable micro-usb doit supporter la data. Beaucoup de ces cables sont livrés pour recharger des appareils, et du coup ne transmettre que de l&rsquo;électricité, pas les données, il nous faut transporter les deux !</li>
</ul>
<p>Et zou, en branchant avec ce seul cable le Pi on peut démarrer et passer à la suite de la config.</p>
<h2 id="configuration">Configuration</h2>
<p>Connectez-vous en SSH sur le Pi, puis réaliser les opérations suivantes.</p>
<p>Créer ensuite un script pour démarrer la souris virtuelle. Chez moi ce sera dans mon dossier /home/olivier, à adapter :</p>
<p>hid_mouse.sh</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># Nettoyage si un gadget est déjà actif</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">if</span> <span class="o">[</span> -d /sys/kernel/config/usb_gadget/g1 <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span> &gt; /sys/kernel/config/usb_gadget/g1/UDC <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  rm -f /sys/kernel/config/usb_gadget/g1/configs/c.1/hid.usb0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  rmdir /sys/kernel/config/usb_gadget/g1/functions/hid.usb0
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  rmdir /sys/kernel/config/usb_gadget/g1/configs/c.1/strings/0x409
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  rmdir /sys/kernel/config/usb_gadget/g1/configs/c.1
</span></span><span class="line"><span class="ln">10</span><span class="cl">  rmdir /sys/kernel/config/usb_gadget/g1/strings/0x409
</span></span><span class="line"><span class="ln">11</span><span class="cl">  rmdir /sys/kernel/config/usb_gadget/g1
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># Création du gadget</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">cd</span> /sys/kernel/config/usb_gadget/
</span></span><span class="line"><span class="ln">16</span><span class="cl">mkdir -p g1
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nb">cd</span> g1
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="c1"># Identifiants USB génériques</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nb">echo</span> 0x1d6b &gt; idVendor  <span class="c1"># Linux Foundation</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="nb">echo</span> 0x0104 &gt; idProduct <span class="c1"># Multifunction Composite Gadget</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="nb">echo</span> 0x0100 &gt; bcdDevice
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="nb">echo</span> 0x0200 &gt; bcdUSB
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># Chaînes d&#39;identification</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">mkdir -p strings/0x409
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;fedcba9876543210&#34;</span> &gt; strings/0x409/serialnumber
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;PiZero&#34;</span> &gt; strings/0x409/manufacturer
</span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;USB Mouse&#34;</span> &gt; strings/0x409/product
</span></span><span class="line"><span class="ln">30</span><span class="cl">
</span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="c1"># Configuration unique</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">mkdir -p configs/c.1
</span></span><span class="line"><span class="ln">33</span><span class="cl">mkdir -p configs/c.1/strings/0x409
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;HID Mouse Config&#34;</span> &gt; configs/c.1/strings/0x409/configuration
</span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="nb">echo</span> <span class="m">250</span> &gt; configs/c.1/MaxPower
</span></span><span class="line"><span class="ln">36</span><span class="cl">
</span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="c1"># Création de la fonction HID souris</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">mkdir -p functions/hid.usb0
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="nb">echo</span> <span class="m">1</span> &gt; functions/hid.usb0/protocol     <span class="c1"># 1 = souris</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="nb">echo</span> <span class="m">1</span> &gt; functions/hid.usb0/subclass     <span class="c1"># 2 = boot interface</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl"><span class="nb">echo</span> <span class="m">8</span> &gt; functions/hid.usb0/report_length
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="c1"># Descripteur HID souris 3 boutons, X/Y, Défilement vertical (molette)</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl"><span class="nb">echo</span> -ne <span class="s1">&#39;\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02\x95\x01\x75\x05\x81\x03\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06\xc0\xc0&#39;</span> &gt; functions/hid.usb0/report_desc
</span></span><span class="line"><span class="ln">45</span><span class="cl">
</span></span><span class="line"><span class="ln">46</span><span class="cl"><span class="c1"># Lier la fonction à la config</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">ln -s functions/hid.usb0 configs/c.1/
</span></span><span class="line"><span class="ln">48</span><span class="cl">
</span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="c1"># Activer le périphérique (récupérer nom UDC dispo)</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl"><span class="nv">UDC_NAME</span><span class="o">=</span><span class="k">$(</span>ls /sys/class/udc <span class="p">|</span> head -n 1<span class="k">)</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$UDC_NAME</span><span class="s2">&#34;</span> &gt; UDC
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl"><span class="c1"># On active le fait que n&#39;importe quel user puisse écrire sur le device, ça évitera d&#39;executer les scripts en root</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">chmod <span class="m">666</span> /dev/hidg0</span></span></code></pre></div><p>Le rendre executable</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">chmod +x /home/olivier/hid_mouse.sh</span></span></code></pre></div><p>Créer un fichier de démarrage pour systemd :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo nano /etc/systemd/system/hidmouse.service</span></span></code></pre></div><p>Et y coller les éléments suivants :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="o">[</span>Unit<span class="o">]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nv">Description</span><span class="o">=</span>USB HID Mouse Gadget Setup
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">After</span><span class="o">=</span>network.target sysinit.target
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">DefaultDependencies</span><span class="o">=</span>no
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="o">[</span>Service<span class="o">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">Type</span><span class="o">=</span>oneshot
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">ExecStart</span><span class="o">=</span>/home/olivier/hid_mouse.sh
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">RemainAfterExit</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="o">[</span>Install<span class="o">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="nv">WantedBy</span><span class="o">=</span>multi-user.target</span></span></code></pre></div><p>Activer le service :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl daemon-reexec
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo systemctl <span class="nb">enable</span> hidmouse.service</span></span></code></pre></div><p>Démarrer et vérifier que tout fonctionne correctement :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl start hidmouse.service
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl status hidmouse.service</span></span></code></pre></div><h2 id="contrôlons-la-souris">Contrôlons la souris</h2>
<p>Normalement à cette étape, ou après un redémarrage, votre Mac a du vous demander d&rsquo;accepter votre nouvelle souris. Vérifions son fonctionnement en envoyant un clic souris depuis le pi.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sleep <span class="m">5</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> -ne <span class="s1">&#39;\x01\x00\x00\x00\x00\x00\x00\x00&#39;</span> &gt; /dev/hidg0
</span></span><span class="line"><span class="ln">3</span><span class="cl">sleep 0.1
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">echo</span> -ne <span class="s1">&#39;\x00\x00\x00\x00\x00\x00\x00\x00&#39;</span> &gt; /dev/hidg0</span></span></code></pre></div><h2 id="créons-un-web-service-pour-cliquer-à-distance">Créons un web service pour cliquer à distance</h2>
<p>Nous avons d&rsquo;abord besoin de la librairie flask :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo apt update
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo apt upgrade -y
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo apt install python3-flask -y</span></span></code></pre></div><p>Puis créer un petit script python pour exposer le web service. Deux méthodes disponibles, l&rsquo;une pour envoyer des scrolls, l&rsquo;autre pour cliquer. Dans le fichier hid_ws.py :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">jsonify</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">Flask</span><span class="p">(</span><span class="vm">__name__</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># A get method that takes as parameters the number of clicks</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/click&#39;</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">&#39;GET&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">get_clicks</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">num_clicks</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">args</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;n&#39;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">click_mouse</span><span class="p">(</span><span class="n">num_clicks</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">return</span> <span class="n">jsonify</span><span class="p">({</span><span class="s1">&#39;status&#39;</span><span class="p">:</span> <span class="s1">&#39;success&#39;</span><span class="p">,</span> <span class="s1">&#39;clicks&#39;</span><span class="p">:</span> <span class="n">num_clicks</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="k">def</span> <span class="nf">click_mouse</span><span class="p">(</span><span class="n">num_clicks</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_clicks</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="c1"># Write the click command to /dev/hidg0</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">&#39;/dev/hidg0&#39;</span><span class="p">,</span> <span class="s1">&#39;wb&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">&#39;</span><span class="se">\x01\x00\x00\x00\x00\x00\x00\x00</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">&#39;</span><span class="se">\x00\x00\x00\x00\x00\x00\x00\x00</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">            <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.3</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="k">def</span> <span class="nf">scroll_mouse</span><span class="p">(</span><span class="n">steps</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="c1"># Write the scroll command to /dev/hidg0</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">&#39;/dev/hidg0&#39;</span><span class="p">,</span> <span class="s1">&#39;wb&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1"># Turn steps into a byte representation</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="n">step_bytes</span> <span class="o">=</span> <span class="n">steps</span><span class="o">.</span><span class="n">to_bytes</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="n">byteorder</span><span class="o">=</span><span class="s1">&#39;little&#39;</span><span class="p">,</span> <span class="n">signed</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">        <span class="c1"># Write the scroll command with direction</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">        <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">&#39;</span><span class="se">\x00\x00\x00</span><span class="s1">&#39;</span> <span class="o">+</span> <span class="n">step_bytes</span> <span class="o">+</span> <span class="sa">b</span><span class="s1">&#39;</span><span class="se">\x00\x00\x00\x00</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="n">f</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s1">&#39;</span><span class="se">\x00\x00\x00\x00\x00\x00\x00\x00</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.3</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/scroll&#39;</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="s1">&#39;GET&#39;</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="k">def</span> <span class="nf">get_scroll</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="n">steps</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">args</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;steps&#39;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">int</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">    <span class="n">scroll_mouse</span><span class="p">(</span><span class="n">steps</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="k">return</span> <span class="n">jsonify</span><span class="p">({</span><span class="s1">&#39;status&#39;</span><span class="p">:</span> <span class="s1">&#39;success&#39;</span><span class="p">,</span> <span class="s1">&#39;steps&#39;</span><span class="p">:</span> <span class="n">steps</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="nd">@app.route</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl"><span class="k">def</span> <span class="nf">index</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Mouse control service is running. Use /click or /scroll endpoints.&#34;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">
</span></span><span class="line"><span class="ln">43</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s1">&#39;__main__&#39;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">exists</span><span class="p">(</span><span class="s1">&#39;/dev/hidg0&#39;</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Error: /dev/hidg0 does not exist. Make sure the hidg kernel module is loaded.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">        <span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s1">&#39;0.0.0.0&#39;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">5000</span><span class="p">)</span></span></span></code></pre></div><p>Là aussi créer un fichier systemd pour que le webservice se lance au démarrage du pi :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo nano /etc/systemd/system/hidclick.service</span></span></code></pre></div><p>Avec comme contenu :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="o">[</span>Unit<span class="o">]</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nv">Description</span><span class="o">=</span>Python Web Service to simulate a click
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">After</span><span class="o">=</span>network.target hidmouse.service
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">Requires</span><span class="o">=</span>hidmouse.service
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="o">[</span>Service<span class="o">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nv">ExecStart</span><span class="o">=</span>/usr/bin/python3 /home/olivier/hid_ws.py
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">Restart</span><span class="o">=</span>always
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">User</span><span class="o">=</span>olivier      
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">WorkingDirectory</span><span class="o">=</span>/home/olivier                  
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="o">[</span>Install<span class="o">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nv">WantedBy</span><span class="o">=</span>multi-user.target</span></span></code></pre></div><p>Activer le service :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl daemon-reexec
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl daemon-reload
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo systemctl <span class="nb">enable</span> hidclick.service</span></span></code></pre></div><h2 id="verifier-que-ça-fonctionne">Verifier que ça fonctionne</h2>
<p>Pour tester tout ceci, vous pouvez par exemple utiliser un terminal depuis votre mac qui attend 5 secondes puis clique, ce qui vous laisse le temps de positionner votre souris sur un élément à cliquer pour tester.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sleep <span class="m">5</span> <span class="o">&amp;&amp;</span> curl <span class="s2">&#34;http://192.168.0.13:5000/click&#34;</span></span></span></code></pre></div><p>Evidemment par la suite vous pouvez piloter votre souris depuis vos scripts pour tout automatiser sans que le programme cible puisse savoir si c&rsquo;est une vraie ou fausse souris !</p>
]]></content:encoded>
    </item>
    <item>
      <title>Utiliser n8n comme agrégateur de veille perso</title>
      <link>https://coupelon.net/posts/veille-automatisee/</link>
      <pubDate>Sun, 20 Apr 2025 16:11:22 +0100</pubDate><author>coupelon&#43;blog@gmail.com (Olivier Coupelon)</author>
      <guid>https://coupelon.net/posts/veille-automatisee/</guid>
      <description>&lt;p&gt;Pour faire ma veille, j&amp;rsquo;ai plein de sources d&amp;rsquo;informations différentes.&#xA;Jusqu&amp;rsquo;à 2024 j&amp;rsquo;utilisais &lt;a href=&#34;https://www.inoreader.com/&#34;&gt;Inoreader&lt;/a&gt;, excellent service même dans sa version gratuite. En version payante, il dispose de quelques moyens de capter le contenu de réseaux sociaux, ce qui peut être un vrai plus, en comparaison à une veille uniquement basée sur le RSS. Dans la version pro il est également possible d&amp;rsquo;avoir accès à des résumés, du filtrage de contenu, et est même intégré à Zapier ou IFTTT. C&amp;rsquo;est chouette, mais ça ne capte pas &lt;strong&gt;tout&lt;/strong&gt; type de contenu, par exemple les sites qui n&amp;rsquo;ont pas de RSS.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Pour faire ma veille, j&rsquo;ai plein de sources d&rsquo;informations différentes.
Jusqu&rsquo;à 2024 j&rsquo;utilisais <a href="https://www.inoreader.com/">Inoreader</a>, excellent service même dans sa version gratuite. En version payante, il dispose de quelques moyens de capter le contenu de réseaux sociaux, ce qui peut être un vrai plus, en comparaison à une veille uniquement basée sur le RSS. Dans la version pro il est également possible d&rsquo;avoir accès à des résumés, du filtrage de contenu, et est même intégré à Zapier ou IFTTT. C&rsquo;est chouette, mais ça ne capte pas <strong>tout</strong> type de contenu, par exemple les sites qui n&rsquo;ont pas de RSS.</p>
<p><a href="https://n8n.io/">n8n</a> d&rsquo;un autre côté, c&rsquo;est un outil qui sert à de l&rsquo;automatisation. En soi, ça ressemble à <a href="https://nodered.org/">Node-RED</a>, <a href="https://www.talend.com/fr/resources/etl-tools/">Talend</a> ou tous les outils de workflow qui permettent de faire du low-code. Sa philosophie à lui, c&rsquo;est d&rsquo;interconnecter des applications entre elles. C&rsquo;est donc un concurrent à Zapier, open-source, avec un bon paquet de connecteurs disponibles vers plein d&rsquo;outils. J&rsquo;avais d&rsquo;abord testé <a href="https://automatisch.io/">Automatisch</a>, similaire mais moins complet, mon choix s&rsquo;est donc arrêté depuis début 2025 sur n8n.</p>
<h2 id="décomposition-dun-workflow">Décomposition d&rsquo;un workflow</h2>
<p>Un workflow typique de veille est pour moi composé de la façon suivante</p>
<h3 id="détection-dune-nouvelle-info">Détection d&rsquo;une nouvelle info</h3>
<p>Jusqu&rsquo;ici je me basais surtout sur des articles de blog, qui ont tous un flux RSS. Pour les récupérer, il faut démarrer un nouveau workflow avec un <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.rssfeedreadtrigger/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.rssFeedReadTrigger#rss-feed-trigger-node">RSS Feed Trigger node</a> qui permet de lancer un workflow à chaque nouvelle entrée, qu&rsquo;il y en ait une ou tout plein.</p>
<p>Parfois (souvent) le flux RSS ne dispose que de l&rsquo;introduction de l&rsquo;article. C&rsquo;est dommage car on aimerait potentiellement accéder à son contenu complet - mais les auteurs ont souvent de bonnes raisons, notamment publicitaires, de ne vous donner accès qu&rsquo;à l&rsquo;introduction. L&rsquo;astuce ici quand c&rsquo;est autorisé, c&rsquo;est de parcourir avec n8n l&rsquo;article et d&rsquo;en extraire le contenu. Pour ce faire, on va suivre le lien du RSS avec le node <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.httpRequest#http-request-node">HTTP Request node</a>, puis utiliser le node <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.html/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.html#extract-html-content">Extract HTML Content</a> de la page récupérée, potentiellement en utilisant un <a href="https://www.w3schools.com/cssref/css_selectors.php">sélecteur CSS</a> pour limiter la récupération au contenu qui nous intéresse.</p>
<p>À chaque fois, dans notre workflow, cela ajoute des variables ayant le contenu récupéré à l&rsquo;étape précédente. Tout ceci pourrait être développé manuellement, mais tout le principe est là : on ne code que ce qui est spécifique à notre besoin, toutes les autres briques et opérations techniques sont déjà disponibles, c&rsquo;est toute la promesse des outils low-code.</p>
<h3 id="résumer-le-contenu">Résumer le contenu</h3>
<p>Sur les articles un peu longs, on a souvent envie d&rsquo;un petit résumé synthétique pour capter les idées principales. Là il faut utiliser le <a href="https://docs.n8n.io/integrations/builtin/cluster-nodes/root-nodes/n8n-nodes-langchain.chainllm/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=%40n8n%2Fn8n-nodes-langchain.chainLlm#basic-llm-chain-node">Basic LLM Chain node</a> couplé à un <a href="https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmollama/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=%40n8n%2Fn8n-nodes-langchain.lmOllama#ollama-model-node">Ollama Model node</a> pour interroger Ollama, très utile pour une utilisation locale, pour des données privées ou sensibles, ou encore le <a href="https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatgooglegemini/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=%40n8n%2Fn8n-nodes-langchain.lmChatGoogleGemini#google-gemini-chat-model-node">Google Gemini Chat Model node</a> qui dispose de pas mal de modèles en utilisation gratuite, mais qui elles doivent être publiques.</p>
<h3 id="poster-le-résumé-sur-discord">Poster le résumé sur Discord</h3>
<p>Enfin, il est possible d&rsquo;envoyer toutes ces données sur le média de son choix. Perso j&rsquo;ai choisi de me créer mon propre groupe Discord, avec un bot piloté par n8n, qui poste tout le contenu dans des channels qui correspondent à mes catégories de veilles. Pour envoyer vers Discord, il y a tout simplement le <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.discord/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.discord#discord-node">Discord node</a>. Evidemment il faut un peu de paramétrage, mais la documentation est très bien faite.</p>
<h3 id="factorisation">Factorisation</h3>
<p>La génération de résumés et l&rsquo;envoi vers Discord sont des fonctionnalités de base de l&rsquo;ensemble de mes workflows. Elles sont donc centralisées, et pour cela j&rsquo;utilise le mécanisme de sous-workflow via le <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.executeworkflowtrigger/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.executeWorkflowTrigger#execute-sub-workflow-trigger-node">Execute Sub-workflow Trigger node</a>. C&rsquo;est vraiment propre, et ça permet de bien séparer et paramétrer les tâches.</p>
<h3 id="pour-aller-plus-loin-">Pour aller plus loin ?</h3>
<p>n8n propose énormément de nodes, il faut simplement passer un peu de temps dans l&rsquo;outil en fonction de ses besoins. Vous pouvez parser régulièrement du contenu avec un trigger <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.scheduleTrigger#schedule-trigger-node">Schedule Trigger node</a>, et extraire le contenu du site en question via des sélecteurs CSS, puis ne conserver que les résultats que vous n&rsquo;aviez pas encore rencontrés lors d&rsquo;une exécution précedente via l&rsquo;utilisation du <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.removeduplicates/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.removeDuplicates#remove-duplicates-node">Remove Duplicates node</a> avec son option <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.removeduplicates/?utm_source=n8n_app&amp;utm_medium=node_settings_modal-credential_link&amp;utm_campaign=n8n-nodes-base.removeDuplicates#remove-items-processed-in-previous-executions">Remove Items Processed in Previous Executions</a>.</p>
<p>Enfin vous pouvez trouver et installer facilement des nodes issus de la communauté sur votre instance, en parcourant tout simplement <a href="https://www.npmjs.com/search?q=keywords%3An8n-community-node-package">npmjs avec le filtre qui va bien</a>.</p>
<h2 id="mise-en-place-technique">Mise en place technique</h2>
<h3 id="n8n-via-docker">n8n via Docker</h3>
<p>Comme souvent je souhaite pouvoir lancer ce service via Docker. Mon conteneur démarre avec les paramètres suivants :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run -d --name n8n -p 5678:5678 -e <span class="nv">N8N_CONCURRENCY_PRODUCTION_LIMIT</span><span class="o">=</span><span class="m">2</span> --add-host<span class="o">=</span>host.docker.internal:host-gateway -v /home/debian/n8n/data:/home/node/.n8n --restart unless-stopped docker.n8n.io/n8nio/n8n</span></span></code></pre></div><p>Le port utilisé est le port 5678, par défaut sur n8n. Le paramètre <code>N8N_CONCURRENCY_PRODUCTION_LIMIT</code> permet de <a href="https://docs.n8n.io/hosting/scaling/concurrency-control/">limiter le nombre d&rsquo;exécutions</a> en production à 2 en même temps. Pratique pour une petite configuration. Par défaut sous Debian l&rsquo;IP de l&rsquo;hôte docker ne dispose pas de résolution DNS, et peut varier en fonction du réseau docker dans lequel il est démarré. L&rsquo;ajout de <code>--add-host=host.docker.internal:host-gateway</code>règle ce problème. C&rsquo;est d&rsquo;ailleurs un paramètre pré-configuré par Docker Desktop. La <a href="https://docs.n8n.io/hosting/installation/docker/#updating">mise à jour du conteneur</a> nécessitera régulièrement de stopper et puller une version plus récente, avec un arrêt de service. C&rsquo;est scriptable, mais ça reste limitant, l&rsquo;idéal serait de se tourner vers un cluster avec rolling update.</p>
<h3 id="mise-en-place-de-ollama">Mise en place de Ollama</h3>
<p>On va se servir de n8n pour récupérer plein de contenu, mais je suis fainéant, je ne veux pas toujours tout lire. Pour résumer du contenu, utiliser un LLM c&rsquo;est vraiment top. Et cela permet très rapidement de savoir si l&rsquo;on souhaite lire l&rsquo;article. Pour ce faire, il faut installer Ollama. Sous Debian, <a href="https://ollama.com/download/linux">la documentation</a> par défaut propose de l&rsquo;installer via le script fourni, ce qui marche très bien.
À noter tout de même que pour que par défaut il n&rsquo;écoute que sur localhost, donc pas accessible sur l&rsquo;interface Docker. Pour y pallier plusieurs solutions, tout dépend du niveau de sécurité recherché. On peut lui spécifier l&rsquo;IP sur le réseau docker recherché, ou le faire écouter sur toutes les interfaces si un firewall est positionné en frontal - ⚠️ autrement il sera accessible sur votre réseau / internet selon votre config.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl edit ollama.service</span></span></code></pre></div><p>Puis ajouter les lignes dans la zone éditable</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="o">[</span>Service<span class="o">]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">Environment</span><span class="o">=</span><span class="s2">&#34;OLLAMA_HOST=0.0.0.0&#34;</span></span></span></code></pre></div><p>Puis enfin redémarrer le service avec</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl daemon-reload <span class="o">&amp;&amp;</span> sudo systemctl restart ollama</span></span></code></pre></div><p>Au niveau des modèles, c&rsquo;est selon ses besoins. Sur mon petit serveur je n&rsquo;ai pas de GPU, et peu de RAM (6 Go), donc je choisis des petits modèles. Pas besoin d&rsquo;un texte ultra qualitatif, tant que la capacité à résumer est là. Récemment Gemma 3 est sorti avec une version à 2 milliards de paramètres que je trouve franchement efficace.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Faire tourner Docker Android sur un VPS ?</title>
      <link>https://coupelon.net/posts/docker-android/</link>
      <pubDate>Thu, 30 Jan 2025 21:28:25 +0100</pubDate><author>coupelon&#43;blog@gmail.com (Olivier Coupelon)</author>
      <guid>https://coupelon.net/posts/docker-android/</guid>
      <description>&lt;p&gt;J’ai la chance d’avoir accès à un VPS hosté par BeYs Cloud (merci 💚 à eux) que j’utilise pour héberger un tas de petits conteneurs de tests. Très content du service, j’ai voulu cette fois voir s’il me serait possible d’émuler un smartphone Android via Docker et de le piloter depuis mon mac. Spoiler : c’est possible. Spoiler 2 : c’est vraaaiment pas fait pour ça.&lt;/p&gt;&#xA;&lt;p&gt;Dans le suite de cet article, nous allons voir comment procéder, les contraintes et astuces nécessaires.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>J’ai la chance d’avoir accès à un VPS hosté par BeYs Cloud (merci 💚 à eux) que j’utilise pour héberger un tas de petits conteneurs de tests. Très content du service, j’ai voulu cette fois voir s’il me serait possible d’émuler un smartphone Android via Docker et de le piloter depuis mon mac. Spoiler : c’est possible. Spoiler 2 : c’est vraaaiment pas fait pour ça.</p>
<p>Dans le suite de cet article, nous allons voir comment procéder, les contraintes et astuces nécessaires.</p>
<p>Tout d’abord, se pose la question de faire tourner un emulateur Android sous Docker. Nos téléphones tournent principalement sur des processeurs ARM, mais l’émulateur Android a la bonne idée d’avoir une version x86 qui permet d’exécuter l’Android Runtime, tout en contenant un jeu d’émulation interne pour permettre l’exécution de code ARM pour les apps. <a href="https://android-developers.googleblog.com/2020/03/run-arm-apps-on-android-emulator.html">Donc au niveau architecture CPU, ça doit passer</a>.</p>
<p>L’émulateur en lui-même nécessite l’accès aux instructions de virtualisation du processeur, via KVM idéalement. C’est important pour ne pas avoir a émuler le processeur x86 lui-même, donc à devoir réinterpréter chacune des instructions. Et là ça coince souvent sur des VPS, non pas parce que ce n’est pas possible, mais souvent parce que ces instructions ne sont pas exposées par le Cloud Provider dans la VM fournie. C’est le cas ici. Donc là encore, rien n’est perdu, mais il va falloir émuler le x86, donc s’attendre a des performances exécrables, en utilisant l’<a href="https://developer.android.com/studio/run/emulator-commandline?hl=fr">option</a> <code>-no-accel</code> de l’émulateur.</p>
<p>Parlons également de RAM. La préconisation c’est d’avoir un moins 8Go de RAM disponible pour faire tourner l’émulateur normalement. Ma machine ne disposant que de 6Go de RAM, et ne faisant pas que ça, il va falloir croiser les doigts. Dans mon cas, je suis parti sur 2Go de RAM alloué au max, ce qui permet de lancer l’émulateur.</p>
<p>Halim Qarroum maintient le projet open-source <a href="https://github.com/HQarroum/docker-android">docker-android</a> dont l’objectif est justement de faire tourner l’émulateur dans un conteneur Docker. Pas mal d’images buildées ici, pour mes tests je me tourne vers le build api-32 de base, sans les outils Google, mais avec l’émulateur intégré évidemment. L’api-33 est disponible, mais demande trop de RAM à priori. On peut tuner la mémoire du conteneur au lancement, mais cette image ne prévoit pas le fait de passer d’autres paramètres. N’ayant pas très envie de rebuilder l’image, on va hacker la ligne de commande pour faire passer le <code>-no-accel</code>. En regardant le script de l’image au démarrage, je trouve cette solution, pas classe, mais suffisant pour faire un test :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run -it --rm -e <span class="nv">MEMORY</span><span class="o">=</span><span class="s2">&#34;2048 -no-accel&#34;</span> -p 5555:5555 halimqarroum/docker-android:api-32</span></span></code></pre></div><p>Ok, ça semble démarrer. And so what ? Maintenant, il faut accéder à notre émulateur sur le port 5555. Et on ne va pas le faire depuis le VPS, puisqu’il n’a pas d’interface graphique, on ne va pas non plus exposer le port 5555 au monde entier - même si j’envoie tout mon respect à la personne qui parviendra a faire quelque chose de concret avec l’émulateur dans cet état 😉</p>
<p>Donc pour accéder au port 5555 de la machine depuis mon mac, on va faire une redirection de port local via SSH. Pour faire ça c’est assez simple :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@host -L5555:localhost:5555</span></span></code></pre></div><p>SSH tourne en tâche de fond, le port 5555 est accessible, donc maintenant tout le reste des commandes se passe en local sur mon mac. On a tout d’abord installé adb et scrcpy.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">brew install android-platform-tools scrcpy</span></span></code></pre></div><p>Une fois que c’est fait, il suffit de se connecter via adb à notre emulateur remote via notre redirection de ports, puis de lancer le screen copy dans une résolution pas trop haute pour optimiser les performances :</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">adb connect 127.0.0.1:5555
</span></span><span class="line"><span class="ln">2</span><span class="cl">scrcpy --tcpip<span class="o">=</span>127.0.0.1:5555 -m1024</span></span></code></pre></div><p>Si tout se passe bien, pendant quelques (très longues) minutes vous aurez droit au boot de la machine. De mon côté, j’ai probablement 1 image toutes les 3 secondes, donc en termes de réactivité… on s’est compris.</p>
<p><img src="/images/docker-android-emulator.png" alt="Ecran de chargement de l&rsquo;émulateur Android"></p>
<p>La bonne nouvelle, c’est qu’au bout d’un moment… aléatoire, vous avez votre emulateur fonctionnel, “utilisable” si vous êtes patient. Ça peut être pratique pour exécuter des scripts, mais certainement pas pour une utilisation réelle, sans accelération matériel. Et même là, c’est beaucoup trop énergivore pour pouvoir être conseillé. Reste qu’avec accès aux instructions de virtualisation du processeur, c’est un outil puissant !</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
