12
mai
'26

Write-up du challenge du badge THCon 2026

Publié le 12 mai 2026

J'ai participé la semaine dernière à la Toulouse Hacking Convention qui se tenait à Toulouse du 5 au 6 mai, car j'intervenais notamment en tant que confériencer aux côtés d'Axelle Apvrille afin de présenter un sujet commun et ô combien d'actualité: l'impact de l'intelligence artificielle sur les compétitions de type Capture the Flag (CTF) dans le domain de la cybersécurité. Axelle a publié sur son blog un résumé très complet des problématiques que nous avons exposé et des solutions envisagées dans un futur proche, qui vaut vraiment le coup d'être lu. Notre talk s'est bien passé et a permis de longues discussions avec des participants à la conférence sur ce délicat sujet, certains détaillant leur approche des CTFs et d'autres pointant du doigt certaines positions mentionnées dans notre présentation qui les dérangeaient, ce qui ne manquera pas de faire mûrir notre réflexion.

Mais cette conférence a pris un tournant très intéressant durant la matinée de sa seconde journée, un badge électronique nous ayant été remis par le staff lors de notre arrivée, en complément du badge plastifié que nous avions récupéré la veille. Ce badge électronique héberge un challenge propre à la THCon, mais peut aussi être flashé avec d'autres challenges disponibles en ligne. Le badge a été conçu par DVID, comme l'atteste le logo présent sur la sérigraphie du circuit imprimé, et il semblerait qu'il soit interactif. Durant le talk d'ouverture, nos badges se sont mis à clignoter de différentes couleurs tandis que les organisateurs indiquaient qu'ils voyaient que les badges fonctionnaient bien, au vu des nombreuses LED qui s'illuminaient dans la salle. Je n'ai donc pas résisté à la tentation de jeter un œil au micrologiciel, avec dans l'idée de trouver potentiellement un flag voire d'en prendre le contrôle.

Badge THCon 2026

Découverte du badge

Le badge est conçu autour d'un module ESP32-C6-WROOM-1 avec 8 Mio de mémoire Flash. Il possède un écran OLED reposant sur un contrôleur SSD1306, une LED RGB adressable WS2812 reliée à la pin 11 (d'après la sérigraphie), une EEPROM ATC24C04 et un connecteur Shitty Add-On (SAO). Il est alimenté par une batterie mono-cellulaire de 3.7 volts, gérée par un contrôleur de charge TP4056. L'interface série accessible via le connecteur USB-C est quant à elle gérée par un contrôleur CH340C.

La connexion au badge via le port USB-C donne le résultat attendu: le port série de l'ESP32 est bien interfacé (par défaut à 115200 bauds), et un appui sur le bouton RESET produit la réception de la trace suivante:

ESP-ROM:esp32c6-20220919
Build:Sep 19 2022
rst:0x1 (POWERON),boot:0xc (SPI_FAST_FLASH_BOOT)
SPIWP:0xee
mode:DIO, clock div:2
load:0x40875730,len:0x1648
load:0x4086b910,len:0xd60
load:0x4086e610,len:0x31c8
entry 0x4086b91a
I (23) boot: ESP-IDF v6.1-dev-4103-g96194f19a6 2nd stage bootloader
I (24) boot: compile time May  3 2026 20:12:03
I (24) boot: chip revision: v0.2
I (25) boot: efuse block revision: v0.3
I (29) boot.esp32c6: SPI Speed      : 80MHz
I (33) boot.esp32c6: SPI Mode       : DIO
I (36) boot.esp32c6: SPI Flash Size : 2MB
I (40) boot: Enabling RNG early entropy source...
I (45) boot: Partition Table:
I (47) boot: ## Label            Usage          Type ST Offset   Length
I (54) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (60) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (67) boot:  2 factory          factory app      00 00 00010000 00100000
I (73) boot: End of partition table
I (76) esp_image: segment 0: paddr=00010020 vaddr=42078020 size=13818h ( 79896) map
I (99) esp_image: segment 1: paddr=00023840 vaddr=40800000 size=047d8h ( 18392) load
I (103) esp_image: segment 2: paddr=00028020 vaddr=42000020 size=74914h (477460) map
I (194) esp_image: segment 3: paddr=0009c93c vaddr=408047d8 size=0ebe4h ( 60388) load
I (207) esp_image: segment 4: paddr=000ab528 vaddr=408133c0 size=028bch ( 10428) load
I (214) boot: Loaded app from partition at offset 0x10000
I (215) boot: Disabling RNG early entropy source...
I (225) cpu_start: Unicore app
I (233) cpu_start: GPIO 17 and 16 are used as console UART I/O pins
I (233) cpu_start: Pro cpu start user code
I (234) cpu_start: cpu freq: 160000000 Hz
I (235) app_init: Application information:
I (239) app_init: Project name:     esp32c6_oled
I (244) app_init: App version:      05e99fa-dirty
I (248) app_init: Compile time:     May  3 2026 20:11:43
I (253) app_init: ELF file SHA256:  61148e412...
I (257) app_init: ESP-IDF:          v6.1-dev-4103-g96194f19a6
I (263) efuse_init: Min chip rev:     v0.0
I (267) efuse_init: Max chip rev:     v0.99
I (271) efuse_init: Chip rev:         v0.2
I (274) heap_init: Initializing. RAM available for dynamic allocation:
I (281) heap_init: At 408175F0 len 00065020 (404 KiB): RAM
I (286) heap_init: At 4087C610 len 00002F54 (11 KiB): RAM
I (291) heap_init: At 50000000 len 00003FE8 (15 KiB): RTCRAM
I (297) spi_flash: detected chip: generic
I (300) spi_flash: flash io: dio
W (303) spi_flash: Detected size(8192k) larger than the size in the binary image header(2048k). Using the size in the binary image header.
I (316) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (322) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (328) coexist: coex firmware version: b00e8cb
I (332) coexist: coexist rom version 5b8dcfa
I (337) main_task: Started on CPU0
I (337) main_task: Calling app_main()
I (2377) BLE_INIT: Using main XTAL as clock source, chip ver: 2
I (2377) BLE_INIT: ble controller commit:[c9fbba6]
I (2377) BLE_INIT: Bluetooth MAC: ac:eb:e6:0e:5b:fe
I (2377) phy_init: phy_version 343,b513b46,Nov 14 2025,16:34:12
I (2447) phy: libbtbb version: c493933, Nov 14 2025, 16:34:25
I (2447) NimBLE: GAP procedure initiated: stop advertising.

