Aller au contenu
blog.fpicard.tech — accueil

← Tous les articles

POC

Un panneau mural sans clavier

Une dalle bandeau de 1480 × 320 sur un mur, un Raspberry Pi derrière, et personne pour débuguer quand ça casse. Cette dernière contrainte a décidé de tout le reste.

17 min de lecture

Le panneau allumé, posé sur un socle à smartphone en bois, au milieu du désordre d'un bureau

Le point de départ de ce projet n’est pas une idée. C’est une déception.

Le Corsair Xeneon Edge m’a fait de l’œil pendant des semaines. Une dalle bandeau de 14,5 pouces, 2560 × 720 en 32:9, tactile cinq points, quatorze aimants intégrés pour la coller sur n’importe quelle surface en acier, 259,90 €. Exactement l’objet que je cherchais, fini, garanti trois ans.

Les deux dalles à l'échelle, et ce qu'il faut derrière chacune
Les deux bandeaux, à l'échelle de leurs diagonales annoncées. Le Corsair est le plus grand des deux — et c'est la colonne de droite qui a décidé.

Puis j’ai lu ce qu’il fait vraiment.

Ce n’est pas un appareil, c’est un deuxième écran. Corsair le classe lui-même en auxiliary display et en secondary display : il a une entrée HDMI, une entrée USB-C en DP Alt-Mode, et rien d’autre. Tout ce qu’il affiche est calculé par une machine qui doit être allumée.

Les widgets, alors ? Ils viennent d’iCUE. La FAQ de Corsair est d’une honnêteté désarmante :

You do not need iCUE to use the XENEON EDGE. […] Without iCUE, the XENEON EDGE will simply work as an additional monitor.

Autrement dit : la couche qui en fait un tableau de bord est iCUE. Sans elle, c’est un moniteur. Et les widgets d’iCUE se téléchargent sur le marché d’Elgato, où ils portent tous la même étiquette — Windows only.

Le compte est vite fait. Pour afficher l’heure sur un mur, il faut un PC Windows allumé en permanence. L’écran, lui, ne sait rien faire seul : ni au réveil, ni après une coupure, ni quand le PC est éteint.

Donc, le faire

Il y a un mur dans mon bureau où il ne se passait rien, et je voulais un écran qui tienne dessus sans avoir l’air d’un écran — autonome, allumé en permanence, sans rien d’autre derrière.

Le format, lui, ne se négociait pas : une dalle bandeau. J’ai pris une Waveshare 11,9 pouces en DSI à 130,99 € — 320 × 1480 natif, tournée en paysage, donc 1480 pixels de large sur 320 de haut. Un ratio de 4,6 pour 1. Pas de carte-mère, pas de logiciel, pas d’écosystème : une dalle nue et une nappe, que le Raspberry Pi pilote directement.

Le Raspberry Pi, je l’avais déjà — un 4B qui ne servait plus. Avec l’alimentation officielle 27 W, 19,84 €, et on verra plus loin pourquoi elle n’était pas facultative, le panneau m’aura donc coûté 150,83 € contre 259,90 € pour le Corsair.

Ce n’est pas ce qui m’a décidé, et je ne compte pas les trois semaines. Mais sortir une carte d’un tiroir pour obtenir l’objet qu’un produit fini ne savait pas être, c’est une satisfaction à part.

Rien n’est conçu pour ce format. Aucun thème, aucune grille, aucun tableau de bord. Les projets de miroir connecté supposent tous un écran vertical ou un 16/9. J’ai donc écrit le mien.

Le panneau posé sur un bureau, entre une bouilloire et un pot à crayons
4,6 pour 1, sur un bureau ordinaire. C'est la proportion qui explique tout le reste : trois tuiles de front, et rien au-dessus ni en dessous.

Mais ce n’est pas le format qui a été structurant. C’est autre chose, que je n’avais pas vu venir : l’appareil n’a pas de clavier, et ses lecteurs principaux sont mes enfants. Personne, devant ce mur, ne lira jamais un message d’erreur pour en faire quelque chose.

Trois semaines, 291 commits

Le dépôt commence le 26 août 2026 et la dernière version date du 21 septembre. Entre les deux, 291 commits — dont 49 le premier jour, 45 le deuxième, 36 le troisième.

