CérénIT - BlogLe blog tech de Nicolas Steinmetz (Time Series, IoT, Web, Ops, Data)Zola2024-02-28T09:30:00+01:00https://cerenit.fr/blog/atom.xmlWeb, Ops, IoT et Time Series - Février 20242024-02-28T09:30:00+01:002024-02-28T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-iot-timeseries-fevrier-2024/<h3 id="code">Code</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://julien.ponge.org/blog/the-power-of-conventional-commits/">The power of conventional commits</a> : je suis assez fan de <a rel="noopener" target="_blank" href="https://gitmoji.dev/">Gitmoji</a> + <a rel="noopener" target="_blank" href="https://www.conventionalcommits.org/">Conventional Commits</a> pour avoir des messages de comits visuels et pertinents. Si la mise en oeuvre est parfois pas très naturel, le plaisir d'avoir un changelog autogénéré et propre ou bien de pouvoir facilement retrouver un commit, son intention et le ticket gitlab associé, cela n'a pas de prix.</li>
<li>En liaison avec le billet précédent, le générateur de changelog <a rel="noopener" target="_blank" href="https://git-cliff.org/">Git-cliff</a> est sorti en version <a rel="noopener" target="_blank" href="https://git-cliff.org/blog/2.0.0">2.0</a> avec notamment une intégration plus poussée avec Github, des templates et plein d'autres choses. Git-cliff depuis la version 1.4 peut aussi générer votre prochain numéro de version sur la base de vos commits et la commande <a rel="noopener" target="_blank" href="https://git-cliff.org/blog/1.4.0#bump-version-">bump</a></li>
<li><a rel="noopener" target="_blank" href="https://blog.anayrat.info/2024/02/05/postgres-a-nouveau-elu-sgbd-de-lannee-en-2023-mais-je-suis-inquiet/">Postgres à nouveau élu SGBD de l'année en 2023, mais je suis inquiet</a>: le cloud et les ORM notamment ont permis de s'affranchir des DBA. Si dans un sens c'est tant mieux, à un certain stade, cela s'avère nécessaire de recourir à l'expertise d'un DBA (même si c'est parfois trop tard). Reste que le problème fondamental est plutôt que les développeurs n'ont plus les fondamentaux en SQL dans ce cas particulier et en architecure logicielle de manière plus globale et c'est peut être surtout ça le vrai problème.</li>
<li><a rel="noopener" target="_blank" href="https://www.ufried.com/blog/continuous_amnesia_issue/">The continuous amnesia issue</a> : notre industrie est malade, on souffre d'une amnésie continue en ignorant les enseignements du passé. Le "jeunisme", "la hype" et le fait qu'au delà de 30 ans, il faut être passé du coté du management font qu'on ne valorise/capitalise pas assez sur ce que nos ainés ont fait.</li>
<li><a rel="noopener" target="_blank" href="https://webup.org/blog/the-high-risk-refactoring/">The High-Risk Refactoring</a> : l'amémioration / la réécriture de code a son lot inhérent de risques techniques et métiers. L'article permet d'appréhener et de cadrer cette décision de façon assez pragmatique pour arriver au niveau de code juste nécessaire.</li>
</ul>
<h3 id="dns">DNS</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://next.ink/126288/licann-propose-le-domaine-internal-pour-votre-reseau-local/">L’ICANN propose le domaine .internal pour votre réseau local</a> : historiquement, il y avait le <a rel="noopener" target="_blank" href="https://en.wikipedia.org/wiki/.local">.local</a> mais dont l'usage a été revu pour du zeroconf notamment. L'usage du <code>.internal</code> est en cours de discussion pour une décision en avril. On va pouvoir (enfin) sortir des domaines fictifs, des domaines publics utilisés en interne (adieu <code>macompany.org</code>) ou encore du "DNS menteur" (<code>macompany.com</code> résolu différemment suivant si on est en interne ou en externe). Néanmoins, <a rel="noopener" target="_blank" href="https://www.icann.org/en/public-comment/proceeding/proposed-top-level-domain-string-for-private-use-24-01-2024/submissions/alexander-robert-27-01-2024">une bonne question émerge</a> : comment gérer et garantir les certificats en <code>.internal</code> que tout le monde peut revendiquer ? Aucune entité de certification publique ne pourra émettre de tels certificats... Cela repose alors la question de la PKI privée et de la diffusion des certificats de la CA pour valider les domaines sur votre parc informatique...</li>
</ul>
<h3 id="ops">OPS</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://traefik.io/blog/announcing-traefik-proxy-v2-11/">Traefik Proxy v2.11 is Now Available! Here are the Latest Updates.</a> : Cette version apporte notamment les directives <code>keepAliveMaxRequests</code> et <code>keepAliveMaxTime</code> pour éviter que trop de connections ouvertes restent entre votre reverse proxy et votre applicatif.</li>
<li><a rel="noopener" target="_blank" href="https://traefik.io/blog/announcing-traefik-proxy-v3-rc/">Announcing Traefik Proxy v3.0 RC1</a>: Au programme: Wasm, OpenTelemetry, HTTP/3, SPIFFE et des choses dans le monde Kubernetes. Alors que la migration V1/V2 avait été un peu pénible, l'équipe de Traefik promet une migration en douceur entre V2/V3.</li>
</ul>
<h3 id="web">Web</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://next.ink/124294/lavif-prend-enfin-ses-aises-sur-internet-cest-quoi-ce-format-dimage/">L’AVIF prend enfin ses aises sur Internet : c’est quoi ce format d’image ? </a> : L'AVIF, un format d'image opensource et qui a pour vocation de remplacer le JPEG est enfin supporté sous Microsoft Edge (les autres navigateurs le supportent depuis 2020/21). Reste plus que le poids des habitudes pour remplacer le bon vieux jpeg par un avif.</li>
<li><a rel="noopener" target="_blank" href="https://korben.info/jxl-avif-nouveaux-formats-image-efficients.html">JXL et AVIF – Les nouveaux champions des formats d’image</a> : en continuité du point précédent, il y a aussi JXL pour JPEG XL et des outils pour générer vos premiers fichiers aux formats AVIF/JXL.</li>
<li><a rel="noopener" target="_blank" href="https://9to5mac.com/2024/02/08/ios-17-4-web-app-eu/">iOS 17.4 seems to remove web app support in the EU</a> - <a rel="noopener" target="_blank" href="https://developer.apple.com/support/dma-and-apps-in-the-eu#dev-qa">Update on apps distributed in the European Union - Support - Apple Developer</a> - <a rel="noopener" target="_blank" href="https://next.ink/127972/en-europe-ios-17-4-enterre-les-applications-web/">En Europe, iOS 17.4 enterre les applications web - Next
</a> : le support des PWA sous iOS 17.4 a sauté au prétexte du DMA européen mais <a rel="noopener" target="_blank" href="https://next.ink/brief_article/leurope-se-penche-sur-larret-des-web-apps-dans-ios-17-4/">l'Europe demande des explications sur le sujet</a>.</li>
<li><a rel="noopener" target="_blank" href="https://whatpwacando.today/">What PWA Can Do Today</a> : Pour savoir ce qu'il est (encore) possible de faire avec une PWA sous iOS et Android.</li>
</ul>
Web, Ops, IoT et Time Series - Janvier 20242024-01-31T09:30:00+01:002024-01-31T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-iot-timeseries-janvier-2024/<p>Après presque 2 ans de silence et le remplacement de Hugo et Bootstrap par <a rel="noopener" target="_blank" href="https://www.getzola.org/">Zola</a> et <a rel="noopener" target="_blank" href="https://tailwindcss.com/">Tailwind</a>/<a rel="noopener" target="_blank" href="https://daisyui.com/">daisyUI</a> l'été dernier pour générer le site, je vous souhaite une bonne année à tous et la résolution de publier plus régulièrement mes trouvailles...</p>
<h3 id="data">Data</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://duckdb.org/2024/01/26/multi-database-support-in-duckdb.html">Multi-Database Support in DuckDB</a> : Cela multiplie les possibilités 😎</li>
</ul>
<blockquote>
<p>TL;DR: DuckDB can attach MySQL, Postgres, and SQLite databases in addition to databases stored in its own format. This allows data to be read into DuckDB and moved between these systems in a convenient manner.</p>
</blockquote>
<h3 id="iot">IoT</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.univ-smb.fr/lorawan/">Devenez un expert LoRaWAN</a> via <a rel="noopener" target="_blank" href="https://www.udemy.com/course/lora-et-lorawan-pour-linternet-des-objets/">LoRa et LoRaWAN pour l'Internet des Objets</a>: si vous cherchez un cours théorique et pratique pour vous former aux protocoles LoRa et LoRaWan, je vous le recommande chaudement.</li>
</ul>
<h3 id="ops">Ops</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://dkron.io/">DKron</a> via <a rel="noopener" target="_blank" href="https://blog.stephane-robert.info/post/dkron-jobs-cron/">Dkron pilote vos crontab</a> : un gestionnaire de cron distribué, avec une jolie interface et uen API - que demander de plus ? Sur un modèle agent/serveur, le serveur dRkon distribue les tâches aux agents dKron concernés. les agents dKron étant déployés sur les serveurs sur lesquels les jobs doivent s'exécuter.</li>
</ul>
<h3 id="reverse-proxy">Reverse Proxy</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://caddyserver.com/">Caddy</a> : si vous avez besoin d'un reverse-proxy avec gestion automatique des certificats et redirection HTTP > HTTPS et <a rel="noopener" target="_blank" href="https://caddyserver.com/features">plein d'autres choses encore</a> mais sans nécessité d'intégration avec Docker comme Traefik, alors jetez un coup d'oeil à Caddy. Il permet également d'avoir un certificat sur localhost. Comme Traefik, il est écrit en Go.</li>
</ul>
<p>J'avoue que la concision de Caddy vs Traefik et le provider <code>file</code> est bien appréciable:</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span># Caddyfile
</span><span>xxx.cerenit.fr {
</span><span> reverse_proxy 127.0.0.1:3000
</span><span>}
</span></code></pre>
<pre data-lang="yaml" style="background-color:#2b303b;color:#c0c5ce;" class="language-yaml "><code class="language-yaml" data-lang="yaml"><span style="color:#65737e;"># Traefik
</span><span style="color:#bf616a;">http</span><span>:
</span><span> </span><span style="color:#bf616a;">middlewares</span><span>:
</span><span> </span><span style="color:#bf616a;">redirectToHttps</span><span>:
</span><span> </span><span style="color:#bf616a;">redirectScheme</span><span>:
</span><span> </span><span style="color:#bf616a;">permanent</span><span>: </span><span style="color:#d08770;">true
</span><span> </span><span style="color:#bf616a;">scheme</span><span>: </span><span style="color:#a3be8c;">https
</span><span> </span><span style="color:#bf616a;">routers</span><span>:
</span><span> </span><span style="color:#bf616a;">grafana</span><span>:
</span><span> </span><span style="color:#bf616a;">entryPoints</span><span>:
</span><span> - </span><span style="color:#a3be8c;">websecure
</span><span> - </span><span style="color:#a3be8c;">web
</span><span> </span><span style="color:#bf616a;">middlewares</span><span>:
</span><span> - </span><span style="color:#a3be8c;">redirectToHttps
</span><span> </span><span style="color:#bf616a;">rule</span><span>: </span><span style="color:#a3be8c;">Host(`xxx.cerenit.fr`)
</span><span> </span><span style="color:#bf616a;">service</span><span>: </span><span style="color:#a3be8c;">grafana@file
</span><span> </span><span style="color:#bf616a;">tls</span><span>:
</span><span> </span><span style="color:#bf616a;">certResolver</span><span>: </span><span style="color:#a3be8c;">le
</span><span> </span><span style="color:#bf616a;">services</span><span>:
</span><span> </span><span style="color:#bf616a;">grafana</span><span>:
</span><span> </span><span style="color:#bf616a;">loadBalancer</span><span>:
</span><span> </span><span style="color:#bf616a;">servers</span><span>:
</span><span> - </span><span style="color:#bf616a;">url</span><span>: </span><span style="color:#a3be8c;">http://127.0.0.1:3000/
</span></code></pre>
<p>Pour un serveur, la migration de Traefik vers Caddy fait passer le fichier de configuration de 172 lignes à 27 - soit presque 6 fois moins ! 😏</p>
<ul>
<li><a rel="noopener" target="_blank" href="https://github.com/lucaslorentz/caddy-docker-proxy">Caddy-Docker-Proxy</a> via <a rel="noopener" target="_blank" href="https://dev.to/jhot/caddy-docker-proxy-like-traefik-but-better-565l">Caddy Docker Proxy, Like Traefik But Better?</a> : si vous souhaitez aller plus loin dans l'intégration Caddy/Docker dans l'objectif de remplacer Traefik, cela semble être une bonne piste. C'est une version modifiée de Caddy pour s'interfacer avec Docker. L'intégration se fait notamment via les labels, comme pour Traefik. A voir si on peut déployer la version standalone en dehors d'un conteneur comme on peut le faire avec Traefik. Cela éviterit ainsi que chaque container à exposer via Caddy-Docker-Proxy rejoigne le réseau ad-hoc.</li>
</ul>
<p>Exemple:</p>
<pre data-lang="yaml" style="background-color:#2b303b;color:#c0c5ce;" class="language-yaml "><code class="language-yaml" data-lang="yaml"><span style="color:#bf616a;">services</span><span>:
</span><span> </span><span style="color:#bf616a;">whoami</span><span>:
</span><span> </span><span style="color:#bf616a;">image</span><span>: </span><span style="color:#a3be8c;">traefik/whoami
</span><span> </span><span style="color:#bf616a;">networks</span><span>:
</span><span> - </span><span style="color:#a3be8c;">caddy
</span><span> </span><span style="color:#bf616a;">labels</span><span>:
</span><span> </span><span style="color:#bf616a;">caddy</span><span>: </span><span style="color:#a3be8c;">whoami.example.com
</span><span> </span><span style="color:#bf616a;">caddy.reverse_proxy</span><span>: "</span><span style="color:#a3be8c;">{{upstreams 80}}</span><span>"
</span><span>
</span><span style="color:#bf616a;">networks</span><span>:
</span><span> </span><span style="color:#bf616a;">caddy</span><span>:
</span><span> </span><span style="color:#bf616a;">external</span><span>: </span><span style="color:#d08770;">true
</span></code></pre>
Vers de nouveaux horizons...2022-06-15T20:30:00+01:002022-06-15T20:30:00+01:00
Unknown
https://cerenit.fr/blog/vers-de-nouveaux-horizons/<p>Je l'évoquais dans le billet "<a href="/blog/bilan-2021-perspectives-2022/">Bilan 2021 et perspectives 2022</a>", je peux en parler maintenant officiellement : j'ai été contacté par <a rel="noopener" target="_blank" href="https://www.flovea.fr/">Flovea</a> pour piloter le projet <a rel="noopener" target="_blank" href="https://flowboxinteractive.com/en/">Flowbox Interactive</a> et mettre en place l'équipe projet associée. </p>
<p>Après trois mois environ de mission permettant de faire connaissance, d'auditer la solution existante, de définir une roadmap et de mettre en place l'équipe projet, mon recrutement en tant que DSI/CIO de Flovéa est acté depuis début avril. J'ai le plaisir de rejoindre une belle équipe pour réaliser un beau projet tant d'un point de vue technique que d'un point de vue du sens et de son utilité. La seule ombre au tableau étant le contexte de pénurie de composants qui illustre bien la dimension "hard" d'un projet "hardware".</p>
<p>L'activité de CérénIT va donc ralentir puis se mettre en mode minimal ; le temps pour moi de finir quelques activités de support pour un client et ne conserver ensuite que l'infogérance de <a rel="noopener" target="_blank" href="https://www.compta-online.com">Compta-Online</a> et un autre projet avec <a rel="noopener" target="_blank" href="https://www.linkedin.com/in/fabriceheuvrard/">Fabrice Heuvrard</a> à destination des experts comptables.</p>
<p>L'animation du meetup <a rel="noopener" target="_blank" href="https://timeseries.fr/">Time Series France</a> sera moins régulière et surement de façon plus opportuniste que précédemment. Je continue à contribuer à <a rel="noopener" target="_blank" href="https://bigdatahebdo.com/">BigData Hebdo</a> même si mes contributions au podcast sont minimes depuis le début d'année.</p>
<p>Je remercie tous les clients et les personnes que j'ai pu accompagner pendant ces 6 ans ; j'ai appris énormément de choses grâce à eux et j'ai pu travailler sur des sujets et dans des entreprises sur/pour lesquel(le)s je n'aurais jamais pensé travailler. Je remercie plus particulièrement :</p>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.linkedin.com/in/fr%C3%A9d%C3%A9ric-rocci-%F0%9F%92%9A-%F0%9F%92%9C-%F0%9F%92%99-bb411a33">Frédéric Rocci</a>, j'aurai du rejoindre Compta Online début 2017, cela ne s'est pas fait mais cela m'a permis de devenir indépendant</li>
<li><a rel="noopener" target="_blank" href="https://www.linkedin.com/in/vincentheuschling/">Vincent Heuschling</a> : il était mon prestataire lorsque j'étais encore chez JCDecaux, il devient mon premier client en 2017 pour lancer les premières fondations de <a rel="noopener" target="_blank" href="https://datatask.io/">DataTask</a>. On a remis ça en 2020/2021 mais les conditions sanitaires et économiques font que je ne peux pas rester sur le projet fin 2021. C'est grace à cette rencontre que je découvre le podcast <a rel="noopener" target="_blank" href="https://bigdatahebdo.com/">BigData Hebdo</a> puis rejoint l'équipe en 2019.</li>
<li><a rel="noopener" target="_blank" href="https://www.linkedin.com/in/thomas-bosviel-70b591103/">Thomas Bosviel</a>, prestataire également chez JCDecaux en 2016/17 et qui me met en contact en 2019 avec <a rel="noopener" target="_blank" href="https://www.linkedin.com/in/fr%C3%A9d%C3%A9ric-mefiant-0935b0a5">Frédéric Mefiant</a> de la SAFT et pouvoir ainsi commencer mon activité "Time Series".</li>
<li><a rel="noopener" target="_blank" href="https://www.linkedin.com/in/denis-rampnoux-5b36144/">Denis Rampnoux</a> pour la mission chez LesFurets.com et <a rel="noopener" target="_blank" href="https://www.linkedin.com/in/youenchene/">Youen Chéné</a> pour la mission chez Saagie.</li>
</ul>
<p>Ces années ont été très riches et passionnantes mais j'aspirai à aller vers d'autres choses ; le projet et la rencontre avec Flovéa semblent être la réponse que j'attendais. Il est donc temps de tourner la page et de découvrir ces nouveaux horizons.</p>
Web, Ops, IoT et Time Series - Mars 20222022-03-30T09:30:00+01:002022-03-30T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-iot-timeseries-mars-2022/<h3 id="conteneur-et-orchestration">Conteneur et Orchestration</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://docs.docker.com/engine/release-notes/#201013">Docker Engine 20.10.13</a> : Docker compose v2 arrive dans docker : ce qui permet de faire <code>docker compose</code> (au lieu de l’original <code>docker-compose</code> coté en python)</li>
<li><a rel="noopener" target="_blank" href="https://blog.vamc19.dev/posts/dockerfile-copy-chmod/"><code>COPY --chmod</code> reduced the size of my container image by 35%</a> : pour réduire la taille de vos images, plutôt que de faire un <code>ADD ...</code> puis un <code>RUN chmod ...</code>, faites directement un <code>ADD/COPY --chmod</code>. Marche aussi avec <code>--chown</code>.</li>
<li><a rel="noopener" target="_blank" href="https://github.com/agoncal/agoncal-talks/blob/master/2020-12-Quarkus/infrastructure/app.yaml#L6-L8">Docker Compose > depends > condition: ready</a> : <code>depends_on</code> a une <a rel="noopener" target="_blank" href="https://github.com/compose-spec/compose-spec/blob/master/spec.md#long-syntax-1">syntaxe longue</a> qui permet de définir une condition sur l'état du service dépendant : démarré (valeur par défaut de la version courte), "sain" (en fonction du résultat d'un healthcheck) ou "terminé avec succès" (si votre service dépend du résultat d'un job ou d'une tâche).</li>
</ul>
<h3 id="numerique">Numérique</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://lescastcodeurs.com/2022/02/24/lcc-273-interview-sur-le-darwinisme-numerique-avec-didier-girard-partie-1/">LCC 273 - Interview sur le darwinisme numérique avec Didier Girard - partie 1</a>, <a rel="noopener" target="_blank" href="https://lescastcodeurs.com/2022/03/07/lcc-274-interview-le-darwinisme-numerique-avec-didier-girard-partie-2/">LCC 274 - Interview le darwinisme numérique avec Didier Girard - partie 2</a> et <a rel="noopener" target="_blank" href="https://lescastcodeurs.com/2022/03/21/lcc-275-interview-sur-le-darwinisme-numerique-avec-didier-girard-partie-3/">LCC 275 - Interview sur le darwinisme numérique avec Didier Girard - partie 3</a> : interview en 3 volets de Didier Girard sur la notion de darwinisme numérique au niveau d'une nation, d'une entreprise et de l'individu.</li>
</ul>
<h3 id="open-data">Open Data</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://adresse.data.gouv.fr/">adresse.data.gouv.fr</a> : le site national des adresses dont l'objectif est de référencer l’intégralité des adresses du territoire et les rendre utilisables par tous.</li>
</ul>
<h3 id="outils">Outils</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://github.com/Extrawurst/gitui">GitUI</a> : si vous trouvez <code>tig</code> pas très intuitif/pratique, GitUI pourrait vous plaire. Prévu pour le terminal, il permet de se ballader facilement dans votre historique git & co. L' outil en codé en Rust.</li>
<li><a rel="noopener" target="_blank" href="https://github.com/konradsz/igrep">igrep</a> : un grep interactif qui permet d'ouvrir le fichier dans un éditeur et d'aller directement à la ligne contenant le motif recherché. Basé sur l'excellent <a rel="noopener" target="_blank" href="https://github.com/BurntSushi/ripgrep/">ripgrep</a>.</li>
</ul>
<h3 id="python">Python</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://github.com/florimondmanca/awesome-asgi">Awesome AGSI</a> : liste de ressources compatibles ASGI (Asynchronous Server Gateway Interface)</li>
<li><a rel="noopener" target="_blank" href="https://youtu.be/SRBWH58JONE">Demystifying Python's Async and Await Keywords</a> : une intro à async/await avec asyncio.</li>
<li><a rel="noopener" target="_blank" href="https://realpython.com/python-zipfile/">Python's zipfile: Manipulate Your ZIP Files Efficiently</a> : le module <a rel="noopener" target="_blank" href="https://docs.python.org/3/library/zipfile.html">zipfile</a> inclus dans la librairie standard Python permet de manipuler aisément des archives Zip. La page illustre les différentes méthodes et capacités du module.</li>
<li><a rel="noopener" target="_blank" href="https://towardsdatascience.com/how-to-write-user-friendly-command-line-interfaces-in-python-cc3a6444af8e">How to Write User-friendly Command Line Interfaces in Python</a> : si le module <code>argparse</code> est assez connu et peut être aussi <a rel="noopener" target="_blank" href="https://github.com/google/python-fire">Fire</a>, c'est l'occasion de découvrir <a rel="noopener" target="_blank" href="https://click.palletsprojects.com/en/8.0.x/">Click</a> (par l'équipe derrière Flask & co et à ne pas confondre avec <a rel="noopener" target="_blank" href="https://github.com/ajalt/clikt">clikt</a> en Kotlin), <a rel="noopener" target="_blank" href="https://typer.tiangolo.com/">Typer</a> (par le fondateur de FastAPI).</li>
<li><a rel="noopener" target="_blank" href="https://rzayev-sehriyar.medium.com/build-a-user-friendly-cli-from-pure-python-functions-348858c61022">Build a User-Friendly CLI from Pure Python Functions</a> : suite de l'article précédent avec la mise en place de <a rel="noopener" target="_blank" href="https://bstlabs.github.io/py-dynacli/">DynaCLI</a> dont le but est de générer des CLI depuis des fonctions pythons "pures".</li>
<li><a rel="noopener" target="_blank" href="https://mathspp.com/blog/pydonts/pass-by-value-reference-and-assignment">Pass-by-value, reference, and assignment | Pydon't 🐍</a> : Python passe-t-il ses variables par valeur ? par référence ou par assignement ?</li>
<li><a rel="noopener" target="_blank" href="https://ccbv.co.uk/">(Dajngo) Classy Class-Based Views</a> : une représentation détaillée des méthodes, attributs et propriétés des "Class based views" de Django</li>
<li><a rel="noopener" target="_blank" href="https://towardsdatascience.com/fugue-and-duckdb-fast-sql-code-in-python-e2e2dfc0f8eb">Fugue and DuckDB: Fast SQL Code in Python</a> : Fugue permet de combiner du SQL et du code Python et DuckDB permet de faire tourner une base OLAP. De quoi accélérer le traitement de vos données en python ?</li>
</ul>
<h3 id="rgpd-privacy-shield">RGPD & Privacy Shield</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://noyb.eu/en/privacy-shield-20-first-reaction-max-schrems">"Privacy Shield 2.0"? - First Reaction by Max Schrems</a> : La Commission Européenne et les USA ont annoncé une nouvelle version du Privacy Shield. Max Schrems est sceptique pour le moment...</li>
<li><a rel="noopener" target="_blank" href="https://matomo.org/blog/2022/01/google-analytics-4-vs-universal-analytics/">Google Analytics 4 (GA4) vs Universal Analytics (UA)</a> : Matomo se livre à un comparatif et une analyse (forcément un peu biaisés) de Google Analytics 4 vs Universal Analytics. Dans tous les cas, la conclusion est de prendre une solution qui répond à vos critères et respectent les règles du jeu (GDPR, etc).</li>
</ul>
Web, Ops, IoT et Time Series - Février 20222022-02-23T09:30:00+01:002022-02-23T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-iot-timeseries-fevrier-2022/<h3 id="code-langages">Code & Langages</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.python-httpx.org">httpx</a> : en gros, <a rel="noopener" target="_blank" href="https://2.python-requests.org">requests</a> mais avec le support de l'asynchrone. L'API semble être la même. httpx peut aussi s'installer en tant que cli.</li>
<li><a rel="noopener" target="_blank" href="https://github.com/TheAlgorithms/Go">The Algorithms - Go</a> : collection d'implémentation d'algorithme en Go à fin d'apprentissage</li>
</ul>
<h3 id="fonts">Fonts</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.luciole-vision.com/">Luciole</a> : La police Luciole a été créée à destination des personnes malvoyantes et apporte un certain confort de lecture et une meilleure lisibilité.</li>
</ul>
<h3 id="hardware">Hardware</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://steveblank.com/2022/01/25/the-semiconductor-ecosystem/">The Semiconductor Ecosystem – Explained</a> : Pour ceux qui veulent en connaitre un peu plus sur le monde des semi-conducteurs.</li>
</ul>
<h3 id="iot">IoT</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://thenewstack.io/anomaly-detection-glimpse-into-the-future-of-iot-data/">Anomaly Detection: Glimpse into the Future of IoT Data</a> : intéressant le triplet Objet IoT, Edge / Data Routeur capable de réaliser des opérations et le noeud central. L'edge computing permet d'éviter de saturer le noeud central et de prendre des décisions au plus près de l'objet IoT.</li>
</ul>
<h3 id="ops">Ops</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://traefik.io/blog/announcing-traefik-proxy-2-6/">Announcing Traefik Proxy 2.6</a> : amélioration sur le protocole HTTP/3, nouveaux middleware de "rate-limiting" au niveau TCP, amélioration au niveau de la Kubernetes API Gateway ou encore support de Consul Enterprise.</li>
<li><a rel="noopener" target="_blank" href="https://github.com/firecracker-microvm/firecracker/releases/tag/v1.0.0">Firecracker v1.0.0</a> : la MicroVM créée par AWS pour Fargate et Lambda passe en version 1.0</li>
<li><a rel="noopener" target="_blank" href="https://podman.io/releases/2022/02/22/podman-release-v4.0.0.html">Podman v4.0 has been released! </a> : nouvelle verison do podman avec pas mal de nouveautés.</li>
<li><a rel="noopener" target="_blank" href="https://slack.engineering/introducing-nebula-the-open-source-global-overlay-network-from-slack/">Introducing Nebula, the open source global overlay network from Slack</a> : Présentation de <a rel="noopener" target="_blank" href="https://github.com/slackhq/nebula">nebula</a>, la solution de connectivité de Slack qui a remplacé leur VPN basé sur IPSEC.</li>
<li><a rel="noopener" target="_blank" href="https://fly.io/blog/our-user-mode-wireguard-year/">Our User-Mode WireGuard Year</a> : retour d'expérience de <a rel="noopener" target="_blank" href="https://fly.io/">Fly.io</a> sur leur usage de Wireguard pour permettre un accès SSH à leur VMs / Conteneur dockers.</li>
<li>Le réseau de zéro - <a rel="noopener" target="_blank" href="https://lafor.ge/wireguard-0/">Partie 1</a> & <a rel="noopener" target="_blank" href="https://lafor.ge/wireguard-1/">Partie 2</a> : pour comprendre ce qu'il se trame sur votre réseau en partant de zéro.</li>
</ul>
<h3 id="outils">Outils</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://jless.io/">jless — a command-line JSON viewer</a> : un outil en ligne de commande pour parcourir/explorer vos fichiers JSON.</li>
</ul>
<h3 id="rgpd-vie-privee">RGPD & Vie Privée</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.cnil.fr/fr/utilisation-de-google-analytics-et-transferts-de-donnees-vers-les-etats-unis-la-cnil-met-en-demeure">Utilisation de Google Analytics et transferts de données vers les États-Unis : la CNIL met en demeure un gestionnaire de site web</a> : vers une interdiction partielle ou complète de Google Analytics ? Prochaine cible, les <a rel="noopener" target="_blank" href="https://web.developpez.com/actu/330644/Un-site-Web-condamne-a-une-amende-par-un-tribunal-allemand-pour-avoir-divulgue-l-adresse-IP-d-un-visiteur-via-Google-Fonts-le-proprietaire-du-site-risque-une-peine-de-prison-en-cas-de-recidive/">Google Fonts</a> ?</li>
<li><a rel="noopener" target="_blank" href="https://www.nextinpact.com/article/49870/google-analytics-retour-sur-mise-en-demeure-cnil">Google Analytics : retour sur la mise en demeure de la CNIL</a> : une synthèse en l'état de la situation par NextInpact.</li>
</ul>
<h3 id="time-series">Time Series</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.timescale.com/blog/year-of-the-tiger-110-million-to-build-the-future-of-data-for-developers-worldwide/">Year of the Tiger: $110 million to build the future of data for developers worldwide</a> : Timescale fait une 3ème levée de 110 M$ et atteint le statut de licorne avec une valorisation à 1 Mds $</li>
<li><a rel="noopener" target="_blank" href="https://tsfel.readthedocs.io/en/latest/">Time Series Feature Extraction Library (TSFEL for short)</a> : Bibliothèque Python à destination des Data Scientistes permettant d'extraire des features de séries temporelles avec 60 fonctions statistiques / temporelles / ...</li>
<li><a rel="noopener" target="_blank" href="https://www.timescale.com/blog/increase-your-storage-savings-with-timescaledb-2-6-introducing-compression-for-continuous-aggregates/">Increase your storage savings with TimescaleDB 2.6: Introducing compression for continuous aggregates</a> : Version 2.6 de TimescaleDB avec principalement de la compression autour des "continous aggregation".</li>
</ul>
Bilan 2021 et perspectives 20222022-02-11T18:00:00+01:002022-02-11T18:00:00+01:00
Unknown
https://cerenit.fr/blog/bilan-2021-perspectives-2022/<p>Routine habituelle de début d'année pour la clôture de ce 5ème exercice (déjà !).</p>
<h3 id="bilan-2021">Bilan 2021</h3>
<p>Au global, une année mitigée qui se termine un peu sur le fil du rasoir au niveau comptable. Pour la partie positive, j'ai l'impression que cette année a été "l'année des possibles" où les efforts commencés les années précédentes commencent à payer. Des premiers projets en Go, des missions Time Series intéressantes et ambitieuses par certains aspects et un projet annexe en Python/Django sur la fin d'année qui consolide différents éléments permettant de gagner en confiance et de réduire un peu ce cher syndrome de l'imposteur avec lequel j'apprends à composer et à dépasser parfois. </p>
<h4 id="cerenit">CérénIT</h4>
<p>D'un point de vue comptable, cela donne :</p>
<table><thead><tr><th></th><th>2021</th><th>2020</th><th>2019</th><th>2018</th><th>2017</th><th>Variation n/n-1</th></tr></thead><tbody>
<tr><td>Chiffre d'affaires</td><td>~130 K€</td><td>~138 K€</td><td>~150 K€</td><td>~132 K€</td><td>~100 K€</td><td>-6%</td></tr>
<tr><td>Résultat après impôts</td><td>~1K€</td><td>~10 K€</td><td>~13.5 K€</td><td>~10 K€</td><td>~20 K€</td><td>-90% 😱</td></tr>
<tr><td>Jours facturés</td><td>164</td><td>175</td><td>197</td><td>178</td><td>160</td><td>-6%</td></tr>
<tr><td>TJM</td><td>793€</td><td>789€</td><td>761€</td><td>742€</td><td>625€</td><td>+0.5%</td></tr>
</tbody></table>
<p>Les années précédentes, les missions annexes (missions courtes d'audit/expertise principalement) venaient apporter le bénéfice annuel et la mission principale couvrait les charges. Cette année, avec une réduction de mes missions principales en fin d'année, les missions annexes ont permis de couvrir les charges mais sans permettre de dégager de bénéfices. Un TJM plus élevé sur ces missions annexes permet d'amortir et de compenser la baisse d'activité.</p>
<table><thead><tr><th></th><th>2021</th><th>2020</th><th>2019</th><th>Variation n/n-1</th></tr></thead><tbody>
<tr><td>Chiffre d'affaires Total</td><td>~130 K€</td><td>~138 K€</td><td>~150 K€</td><td>-6%</td></tr>
<tr><td>Chiffre d'affaires Time Series</td><td>~25K€</td><td>~5 K€</td><td>~2 K€</td><td>+400% 💪</td></tr>
</tbody></table>
<p>Une des grandes satisfactions de l'année est incontestablement l'essor de l'activité Time Series. Etre référencé comme <a rel="noopener" target="_blank" href="https://www.influxdata.com/community-showcase/influxaces/">InfluxAce</a> m'a apporté une mission chez <a href="/reference/axens/">Axens</a> sur les <a href="/blog/influxdb-shard-duration-retention-policy/">Shard Duration & Retention Policies</a> et quelques contacts/prospects qui n'ont pas pu aboutir durant cette année. C'est aussi la poursuite de mon accompagnement des équipes de la <a href="/reference/saft-influxdb2/">SAFT</a> pour la 3ème année consécutive et la mise à jour d'InfluxDB OSS v1.x vers v2.x avec un sujet de <a href="/blog/influxdb-alerts-tasks-checks-notifications/">migration des alertes Kapacitor vers des Tasks en Flux</a>.</p>
<h4 id="autres-activites">Autres activités</h4>
<p>Pour le reste, j'ai eu le plaisir de :</p>
<ul>
<li>contribuer au podcast <a rel="noopener" target="_blank" href="https://bigdatahebdo.com/">BigData Hebo</a> avec une participation à 17 épisodes et la publication de 44 brèves,</li>
<li>continuer à animer le <a rel="noopener" target="_blank" href="https://timeseries.fr/">Meetup Time Series France</a> avec 5 éditions et en parvenant à lui donner une dimension autour des usages,</li>
<li>continuer à approfondir mes connaissances autour de Warp 10 avec notamment la publication d'un <a rel="noopener" target="_blank" href="https://blog.senx.io/n8n-warp-10-automate-your-time-series-manipulations/">billet sur le blog de SenX sur n8n & Warp 10</a> ou le fait de <a rel="noopener" target="_blank" href="https://blog.senx.io/working-with-geoshapes-code-contest-results/">gagner le challenge sur les GeoShapes</a> 🥇,</li>
<li>finalement parvenir à commencer à jouer avec des ESP32 en toute fin d'année en <a rel="noopener" target="_blank" href="http://localhost:1313/blog/air-quality-iot-esp32-senseair-thingspeak-mqtt-warp10-discovery/">faisant des détecteurs de CO2</a>.</li>
</ul>
<p>Pas de contribution de ma société à un quelconque projet comme en 2020 avec une contribution financière au projet <a rel="noopener" target="_blank" href="https://makair.life/">Makair</a> suite à la <a href="/blog/soutenir-une-initiative-en-temps-de-covid19/">question du rôle social d'une entreprise dans notre société en temps de COVID</a>. Petite déception à ce niveau-là.</p>
<h3 id="perspectives-2022">Perspectives 2022</h3>
<p>2021 s'est terminée avec une certaine forme de lassitude autour de l'activité de freelance (et de la solitude du freelance, même si j'ai toujours cherché à gommer cet aspect dans mes missions) et des activités autour de l'automatisation/industrialisation comme une fin en soit. J'éprouvais le besoin de créer un commun avec des personnes et je voulais accélérer la bascule vers l'IoT industriel mais ne sachant pas par quel bout attaquer le sujet. Une mission "DevOps AWS pour déployer un outil de CI/CD" sans autre finalité ne m'intéressait pas/plus (🤢).</p>
<p>Et là, de nulle part surgit une prise de contact sur LinkedIn par une recruteuse pour rejoindre une entreprise dans l'IoT industriel, avec une dimension environnementale qui cherchait un profil de responsable technique DevOps/IoT. Les astres ne pouvant pas être plus alignés, les cases du job idéal / de la mission idéale étant toutes cochées, je ne pouvais pas laisser passer cette opportunité. Depuis mi-janvier, j'ai donc commencé une mission pour cette entreprise pour qu'on fasse connaissance et pour avancer rapidement sur le sujet le temps que d'autres points soient traités de leur côté. De jolies choses semblent se dessiner et devraient aboutir prochainement... 🤩 </p>
<p>Affaire à suivre comme on dit, 2022 semble pleine de promesses, de défis et de changements ! 😎</p>
Web, Ops, IoT et Time Series - Janvier 20222022-01-26T09:30:00+01:002022-01-26T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-data-timeseries-janvier-2022/<h3 id="ide">IDE</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://philippart-s.github.io/blog/articles/ide/gitpod/">Gitpod à la place d’Intellij ou de VSCode ?</a> : Si l'IDE dans le cloud vous intéresse, cet article est assez détaillé sur sa mise en place et sa personnalisation.</li>
</ul>
<h3 id="iot">IoT</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.2improveit.eu/blog/2021/11/use-mqtt-with-the-wio-terminal-and-tinygo/">Use MQTT with the Wio Terminal and TinyGo</a> : <a rel="noopener" target="_blank" href="https://tinygo.org/">TinyGo</a> est une version de Go à destination des micro-controlleurs. Le billet d'écrit comment s'abonner à un topic MQTT et afficher un message sur le Wio Terminal.</li>
<li><a rel="noopener" target="_blank" href="https://www.openhab.org/blog/2021-12-19-openhab-3-2-release.html">openHAB 3.2 Release</a> : cette version apporte notamment des améliorations au niveau du moteur de règle avec un version Javascript, le support de Blockly ou encore d'un modèle de règle (rule template).</li>
<li><a rel="noopener" target="_blank" href="https://github.com/stm32duino/wiki/wiki">stm32duino wiki</a> : si vous envisagez de faire un projet arduino avec des cartes ST Micro Electronics STM32...</li>
<li><a rel="noopener" target="_blank" href="https://atadiat.com/en/e-mqtt-101-tutorial-introduction-and-eclipse-mosquitto/">MQTT 101 Tutorial: Introduction and Hands-on using Eclipse Mosquitto</a> : Introduction et éventuel atelier pratique pour découvrir MQTT avec le broker <a rel="noopener" target="_blank" href="https://mosquitto.org/">Mosquitto</a>.</li>
<li><a rel="noopener" target="_blank" href="https://www.hivemq.com/mqtt-essentials/">MQTT Essentials</a> : si vous avez besoin de vous (re)mettre à niveau sur MQTT, une série de billets couvrant les différents aspects du protocole et son fonctionnement.</li>
<li><a rel="noopener" target="_blank" href="https://www.hivemq.com/mqtt-5/">MQTT5 Essentials</a> : la suite avec un focus sur les apports de MQTT v5.</li>
</ul>
<h3 id="monitoring-observabilite">Monitoring & Observabilité</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://grafana.com/blog/2022/01/03/introducing-grafana-university-our-virtual-hands-on-education-platform-thats-free-and-easy-to-use/">Introducing Grafana University: our virtual hands-on education platform that's free and easy to use</a> : Grafana Labs ouvre les portes de son université pour se former à ses produits.</li>
</ul>
<h3 id="python">Python</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://realpython.com/python-sockets/">Socket Programming in Python (Guide)</a> : Pour tout savoir sur les sockets en Python.</li>
</ul>
<h3 id="reseau">Réseau</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://blog.tonari.no/introducing-innernet">Introducing 'innernet'</a> : <a rel="noopener" target="_blank" href="https://github.com/tonarino/innernet">innernet</a> est un gestionnaire de réseau basé sur WireGuard. Il permet de déclarer l'ensemble de votre réseau wireguard et de définir des politiques réseaux (VLAN, Associations, etc)</li>
</ul>
<h3 id="time-series">Time Series</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://github.com/lmmentel/awesome-time-series">lmmentel /awesome-time-series</a> : un dépot github recensant des projets / librairies / ouvrages / documentation sur les séries temporelles.</li>
<li><a rel="noopener" target="_blank" href="https://www.postgresql.org/about/news/influxdb-fdw-111-released-2377/">InfluxDB FDW 1.1.1 released</a> : <a rel="noopener" target="_blank" href="https://github.com/pgspider/influxdb_fdw">InfluxDB FDW</a> est un Foreign Data Wrapper pour Postgresql 10+ qui permet de se connecter à une source InfluxDB 1.x</li>
<li><a rel="noopener" target="_blank" href="https://blog.senx.io/santa-asset-tracking-and-delivery-service/">Santa asset tracking and delivery service</a> : une démo de suivi d'actif avec Warp 10 et Discovery en prenant l'exemple de la livraison des cadeaux de Noel.</li>
</ul>
<h3 id="web">Web</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://arnaudr.io/2020/08/10/goaccess-14-a-detailed-tutorial/">GoAccess 1.4, a detailed tutorial</a> : en cherchant à déployer une instance <a rel="noopener" target="_blank" href="https://www.awstats.org/">AWStats</a> pour avoir des statistiques de visites sur la base des logs du serveur web nginx, je suis tombé sur <a rel="noopener" target="_blank" href="https://goaccess.io/">GoAccess</a> qui semble offir les mêmes fonctionnalités et même plus tout en étant plus simple à déployer/configurer.</li>
</ul>
IoT - Qualité de l'air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery2022-01-11T18:00:00+01:002022-01-11T18:00:00+01:00
Unknown
https://cerenit.fr/blog/air-quality-iot-esp32-senseair-thingspeak-mqtt-warp10-discovery/<p>Le projet <a rel="noopener" target="_blank" href="http://nousaerons.fr/">Nous Aérons</a> propose de réaliser ses propres <a rel="noopener" target="_blank" href="http://nousaerons.fr/makersco2/#fabriquer">détecteurs de CO2</a> avec un ESP32 avec un écran comme le <a rel="noopener" target="_blank" href="http://www.lilygo.cn/prod_view.aspx?TypeId=50033&Id=1126&FId=t3:50033:3">Lilygo TTGo T-Display</a> et un capteur <a rel="noopener" target="_blank" href="https://senseair.com/products/size-counts/s8-lp/">Senseair S8-LP</a>.</p>
<p>L'idée est donc de déployer plusieurs capteurs, faire remonter les valeurs via <a rel="noopener" target="_blank" href="https://thingspeak.com/">ThingSpeak</a> et ensuite les ingérer puis analyser avec <a rel="noopener" target="_blank" href="https://warp10.io/">Warp 10</a> et faire un dashboard avec <a rel="noopener" target="_blank" href="https://warp10.io/content/05_Ecosystem/02_Visualization/02_Discovery/00_Overview">Discovery</a>.</p>
<p><a href="/blog/iot_air_quality_schema.png"><img src="/blog/iot_air_quality_schema.png" alt="schema du projet" /></a></p>
<h3 id="montage">Montage</h3>
<p>Pour le montage, je vous invite à consuler principalement :</p>
<ul>
<li><a rel="noopener" target="_blank" href="https://co2.rinolfi.ch/">Capteur de CO2</a> et le code </li>
<li>Les instructions des projets "BEL AIR" et "GRAND AIR" à récupérer via le site de <a rel="noopener" target="_blank" href="http://nousaerons.fr/makersco2/#fabriquer">Nous Aérons</a></li>
<li><a rel="noopener" target="_blank" href="http://f6kfa.fr/premiers-pas-application-de-demo-du-ttgo-t-display/">Premiers pas ESP32 : Application de démo du TTGO T-Display</a> : pour la configuration d'Arduino IDE - pour l'application de démo, les chemins ont changé par contre.</li>
<li>Sous OSX, j'ai du récupérer la version 1.6+ du <a rel="noopener" target="_blank" href="http://www.wch.cn/downloads/CH34XSER_MAC_ZIP.html">driver du chipset</a> et être en mesure d'avoir le port <code>/dev/cu.wchusbserial*</code> afin de pouvoir uploader le code depuis Arduino IDE vers l'ESP32.</li>
</ul>
<h3 id="thingspeak">ThingSpeak</h3>
<p>L'exemple de code fourni utilise le service <a rel="noopener" target="_blank" href="https://thingspeak.com/">ThingSpeak</a> pour la remontée des valeurs. Comme il s'agit de mon premier projet Arduino et que cela fonctionne, j'ai cherché à rester dans les clous du code proposé et tester par la même occasion ce service. J'aurais pu directement poster les valeurs sur mon instance Warp 10 mais c'est aussi l'occasion de tester la récupération d'informations via le <a rel="noopener" target="_blank" href="https://warpfleet.senx.io/browse/io.warp10/warp10-plugin-mqtt">client MQTT de Warp 10</a>.</p>
<p>Il vous faut :</p>
<ul>
<li>Créer un compte</li>
<li>Créer un channel avec le champ 1 qui accueillera vos données</li>
<li>Récupérer la clé d'API en écriture</li>
<li>Noter votre Channel ID</li>
</ul>
<h3 id="code-arduino">Code Arduino</h3>
<p>Disclaimer : c'est mon premier projet Arduino.</p>
<p>En repartant du code fourni sur le site <a rel="noopener" target="_blank" href="https://co2.rinolfi.ch/">Capteur de CO2</a>, j'ai fait quelques ajustements :</p>
<ul>
<li>Remplacer l'appel HTTP par la librairie <a rel="noopener" target="_blank" href="https://github.com/mathworks/thingspeak-arduino/">ThingSpeak</a></li>
<li>La librairie retourne des <a rel="noopener" target="_blank" href="https://github.com/mathworks/thingspeak-arduino/blob/master/src/ThingSpeak.h#L62">status code</a> : <code>200</code> si c'est OK, <code>40x</code> si incorrect et <code>-XXX</code> si erreur ; j'ai un amélioré le message de debug pour savoir si l'insertion était OK ou KO.</li>
<li>Ajustement de positionnement du "CO2" et du "ppm" sur l'écran pour une meilleure lisibilité.</li>
</ul>
<p>Il vous faut modifier :</p>
<ul>
<li>la configuration wifi : <code>ssid1</code>, <code>password1</code> et éventuellement <code>ssid2</code>, <code>password2</code></li>
<li>la configuration ThingSpeak : <code>channelID</code>, <code>writeAPIKey</code></li>
</ul>
<p>Compiler le tout et uploader le code sur votre ESP32.</p>
<pre data-lang="c" style="background-color:#2b303b;color:#c0c5ce;" class="language-c "><code class="language-c" data-lang="c"><span style="color:#65737e;">/************************************************
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * Capteur de CO2 par Grégoire Rinolfi
</span><span style="color:#65737e;"> * https://co2.rinolfi.ch
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> ***********************************************/
</span><span style="color:#b48ead;">#include </span><span><</span><span style="color:#a3be8c;">TFT_eSPI.h</span><span>>
</span><span style="color:#b48ead;">#include </span><span><</span><span style="color:#a3be8c;">SPI.h</span><span>>
</span><span style="color:#b48ead;">#include </span><span><</span><span style="color:#a3be8c;">Wire.h</span><span>>
</span><span style="color:#b48ead;">#include </span><span><</span><span style="color:#a3be8c;">WiFiMulti.h</span><span>>
</span><span style="color:#b48ead;">#include </span><span><</span><span style="color:#a3be8c;">ThingSpeak.h</span><span>>
</span><span>
</span><span>WiFiMulti wifiMulti;
</span><span>TFT_eSPI tft = </span><span style="color:#bf616a;">TFT_eSPI</span><span>(</span><span style="color:#d08770;">135</span><span>, </span><span style="color:#d08770;">240</span><span>);
</span><span>
</span><span style="color:#65737e;">/************************************************
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * Paramètres utilisateur
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> ***********************************************/
</span><span>
</span><span style="color:#b48ead;">#define </span><span>TXD2 </span><span style="color:#d08770;">21 </span><span style="color:#65737e;">// série capteur TX
</span><span style="color:#b48ead;">#define </span><span>RXD2 </span><span style="color:#d08770;">22 </span><span style="color:#65737e;">// série capteur RX
</span><span style="color:#b48ead;">#define </span><span>BOUTON_CAL </span><span style="color:#d08770;">35
</span><span style="color:#b48ead;">#define </span><span>DEBOUNCE_TIME </span><span style="color:#d08770;">1000
</span><span>
</span><span style="color:#b48ead;">const char</span><span>* ssid1 = "</span><span style="color:#a3be8c;">wifi1</span><span>";
</span><span style="color:#b48ead;">const char</span><span>* password1 = "</span><span style="color:#a3be8c;">XXXXXXXXXXXXXXXX</span><span>";
</span><span style="color:#b48ead;">const char</span><span>* ssid2 = "</span><span style="color:#a3be8c;">wifi2</span><span>";
</span><span style="color:#b48ead;">const char</span><span>* password2 = "</span><span style="color:#a3be8c;">XXXXXXXXXXXXXXXX</span><span>";
</span><span>
</span><span style="color:#b48ead;">unsigned long</span><span> channelID = XXXXXXXXXXXXXXXX;
</span><span style="color:#b48ead;">char</span><span>* readAPIKey = "</span><span style="color:#a3be8c;">XXXXXXXXXXXXXXXX</span><span>";
</span><span style="color:#b48ead;">char</span><span>* writeAPIKey = "</span><span style="color:#a3be8c;">XXXXXXXXXXXXXXXX</span><span>";
</span><span style="color:#b48ead;">unsigned int</span><span> dataFieldOne = </span><span style="color:#d08770;">1</span><span>; </span><span style="color:#65737e;">// Field to write temperature data
</span><span style="color:#b48ead;">const unsigned long</span><span> postingInterval = </span><span style="color:#d08770;">12</span><span style="color:#b48ead;">L </span><span>* </span><span style="color:#d08770;">1000</span><span style="color:#b48ead;">L</span><span>; </span><span style="color:#65737e;">// 12s
</span><span style="color:#b48ead;">unsigned long</span><span> lastTime = </span><span style="color:#d08770;">0</span><span>;
</span><span>
</span><span style="color:#65737e;">// gestion de l'horloge pour la validation des certificats HTTPS
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">setClock</span><span>() {
</span><span> </span><span style="color:#bf616a;">configTime</span><span>(</span><span style="color:#d08770;">0</span><span>, </span><span style="color:#d08770;">0</span><span>, "</span><span style="color:#a3be8c;">pool.ntp.org</span><span>", "</span><span style="color:#a3be8c;">time.nist.gov</span><span>");
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(</span><span style="color:#bf616a;">F</span><span>("</span><span style="color:#a3be8c;">Waiting for NTP time sync: </span><span>"));
</span><span> time_t nowSecs = </span><span style="color:#96b5b4;">time</span><span>(nullptr);
</span><span> </span><span style="color:#b48ead;">while </span><span>(nowSecs < </span><span style="color:#d08770;">8 </span><span>* </span><span style="color:#d08770;">3600 </span><span>* </span><span style="color:#d08770;">2</span><span>) {
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">500</span><span>);
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(</span><span style="color:#bf616a;">F</span><span>("</span><span style="color:#a3be8c;">.</span><span>"));
</span><span> </span><span style="color:#bf616a;">yield</span><span>();
</span><span> nowSecs = </span><span style="color:#96b5b4;">time</span><span>(nullptr);
</span><span> }
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>();
</span><span> </span><span style="color:#b48ead;">struct</span><span> tm timeinfo;
</span><span> </span><span style="color:#bf616a;">gmtime_r</span><span>(&nowSecs, &timeinfo);
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(</span><span style="color:#bf616a;">F</span><span>("</span><span style="color:#a3be8c;">Current time: </span><span>"));
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(</span><span style="color:#96b5b4;">asctime</span><span>(&timeinfo));
</span><span>}
</span><span>
</span><span style="color:#65737e;">/************************************************
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * Thinkgspeak functions
</span><span style="color:#65737e;"> * https://fr.mathworks.com/help/thingspeak/read-and-post-temperature-data.html
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> ***********************************************/
</span><span>
</span><span style="color:#b48ead;">float </span><span style="color:#8fa1b3;">readTSData</span><span>( </span><span style="color:#b48ead;">long </span><span style="color:#bf616a;">TSChannel</span><span>,</span><span style="color:#b48ead;">unsigned int </span><span style="color:#bf616a;">TSField </span><span>){
</span><span>
</span><span> </span><span style="color:#b48ead;">float</span><span> data = ThingSpeak.</span><span style="color:#bf616a;">readFloatField</span><span>( TSChannel, TSField, readAPIKey );
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>( "</span><span style="color:#a3be8c;"> Data read from ThingSpeak: </span><span>" + </span><span style="color:#bf616a;">String</span><span>( data, </span><span style="color:#d08770;">9 </span><span>) );
</span><span> </span><span style="color:#b48ead;">return</span><span> data;
</span><span>
</span><span>}
</span><span>
</span><span style="color:#65737e;">// Use this function if you want to write a single field.
</span><span style="color:#b48ead;">int </span><span style="color:#8fa1b3;">writeTSData</span><span>( </span><span style="color:#b48ead;">long </span><span style="color:#bf616a;">TSChannel</span><span>, </span><span style="color:#b48ead;">unsigned int </span><span style="color:#bf616a;">TSField</span><span>, </span><span style="color:#b48ead;">float </span><span style="color:#bf616a;">data </span><span>){
</span><span> </span><span style="color:#b48ead;">int</span><span> writeSuccess = ThingSpeak.</span><span style="color:#bf616a;">writeField</span><span>( TSChannel, TSField, data, writeAPIKey ); </span><span style="color:#65737e;">// Write the data to the channel
</span><span> </span><span style="color:#b48ead;">if</span><span>(writeSuccess == </span><span style="color:#d08770;">200</span><span>){
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Channel updated successfully!</span><span>");
</span><span> }
</span><span> </span><span style="color:#b48ead;">else</span><span>{
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Problem updating channel. HTTP error code </span><span>" + </span><span style="color:#bf616a;">String</span><span>(writeSuccess));
</span><span> }
</span><span> </span><span style="color:#b48ead;">return</span><span> writeSuccess;
</span><span>}
</span><span>
</span><span style="color:#65737e;">// Use this function if you want to write multiple fields simultaneously.
</span><span style="color:#b48ead;">int </span><span style="color:#8fa1b3;">write2TSData</span><span>( </span><span style="color:#b48ead;">long </span><span style="color:#bf616a;">TSChannel</span><span>, </span><span style="color:#b48ead;">unsigned int </span><span style="color:#bf616a;">TSField1</span><span>, </span><span style="color:#b48ead;">long </span><span style="color:#bf616a;">field1Data</span><span>, </span><span style="color:#b48ead;">unsigned int </span><span style="color:#bf616a;">TSField2</span><span>, </span><span style="color:#b48ead;">long </span><span style="color:#bf616a;">field2Data </span><span>){
</span><span>
</span><span> ThingSpeak.</span><span style="color:#bf616a;">setField</span><span>( TSField1, field1Data );
</span><span> ThingSpeak.</span><span style="color:#bf616a;">setField</span><span>( TSField2, field2Data );
</span><span>
</span><span> </span><span style="color:#b48ead;">int</span><span> writeSuccess = ThingSpeak.</span><span style="color:#bf616a;">writeFields</span><span>( TSChannel, writeAPIKey );
</span><span> </span><span style="color:#b48ead;">if</span><span>(writeSuccess == </span><span style="color:#d08770;">200</span><span>){
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Channel updated successfully!</span><span>");
</span><span> }
</span><span> </span><span style="color:#b48ead;">else</span><span>{
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Problem updating channel. HTTP error code </span><span>" + </span><span style="color:#bf616a;">String</span><span>(writeSuccess));
</span><span> }
</span><span> </span><span style="color:#b48ead;">return</span><span> writeSuccess;
</span><span>}
</span><span>
</span><span style="color:#65737e;">/************************************************
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> * Code de gestion du capteur CO2 via ModBus
</span><span style="color:#65737e;"> * inspiré de : https://github.com/SFeli/ESP32_S8
</span><span style="color:#65737e;"> *
</span><span style="color:#65737e;"> ***********************************************/
</span><span style="color:#b48ead;">volatile </span><span>uint32_t DebounceTimer = </span><span style="color:#d08770;">0</span><span>;
</span><span>
</span><span>byte CO2req[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X04</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X03</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X01</span><span>, </span><span style="color:#d08770;">0XD5</span><span>, </span><span style="color:#d08770;">0XC5</span><span>};
</span><span>byte ABCreq[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X03</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X1F</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X01</span><span>, </span><span style="color:#d08770;">0XA1</span><span>, </span><span style="color:#d08770;">0XC3</span><span>};
</span><span>byte disableABC[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X06</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X1F</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0XAC</span><span>, </span><span style="color:#d08770;">0X03</span><span>}; </span><span style="color:#65737e;">// écrit la période 0 dans le registre HR32 à adresse 0x001f
</span><span>byte enableABC[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X06</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X1F</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0XB4</span><span>, </span><span style="color:#d08770;">0XAC</span><span>, </span><span style="color:#d08770;">0X74</span><span>}; </span><span style="color:#65737e;">// écrit la période 180
</span><span>byte clearHR1[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X06</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X9D</span><span>, </span><span style="color:#d08770;">0XC5</span><span>}; </span><span style="color:#65737e;">// ecrit 0 dans le registe HR1 adresse 0x00
</span><span>byte HR1req[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X03</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X01</span><span>, </span><span style="color:#d08770;">0X90</span><span>, </span><span style="color:#d08770;">0X05</span><span>}; </span><span style="color:#65737e;">// lit le registre HR1 (vérifier bit 5 = 1 )
</span><span>byte calReq[] = {</span><span style="color:#d08770;">0xFE</span><span>, </span><span style="color:#d08770;">0X06</span><span>, </span><span style="color:#d08770;">0X00</span><span>, </span><span style="color:#d08770;">0X01</span><span>, </span><span style="color:#d08770;">0X7C</span><span>, </span><span style="color:#d08770;">0X06</span><span>, </span><span style="color:#d08770;">0X6C</span><span>, </span><span style="color:#d08770;">0XC7</span><span>}; </span><span style="color:#65737e;">// commence la calibration background
</span><span>byte Response[</span><span style="color:#d08770;">20</span><span>];
</span><span>uint16_t crc_02;
</span><span style="color:#b48ead;">int</span><span> ASCII_WERT;
</span><span style="color:#b48ead;">int</span><span> int01, int02, int03;
</span><span style="color:#b48ead;">unsigned long</span><span> ReadCRC; </span><span style="color:#65737e;">// CRC Control Return Code
</span><span>
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">send_Request </span><span>(byte * </span><span style="color:#bf616a;">Request</span><span>, </span><span style="color:#b48ead;">int </span><span style="color:#bf616a;">Re_len</span><span>)
</span><span>{
</span><span> </span><span style="color:#b48ead;">while </span><span>(!Serial1.</span><span style="color:#bf616a;">available</span><span>())
</span><span> {
</span><span> Serial1.</span><span style="color:#bf616a;">write</span><span>(Request, Re_len); </span><span style="color:#65737e;">// Send request to S8-Sensor
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">50</span><span>);
</span><span> }
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">Requete : </span><span>");
</span><span> </span><span style="color:#b48ead;">for </span><span>(int02 = </span><span style="color:#d08770;">0</span><span>; int02 < Re_len; int02++) </span><span style="color:#65737e;">// Empfangsbytes
</span><span> {
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(Request[int02],HEX);
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(" ");
</span><span> }
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>();
</span><span>
</span><span>}
</span><span>
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">read_Response </span><span>(</span><span style="color:#b48ead;">int </span><span style="color:#bf616a;">RS_len</span><span>)
</span><span>{
</span><span> int01 = </span><span style="color:#d08770;">0</span><span>;
</span><span> </span><span style="color:#b48ead;">while </span><span>(Serial1.</span><span style="color:#bf616a;">available</span><span>() < </span><span style="color:#d08770;">7 </span><span>)
</span><span> {
</span><span> int01++;
</span><span> </span><span style="color:#b48ead;">if </span><span>(int01 > </span><span style="color:#d08770;">10</span><span>)
</span><span> {
</span><span> </span><span style="color:#b48ead;">while </span><span>(Serial1.</span><span style="color:#bf616a;">available</span><span>())
</span><span> Serial1.</span><span style="color:#bf616a;">read</span><span>();
</span><span> </span><span style="color:#b48ead;">break</span><span>;
</span><span> }
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">50</span><span>);
</span><span> }
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">Reponse : </span><span>");
</span><span> </span><span style="color:#b48ead;">for </span><span>(int02 = </span><span style="color:#d08770;">0</span><span>; int02 < RS_len; int02++) </span><span style="color:#65737e;">// Empfangsbytes
</span><span> {
</span><span> Response[int02] = Serial1.</span><span style="color:#bf616a;">read</span><span>();
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(Response[int02],HEX);
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>(" ");
</span><span> }
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>();
</span><span>}
</span><span>
</span><span style="color:#b48ead;">unsigned short int </span><span style="color:#8fa1b3;">ModBus_CRC</span><span>(</span><span style="color:#b48ead;">unsigned char </span><span>* </span><span style="color:#bf616a;">buf</span><span>, </span><span style="color:#b48ead;">int </span><span style="color:#bf616a;">len</span><span>)
</span><span>{
</span><span> </span><span style="color:#b48ead;">unsigned short int</span><span> crc = </span><span style="color:#d08770;">0xFFFF</span><span>;
</span><span> </span><span style="color:#b48ead;">for </span><span>(</span><span style="color:#b48ead;">int</span><span> pos = </span><span style="color:#d08770;">0</span><span>; pos < len; pos++) {
</span><span> crc ^= (</span><span style="color:#b48ead;">unsigned short int</span><span>)buf[pos]; </span><span style="color:#65737e;">// XOR byte into least sig. byte of crc
</span><span> </span><span style="color:#b48ead;">for </span><span>(</span><span style="color:#b48ead;">int</span><span> i = </span><span style="color:#d08770;">8</span><span>; i != </span><span style="color:#d08770;">0</span><span>; i--) { </span><span style="color:#65737e;">// Loop over each bit
</span><span> </span><span style="color:#b48ead;">if </span><span>((crc & </span><span style="color:#d08770;">0x0001</span><span>) != </span><span style="color:#d08770;">0</span><span>) { </span><span style="color:#65737e;">// If the LSB is set
</span><span> crc >>= </span><span style="color:#d08770;">1</span><span>; </span><span style="color:#65737e;">// Shift right and XOR 0xA001
</span><span> crc ^= </span><span style="color:#d08770;">0xA001</span><span>;
</span><span> }
</span><span> </span><span style="color:#b48ead;">else </span><span style="color:#65737e;">// else LSB is not set
</span><span> crc >>= </span><span style="color:#d08770;">1</span><span>; </span><span style="color:#65737e;">// Just shift right
</span><span> }
</span><span> } </span><span style="color:#65737e;">// Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
</span><span> </span><span style="color:#b48ead;">return</span><span> crc;
</span><span>}
</span><span>
</span><span style="color:#b48ead;">unsigned long </span><span style="color:#8fa1b3;">get_Value</span><span>(</span><span style="color:#b48ead;">int </span><span style="color:#bf616a;">RS_len</span><span>)
</span><span>{
</span><span>
</span><span style="color:#65737e;">// Check the CRC //
</span><span> ReadCRC = (uint16_t)Response[RS_len-</span><span style="color:#d08770;">1</span><span>] * </span><span style="color:#d08770;">256 </span><span>+ (uint16_t)Response[RS_len-</span><span style="color:#d08770;">2</span><span>];
</span><span> </span><span style="color:#b48ead;">if </span><span>(</span><span style="color:#bf616a;">ModBus_CRC</span><span>(Response, RS_len-</span><span style="color:#d08770;">2</span><span>) == ReadCRC) {
</span><span> </span><span style="color:#65737e;">// Read the Value //
</span><span> </span><span style="color:#b48ead;">unsigned long</span><span> val = (uint16_t)Response[</span><span style="color:#d08770;">3</span><span>] * </span><span style="color:#d08770;">256 </span><span>+ (uint16_t)Response[</span><span style="color:#d08770;">4</span><span>];
</span><span> </span><span style="color:#b48ead;">return</span><span> val * </span><span style="color:#d08770;">1</span><span>; </span><span style="color:#65737e;">// S8 = 1. K-30 3% = 3, K-33 ICB = 10
</span><span> }
</span><span> </span><span style="color:#b48ead;">else </span><span>{
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">CRC Error</span><span>");
</span><span> </span><span style="color:#b48ead;">return </span><span style="color:#d08770;">99</span><span>;
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#65737e;">// interruption pour lire le bouton d'étalonnage
</span><span style="color:#b48ead;">bool</span><span> demandeEtalonnage = </span><span style="color:#d08770;">false</span><span>;
</span><span style="color:#b48ead;">void</span><span> IRAM_ATTR </span><span style="color:#8fa1b3;">etalonnage</span><span>() {
</span><span> </span><span style="color:#b48ead;">if </span><span>( </span><span style="color:#bf616a;">millis</span><span>() - DEBOUNCE_TIME >= DebounceTimer ) {
</span><span> DebounceTimer = </span><span style="color:#bf616a;">millis</span><span>();
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Etalonnage manuel !!</span><span>");
</span><span>
</span><span> tft.</span><span style="color:#bf616a;">fillScreen</span><span>(TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">3</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_WHITE);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Etalonnage</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>()/</span><span style="color:#d08770;">2</span><span>);
</span><span>
</span><span> demandeEtalonnage = </span><span style="color:#d08770;">true</span><span>;
</span><span> }
</span><span>}
</span><span>
</span><span style="color:#65737e;">// nettoie l'écran et affiche les infos utiles
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">prepareEcran</span><span>() {
</span><span> tft.</span><span style="color:#bf616a;">fillScreen</span><span>(TFT_BLACK);
</span><span> </span><span style="color:#65737e;">// texte co2 à gauche
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">4</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_WHITE);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">CO</span><span>",</span><span style="color:#d08770;">25</span><span>, </span><span style="color:#d08770;">120</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">3</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">2</span><span>",</span><span style="color:#d08770;">60</span><span>, </span><span style="color:#d08770;">125</span><span>);
</span><span>
</span><span> </span><span style="color:#65737e;">// texte PPM à droite ppm
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">ppm</span><span>",</span><span style="color:#d08770;">215</span><span>, </span><span style="color:#d08770;">120</span><span>);
</span><span>
</span><span> </span><span style="color:#65737e;">// écriture du chiffre
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_GREEN,TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">8</span><span>);
</span><span>}
</span><span>
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">setup</span><span>() {
</span><span> </span><span style="color:#65737e;">// bouton de calibration
</span><span> </span><span style="color:#bf616a;">pinMode</span><span>(BOUTON_CAL, INPUT);
</span><span>
</span><span> </span><span style="color:#65737e;">// ports série de debug et de communication capteur
</span><span> Serial.</span><span style="color:#bf616a;">begin</span><span>(</span><span style="color:#d08770;">115200</span><span>);
</span><span> Serial1.</span><span style="color:#bf616a;">begin</span><span>(</span><span style="color:#d08770;">9600</span><span>, SERIAL_8N1, RXD2, TXD2);
</span><span>
</span><span> </span><span style="color:#65737e;">// initialise l'écran
</span><span> tft.</span><span style="color:#bf616a;">init</span><span>();
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">20</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setRotation</span><span>(</span><span style="color:#d08770;">1</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">fillScreen</span><span>(TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">setTextDatum</span><span>(MC_DATUM); </span><span style="color:#65737e;">// imprime la string middle centre
</span><span>
</span><span> </span><span style="color:#65737e;">// vérifie l'état de l'ABC
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(ABCreq, </span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">Période ABC : </span><span>");
</span><span> Serial.</span><span style="color:#96b5b4;">printf</span><span>("</span><span style="color:#d08770;">%02ld</span><span>", </span><span style="color:#bf616a;">get_Value</span><span>(</span><span style="color:#d08770;">7</span><span>));
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>();
</span><span> </span><span style="color:#b48ead;">int</span><span> abc = </span><span style="color:#bf616a;">get_Value</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span>
</span><span> </span><span style="color:#65737e;">// active ou désactive l'ABC au démarrage
</span><span> </span><span style="color:#b48ead;">if</span><span>(</span><span style="color:#bf616a;">digitalRead</span><span>(BOUTON_CAL) == LOW){
</span><span> </span><span style="color:#b48ead;">if</span><span>(abc == </span><span style="color:#d08770;">0</span><span>){
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(enableABC, </span><span style="color:#d08770;">8</span><span>);
</span><span> }</span><span style="color:#b48ead;">else</span><span>{
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(disableABC, </span><span style="color:#d08770;">8</span><span>);
</span><span> }
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> </span><span style="color:#bf616a;">get_Value</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> }
</span><span>
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_BLUE,TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Autocalibration</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, </span><span style="color:#d08770;">10</span><span>);
</span><span> </span><span style="color:#b48ead;">if</span><span>( abc != </span><span style="color:#d08770;">0 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>(</span><span style="color:#bf616a;">String</span><span>(abc)+"</span><span style="color:#a3be8c;">h</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, </span><span style="color:#d08770;">40</span><span>);
</span><span> }</span><span style="color:#b48ead;">else</span><span>{
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">OFF</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, </span><span style="color:#d08770;">40</span><span>);
</span><span> }
</span><span>
</span><span> </span><span style="color:#65737e;">// gestion du wifi
</span><span> wifiMulti.</span><span style="color:#bf616a;">addAP</span><span>(ssid1, password1);
</span><span> wifiMulti.</span><span style="color:#bf616a;">addAP</span><span>(ssid2, password2);
</span><span>
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">Connexion au wifi</span><span>");
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_WHITE,TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Recherche wifi</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2</span><span>);
</span><span>
</span><span> </span><span style="color:#b48ead;">int</span><span> i = </span><span style="color:#d08770;">0</span><span>;
</span><span> </span><span style="color:#b48ead;">while</span><span>(wifiMulti.</span><span style="color:#bf616a;">run</span><span>() != WL_CONNECTED && i < </span><span style="color:#d08770;">3</span><span>){
</span><span> Serial.</span><span style="color:#bf616a;">print</span><span>("</span><span style="color:#a3be8c;">.</span><span>");
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">500</span><span>);
</span><span> i++;
</span><span> }
</span><span> </span><span style="color:#b48ead;">if</span><span>(wifiMulti.</span><span style="color:#bf616a;">run</span><span>() == WL_CONNECTED){
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_GREEN,TFT_BLACK);
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Connecté au wifi</span><span>");
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">IP address: </span><span>");
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>(WiFi.</span><span style="color:#bf616a;">localIP</span><span>());
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Wifi OK</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, </span><span style="color:#d08770;">100</span><span>);
</span><span> </span><span style="color:#bf616a;">setClock</span><span>();
</span><span> }</span><span style="color:#b48ead;">else</span><span>{
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_RED,TFT_BLACK);
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">Echec de la connexion wifi</span><span>");
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Pas de wifi</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, </span><span style="color:#d08770;">100</span><span>);
</span><span> }
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">3000</span><span>); </span><span style="color:#65737e;">// laisse un temps pour lire les infos
</span><span>
</span><span> </span><span style="color:#65737e;">// préparation de l'écran
</span><span> </span><span style="color:#bf616a;">prepareEcran</span><span>();
</span><span>
</span><span> </span><span style="color:#65737e;">//interruption de lecture du bouton
</span><span> </span><span style="color:#bf616a;">attachInterrupt</span><span>(BOUTON_CAL, etalonnage, FALLING);
</span><span>}
</span><span>
</span><span style="color:#b48ead;">unsigned long</span><span> ancienCO2 = </span><span style="color:#d08770;">0</span><span>;
</span><span style="color:#b48ead;">int</span><span> seuil = </span><span style="color:#d08770;">0</span><span>;
</span><span>
</span><span style="color:#b48ead;">void </span><span style="color:#8fa1b3;">loop</span><span>() {
</span><span>
</span><span> </span><span style="color:#65737e;">// effectue l'étalonnage si on a appuyé sur le bouton
</span><span> </span><span style="color:#b48ead;">if</span><span>( demandeEtalonnage ){
</span><span> demandeEtalonnage = </span><span style="color:#d08770;">false</span><span>;
</span><span> </span><span style="color:#65737e;">// nettoye le registre de verification
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(clearHR1, </span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">100</span><span>);
</span><span> </span><span style="color:#65737e;">// demande la calibration
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(calReq, </span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">4500</span><span>); </span><span style="color:#65737e;">// attend selon le cycle de la lampe
</span><span>
</span><span> </span><span style="color:#65737e;">// lit le registre de verification
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(HR1req, </span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> </span><span style="color:#b48ead;">int</span><span> verif = </span><span style="color:#bf616a;">get_Value</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>("</span><span style="color:#a3be8c;">resultat calibration </span><span>"+</span><span style="color:#bf616a;">String</span><span>(verif));
</span><span> </span><span style="color:#b48ead;">if</span><span>(verif == </span><span style="color:#d08770;">32</span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_GREEN);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">OK</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>()/</span><span style="color:#d08770;">2</span><span>+</span><span style="color:#d08770;">30</span><span>);
</span><span> }</span><span style="color:#b48ead;">else</span><span>{
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_RED);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Erreur</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>()/</span><span style="color:#d08770;">2</span><span>+</span><span style="color:#d08770;">20</span><span>);
</span><span> }
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">3000</span><span>);
</span><span> </span><span style="color:#bf616a;">prepareEcran</span><span>();
</span><span> seuil = </span><span style="color:#d08770;">0</span><span>;
</span><span> }
</span><span>
</span><span> </span><span style="color:#65737e;">// lecture du capteur
</span><span> </span><span style="color:#bf616a;">send_Request</span><span>(CO2req, </span><span style="color:#d08770;">8</span><span>);
</span><span> </span><span style="color:#bf616a;">read_Response</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span> </span><span style="color:#b48ead;">unsigned long</span><span> CO2 = </span><span style="color:#bf616a;">get_Value</span><span>(</span><span style="color:#d08770;">7</span><span>);
</span><span>
</span><span> String CO2s = "</span><span style="color:#a3be8c;">CO2: </span><span>" + </span><span style="color:#bf616a;">String</span><span>(CO2);
</span><span> Serial.</span><span style="color:#bf616a;">println</span><span>(CO2s);
</span><span>
</span><span> </span><span style="color:#65737e;">// efface le chiffre du texte
</span><span> </span><span style="color:#b48ead;">if</span><span>(CO2 != ancienCO2){
</span><span> tft.</span><span style="color:#bf616a;">fillRect</span><span>(</span><span style="color:#d08770;">0</span><span>,</span><span style="color:#d08770;">0</span><span>, tft.</span><span style="color:#bf616a;">width</span><span>(), </span><span style="color:#d08770;">60</span><span>, TFT_BLACK);
</span><span> }
</span><span>
</span><span> </span><span style="color:#b48ead;">if</span><span>( CO2 < </span><span style="color:#d08770;">800 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_GREEN,TFT_BLACK);
</span><span> </span><span style="color:#b48ead;">if</span><span>( seuil != </span><span style="color:#d08770;">1 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">fillRect</span><span>(</span><span style="color:#d08770;">0</span><span>,</span><span style="color:#d08770;">61</span><span>, tft.</span><span style="color:#bf616a;">width</span><span>(), </span><span style="color:#d08770;">25</span><span>, TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Air Excellent</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2 </span><span>+ </span><span style="color:#d08770;">10</span><span>);
</span><span> }
</span><span> seuil = </span><span style="color:#d08770;">1</span><span>;
</span><span> }</span><span style="color:#b48ead;">else if</span><span>( CO2 >= </span><span style="color:#d08770;">800 </span><span>&& CO2 < </span><span style="color:#d08770;">1000</span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_ORANGE,TFT_BLACK);
</span><span> </span><span style="color:#b48ead;">if</span><span>( seuil != </span><span style="color:#d08770;">2 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">fillRect</span><span>(</span><span style="color:#d08770;">0</span><span>,</span><span style="color:#d08770;">61</span><span>, tft.</span><span style="color:#bf616a;">width</span><span>(), </span><span style="color:#d08770;">25</span><span>, TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Air Moyen</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2 </span><span>+ </span><span style="color:#d08770;">10</span><span>);
</span><span> }
</span><span> seuil = </span><span style="color:#d08770;">2</span><span>;
</span><span> }</span><span style="color:#b48ead;">else if </span><span>(CO2 >= </span><span style="color:#d08770;">1000 </span><span>&& CO2 < </span><span style="color:#d08770;">1500</span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_RED,TFT_BLACK);
</span><span> </span><span style="color:#b48ead;">if</span><span>( seuil != </span><span style="color:#d08770;">3 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">fillRect</span><span>(</span><span style="color:#d08770;">0</span><span>,</span><span style="color:#d08770;">61</span><span>, tft.</span><span style="color:#bf616a;">width</span><span>(), </span><span style="color:#d08770;">25</span><span>, TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Air Mediocre</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2 </span><span>+ </span><span style="color:#d08770;">10</span><span>);
</span><span> }
</span><span> seuil = </span><span style="color:#d08770;">3</span><span>;
</span><span> }</span><span style="color:#b48ead;">else</span><span>{
</span><span> tft.</span><span style="color:#bf616a;">setTextColor</span><span>(TFT_RED,TFT_BLACK);
</span><span> </span><span style="color:#b48ead;">if</span><span>( seuil != </span><span style="color:#d08770;">4 </span><span>){
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">2</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">fillRect</span><span>(</span><span style="color:#d08770;">0</span><span>,</span><span style="color:#d08770;">61</span><span>, tft.</span><span style="color:#bf616a;">width</span><span>(), </span><span style="color:#d08770;">25</span><span>, TFT_BLACK);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>("</span><span style="color:#a3be8c;">Air Vicie</span><span>", tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2 </span><span>+ </span><span style="color:#d08770;">10</span><span>);
</span><span> }
</span><span> seuil = </span><span style="color:#d08770;">4</span><span>;
</span><span> }
</span><span>
</span><span> tft.</span><span style="color:#bf616a;">setTextSize</span><span>(</span><span style="color:#d08770;">8</span><span>);
</span><span> tft.</span><span style="color:#bf616a;">drawString</span><span>(</span><span style="color:#bf616a;">String</span><span>(CO2), tft.</span><span style="color:#bf616a;">width</span><span>() / </span><span style="color:#d08770;">2</span><span>, tft.</span><span style="color:#bf616a;">height</span><span>() / </span><span style="color:#d08770;">2 </span><span>- </span><span style="color:#d08770;">30</span><span>);
</span><span>
</span><span>
</span><span> </span><span style="color:#65737e;">// envoi de la valeur sur le cloud
</span><span> </span><span style="color:#b48ead;">if</span><span>((</span><span style="color:#bf616a;">millis</span><span>() - lastTime) >= postingInterval) {
</span><span> </span><span style="color:#b48ead;">if</span><span>((wifiMulti.</span><span style="color:#bf616a;">run</span><span>() == WL_CONNECTED)) {
</span><span> WiFiClient client;
</span><span> ThingSpeak.</span><span style="color:#bf616a;">begin</span><span>( client );
</span><span>
</span><span> </span><span style="color:#bf616a;">writeTSData</span><span>( channelID , dataFieldOne , CO2 );
</span><span> lastTime = </span><span style="color:#bf616a;">millis</span><span>();
</span><span> }
</span><span> }
</span><span>
</span><span> ancienCO2 = CO2;
</span><span> </span><span style="color:#bf616a;">delay</span><span>(</span><span style="color:#d08770;">10000</span><span>); </span><span style="color:#65737e;">// attend 10 secondes avant la prochaine mesure
</span><span>}
</span></code></pre>
<h3 id="mqtt">MQTT</h3>
<p>Sur ThingSpeak, aller dans <a rel="noopener" target="_blank" href="https://thingspeak.com/devices/mqtt">Devices > MQTT</a> et compléter si besoin avec la lecture de la documentation <a rel="noopener" target="_blank" href="https://fr.mathworks.com/help/thingspeak/mqtt-basics.htm">MQTT Basics</a>:</p>
<ul>
<li>Créer un device,</li>
<li>Ajouter la/les channel(s) voulu(s),</li>
<li>Limiter les droits à la partie "subscribe" ; notre client en effet n'est pas prévu pour publier des données sur ThingSpeak,</li>
<li>Conserver précautionneusement vos identifiants MQTT.</li>
</ul>
<p>Sur l'instance Warp 10, déployer le <a rel="noopener" target="_blank" href="https://warpfleet.senx.io/browse/io.warp10/warp10-plugin-mqtt">plugin MQTT</a> :</p>
<p>Avec le script <code>/path/to/warp10/mqtt/test.mc2</code> :</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>// subscribe to the topics, attach a WarpScript™ macro callback to each message
</span><span>// the macro reads ThingSpeak message to extract the first byte of payload,
</span><span>// the server timestamp, the channel id and the value
</span><span>
</span><span>'Loading MQTT ThingSpeak Air Quality Warpscript™' STDOUT
</span><span>{
</span><span> 'host' 'mqtt3.thingspeak.com'
</span><span> 'port' 1883
</span><span> 'user' 'XXXXXXXXXX'
</span><span> 'password' 'XXXXXXXXXX'
</span><span> 'clientid' 'XXXXXXXXXX'
</span><span> 'topics' [
</span><span> 'channels/channelID 1/subscribe'
</span><span> 'channels/channelID 2/subscribe'
</span><span> 'channels/channelID 3/subscribe'
</span><span> ]
</span><span> 'timeout' 20000
</span><span> 'parallelism' 1
</span><span> 'autoack' true
</span><span>
</span><span> 'macro'
</span><span> <%
</span><span> //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
</span><span> 'message' STORE
</span><span> <% $message ISNULL ! %>
</span><span> <%
</span><span> // message structure :
</span><span> // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
</span><span> $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
</span><span> $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
</span><span> $TSmessage 'channel_id' GET 'channelId' STORE
</span><span> $TSmessage 'field1' GET 'sensorValue' STORE
</span><span>
</span><span> $message MQTTTOPIC ' ' +
</span><span> $ts ISO8601 + ' ' +
</span><span> $channelId TOSTRING + ' ' +
</span><span> $sensorValue +
</span><span> STDOUT // print to warp10.log
</span><span> %> IFT
</span><span> %>
</span><span>}
</span></code></pre>
<p>Vous devriez avoir dans <code>/path/to/warp10/log/warp10.log</code> :</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>Loading MQTT ThingSpeak Air Quality Warpscript™
</span><span>channels/<channelID 1>/subscribe 2022-01-11T10:30:51.000000Z <channelID 1> 820.00000
</span><span>channels/<channelID 2>/subscribe 2022-01-11T10:30:53.000000Z <channelID 2> 715.00000
</span><span>channels/<channelID 3>/subscribe 2022-01-11T10:30:54.000000Z <channelID 3> 410.00000
</span></code></pre>
<p>Maintenant que l'intégration MQTT est validée, supprimez ce fichier et passons à la gestion de la persistence des données dans Warp 10.</p>
<p>Avec le script suivant :</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>// subscribe to the topics, attach a WarpScript™ macro callback to each message
</span><span>// the macro reads ThingSpeak message to extract the first byte of payload,
</span><span>// the server timestamp, the channel id and the value.
</span><span>
</span><span>{
</span><span> 'host' 'mqtt3.thingspeak.com'
</span><span> 'port' 1883
</span><span> 'user' 'XXXXXXXXXX'
</span><span> 'password' 'XXXXXXXXXX'
</span><span> 'clientid' 'XXXXXXXXXX'
</span><span> 'topics' [
</span><span> 'channels/channelID 1/subscribe'
</span><span> 'channels/channelID 2/subscribe'
</span><span> 'channels/channelID 3/subscribe'
</span><span> ]
</span><span> 'timeout' 20000
</span><span> 'parallelism' 1
</span><span> 'autoack' true
</span><span>
</span><span> 'macro'
</span><span> <%
</span><span> //in case of timeout, the macro is called to flush buffers, if any, with NULL on the stack.
</span><span> 'message' STORE
</span><span> <% $message ISNULL ! %>
</span><span> <%
</span><span> // message structure :
</span><span> // {elevation=null, latitude=null, created_at=2022-01-11T10:02:27Z, field1=412.00000, field7=null, field6=null, field8=null, field3=null, channel_id=1630275, entry_id=417, field2=null, field5=null, field4=null, longitude=null, status=null}
</span><span> $message MQTTPAYLOAD 'ascii' BYTES-> JSON-> 'TSmessage' STORE
</span><span> $TSmessage 'created_at' GET TOTIMESTAMP 'ts' STORE
</span><span> $TSmessage 'channel_id' GET 'channelId' STORE
</span><span> $TSmessage 'field1' GET 'sensorValue' STORE
</span><span>
</span><span> // Tableau de correspondance entre mes channel IDs et mes devices en vue de définir des labels pour les GTS
</span><span> {
</span><span> <channelID 1> 'air1'
</span><span> <channelID 2> 'air2'
</span><span> <channelID 3> 'air3'
</span><span> } 'deviceMap' STORE
</span><span> // Récupération du nom du device dans la variable senssorId
</span><span> $deviceMap $channelId GET 'sensorId' STORE
</span><span>
</span><span> // Création d'une GTS air.quality.home
</span><span> // Le label "device" aura pour valeur le nom du device, via la variable sensorId
</span><span> // On crée une entrée qui correspond à la valeur que nous venons de récupérer
</span><span> // sensorValue est une string, il faut la repasser sur un format numérique
</span><span> // Une fois la GTS reconstituée avec son entrée, on la periste en base via UPDATE
</span><span> '<writeToken>' 'writeToken' STORE
</span><span> NEWGTS 'air.quality.home' RENAME
</span><span> { 'device' $sensorId } RELABEL
</span><span> $ts NaN NaN NaN $sensorValue TODOUBLE TOLONG ADDVALUE
</span><span> $writeToken UPDATE
</span><span> %> IFT
</span><span> %>
</span><span>}
</span></code></pre>
<p>Depuis le <a rel="noopener" target="_blank" href="https://studio.senx.io">WarpStudio</a>, vérifiez la disposnibilité de vos données :</p>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span>'<readToken>' 'readToken' STORE
</span><span>[ $readToken 'air.quality.home' {} NOW -1000 ] FETCH
</span></code></pre>
<p><a href="/blog/iot_air_quality_warp10_studio.png"><img src="/blog/iot_air_quality_warp10_studio.png" alt="warp10 - validation de l'ingestion des données IoT" /></a></p>
<p>Ensuite, il nous reste plus qu'à faire une petite macro et un dashboard pour présenter les données.</p>
<p>Pour la macro :</p>
<ul>
<li>On lui passe un nom de device en paramètre qui servira à filtrer sur le label</li>
<li>Elle retourne une GTS avec l'ensemble des valeurs disponibles</li>
</ul>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span><%
</span><span> {
</span><span> 'name' 'cerenit/iot/co2'
</span><span> 'desc' 'Provide CO2 levels per device'
</span><span> 'sig' [ [ [ [ 'device:STRING' ] ] [ 'result:GTS' ] ] ]
</span><span> 'params' {
</span><span> 'device' 'String'
</span><span> 'result' 'GTS'
</span><span> }
</span><span> 'examples' [
</span><span> <'
</span><span>air1 @cerenit/iot/co2
</span><span> '>
</span><span> ]
</span><span> } INFO
</span><span>
</span><span> // Actual code
</span><span> SAVE 'context' STORE
</span><span>
</span><span> 'device' STORE // Save parameter as year
</span><span>
</span><span> '<readToken>' 'readToken' STORE
</span><span> [ $readToken 'air.quality.home' { 'device' $device } MAXLONG MINLONG ] FETCH
</span><span> 0 GET
</span><span>
</span><span> $context RESTORE
</span><span>%>
</span><span>'macro' STORE
</span><span>$macro
</span></code></pre>
<p>Et pour le dashboard Discovery :</p>
<ul>
<li>Chaque tile utilise la macro que l'on vient de réaliser en lui passant le device en paramètre,</li>
<li>Chaque tile affiche un système de seuils avec des couleurs associées.</li>
</ul>
<pre style="background-color:#2b303b;color:#c0c5ce;"><code><span><%
</span><span>{
</span><span> 'title' 'Home CO2 Analysis'
</span><span> 'description' 'esp32 + Senseair S8 sensors at home'
</span><span> 'options' {
</span><span> 'scheme' 'CHARTANA'
</span><span> }
</span><span> 'tiles' [
</span><span> {
</span><span> 'title' 'Informations'
</span><span> 'type' 'display'
</span><span> 'w' 6 'h' 1 'x' 0 'y' 0
</span><span> 'data' {
</span><span> 'data' 'D&eacute;tails et informations compl&eacute;mentaires : <a href="https://www.cerenit.fr/blog/air-quality-iot-esp32-senseair-thingspeak-mqtt-warp10-discovery/">IoT - Qualit&eacute; de l air avec un esp32 (TTGo T-Display), le service ThingSpeak, du MQTT, Warp 10 et Discovery</a>'
</span><span> }
</span><span> }
</span><span> {
</span><span> 'title' 'Device AIR1'
</span><span> 'type' 'line'
</span><span> 'w' 6 'h' 2 'x' 0 'y' 1
</span><span> 'macro' <% 'air1' @cerenit/macros/co2 %>
</span><span> 'options' {
</span><span> 'thresholds' [
</span><span> { 'value' 400 'color' '#008000' }
</span><span> { 'value' 600 'color' '#329932' }
</span><span> { 'value' 800 'color' '#66b266' }
</span><span> { 'value' 960 'color' '#ffdb99' }
</span><span> { 'value' 1210 'color' '#ffa500' }
</span><span> { 'value' 1760 'color' '#ff0000' }
</span><span> ]
</span><span> }
</span><span> }
</span><span> {
</span><span> 'title' 'Device AIR2'
</span><span> 'type' 'line'
</span><span> 'w' 6 'h' 2 'x' 6 'y' 1
</span><span> 'macro' <% 'air2' @cerenit/macros/co2 %>
</span><span> 'options' {
</span><span> 'thresholds' [
</span><span> { 'value' 400 'color' '#008000' }
</span><span> { 'value' 600 'color' '#329932' }
</span><span> { 'value' 800 'color' '#66b266' }
</span><span> { 'value' 960 'color' '#ffdb99' }
</span><span> { 'value' 1210 'color' '#ffa500' }
</span><span> { 'value' 1760 'color' '#ff0000' }
</span><span> ]
</span><span> }
</span><span> }
</span><span> {
</span><span> 'title' 'Device AIR3'
</span><span> 'type' 'line'
</span><span> 'w' 6 'h' 2 'x' 0 'y' 3
</span><span> 'macro' <% 'air3' @cerenit/macros/co2 %>
</span><span> 'options' {
</span><span> 'thresholds' [
</span><span> { 'value' 400 'color' '#008000' }
</span><span> { 'value' 600 'color' '#329932' }
</span><span> { 'value' 800 'color' '#66b266' }
</span><span> { 'value' 960 'color' '#ffdb99' }
</span><span> { 'value' 1210 'color' '#ffa500' }
</span><span> { 'value' 1760 'color' '#ff0000' }
</span><span> ]
</span><span> }
</span><span> }
</span><span> ]
</span><span>}
</span><span>{ 'url' 'https://w.ts.cerenit.fr/api/v0/exec' }
</span><span>@senx/discovery2/render
</span><span>%>
</span></code></pre>
<p>Le résultat est alors :</p>
<p><a href="/blog/iot_air_quality_warp10_discovery.png"><img src="/blog/iot_air_quality_warp10_discovery.png" alt="warp10 - dashboard IoT CO2" /></a></p>
<p>Bilan de ce que nous avons vu :</p>
<ul>
<li>Comment monter son capteur de CO2 en profitant des ressources mises à disposition par le projet "Nous Aérons",</li>
<li>Comment envoyer les données du capteur vers le service ThingSpeak,</li>
<li>Comment récupérer les données du service ThingSpeak via le protoole MQTT et les stocker dans Warp 10,</li>
<li>Comment créer un dashboard Discovery avec une macro permettant de récupérer les données et mettre en place un système de seuils.</li>
</ul>
<p>L'ensemble des fichiers peuvent être récupérés depuis <a rel="noopener" target="_blank" href="https://code.cerenit.fr/cerenit/iot-air-quality">cerenit/iot-air-quality</a>.</p>
Web, Ops, Data et Time Series - Décembre 20212021-12-15T09:30:00+01:002021-12-15T09:30:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-data-timeseries-decembre-2021/<h3 id="code-frameworks">Code & Frameworks</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.djangoproject.com/weblog/2021/dec/07/django-40-released/">Django 4.0 released</a> : compatible python 3.8+, il appot son lot de nouveautés et notamment la capacité de personnaliser un peu plus le rendu des formulaires pour ce qui me concerne.</li>
</ul>
<h3 id="conteneurs-orchestration">Conteneurs & Orchestration</h3>
<ul>
<li><a rel="noopener" target="_blank" href="http://jpetazzo.github.io/2021/11/30/docker-build-container-images-antipatterns/">Anti-Patterns When Building Container Images</a> : Jérome Petazzoni donne une liste de mauvaises pratiques et des solutions pour y remédier.</li>
</ul>
<h3 id="iot">IoT</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.raspberrypi.com/news/new-old-functionality-with-raspberry-pi-os-legacy/">“New” old functionality with Raspberry Pi OS (Legacy)</a> : la fondation Raspbery Pi annonce l'arrivée d'un OS 64 bits (enfin) mais aussi la mise à disposition d'une version legacy de Raspberry Pi OS basée sur Debian 10/Buster pour ceux qui rencontrent des problèmes avec le passage à Debian 11/Bullseye.</li>
</ul>
<h3 id="monitoring-observabilite">Monitoring & Observabilité</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://grafana.com/blog/2021/12/01/grafana-8.3-released-recorded-queries-panel-suggestions-new-panels-added-security-and-more/">Grafana 8.3 released: Recorded queries, panel suggestions, new panels, added security, and more</a> & <a rel="noopener" target="_blank" href="https://grafana.com/docs/grafana/latest/whatsnew/whats-new-in-v8-3/?pg=blog">What’s new in Grafana v8.3</a> : Ajout d'une recommendation/suggestion de panel, le nouvel alerting est déployé par défaut, Candelstick en mode beta pour les données financières et amélioration du panel GeoMap pour la version OSS.
title: "Web, Ops, Data et Time Series - NovemDécembre 2021"</li>
</ul>
<h3 id="tests">Tests</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://robotframework.org/">RobotFramework</a> : robot opensource d'automatisation tant pour des tests que des process d'automatisation robotique, il semble assez complet pour permettre de faire des tests assez complets tout en proposant une interface relativement simple. A voir ce que cela donne...</li>
<li><a rel="noopener" target="_blank" href="https://dredd.org/">Dredd</a> : pour tester vos API au format Blueprint ou OpenAPI</li>
<li><a rel="noopener" target="_blank" href="https://blog.kalvad.com/keep-calm-and-release-your-api-in-prod/">Keep calm and release your API in prod</a> : <a rel="noopener" target="_blank" href="https://github.com/taverntesting/tavern">Tavern</a> permet de tester des API HTTP via une déclariaton des scénarios en YAML. Il s'appuie sur pytests, requests et dispose d'une intégration MQTT. Le billet montre un cas d'exemple.</li>
</ul>
<h3 id="time-series">Time Series</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://blog.senx.io/demystifying-the-use-of-the-parquet-file-format-for-time-series/">Demystifying the use of the Parquet file format for time series</a> : retour sur le format Parquet et son usage pour des séries temporelles. Au delà de l'explication, il est intéressant de mettre cela en perspective vis à vis d'InfluxData qui a prévu que son moteur de stockage Iox soit notamment basé sur Parquet.</li>
</ul>
Web, Ops, Data et Time Series - Novembre 20212021-11-24T14:00:00+01:002021-11-24T14:00:00+01:00
Unknown
https://cerenit.fr/blog/web-ops-data-timeseries-novembre-2021/<h3 id="containers-orchestration">Containers & Orchestration</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.hashicorp.com/blog/announcing-hashicorp-nomad-1-2">Announcing General Availability of HashiCorp Nomad 1.2</a> : Arrivée des "system batchs jobs" prévu pour gérer des jobs à destination du cluster nomad en lui même (purge, backup, etc) et non des clients. Cette version apporte également des améliorations au niveau de l'interface, ainsi que les "nomad pack", format de distribution de vos applications à destination de nomad.</li>
</ul>
<h3 id="iot">IoT</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.framboise314.fr/sortie-de-raspberry-pi-os-bullseye-et-raspberry-pi-4-a-18ghz/">Sortie de Raspberry Pi OS Bullseye et Raspberry Pi 4 à 1,8GHz</a> : Première version de Raspberry Pi OS basée sur Debian 11 et possible overclocking du CPU des RPi4 à 1.8 Ghz (au lieu de 1.5 Ghz)</li>
</ul>
<h3 id="monitoring-observabilite">Monitoring & Observabilité</h3>
<ul>
<li><a rel="noopener" target="_blank" href="https://vector.dev/releases/0.18.0/">Vector v0.18.0 release notes</a> : une version avec beaucoup de changements - je vous laisse aller voir les release notes.</li>
</ul>
<h3 id="time-series">Time Series</h3>
<p>Annonces & Produits :</p>
<ul>
<li><a rel="noopener" target="_blank" href="https://blog.timescale.com/blog/massive-scale-for-time-series-workloads-introducing-continuous-aggregates-for-distributed-hypertables-in-timescaledb-2-5/">Timescale 2.5.0</a> : support de Postgresql 14, continuous aggregates for distributed hypertables (la fonction fonctionne donc maintenant en multi-nodes) et support des timezone pour la fonction time_bucket_ng</li>
<li><a rel="noopener" target="_blank" href="https://blog.senx.io/warpstudio-2-0-6/">Warp Studio 2.0.6</a> : version mineure du studio pour la gesion de CORS-RFC1918 ; c'est pour utiliser le studio avec vos instances locales depuis Chrome 92 (et bientôt les autres navigateurs) du fait des restrictrions d'accès mises en place dans les navigateurs.</li>
<li><a rel="noopener" target="_blank" href="https://www.influxdata.com/blog/release-announcement-influxdb-oss-2-1-0/">Release Announcement: InfluxDB OSS 2.1.0 | InfluxData</a> : Arrivée des annotations et des notebooks, le client influx n'est plus distribué avec le serveur (sauf dans l'image Docker), améliorations de flux, amélioration de l'API et de la CLI et mise à jour de l'extension VSCode.</li>
<li><a rel="noopener" target="_blank" href="https://towardsdatascience.com/announcing-pycarets-new-time-series-module-b6e724d4636c"> Announcing PyCaret’s New Time Series Module</a> :la librairie "low code" de machine learning <a rel="noopener" target="_blank" href="https://pycaret.org/">PyCaret</a> se dote d'un module de gestion de séries temporelles comprenant 30+ modèles (ARIMA, SARIMA, FBProphet, etc) et fonctions.</li>
</ul>
<p>Articles :</p>
<ul>
<li><a rel="noopener" target="_blank" href="https://www.quantmetry.com/blog/intelligence-artificielle-et-data-quality-comment-corriger-des-donnees-historiques-impactees-par-la-covid-19-pour-ameliorer-la-qualite-des-previsions/">Intelligence Artificielle et Data Quality : comment corriger des données historiques impactées par la Covid 19 pour améliorer la qualité des prévisions ?</a> : RETEX sur les appels à un call center : comment prendre en compte (ou pas) les variations liées au confinement sur les appels à un call center. L'article présente quatre stratégies et leurs résultats.</li>
<li><a rel="noopener" target="_blank" href="https://blog.senx.io/data-replication-with-warp-10/">Data replication with Warp 10</a> : présentation du fonctionnement de datalog, le module de réplication des données dans Warp 10.</li>
<li><a rel="noopener" target="_blank" href="https://blog.senx.io/n8n-warp-10-automate-your-time-series-manipulations/">n8n & Warp 10 - Automate your time series manipulations</a> : la version anglaise hébergée sur le blog de Senx de mon billet sur <a rel="noopener" target="_blank" href="https://www.cerenit.fr/blog/n8n-warp10-automate-your-time-series/">n8n & Warp 10</a></li>
</ul>