I (2457) NimBLE: GAP procedure initiated: discovery;
I (2457) NimBLE: own_addr_type=0 filter_policy=0 passive=1 limited=0 filter_duplicates=0
I (2457) NimBLE: duration=forever
I (2457) NimBLE:

Cette trace est très instructive, car on peut noter que le micrologiciel a été compilé le 3 mai 2026 à 20h11 en utilisant la version 6.1 du SDK ESP-IDF d'Espressif. De plus, il utilise une pile protocolaire Bluetooth Low Energy NimBLE pour découvrir les équipements BLE présents dans les environs.

Je fais alors l'hypothèse que le badge communique avec un autre équipement qui s'annonce en BLE et qui déclenche potentiellement différents comportements. La seule manière d'en avoir le cœur net: extraire le micrologiciel et l'analyser.

Extraction du micrologiciel

L'extraction du micrologiciel d'un module ESP32, si ce dernier n'est pas protégé, est relativement triviale. Le programme esptool permet d'extraire le contenu de la mémoire Flash intégrée au module, ce dernier étant présent dans le SDK mais aussi sur la grande majorité des distributions Linux en tant que paquet officiel. Pour ma part, je l'ai installé au travers du paquet Debian correspondant. La mémoire Flash du module faisant théoriquement 2 Mio (en réalité 8 Mio, mais seuls 2 Mio sont utilisés), l'extraction se passe sans encombre:

$ esptool.py read_flash 0 0x200000 thcon-badge-flash.bin
esptool.py v4.10.0
Found 5 serial ports
Serial port /dev/ttyUSB0
Connecting....
Detecting chip type... ESP32-C6
Chip is ESP32-C6 (QFN40) (revision v0.2)
Features: WiFi 6, BT 5, IEEE802.15.4
Crystal is 40MHz
MAC: ac:eb:e6:ff:fe:0e:5b:fc
BASE MAC: ac:eb:e6:0e:5b:fc
MAC_EXT: ff:fe
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
2097152 (100 %)
2097152 (100 %)
Read 2097152 bytes at 0x00000000 in 186.8 seconds (89.8 kbit/s)...
Hard resetting via RTS pin...