Quarante-neuf commits en un jour, ça demande une explication, et je préfère la donner tout de suite : je n’ai pas écrit ce code tout seul. Je l’ai écrit avec une IA, en conversation, pendant trois semaines. C’est ce qu’on appelle maintenant du vibecoding, et j’y reviens à la fin — parce que c’est sans doute ce qu’il y a de plus intéressant dans ce projet.

Le rythme a aussi une raison matérielle : la dalle n’est arrivée que le 4 septembre. Pendant la première semaine, j’ai développé contre une géométrie que je n’avais pas, dans une fenêtre de navigateur redimensionnée à la main. Tout ce qui pouvait se décider sans le matériel a été décidé à ce moment-là, et c’est probablement ce qui a sauvé le projet : quand la dalle est arrivée, il restait un seul réglage à faire, l’orientation.

Les quatre lignes que j’avais écrites dans ma feuille de route avant la livraison valent d’être relues avec ce qui s’est réellement passé :

Ce qui était prévuCe qui s’est passé
Rotation par video=HDMI-A-1:…rotate=90écartée par la mesure — trois voies essayées, aucune ne fonctionne en KMS
Matrice de calibration tactile par règle udevinutile — le compositeur applique la même transformation au tactile
Sous-tension à régler avant de brancherdisparue — la dalle alimente le Pi
« La dalle tire son 5 V du rail du Pi »faux — la nappe DSI ne porte que la vidéo et l’I²C

Quatre prévisions, quatre erreurs. Aucune n’a coûté cher parce qu’aucune n’avait été implémentée avant d’être mesurée.

Un écran sans clavier change tout

C’est la règle dont tout découle, et je l’ai écrite en tête du dépôt :

Dégradé, jamais blanc. Une coupure réseau grise une tuile, elle ne vide pas l’écran. Un layout fautif rend les tuiles valides et grise la fautive avec son message. Un certificat absent laisse l’administration en HTTP plutôt que de la rendre injoignable.

Formulé autrement : tout ce qui peut échouer doit échouer en affichant encore quelque chose. Sur un poste de travail, une page blanche est une invitation à ouvrir la console. Sur un mur, c’est un appareil mort.

J’ai appris la différence le 27 août, et de la pire manière.

La correction tient en trois lignes, et elle m’a coûté une soirée à comprendre :

deploy/etc/kiosk.service
[Service]
# systemd considere Caddy demarre avant qu'il n'ecoute : on attend la
# premiere reponse HTTP, pas le demarrage de l'unite.
ExecStartPre=/bin/sh -c 'for i in $(seq 1 30); do curl -sf http://localhost/ >/dev/null && exit 0; sleep 1; done; exit 1'
ExecStart=/usr/bin/chromium --kiosk http://localhost
Restart=always

Le même raisonnement a produit le volet de maintenance, qui descend du bord haut comme sur un téléphone. Il porte l’état des sources, la carte d’identité réseau de la machine — IP, MAC, SSID, passerelle, DNS, état de SSH — et trois boutons : Recharger, Redémarrer, Éteindre.

Il existe pour une seule raison : la page d’administration passe par le réseau. Le jour où le LAN tombe, elle est injoignable exactement quand on en a besoin. Le volet, lui, parle à 127.0.0.1.

Et il se referme seul au bout de quinze secondes — faute de quoi un enfant qui l’ouvre et s’en va laisserait le tableau caché.

La page « véhicule » : la tuile de la borne de recharge est hors ligne, les deux autres continuent
« Dégradé, jamais blanc », en situation : la borne ne répond pas, sa tuile le dit en rouge — et les deux tuiles voisines affichent toujours la dernière charge et la consommation du mois. Sur un poste de travail, on ouvrirait la console ; ici, il faut que la page reste lisible sans personne pour la réparer.
La page « école » : compte à rebours des vacances, semaine A ou B, et « pas d'école »
Cinq des vingt-six tuiles ne servent qu'à la famille. Celle de gauche compte les jours jusqu'au retour, celle de droite répond à la seule question qui se pose à 7 h du matin.

Zéro dépendance, zéro build

Le navigateur charge les fichiers tels qu’ils sont écrits. Pas de framework, pas de bundler, pas de chaîne de build.

Ce n’est pas du purisme. Un bundle React ici coûterait 200 Ko pour afficher douze chiffres, et une chaîne de build à maintenir pour un appareil censé tenir des mois sans intervention. La contrepartie est le genre de simplicité qu’on ne regrette pas : git pull sur le Pi est le déploiement.