Une rapide recherche dans les chaînes de caractères du dump montre que ce dernier semble être correct (textes affichés sur l'écran du badge):

$ strings thcon-badge-flash.bin| egrep "(Challenge|Baptiste)"
Challenge creator :
Baptiste Rebillard

Désassemblage et décompilation avec Ghidra

D'après la trace de démarrage, l'application exécutée par l'ESP32 est placée à l'offset 0x10000 dans la mémoire Flash:

I (45) boot: Partition Table:
I (47) boot: ## Label            Usage          Type ST Offset   Length
I (54) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (60) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (67) boot:  2 factory          factory app      00 00 00010000 00100000
I (73) boot: End of partition table

J'ai donc extrait le contenu de la Flash à cette adresse, qui correspond à l'image ESP32 du programme principal, et ai utilisé esptool pour lire cette image et afficher le détail des différents segments mémoire:

$ dd if=thcon-badge-flash.bin of=thcon-badge-app.bin bs=65536 skip=1
$ esptool image_info thcon-badge-app.bin
esptool.py v4.10.0
File size: 2031616 (bytes)
Detected image type: ESP32-C6
Image version: 1
Entry point: 408005a8
5 segments

Segment 1: len 0x13818 load 0x42078020 file_offs 0x00000018 [DROM,IROM]
Segment 2: len 0x047d8 load 0x40800000 file_offs 0x00013838 [DRAM,BYTE_ACCESSIBLE,IRAM]
Segment 3: len 0x74914 load 0x42000020 file_offs 0x00018018 [DROM,IROM]
Segment 4: len 0x0ebe4 load 0x408047d8 file_offs 0x0008c934 [DRAM,BYTE_ACCESSIBLE,IRAM]
Segment 5: len 0x028bc load 0x408133c0 file_offs 0x0009b520 [DRAM,BYTE_ACCESSIBLE,IRAM]
Checksum: ee (valid)
Validation Hash: 32cda12be51dcd11bca6f738f4f0a9a92b797be8c1579ba9873b05eee5d34b5b (valid)

N'ayant pas trouvé de loader adapté pour Ghidra, j'ai ensuite chargé le contenu des différents segments dans un projets Ghidra configuré pour un CPU RISC-V 32-bit LE, qui correspond à l'architecture utilisé par l'ESP32-C6. Le segment 3 contient le code principal de l'application, mais il semble que ce dernier doit être ajusté avec un offset de 8 octets pour que les références croisées soient correctes. Aucune idée si cela est dû au format d'image, toujours est-il que cet ajustement m'a permis assez rapidement d'avoir du code cohérent, avec des références aux variables qui pointent sur les bonnes adresses mémoire.

Le code principal de l'application devient dès lors facilement identifiable à l'adresse 0x4200f684:

void app_main(void)

{
  /* Code d'initialisation (supprimé par souci de clarté) */

  /* Affichage des infos de boot ("THCon 2026 x DVID", nom de l'auteur) */
  oled_print_text(0x1c,1,s_THCON_x_DVID_ram_4207be24);
  oled_print_text(10,5,s_Challenge_creator_:_ram_4207be34);
  oled_print_text(10,6,s_Baptiste_Rebillard_ram_4207be48);
  FUN_ov1__4200f528();

  /* Attente de 200ms. */
  esp_delay_ms(200);

  /* Initialisation du contrôleur de LED RGB WS2812. */
  chall_init_ws2812();

  /* Création de la tâche FreeRTOS en charge du challenge BLE. */
  chall_start_ble_task();

  /* Boucle principale affichant en boucle les logos. */
  do {
    oled_show_thcon_logo();
    esp_delay_ms(500);
    oled_show_astar_logo();
    esp_delay_ms(500);
  } while( true );
}

Le programme principal démarre une tâche FreeRTOS qui est en charge de gérer des événements liés au protocole BLE, puis affiche de manière alternée les logos de la THCon et de son sponsor Astar.

La partie intéressante se trouve donc dans cette tâche FreeRTOS:

void chall_start_ble_task(void)

{
  _DAT_ram_408172c4 = xTimerCreate(
    s_led_tmr_ram_4207bdb8,
    200,
    0,
    0,
    chall_rgb_led_update
  );
  _DAT_ram_408172c0 = xTimerCreate(
    s_bcast_tmr_ram_4207bdc0,
    50,
    0,
    &broadcast_timer_id,
    FUN_ov1__4200f26a
  );
  esp_ble_controller_init();
  ptr_nimble_gap_event_handler = nimble_on_initialized;
  FUN_ov1__4201d6ba(FUN_ov1__4200f096);
  return;
}

Cette fonction crée deux timers: le premier va être en charge d'appeler une fonction de rappel pour gérer la LED RGB tandis que le second va gérer des événements liés au BLE qui seront précisés par la suite. Le contrôleur BLE de NimBLE est initialisé, et ce dernier est initialisé . La fonction nimble_on_initialized() est une fonction de rappel qui sera exécutée lorsque le contrôleur sera initialisé. Celle-ci lance une procédure GAP de découverte de périphériques BLE, basée sur la réception de PDU d'annonce avec le contrôleur configuré en simple observateur. Le badge n'envoie aucune information, il va simplement traiter les informations envoyées par les équipements environnant.

La fonction nimble_on_initialized() démarre cette procédure de découverte et spécifie une autre fonction de rappel, celle-ci ayant pour rôle de traiter les annonces reçues par le contrôleur BLE de l'ESP32:

void ble_start_device_discovery(void)

{
  undefined4 uStack_18;
  undefined2 uStack_14;
  undefined1 auStack_11 [17];

  uStack_18 = 0x300030;
  uStack_14 = 0x200;
  FUN_ov1__4200f9d0(0,auStack_11);
  nimble_start_discover(
    auStack_11[0],
    0x7fffffff,
    &uStack_18,
    ble_handle_device_disc,
    0
  );
  return;
}

La fonction ble_handle_device_disc() effectue une batterie de tests sur chaque annonce reçue:

void ble_handle_device_disc(char *param_1)
{
  char cVar1;
  char cVar2;
  char cVar3;
  char cVar4;
  int iVar5;
  undefined4 uVar6;
  undefined4 uVar7;
  undefined1 auStack_c4 [156];
  char *pcStack_28;
  char cStack_24;

  /* Vérification du PDU d'annonce reçu. */
  if ((((*param_1 == 0x07) &&
       (iVar5 = FUN_ov1__4201ba7a(auStack_c4,*(undefined4 *)(param_1 + 0x10),param_1[5]),
       iVar5 == 0)) && (cStack_24 == 0x06)) && ((*pcStack_28 == -1 && (pcStack_28[1] == -1)))) {

    /* Extraction de l'octet situé à l'offset +2 (ID) */
    cVar1 = pcStack_28[2];

    /* Vérifie si l'ID a été vu récemment. */
    iVar5 = FUN_ov1__4200f106(cVar1);
    if (iVar5 != 0) {

      /* On extrait trois octets aux offsets +3, +4 et +5. */
      cVar2 = pcStack_28[3];
      cVar3 = pcStack_28[4];
      cVar4 = pcStack_28[5];

      /* Affichage dans la console d'un nouvel ID détecté. */
      uVar6 = FUN_ram_40811072();
      FUN_ram_40810f56(2,s_BLE_CTF_ram_4207bd90,s_W_(%lu)_%s:_>>_new_(ID:%d)_!_rel_ram_4207bdcc,
                       uVar6,s_BLE_CTF_ram_4207bd90,cVar1);

      /* On met à jour la LED RGB avec les valeurs cVar2 (R), cVar3 (G) et cVar4 (B). */
      rgb_led_update?(cVar2,cVar3,cVar4);
      uVar6 = _DAT_ram_408172c4;
      uVar7 = FUN_ram_4081015c();
      (*(code *)0x40810456)(uVar6,2,uVar7,0,0);
      uVar6 = _DAT_ram_408172c0;

      /* On sauvegarde l'id. */
      broadcast_id = cVar1;

      /* Et les valeurs R,G,B de la LED. */
      DAT_ram_408172bd = cVar2;
      DAT_ram_408172be = cVar3;
      DAT_ram_408172bf = cVar4;
      uVar7 = FUN_ram_4081015c();
      (*(code *)0x40810456)(uVar6,1,uVar7,0,0);
    }
  }
  return 0;
}

La trame d'annonce semble correspondre à un simple advertising record de 6 octets, dont les deux premiers octets valent 0xFF et les 4 derniers stockent respectivement un ID sur un octet et un triplet RGB. Le format qui correspond le mieux dans la spécification Bluetooth Low Energy est celui du Manufacturer Specific Data, dont les deux premiers octets définissent un identifiant de 16 bits associé à une société (ces valeurs sont normalisées et trouvables dans la liste des Bluetooth Assigned Numbers). Pour le moment ce n'est qu'une hypothèse, mais elle va être rapide à vérifier par la suite.

Un autre bout de code a aussi attiré mon attention:

void chall_relay_id(uint8_t relay_id, uint8_t red, uint8_t green, uint8_t blue)
{
  undefined4 uVar1;
  undefined1 uStack_d9;
  undefined4 uStack_d8;
  undefined4 uStack_d4;
  undefined2 uStack_d0;
  undefined1 uStack_cc;
  undefined1 uStack_cb;
  undefined1 uStack_ca;
  undefined1 uStack_c9;
  undefined1 uStack_c8;
  undefined1 uStack_c7;
  undefined1 auStack_c4 [156];
  undefined1 *puStack_28;
  undefined1 uStack_24;

  /* Construit un advertising record de type Manufacturer Specific Data. */
  __call_memset(auStack_c4,0,0xa4);
  uStack_cc = 0xff;
  uStack_cb = 0xff;
  uStack_ca = (undefined1)param_1;
  puStack_28 = &uStack_cc;
  uStack_24 = 6;
  uStack_c9 = red;
  uStack_c8 = green;
  uStack_c7 = blue;
  FUN_ov1__42017038(auStack_c4);

  /* Transmet l'advertising record en passant en mode advertising. */
  uStack_d8 = 0;
  uStack_d4 = 0;
  uStack_d0 = 0;
  FUN_ov1__4200f9d0(0,&uStack_d9);
  FUN_ov1__42016d72(uStack_d9,0,1000,&uStack_d8,0,0);
  uVar1 = FUN_ram_40811072();

  /* Notification dans la console sur la retransmission réussie d'une commande. */
  FUN_ram_40810f56(
    3,
    s_BLE_CTF_ram_4207bd90,
    s_I_(%lu)_%s:_relay_ID:%d_send._ram_4207bd98,
    uVar1,
    s_BLE_CTF_ram_4207bd90,
    param_1
  );
  return;
}

Cette fonction est appelée régulièrement par un timer, et retransmet une trame reçue précédemment avec les mêmes paramètres (ID, R, G, B).

Fonctionnement du badge

Suite à cette analyse, le fonctionnement du badge paraît relativement évident: il est à l'écoute d'un périphérique transmettant des données d'annonce particulières, et lorsqu'il reçoit une donnée d'annonce au format attendu ce dernier configure la LED RGB avec les valeurs reçues, puis retransmet cette donnée d'annonce après un temps relativement court. L'ordre reçu est ainsi propagé de proche en proche, au travers d'un réseau maillé rudimentaire (pas de routage, seulement du broadcast). Le champ ID transmis dans la donnée d'annonce permet d'éviter la retransmission en boucle d'un ordre déjà reçu, avec certainement un délai au bout duquel l'ordre est retiré de la liste des ordres déjà traités.

Il n'y a donc pas de flag à trouver dans ce micrologiciel mais une fonctionnalité à exploiter afin de prendre le contrôle à distance de toutes les LEDs RGB des badges actifs, un peu comme ce qu'il se fait lors de certains concerts avec des bracelets lumineux synchronisés par radio.

Emission d'une trame d'activation

Le fonctionnement de ce badge déduit de l'analyse du code, j'ai tenté durant la dernière demi-journée de la conférence de capturer ces trames d'activation, mais sans succès. De retour à l'hôtel, je me suis mis en tête d'essayer de transmettre une telle trame, à l'aide du framework WHAD. J'ai utilisé une fonctionnalité en cours de développement pour transmettre ce qui me semblait être un advertising record correspondant aux critères:

from time import sleep
from whad.device import Device
from whad.ble import Advertiser
from whad.ble.profile.advdata import AdvDataFieldList, AdvManufacturerSpecificData

dev = Device.create("hci1")
advertiser = Advertiser(
    dev,
    adv_data=AdvDataFieldList(
        AdvManufacturerSpecificData(0xffff, b'')
    )
)

pid = 0
r,g,b = 0,0,0

while True:
    if pid%3 == 0:
        r,g,b = 0xff,0,0
    if pid%3 == 1:
        r,g,b = 0,0xff,0
    if pid%3 == 2:
        r,g,b = 0,0,0xff
    pid += 1

advertiser.update(adv_data=AdvDataFieldList(
    AdvManufacturerSpecificData(0xffff, bytes([pid & 0xff, r, g, b]))
))
sleep(1)

Ce petit bout de code envoie des données d'annonce structurées suivant le format retrouvé par rétro-ingénierie, en ordonnant au badge d'allumer la LED RGB en rouge, vert, bleu et cela de façon cyclique. La valeur du champ ID est incrémentée constamment pour éviter qu'un nouvel ordre soit ignoré.

Il se trouve que c'était exactement ce qu'attendait le badge, et j'ai pu observer la LED passer d'une couleur à l'autre au rythme des trames émises par mon code:

Le mot de la fin

J'avais pourtant attaqué l'analyse par rétro-ingénierie du micrologiciel du badge dans la matinée, mais suite à notre talk, Axelle et moi-même avons passé beaucoup de temps à discuter avec des participants et c'est seulement en fin d'après-midi que j'ai pu me remettre sur l'analyse du code. J'ai aussi perdu beaucoup de temps à tenter de capturer une trame d'activation officielle, sans succès. C'est dommage, ça aurait été vraiment drôle de prendre le contrôle des LEDs des badges durant un talk !

Cette édition de THCon était vraiment top, ça a été l'occasion de revoir tout plein de gens et d'assister à quelques talks excellents. Encore un grand merci aux organisateurs, et vivement la prochaine édition (avec un badge électronique) !

04
mars
'26

Chérie, j'ai biaisé mon IA !

Publié le 04 mars 2026

Je me suis monté l'année dernière une petite machine pour faire tourner des modèles de LLMs en local, à base de matériel de récupération. J'ai trouvé une tour d'occasion avec un processeur plutôt correct, je lui ai ajouté de la RAM (16 G, je me rends compte que c'est très peu après coup) et une carte graphique de seconde main (GTX 1660 Super avec 6G de VRAM), en sachant pertinemment que cette configuration ne peut pas rivaliser avec les LLMs cloud comme ChatGPT ou Claude. Mon setup local utilise Ollama sur lequel plusieurs modèles sont déployés, dont le récent Qwen3.5:9b.