Mais la règle a une exception, et c’est elle qui l’explique le mieux.

Trois tuiles posent un emoji en filigrane — météo, vacances, semaine A/B. Un emoji n’est pas un fichier du dépôt : c’est un glyphe fourni par une police du système. Raspberry Pi OS minimal n’en a aucune.

fc-list | grep -ci emoji
# 0

Chromium ne dessine alors rien. Pas d’erreur, pas de carré blanc, pas une ligne en console. La tuile s’affiche, correctement, sans son filigrane — et on peut passer à côté longtemps.

La leçon est plus large que l’emoji : tout ce qui sort du dépôt doit être posé par le script d’amorçage, sinon une carte neuve arrive incomplète sans le dire. bootstrap.sh installe donc fonts-noto-color-emoji.

Le runtime possède les timers

Un widget n’appelle jamais setInterval, fetch ni addEventListener sur window. Il passe par ctx.every() et ctx.on().

Ça paraît cérémonieux pour un projet sans dépendances. C’est ce qui permet de suspendre les pages non affichées — sans quoi cinq pages de six widgets feraient tourner trente collectes en permanence, sur un Pi qui porte déjà Chromium.

Le navigateur ne parle jamais à une source

Ni adresse, ni identifiant, ni jeton ne descendent dans la page. Le shell demande /relais/z420/data.json ; seul le serveur sait où cela mène. Le CORS se résout au passage, sans y penser.

Quatre surfaces réseau, volontairement séparées :

PortSertAuthentification
80le shell, en clair — c’est ce que charge le kiosqueaucune
443le même shell en HTTPS, si un certificat est configuréaucune
8080l’administrationmot de passe
8081le service d’écriturelié à 127.0.0.1

Le port qui alimente l’écran ne sert jamais de point d’écriture.

Ce que le matériel a coûté

C’est la partie où j’ai perdu le plus de temps, et pour une raison qui n’a rien à voir avec le logiciel.

L'arrière de l'ensemble : le Raspberry Pi vissé sur la carte de la dalle
Tout l'appareil, vu de dos. Le Pi est vissé sur la carte de la dalle, la nappe orange part sur la gauche. Il n'y a rien d'autre — pas de carte d'alimentation, pas de convertisseur.

Le Pi remontait des sous-tensions en continu : de l’ordre de 160 par heure. Le réflexe est d’accuser la puissance du chargeur. C’est faux.

vcgencmd get_throttled
# 0x50005  -> sous-tension detectee, et depuis le demarrage

J’ai commandé une alimentation officielle 27 W, puis mesuré une heure pleine, kiosque en marche :

mesure-alim.py — une heure, au repos
fenêtre           : 3600 s
sous-tensions     : +0  (total 0)
get_throttled     : 0x0
température       : 68,2 → 69,6 °C

Zéro, contre une référence d’environ 160 par heure.

Et la cause est établie : c’est le câble, pas la puissance. Les 22,5 W du chargeur précédent suffisaient sur le papier ; c’est la chute de tension dans un câble USB-C quelconque qui provoquait les sous-tensions. Le bloc officiel a un câble captif dimensionné pour 5 A, qui n’en transporte que 1,5 — donc une chute négligeable.

L'alimentation officielle 27 W et son câble captif
Le câble est dimensionné pour 5 A et n'en transporte que 1,5. C'est tout l'écart entre 160 sous-tensions par heure et zéro.

Dernier retournement : la dalle n’est pas alimentée par le Pi. La nappe DSI ne porte que la vidéo et l’I²C ; le courant arrive par le Type-C de la dalle, qui alimente le Pi à son tour. Toute mon inquiétude sur le rail 5 V portait sur un circuit qui n’existe pas.

La nappe DSI, branchée sur le connecteur DISPLAY du Raspberry Pi
La nappe orange, et rien d'autre. Vidéo et I²C — aucun conducteur de puissance. J'ai porté trois semaines une inquiétude sur un circuit que cette photo dément.

Deux réglages, et un seul comptait

En DSI, il n’y a pas de hdmi_timings : la dalle s’annonce elle-même. La rotation ne passe pas non plus par le noyau — trois voies essayées en KMS, aucune ne fonctionne. C’est le compositeur qui tourne :

~/.config/sway/config
output DSI-1 transform 90

Une ligne. Et le tactile suit sans rien demander : sway lui applique la même transformation, ce qui rend inutile la règle udev que j’avais prévue.

Le résultat

Version 3.12.1, en service sur sa dalle définitive.

HôteRaspberry Pi 4B 2 Go, Raspberry Pi OS Lite Trixie
Affichagesway + Chromium en kiosque sur http://localhost
DalleWaveshare 11.9” DSI, 320 × 1480 natif, tournée en paysage
ServeurCaddy — fichiers statiques et relais
Démarrage13,2 s, NRestarts=0
Widgets26
Préréglages prêts à l’emploi48
Dépendancesaucune

Les tuiles vont de l’heure et la météo aux métriques d’un PC, un serveur Minecraft, une borne de recharge, la supervision Zabbix — et cinq tuiles qui ne servent qu’à la maison : les vacances scolaires, la semaine A ou B du collège, ce qu’il faut prendre ce matin, quel bac sortir ce soir, et un « ça va où ? » qui répond au toucher.

La page d'accueil à la géométrie native de la dalle
1480 × 320, taille réelle. L'heure, la météo locale, la phase de lune — et c'est à peu près tout ce qu'une bande de 320 pixels de haut peut porter de front.
Les actualités, avec leur position dans le flux indiquée en en-tête
« 1/15 » en en-tête : la tuile dit toujours où elle en est. Sur un écran qu'on ne pilote pas, une pagination muette est une pagination inutile.

Le geste est la seule interface. Balayage horizontal pour changer de page, vertical pour changer de variante, appui court pour suspendre la rotation, appui long de 0,7 s pour épingler une page.

Un détail m’a occupé plus longtemps qu’il ne devrait : l’axe se décide au premier mouvement de 12 pixels, puis se fige. Sans ce verrou, un geste légèrement diagonal déclenchait les deux à la fois.

La page « déchets » : quel bac sortir ce soir, et un jeu de tri qui répond au toucher
À gauche, le bac à sortir — « mercredi soir », avec la levée du lendemain. À droite, « Ça va où ? » : un objet, six destinations, la réponse au doigt. C'est la seule tuile du panneau avec laquelle on joue.

Ce que je n’aurais pas fait seul

Il faut que je sois clair, parce que c’est la chose la plus honnête que je puisse écrire sur ce projet : sans IA, je ne m’y serais jamais mis.

Je suis ingénieur systèmes et réseaux. Je lis du code, j’en corrige, j’en écris quand il le faut. Mais vingt-six widgets en JavaScript sans framework, un moteur de grille, un schéma de layout versionné, une page d’administration complète — j’aurais estimé le chantier à plusieurs mois de soirées, et je ne l’aurais pas commencé. Pas par incompétence : par arbitrage. Le coût d’entrée était plus élevé que l’envie.

C’est exactement ce que l’IA a déplacé. Pas la compétence — l’accès. Une idée du samedi matin devient une maquette le samedi soir, et c’est au moment où on la voit tourner qu’on décide si elle mérite trois semaines.

Il faut une idée, et quelqu’un pour trancher

Ce n’est pas de la dictée. Il faut une idée de départ — personne ne m’a soufflé qu’un mur de mon bureau était vide — et il faut vérifier ce qui sort, étape par étape.

Ça n’est pas de l’hygiène de principe, c’est structurel : une IA produit une réponse fausse avec exactement le même aplomb qu’une réponse juste. Elle ne sait pas qu’elle se trompe, donc elle ne le dit pas. Les quatre prévisions de ma feuille de route plus haut, elle les aurait implémentées toutes les quatre sans sourciller — c’est moi qui ai mesuré, et c’est la mesure qui les a démenties.

Le corollaire est moins intuitif : une IA est complaisante. Elle propose, et ce qu’elle propose est presque toujours raisonnable en général. Sur ce projet, elle m’a proposé un framework, une chaîne de build, SQLite pour la configuration, une règle udev pour calibrer le tactile. Quatre réponses parfaitement défendables. Quatre réponses fausses ici — pour un appareil sans clavier qui doit tenir des mois et dont le déploiement doit rester un git pull.

Cadrer et trancher, c’est le vrai travail. Et ça ne se délègue pas, parce que ça demande de savoir ce qu’on veut vraiment — ce qu’une conversation ne contient jamais entièrement.

Ce qu’elle n’a pas fait