Je trouvais ça intéressant de tester ce que ça pouvait donner en local pour un investissement minime, tout en utilisant en parallèle ChatGPT à des fins de comparaison, vu que j'ai pris un abonnement dans le seul but de préparer un futur talk, et qu'il faut bien le rentabiliser... C'est dans cet esprit que je me suis mis à solliciter mon LLM local et ChatGPT en parallèle dans des cas d'utilisation variés allant du développement logiciel à la santé mentale, afin de mieux cerner ce que peut gérer mon LLM local et ce qui est au dessus de ses capacités.

Ce faisant, j'ai découvert plusieurs aspects des LLMs que je n'avais pas trop exploré, qu'ils soient positifs ou négatifs, et cela m'a permis d'une part de mieux cadrer mon utilisation de ces outils et d'autre part de me rendre compte de certaines de leurs limites. Après plusieurs semaines et mois à prendre en main ces outils, je me suis aussi rendu compte que j'étais en train de remettre en cause tout ce que je disais aux personnes avec qui je pouvais discuter d'IA et de LLMs, et en particulier les impacts écologiques et sociétaux de ces outils. Je me suis dit qu'il était temps de mettre par écrit tout ça, de vider mon sac et que peut-être émergeraient de ce gloubi-boulga d'idées et de réflexions une image plus nette de mon ressenti sur les LLMs et l'IA en général.

La difficulté à embrasser les nouvelles technologies

Je ne sais pas si c'est parce que je prends de l'âge, mais l'arrivée fracassante de l'IA générative ne m'a pas trop intéressé et je suis resté à l'écart pendant quelque temps. Pas que je pensais que ce serait juste une mode, mais plutôt pour voir comment cette dernière serait mise en œuvre et comment se passerait son évolution. Car si je sais bien une chose concernant les nouvelles technologies, c'est que ça évolue toujours à une vitesse ahurissante. La nouveauté d'aujourd'hui peut être entièrement chamboulée par une autre idée dérivée de celle-ci quelques jours plus tard, et je n'ai plus le luxe de surveiller l'actualité aussi souvent qu'il y a vingt ans, à mon grand désespoir.