Il serait malhonnête de s’arrêter là, parce que la liste des choses que l’IA n’a pas résolues est précisément celle des problèmes qui m’ont coûté du temps :

  • Les 160 sous-tensions par heure. Aucune conversation ne les aurait trouvées. Il a fallu brancher un wattmètre, lancer une mesure d’une heure, et accepter que la réponse contredise l’hypothèse de départ.
  • Les trois voies de rotation en KMS. Elles paraissaient toutes correctes sur le papier. Aucune ne fonctionne.
  • Le kiosque qui n’essaie plus jamais. Le défaut n’est visible que sur le mur, après une coupure de courant, un soir.
  • « La dalle tire son 5 V du rail du Pi ». Une supposition que j’ai portée trois semaines, et qu’une nappe DSI démentait depuis le début.

Le partage est net, et il n’a rien de décevant : l’IA a supprimé le coût d’entrée du logiciel ; le monde physique a continué de facturer plein tarif.

Ce qui change vraiment, du coup, ce n’est pas la vitesse d’écriture. C’est que le temps qu’on ne passe plus à produire du code, on le passe à mesurer — et c’est là que les vrais problèmes étaient.

Et surtout : elle n’a rien hébergé

C’est le point que j’ai failli écrire comme une nuance, et c’est en fait le cœur.

Ce panneau affiche la supervision Zabbix de mon parc, Plex via Tautulli, un serveur Minecraft, une borne de recharge, un agrégateur FreshRSS, qui est connecté au VPN, les capteurs d’une station de travail. Rien de tout ça n’est sorti d’une conversation. Ce sont des années d’infrastructure montée chez moi, supervisée, sauvegardée et maintenue — avec ses pannes, ses migrations et ses nuits blanches.

Sans ce substrat, le plus beau moteur de widgets du monde affiche l’heure et la météo. Autrement dit : un réveil.

Je n’ai pas fait écrire un tableau de bord. J’ai fait écrire une vitrine pour quelque chose qui existait déjà, et dont l’IA n’a pas posé une brique.

C’est pour ça que la question « quelle part revient à qui ? » me paraît mal posée. Ce n’est pas un pourcentage de lignes — ce serait mesurer un livre au poids de son encre. C’est une antériorité : il fallait que tout ça tourne déjà, chez moi, pour qu’il y ait quoi que ce soit à afficher.

Ce qui reste

Le boîtier — cotes relevées, dessin en cours. C’est la seule chose qui sépare ce panneau d’un objet fini, et ce sera un autre article.

En attendant, l’ensemble tient debout sur un socle à smartphone en bois, de ceux qu’on trouve chez Ikea pour trois euros. Ça n’a rien d’élégant, et ça a une vertu que je n’avais pas prévue : ça empêche de considérer le projet comme terminé. Un appareil posé de travers sur un bout de hêtre se rappelle à vous tous les matins.

Et un agrégateur : un service unique côté serveur qui collecte les sources lentes et les expose en SSE. EventSource reconnecte seul, avec un backoff intégré — sur un appareil qui doit tenir des mois, c’est zéro logique de reprise à écrire. L’articulation est déjà en place : le jour venu, aucun widget ne changera, seule la source à laquelle il s’abonne.


Ce que je retiens de ces trois semaines n’est pas une technique mais une discipline : chaque fois que j’ai voulu régler un problème avant de l’avoir mesuré, je me suis trompé. Les quatre prévisions de la feuille de route, la puissance du chargeur, la rotation par le noyau — quatre fois sur quatre, la mesure disait autre chose.

Un écran sans clavier ne pardonne pas les suppositions. Il les affiche.

Et c’est peut-être la vraie leçon du vibecoding : il rend le code bon marché, ce qui laisse d’autant moins d’excuses pour ne pas aller mesurer le reste.

Sommaire

Sommaire

  1. Donc, le faire
  2. Trois semaines, 291 commits
  3. Un écran sans clavier change tout
  4. Zéro dépendance, zéro build
  5. Le runtime possède les timers
  6. Le navigateur ne parle jamais à une source
  7. Ce que le matériel a coûté
  8. Deux réglages, et un seul comptait
  9. Le résultat
  10. Ce que je n’aurais pas fait seul
  11. Il faut une idée, et quelqu’un pour trancher
  12. Ce qu’elle n’a pas fait
  13. Et surtout : elle n’a rien hébergé
  14. Ce qui reste