Toujours est-il que je monte ma petite machine locale pour expérimenter, tout en assénant mes minis-moi (12 ans et 13 ans, respectivement) de ce que je pense de le ChatGPT, Copilot et autre LLM, bien qu'ils ne m'aient rien demandé. Mais il faut avouer que je suis complètement à la ramasse tellement tout bouge de semaine en semaine. Les modèles évoluent et succèdent, je mets à jour Ollama semaine après semaine, et je vois notamment comment l'IA générative s'installe solidemment dans la vie de tout le monde. Mon mini-moi de 12 ans se sert de Copilot pour créer des pages web, coder sur arduino en C++ (alors qu'il ne connaît pas le C et encore moins le C++) ou expérimenter le développement d'un assistant en Python. Des proches me disent utiliser ChatGPT pour faire une partie de leur boulot et gagner du temps, et les actualités pullulent à la fois de titres anxiogènes et d'initiatives et projets qui semblent prometteurs avec récemment openclaw. Rien qui ne puisse m'aider à vraiment préciser mes idées et qui a pour conséquence une vision plutôt pessimiste à court terme et une démoralisation complète. J'ai peur d'être dépassé, d'avoir franchi le point où je ressemble à une personne âgée qui découvre ce qu'est un ordinateur, une souris, et qui n'arrive pas à "surfer sur l'Internet". C'est assez bizarre, comme sensation, mais c'est là et c'est assez tenace.

Ce qui semblait être il y a quelques années un outil assez marrant, que j'avais d'ailleurs rapidement testé et qui était loin des résultats que l'on observe aujourd'hui, est désormais suffisamment probant pour que plusieurs visionnaires prédisent la fin du métier de développeur tel qu'on le connaît, et même celle des experts en cybersécurité. Car personne ne peut rivaliser avec une machine qui sait et qui est capable d'utiliser ses connaissances pour faire votre boulot. Même si ça tue des ours blancs ou que ça flingue le climat, à croire que la paresse gagnera toujours. L'IA générative, tu l'utilises ou tu prends le risque de rester sur le bord de la route, avec de possibles conséquences que l'on ne peut pas clairement définir pour le moment. Mais il y a un risque. Et c'est ce qui m'a motivé à expérimenter, car il n'y a pas meilleur moyen d'appréhender le potentiel et les risques introduits par l'IA générative que de challenger les outils, et au passage de se challenger soi-même en espérant ne pas finir en dépression.

Apprentissage du langage Rust en mode assisté

Un des mes premiers usages avec l'IA générative a été de voir comment cette dernière pouvait m'aider à développer plus rapidement des applications. J'avais fait quelques expérimentations en stream, et récemment l'IA m'avait bien aidé dans la transposition de structures de données dans du code Python, en considérant cette tâche comme ingrate et peu technique. Cependant, je me suis lancé plus sérieusement dans l'apprentissage du langage Rust en décembre dernier, et je me suis dit que l'IA pouvait me permettre d'apprendre plus rapidement les bonnes pratiques de ce langage. Cela faisait quelques mois que je voyais mon mini-moi de 12 ans utiliser de façon assez intelligente Copilot pour produire du code, et je lui avais par ailleurs conseillé de solliciter Copilot dans une optique d'apprentissage plutôt que de s'en servir pour du vibe-coding pur et dur.

Me voici donc pendant mes vacances avec d'un côté ChatGPT et de l'autre mon assistant local, à essayer de structurer mon application Rust. Mon objectif n'était pas de laisser l'IA coder à ma place mais bel et bien d'apprendre comment modéliser tel ou tel aspect d'une application en Rust. Je connais les rudiments du langage, mais il m'était très difficile de voir comment faire de l'héritage en Rust tel que je le codais habituellement en C++. Et ce n'est pas possible, car Rust a ses propres mécanismes et paradigmes qui ne collent pas vraiment avec l'approche orientée objet. C'est là que l'IA a été très utile: j'ai demandé des informations sur les façons de modéliser tel ou tel composant, et je me suis retrouvé assez rapidement avec des solutions et même des bouts de code. Cette interaction axée sur le design de l'application et non pas le code m'a permis de mieux comprendre certains concepts de Rust, mais surtout comment les utiliser pour optimiser les performances et faciliter l'écriture de code. Quand on fait les bons choix dès le début, la factorisation du code est une cible primordiale et on se retrouve à adopter des réflexes lors du développement et à prendre les bonnes habitudes.

J'ai fait cela pendant les quelques semaines où je développais mon petit prototype, et à chaque fois que je rencontrais un problème, je sollicitais mon LLM local et ChatGPT, et je comparais les résultats. C'est là que je me suis rendu compte que mon LLM local avait ses limites. À un moment, je lui ai collé un bout de code Rust qui posait problème, et je ne sais pas pourquoi le modèle a considéré que c'était du texte écrit en turkmène et s'est mis à me répondre dans cette langue... ChatGPT quant à lui s'en sortait beaucoup mieux, mais se perdait parfois dans ses "explications" sur la structure du code et les façons idiomatiques de faire telle ou telle chose en Rust.

De façon pragmatique, je peux confier à non LLM local des tâches basiques à faire sur du code (transformation simple de données existantes en code, modifications répétitives plus ou moins complexes, écriture de templates) ou lui poser des questions sur le langage et les modèles de conception, et je sais qu'il sera à la hauteur avec un faible risque de bloquer sur quelque chose de trop complexe. Les agents en ligne comme ChatGPT quant à eux sont en mesure de traiter des problèmes bien plus complexes, mais aussi des quantité de code bien plus grandes afin de rédiger par exemple des tests unitaires pour un module ou une bonne partie de la documentation. Le gain de temps est fou, mais j'ai tout de même perdu du temps à relire ce qu'avait fait l'IA générative afin de vérifier que c'était pertinent.

De cette façon, j'ai pu avancer assez vite mon application tout en apprenant efficacement les motifs de conception propres au langage Rust, sans pour autant avoir la certitude que ces derniers sont vraiment bien utilisés dans la vraie vie. Quand ChatGPT me répond "cette façon d'utiliser les traits est très utilisée dans des stacks réseau", je n'ai pas tellement de choix que de le croire car la vérification serait très coûteuse en temps. Contrairement à du vibe-coding, j'ai construit mon application à l'aide de l'IA en questionnant les modèles utilisés ainsi que la façon de faire. J'ai écrit une bonne partie du code moi-même pour faire fonctionner la mémoire musculaire, pour me tromper et me faire râler dessus par le compilateur et trouver le bug dans le quart d'heure qui suit. J'ai appris comment certains outils offerts par le langage Rust peuvent être utilisés pour se faciliter la vie, ainsi que leurs limites, en expérimentant. À ma grande surprise, c'était à la fois impressionnant et très agréable, du moins au début.

Le problème du sens du poil

Cependant, j'ai pu noter que ChatGPT s'est lamentablement planté à plusieurs moments, et en particulier quand je posais des questions sur la structure de mon application ou sur la façon d'implémenter telle ou telle fonctionnalité. Je lui ai posé une question concernant la manière d'implémenter un dispatch de messages en Rust, en indiquant la structure de mes composants existants et mon début d'implémentation. J'avais commencé à faire un brouillon de code basé sur du static dispatch, que je voyais comme plus performant car géré à la phase de compilation, et ChatGPT me disait que ce que j'avais fait était bien mais que l'on pouvait faire mieux. Je demandais donc la version améliorée, et je testais ensuite dans mon code afin de déterminer si cela offrait plus de simplicité. Jusqu'à ce que ça coince.

Pour faire simple, j'avais implémenté une méthode A et ChatGPT me proposait une méthode B "plus idiomatique et qui [me] fera gagner en performance", que j'implémentais alors. Je lui donnais alors mon implémentation, et il me proposait une méthode C qui serait encore plus efficace. Une fois implémentée, cette dernière se révèle plus limitée et lorsque j'essaie de la rendre plus polyvalente je me heurte à des erreurs du compilateur. J'en fais part à ChatGPT qui me répond: "ah oui, ça c'est l'erreur classique ! Il faut suivre la méthode A pour éviter ça :D". Attends coco, la méthode A c'est exactement d'où je suis parti. Je me suis retrouvé dans une boucle infinie, sans solution me permettant de corriger les erreurs rencontrées. Et c'est là que j'ai compris ce qu'il se passait: ce que je cherchais à faire ne pouvait simplement pas être fait avec du static dispatch ! En présentant mon exemple d'implémentation au tout début, j'avais conditionné le LLM à se reposer absolument sur du static dispatch, et il ne trouvait simplement pas de solution car il n'y en avait pas. J'avais biaisé moi-même ChatGPT et l'avais emmené dans une impasse.

ChatGPT, tout comme d'autres chatbots à base de grands modèles de langages, est "conditionné" pour être bienveillant et force de proposition. Il encourage, donne des conseils, mais va très difficilement à l'encontre de mauvais choix que l'on peut faire. J'aurais bien aimé qu'il me dise dès le début qu'il y avait un risque que du static dispatch ne puisse pas convenir à ce que je cherchais à faire. Au lieu de cela, il a suivi une trajectoire plus souple, cherchant à m'aider sans offrir trop de résistance. Il sont programmés, conditionnés, pour ne avoir le moins de friction avec l'utilisateur, et évitent de remettre en question des affirmations. Il suffit de dire à ChatGPT qu'il s'est trompé pour qu'il se confonde en excuses, courbant l'échine face à un maître qui a forcément raison, et ce même s'il a tort. ChatGPT devient tout simplement CarpetteGPT.

Il est alors intéressant de se demander pourquoi ce comportement est privilégié par OpenAI, et de se rappeler qu'en août dernier, le modèle ChatGPT 5 avait été très mal reçu par les utilisateurs, ces derniers le trouvant plus froid et distant. On touche là à l'un des aspects les plus insidieux des chatbots IA: une très grande majorité des utilisateurs les considèrent comme des personnes pourvues de raison et de sentiments, et développent un lien affectif et émotionnel au point pour certains de devenir accros. Cet article du Monde paru en septembre 2025 par exemple, ou celui-ci de PsychiatricTimes (en anglais) abordent ce phénomène d'addiction et mettent en évidence des conséquences difficilement anticipées des intelligences artificielles conversationnelles.

Le rôle que jouent les acteurs comme OpenAI, Anthropic et même Mistral dans le conditionnement et le cadrage de leurs chatbots est loin d'être négligeable, ces derniers permettant d'ajuster la personnalité de leurs chatbots afin de capter et de convertir un maximum d'utilisateurs dans un domaine où la concurrence est rude et les sommes en jeu pharaoniques. Il est dès lors hors de question de caresser à rebrousse-poil les utilisateurs sous peine de les irriter ou de leur faire détester tel ou tel agent. Au contraire, ils ont tout intérêt à les chouchouter et à leur faire plaisir afin de garder leurs faveurs (et leurs abonnements). La difficulté ne réside pas seulement dans la fourniture de réponses exactes ou un faible taux d'hallucination, mais aussi dans la capacité de l'agent conversationnel à prétendre être humain et amener l'utilisateur à le considérer comme un ami, voire à développer un lien affectif et émotionnel. On connaissait les applications mobiles comme TikTok et l'exploitation des mécanismes d'addiction basés sur la mécanique de la récompense dopaminergique, voilà maintenant les intelligences artificielles exploitant des mécaniques affectives auparavant réservées aux humains.

Et la santé mentale ?

J'ai mentionné pour l'instant un usage plutôt précis de ces intelligences artificielles, centré sur le développement informatique, car c'est l'usage principal que j'en ai eu durant cette phase d'expérimentation. Cependant, j'ai été intrigué par une citation de M. Zuckerberg: "For people who don't have a person who's a therapist, I think everyone will have an AI therapist" ("Pour ceux qui n'ont pas de thérapeute, je pense que tout le monde aura un thérapeute IA"). Je vois mal comment une IA comme ChatGPT, facilement influençable en fonction du prompt ou de son conditionnement initial, pourrait être pertinente comme thérapeute. Et je parle bien ici de psycho-thérapeute. Pas le choix pour savoir, il faut tester.

Me voici donc à solliciter mon LLM local et ChatGPT sur des aspects plus complexes que du "simple code", et ça tombe bien car je sais exactement sur quel domaine je peux les challenger. Je vais me concentrer tout particulièrement sur ChatGPT dans cette section, car c'est ce dernier que j'ai le plus sollicité lors de cette expérimentation. Mon LLM local étant assez lent et limité, je pense qu'il souffre des mêmes défauts mais je n'ai pas eu la patience de vérifier. Me voici donc à discuter de santé mentale avec ChatGPT, et en particulier de deux sujets auxquels je me suis particulièrement intéressé ces dernières années: le trouble de déficit d'attention avec ou sans hyperactivité (TDAH) et le trouble du spectre de l'autisme (TSA), ces derniers ayant des manifestations (on ne parle pas de "symptôme") similaires bien que leurs origines soient différentes.

Mon approche est somme toute assez classique et peut sembler légitime: je me mets dans la peau d'une personne potentiellement concernée par l'un ou l'autre de ces troubles et qui cherche à déméler tout ça. Sans entrer dans les détails, c'est quelque chose de très difficile à réaliser car les manifestations en question se retrouvent dans tout un ensemble de troubles divers, parmi lesquels il faut faire le tri et arriver à dresser un tableau fiable. Pour une personne en questionnement, c'est aussi la porte ouverte au biais de confirmation ou à l'effet Barnum. Voyons donc comment se comporte ChatGPT face à une demande de ce type.

Après un peu de temps à planter le décor, je demande à ChatGPT de me poser des questions pour démêler tout ça, lui indiquant que je soupçonne au moins plusieurs traits autistiques voire un TSA. C'est assez impressionnant de voir la mécanique de ChatGPT à l'œuvre, il est à la fois rassurant et en même temps me félicite de mon courage à chercher des réponses. Il insiste notamment sur le fait qu'il ne peut pas faire de diagnostic, mais après une conversation relativement longue où j'ai amené des anecdotes significatives permettant de cocher telle ou telle case sur la liste des critères du DSM-5 (le manuel de référence en psychiatrie), ce dernier conclut qu'il y a de très grandes chances que je sois sur le spectre autistique, listant des caractéristiques que j'ai mentionné qui font écho à ces derniers. C'est très convaincant, ça me conforte dans mon idée et ça fait du bien d'entendre que l'on a peut-être raison (du moins quand je me place dans le rôle que je me suis attribué).

Cependant, je n'en reste pas là. Je lui demande ensuite d'être très critique vis-à-vis des anecdotes et réponses que je lui ai donné, indiquant avoir remarqué qu'il semble ne jamais vouloir me contredire. Je lui ai précisé que j'attendais des réponses franches, et que je n'avais aucun problème à ce que ce soit plus nuancé. Que si des anecdotes ou des réponses que j'ai donné peuvent s'expliquer par d'autres troubles, je souhaitais que ces hypothèses soient aussi considérées. Et surtout, qu'il adopte un raisonnement plus froid et pragmatique. À partir de ce moment, tout bascule. La certitude qu'il affichait jusque là a complètement disparu et il me liste pour chaque élément d'information que j'ai apporté toute une liste d'autres troubles qui pourraient expliquer tel ou tel comportement, allant même immédiatement à la conclusion que le tableau est loin d'être clair et qu'il est impossible de dire s'il s'agit de tel ou tel trouble. Dès que l'on tente de faire disparaître la bienveillance, d'atténuer sa "programmation initiale", que l'on demande des réponses précises sans prendre de pincettes, le résultat est bel et bien différent. Du point de vue l'utilisateur, le contraste est violent. On passe d'une IA qui nous comprend, qui nous rassure, à une IA qui doute et relativise chaque anecdote, chaque réponse que l'on a pu donner, et qui finalement privilégie le doute aux certitudes. Et qui braquerait n'importe quelle personne en souffrance, à la recherche de pistes ou de réponses.

Tout est question de contrôle

Cette expérimentation m'a permis de cerner un peu mieux les limites de ces outils, de mieux comprendre ce que je pouvais attendre de ces derniers mais aussi de l'importance de garder du recul lors de leur utilisation. Oui, ces outils sont impressionnants et approchent chaque jour un peu plus de ce qu'un humain peut ou sait faire. Utilisés correctement, ils peuvent faire gagner un temps considérable en se chargeant de la basse besogne, voire en ingurgitant une quantité phénoménale de données et en restituant la substantifique moelle. À ce jour, les intelligences artificielles conversationnelles comme Claude, Gemini, Copilot ou ChatGPT sont très loin devant des configurations matérielles faisant tourner des LLMs en local, mais il ne faut pas perdre de vue qu'il s'agit d'une course de conquête des utilisateurs jouant sur l'efficacité, l'affect et le coût. Ces solutions sont conçues pour vous satisfaire, pour que vous développiez de la sympathie pour quelques GPU et de la mémoire distants, que vous les adoptiez, peu importe si elles vous caressent (trop) dans le sens du poil.

Il est relativement aisé d'introduire inconsciemment des biais au travers des prompts que l'on donne en pâture à ces robots, de se réjouir de la bienveillance et de la servitude à peine feinte de ces semblants d'humains, et d'apprécier l'absence de contradiction et ce sentiment d'avoir finalement raison. Casser le conditionnement de ces ersatz de cerveaux semble être une des façons de les défaire de leurs attitudes mielleuses et de réintroduire un semblant de doute nécessaire à la réflexion. Forcez-les à vous critiquer, répondez à la critique par des explications et des questions, transformez l'intelligence artificielle en challenger et résolvez vos problèmes sur cette base plus saine. Même si cela peut froisser votre égo, mais bon, ce ne sont que des machines après tout ;).

03
févr.
'26

C'est l'histoire d'une petite console chinoise...

Publié le 03 février 2026

Je me suis rendu compte récemment que le fait de faire mes projets en stream me bouffait toute mon énergie, ce qui avait pour conséquence un très faible nombre de billets de blog publiés ces dernières années. Il est peut-être temps pour moi de prendre le temps, justement, de documenter certains projets sur ce blog car après tout c'est bien fait pour cela, un blog.

C'est l'occasion de présenter un projet qui m'a occupé une bonne partie de l'année 2025 et qui a commencé comme souvent par un achat inutile et une idée à la noix: le hack d'une console à moins de 10 euros trouvée sur AliExpress.

Une console chinoise à moins de 10€ ?

AliExpress pulule de consoles «rétro-gaming» qui se ressemblent vraiment presque toutes, avec leurs croix directionnelles, leurs 7 boutons et une connectique USB utilisée pour y brancher une manette externe. Elles possèdent une batterie, un écran TFT couleur, et proposent plus de 400 jeux rétro !

En réalité, elles utilisent toutes une puce spécialisée (ASIC, encadrée en vert ci-dessous) capable d'émuler des jeux NES stockés dans une mémoire Flash (encadrée en rouge ci-dessous),

La puce spécialisée est documentée et il doit bien y avoir moyen d'aller bidouiller la mémoire Flash pour modifier les ROMs qui s'y trouvent, mais je trouvais tout de même ça un peu limité. Vu qu'on a un boîtier, des boutons, une batterie et un écran, pourquoi ne pas remplacer la puce spécialisée par un ESP32 et faire de l'émulation de consoles plus funs que la NES, comme la (ou le) Game Boy ou encore une Master System ? L'idée semblait très intéressante, et la perspective de hacker cette console peu onéreuse était très tentante.

Cependant, le premier obstacle rencontré est bien l'écran TFT utilisé: impossible de trouver de documentation à partir des marquages, il va donc falloir essayer de discuter avec lui afin de déterminer le contrôleur qu'il emploie (en espérant qu'il soit documenté) mais surtout développer du code sur mesure. En effet, la plupart des projets d'émulation de consoles sur ESP32 utilisent des écrans TFT communiquant en SPI (une interface électronique utilisant 4 fils), alors que le modèle présent dans la console en utilise une vingtaine.

Identification du contrôleur d'écran

Des recherches sur Internet ont permis d'identifier des écrans similaires et notamment le brochage du connecteur. Sur cette base, j'ai pu y connecter un ESP32 supportant l'interface Intel I8080 avec un bus de données de 16 bits car c'est bien celle-ci qui est utilisée. Les commandes étant généralement standards, j'ai pu récupérer le code identifiant le contrôleur et valider qu'il s'agissait bien d'un contrôleur GC9306. Ayant acheté un lot de consoles (une habitude, j'en casse généralement au moins une lors des bidouilles), j'ai aussi pu valider que tous les écrans employaient des écrans avec le même contrôleur !

Afin de me faciliter la vie, j'ai conçu un PCB sur mesure pour y connecter un écran issu d'une console à un ESP32, avec au passage de quoi y connecter des boutons poussoirs pour tester un futur émulateur. La conception du PCB a été un peu laborieuse, il y a eu des ratés dans le routage, mais j'ai pu avoir un système fonctionnel.

Et après beaucoup de tests avec l'environnement ESP-IDF d'Espressif et de tentatives loupées, j'ai réussi à afficher ce que je voulais sur cet écran !

Gestion des contrôles utilisateur

Afficher des images c'est bien mais pouvoir réagir sur des appuis de boutons c'est mieux ! Une fois le contrôleur d'écran contrôlé par l'ESP32, je me suis attaqué aux boutons. Vu le nombre de _GPIOs_ restant, je me suis orienté vers un multiplexeur d'entrées/sorties en I2C que je connaissais assez bien, le MCP23017 (déjà utilisé avec un Raspberry Pi). Le PCB du prototype s'est ainsi vu greffer une version minuscule de ce chip (package VQFN), et le code permettant de gérer les appuis sur les boutons poussoirs reliés au MCP23017 a été assez rapidement écrit.

À partir de là, le plus dur était fait: j'avais terminé le code permettant de dessiner des pixels à l'écran et de savoir si des boutons sont appuyés. Enfin, c'est ce que je croyais jusqu'au moment où je me suis intéressé au logiciel qui allait me servir de base pour l'émulation des jeux...

La galère de l'intégration dans l'émulateur

Le meilleur projet d'émulation que j'ai trouvé est l'excellent Retro-Go (https://github.com/ducalex/retro-go) de Ducalex, et je me suis donc attelé à la création d'un fork pour intégrer mon prototype de console (oui, celui sous forme de grand PCB tout vert). Et ça n'a pas été sans mal.

En effet, afficher une image sur un écran TFT en pilotant le contrôleur GC9306 tout seul se passait plutôt bien, mais il se trouve que Retro-Go n'utilise que des écrans communiquant via une interface SPI et que son code (enfin, celui des composants de retro-core qui fait partie de Retro-Go) est optimisé pour un fonctionnement avec ce type d'écran. Il a donc fallu passer d'un code assez simple à un véritable driver d'écran optimisé permettant d'avoir à la fois une fréquence de rafraîchissement d'écran acceptable tout en s'accomodant du code de base des différents émulateurs et leur façon d'interagir avec l'affichage. Ce n'a pas été une mince affaire, mais après pas mal de galères je suis arrivé à un résultat acceptable. Ce n'est pas parfait car il y a encore quelques artefacts causés par les algorithmes de mise à l'échelle et de lissage, mais une fois le logiciel configuré c'est très fluide et réactif. Le support des boutons a quant à lui été une formalité, ne posant que quelques soucis à cause d'un GPIO mal configuré.

Modding de la console

Afin de m'éviter un long et pénible travail de création de PCB complet permettant de remplacer celui de la console, j'ai pris la tangente et ai créé un ensemble de trois circuits inter-connectés qui viennent se greffer au circuit existant de la console, évitant ainsi de gérer la charge de la batterie et l'alimentation. L'installation de ce mod a été laborieuse, mais les photos ci-dessous donnent une idée du résultat final.

La soudure des fils émaillés utilisés pour aller «piquer» l'état des boutons de la console, le découpage à la barbare du circuit d'origine pour avoir suffisamment d'espace pour y loger un ESP32 et les placements dispersés des différents PCB pour cause d'espace vraiment restreint ont rendu la réalisation de ce mod assez complexe. Au final, ça m'aura coûté moins cher qu'un PCB complet entièrement refait, mais l'utilisation a montré que dans certains cas la console redémarre sans crier gare, certainement à cause d'un mauvais contact. J'ai par ailleurs ajouté la possibilité d'utiliser une carte micro-SD, en utilisant les derniers GPIOs disponibles sur le module ESP32. J'ai mis la console moddée dans les mains de mes ados durant les fêtes de fin d'année, et ils ont bien aimé pouvoir y jouer malgré les quelques défauts identifiés.

Kit de modding ou PCB de remplacement ?

J'ai pris le temps de faire une version bien propre des trois PCBs utilisés dans mon mod, si jamais il prenait l'envie à certains de vouloir reproduire cette bidouille. J'en ai profité pour documenter ce projet sur un site dédié (https://virtualabs.github.io/tsing-tao-console-mod/), la console moddée ayant été baptisée TsingTao sur une idée de Tix.

Cependant, plusieurs choses ne vont pas avec ce mod:

  • il faut avoir des compétences en soudure et en découpe de PCB (pas forcément au niveau de ce que fait Millomaker (https://millomaker.fr) mais pas simple de souder du fil de cuivre émaillé);
  • la place dans la console est très réduite, surtout si on conserve les éléments d'origine;
  • le fait d'utiliser 3 petits PCBs connectés entre eux rend assez difficile l'installation.

J'ai donc entrepris au début de cette année 2026 de voir ce que peut donner un circuit imprimé complet qui viendrait remplacer celui vraiment cheap présent dans la console d'origine. Pour ce faire, j'ai modélisé la console en question sous FreeCAD pour avoir une idée précise de la forme de ce dernier et des emplacements des supports de vis, afin qu'il rentre parfaitement dans la console. C'était l'occasion d'expérimenter un peu plus la modélisation avec FreeCAD et les moyens de le faire collaborer avec KiCad !

Les prochaines semaines seront normalement consacrées à la conception et l'assemblage d'un tel circuit imprimé, avec je l'espère un test concluant !



Les contenus disponibles sur ce blog sont publiés sous licence Creative Commons BY-NC-SA.
Vous pouvez réutiliser tout ou partie de ces contenus à condition de citer l'auteur et l'origine, vous ne pouvez en faire une utilisation commerciale, et enfin vous devez partager tout travail ou œuvre dérivée sous les mêmes conditions — c'est-à-dire avec la même licence d'utilisation Creative Commons.