ESP32 · Bases — ESP32-S3, PlatformIO et Arduino-ESP32#
← ALT in&out XP · Programme · Réseau →
Ces pages s'adressent à des étudiant.e.s qui programment déjà en C++ et en Godot : on va droit aux spécificités de l'ESP32. Pour une initiation pas à pas, voir le Jour 2.
| Page | Contenu |
|---|---|
| Bases (ici) | la carte, PlatformIO, GPIO / ADC / PWM / interruptions, filtrage, encodeur, servo, buzzer, rubans LED, souris USB |
| Réseau | Wi-Fi, WiFiManager, mDNS, OSC avec Godot, ESP-NOW |
| Web | serveur HTTP, API JSON, site LittleFS, requêtes HTTP sortantes, WebSocket (navigateur et Godot) |
| Multitâche | FreeRTOS : tâches, files, mutex, notifications |
| Protocoles | UART, I²C (accéléromètre MPU6050), SPI (lecteur RFID) |
| OTA & NVS | mise à jour par Wi-Fi, réglages persistants |
| Matrices LED | dalles HUB75, image envoyée par Godot, grille de dalles |
Tous les exemples ont été compilés pour l'ESP32-S3 avec la plateforme PlatformIO espressif32 (Arduino-ESP32 2.0.17).
La carte : ESP32-S3 Super Mini#
| CPU | Xtensa LX7 double cœur, 240 MHz |
| Mémoire | 512 Ko de SRAM, 2 Mo de PSRAM, 4 Mo de flash |
| Radio | Wi-Fi 4 (2,4 GHz), Bluetooth 5 LE |
| USB | natif (OTG) : port série, JTAG, ou périphérique HID / MIDI… |
| Périphériques | 3 UART (+ le port série USB), 2 I²C, 2 SPI, PWM (LEDC), 2 ADC 12 bits, touch capacitif, RMT, I²S, LCD/caméra |
| E/S | 3,3 V uniquement, toutes les fonctions peuvent être routées sur (presque) n'importe quelle broche (matrice GPIO) |

| À savoir | GPIO |
|---|---|
| LED RGB intégrée (WS2812) | 48 |
| Bouton BOOT (strapping) | 0 |
| Strapping, à éviter au démarrage | 0, 3, 45, 46 |
| USB natif (D− / D+) | 19, 20 |
| UART0 (TX / RX) | 43, 44 |
| ADC1 (utilisable avec le Wi-Fi) | 1 à 10 |
| ADC2 (inutilisable quand le Wi-Fi est actif) | 11 à 20 |
| Touch | 1 à 14 |
PlatformIO#
Un projet = un dossier avec platformio.ini, src/main.cpp, lib/ (librairies locales), include/.
Le platformio.ini peut définir plusieurs environnements ([env:nom]), par exemple un par mode USB. Pour ne pas tout répéter, on regroupe les réglages communs dans une section de base, et chaque environnement en hérite avec extends :
[esp32s3]: une section ordinaire (sans le préfixeenv:), donc pas un environnement compilable, juste un modèle.extends = esp32s3: l'environnement reprend toutes les options de cette section ; il ne reste qu'à ajouter ou remplacer ce qui change.- Une option redéfinie remplace celle du modèle, elle ne s'y ajoute pas. Pour compléter une liste (
build_flags,lib_deps), on rappelle la valeur du modèle avec${esp32s3.build_flags}. - Un environnement peut hériter d'un autre environnement :
extends = env:usb-jtag.
; ---------- commun à tous les environnements ----------
[env]
platform = espressif32 ; Arduino-ESP32 2.0.17 (ESP-IDF 4.4)
board = esp32-s3-devkitc-1
framework = arduino
board_upload.flash_size = 4MB
board_build.partitions = default.csv ; 2 × 1,25 Mo d'application (OTA possible) + NVS + SPIFFS
board_build.arduino.memory_type = qio_qspi
monitor_speed = 115200
monitor_filters = esp32_exception_decoder ; décode les plantages (backtrace → fichier:ligne)
build_flags =
-DBOARD_HAS_PSRAM
-DARDUINO_USB_CDC_ON_BOOT=1 ; Serial = port série USB
-DCORE_DEBUG_LEVEL=3 ; logs log_e/log_w/log_i (0 = aucun, 5 = tout)
lib_deps =
hideakitai/ArduinoOSC
links2004/WebSockets
adafruit/Adafruit NeoPixel
; ---------- USB-Serial-JTAG : upload et reset automatiques, débogage JTAG ----------
[env:usb-jtag]
build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1
debug_tool = esp-builtin
; ---------- USB OTG (TinyUSB) : clavier, manette, MIDI… ----------
[env:usb-otg]
build_flags = ${env.build_flags} -DARDUINO_USB_MODE=0
board_upload.use_1200bps_touch = yes ; passe la carte en mode programmation via le port série
board_upload.wait_for_upload_port = yes
board_upload.require_upload_port = yes
board_upload.after_reset = watchdog_reset ; redémarre la carte après l'upload
; ---------- mise à jour par le Wi-Fi (voir OTA & NVS) ----------
[env:ota]
extends = env:usb-jtag
upload_protocol = espota
upload_port = enjmin-01.local
upload_flags = --auth=enjmin
; ---------- exemple : un projet qui a besoin d'une librairie en plus ----------
[env:hub75]
extends = env:usb-jtag
lib_deps =
${env.lib_deps}
https://github.com/mrcodetastic/ESP32-HUB75-MatrixPanel-DMA.git
adafruit/Adafruit GFX Library
Dans VS Code, on choisit l'environnement actif dans la barre d'état (icône env:…) ; en ligne de commande, avec -e.
| Commande (terminal PlatformIO) | Effet |
|---|---|
pio run -t upload |
compile et envoie (environnement par défaut) |
pio run -e usb-otg -t upload |
idem avec un autre environnement |
pio device monitor |
moniteur série (Ctrl + C pour quitter) |
pio run -t clean |
nettoie la compilation |
pio run -t erase |
efface toute la flash (NVS comprise) |
pio debug / onglet Run and Debug |
débogage pas à pas par l'USB (env usb-jtag) |
Si l'upload échoue (programme qui plante au démarrage, premier upload en mode OTG…) : maintenez BOOT, appuyez sur RST, relâchez BOOT, puis relancez l'upload.
Arduino-ESP32 2.x ou 3.x ?#
La plateforme officielle espressif32 de PlatformIO est restée sur Arduino-ESP32 2.0.17. Pour la version 3.x (ESP-IDF 5), on utilise le fork communautaire pioarduino :
platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip
Les exemples de ces pages sont écrits pour la 2.0.17. Principales différences avec la 3.x :
| 2.0.x | 3.x | |
|---|---|---|
| PWM | ledcSetup(channel, freq, bits) + ledcAttachPin(pin, channel) + ledcWrite(channel, v) |
ledcAttach(pin, freq, bits) + ledcWrite(pin, v) |
| LED RGB intégrée | neopixelWrite(48, r, g, b) |
rgbLedWrite(48, r, g, b) |
| Réception ESP-NOW | cb(const uint8_t* mac, const uint8_t* data, int len) |
cb(const esp_now_recv_info_t* info, const uint8_t* data, int len) |
| Timers matériels | timerBegin(0, 80, true) |
timerBegin(frequency) |
Infos système, logs et PSRAM#
#include <Arduino.h>
void setup() {
Serial.begin(115200);
delay(2000); // laisse le temps au moniteur série USB de se connecter
Serial.printf("Puce : %s rév. %d, %d cœurs à %d MHz\n",
ESP.getChipModel(), ESP.getChipRevision(), ESP.getChipCores(), ESP.getCpuFreqMHz());
Serial.printf("Flash : %d Mo\n", ESP.getFlashChipSize() / (1024 * 1024));
Serial.printf("RAM libre : %d Ko (sur %d)\n", ESP.getFreeHeap() / 1024, ESP.getHeapSize() / 1024);
Serial.printf("PSRAM libre: %d Ko (sur %d)\n", ESP.getFreePsram() / 1024, ESP.getPsramSize() / 1024);
Serial.printf("Programme : %d Ko (place libre : %d Ko)\n", ESP.getSketchSize() / 1024, ESP.getFreeSketchSpace() / 1024);
Serial.printf("MAC : %012llX\n", ESP.getEfuseMac());
Serial.printf("SDK : %s, Arduino-ESP32 %d.%d.%d\n", ESP.getSdkVersion(),
ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH);
// Logs de l'ESP-IDF : niveau réglé par -DCORE_DEBUG_LEVEL (0 à 5) dans platformio.ini
log_i("message d'info (affiché si CORE_DEBUG_LEVEL >= 3)");
log_e("message d'erreur (affiché si CORE_DEBUG_LEVEL >= 1)");
// Un gros tableau en PSRAM plutôt qu'en RAM interne
uint8_t* buffer = (uint8_t*) ps_malloc(1024 * 1024);
Serial.printf("1 Mo alloué en PSRAM : %s\n", buffer ? "oui" : "non");
free(buffer);
}
void loop() {}
log_e/log_w/log_i/log_daffichent fichier, ligne et fonction ; ils disparaissent à la compilation selonCORE_DEBUG_LEVEL.- La PSRAM est plus lente que la SRAM interne : parfaite pour de gros tampons (images, audio, historique de capteurs), pas pour du code critique ni pour les tampons DMA.
GPIO, ADC, PWM, touch, interruptions#
Montage : potentiomètre sur GPIO 4, LED + 220 Ω sur GPIO 5, bouton sur GPIO 6, fil tactile sur GPIO 7.
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#define LED_PIN 5 // LED externe (avec résistance)
#define BUTTON_PIN 6 // bouton vers GND
#define POT_PIN 4 // potentiomètre (ADC1 : GPIO 1 à 10)
#define TOUCH_PIN 7 // fil / surface conductrice (touch : GPIO 1 à 14)
// --- Interruption : le compteur est modifié depuis l'ISR → volatile ---
volatile uint32_t pressCount = 0;
volatile uint32_t lastPress = 0;
void IRAM_ATTR onPress() { // IRAM_ATTR : la fonction reste en RAM (obligatoire pour une ISR)
uint32_t t = millis();
if (t - lastPress > 200) { // anti-rebond simple
pressCount++;
lastPress = t;
}
}
void setup() {
Serial.begin(115200);
// --- PWM matériel (LEDC) : 5 kHz, 12 bits ---
ledcSetup(0, 5000, 12); // canal 0
ledcAttachPin(LED_PIN, 0);
// --- ADC : résolution et plage de tension ---
analogReadResolution(12); // 0 à 4095
analogSetPinAttenuation(POT_PIN, ADC_11db); // mesure jusqu'à ~3,1 V
pinMode(BUTTON_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(BUTTON_PIN), onPress, FALLING);
}
void loop() {
int raw = analogRead(POT_PIN);
int mv = analogReadMilliVolts(POT_PIN); // valeur calibrée en millivolts
ledcWrite(0, raw); // la LED suit le potentiomètre
Serial.printf("pot=%4d (%4d mV) touch=%6d appuis=%u\n",
raw, mv, touchRead(TOUCH_PIN), pressCount);
delay(100);
}
- LEDC : 8 canaux PWM matériels, fréquence et résolution réglables (jusqu'à 14 bits). Sert aussi aux servos (50 Hz) et aux buzzers (
ledcWriteTone). - ADC : la mesure brute n'est pas linéaire ;
analogReadMilliVoltsapplique la calibration usine. Bruit typique : quelques dizaines de LSB → moyennez. - Touch : sur l'S3, la valeur augmente quand on touche.
- ISR :
IRAM_ATTR, très courte, pas deSerial, pas d'allocation, pas dedelay. Pour un vrai traitement, réveillez une tâche (voir Multitâche).
La LED RGB intégrée, sans librairie#
#include <Arduino.h>
// La LED RGB intégrée (WS2812 sur GPIO 48) se pilote sans librairie
void setup() {}
void loop() {
for (int h = 0; h < 360; h += 2) {
// teinte → RVB, version courte
float r = max(0.0f, cosf((h ) * DEG_TO_RAD));
float g = max(0.0f, cosf((h - 120) * DEG_TO_RAD));
float b = max(0.0f, cosf((h - 240) * DEG_TO_RAD));
neopixelWrite(48, r * 40, g * 40, b * 40); // (broche, rouge, vert, bleu) 0 à 255
delay(20);
}
}
Pour des rubans de LED, utilisez Adafruit NeoPixel ou FastLED (le pilote passe par le périphérique RMT, sans charge pour le CPU).
Lire un capteur proprement : filtrage#
Une mesure analogique brute tremble toujours un peu. Trois outils, à combiner :
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#define SENSOR_PIN 4
// --- 1. Moyenne glissante sur N mesures ---
const int N = 16;
int samples[N];
int sampleIndex = 0;
long sampleSum = 0;
int movingAverage(int value) {
sampleSum += value - samples[sampleIndex]; // on retire la plus ancienne, on ajoute la nouvelle
samples[sampleIndex] = value;
sampleIndex = (sampleIndex + 1) % N;
return sampleSum / N;
}
// --- 2. Filtre exponentiel (passe-bas) : une seule variable ---
float smoothed = 0;
const float ALPHA = 0.1; // 0..1 : petit = très lissé mais plus lent
float exponentialFilter(int value) {
smoothed += ALPHA * (value - smoothed);
return smoothed;
}
// --- 3. Seuil avec hystérésis : évite de clignoter autour du seuil ---
const int THRESHOLD_ON = 2200;
const int THRESHOLD_OFF = 1800;
bool active = false;
bool hysteresis(float value) {
if (!active && value > THRESHOLD_ON) active = true;
if (active && value < THRESHOLD_OFF) active = false;
return active;
}
void setup() {
Serial.begin(115200);
int first = analogRead(SENSOR_PIN);
for (int i = 0; i < N; i++) samples[i] = first; // on pré-remplit pour éviter un départ à 0
sampleSum = (long) first * N;
smoothed = first;
}
void loop() {
int raw = analogRead(SENSOR_PIN);
int avg = movingAverage(raw);
float expo = exponentialFilter(raw);
bool on = hysteresis(expo);
// Format « nom:valeur,nom:valeur » : affichable en courbes par un traceur série (Arduino IDE, Serial Studio…)
Serial.printf("raw:%d,avg:%d,expo:%.0f,on:%d\n", raw, avg, expo, on ? 4095 : 0);
delay(10);
}
- Moyenne glissante : simple et efficace, mais retarde le signal de N/2 mesures.
- Filtre exponentiel : une seule variable, réglage par
ALPHA. C'est le plus utilisé. - Hystérésis : deux seuils (allumage plus haut que l'extinction) pour transformer un signal analogique en tout-ou-rien sans clignotement.
- Une ligne
nom:valeur,nom:valeurpar mesure s'affiche en courbes dans un traceur série (Arduino IDE, Serial Studio…) : pratique pour réglerALPHAet les seuils à l'œil.
Encodeur rotatif#
Un bouton qui tourne sans fin, avec des crans : idéal pour un menu, un volume, une roue de contrôle. Le périphérique PCNT de l'ESP32 compte les impulsions en matériel, sans jamais en perdre, même en tournant vite. Librairie : lib_deps = madhephaestus/ESP32Encoder.
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#include <ESP32Encoder.h>
// Encodeur rotatif (type KY-040) : A et B vers deux GPIO, C (commun) vers GND, bouton SW vers un 3e GPIO
#define ENC_A 5
#define ENC_B 6
#define ENC_SW 7
ESP32Encoder encoder; // compte les crans avec le périphérique matériel PCNT : aucun pas perdu, zéro CPU
void setup() {
Serial.begin(115200);
pinMode(ENC_SW, INPUT_PULLUP);
ESP32Encoder::useInternalWeakPullResistors = puType::up; // pull-up internes sur A et B
encoder.attachHalfQuad(ENC_A, ENC_B); // 2 impulsions par cran (attachFullQuad : 4, attachSingleEdge : 1)
encoder.setFilter(1023); // filtre matériel anti-rebond
encoder.clearCount();
}
int64_t lastCount = 0;
void loop() {
int64_t count = encoder.getCount() / 2; // en crans
if (count != lastCount) {
Serial.printf("position : %lld (%+lld)\n", count, count - lastCount);
lastCount = count;
}
if (digitalRead(ENC_SW) == LOW) { // appui sur l'axe : remise à zéro
encoder.clearCount();
delay(200);
}
delay(5);
}
- Si le sens est inversé, échangez A et B (dans le câblage ou dans le code).
- Certains encodeurs donnent 4 impulsions par cran :
attachFullQuadet division par 4. - Jusqu'à 4 encodeurs comptés en matériel (4 unités PCNT sur l'S3).
Servomoteur#
Pas besoin de librairie : un servo se pilote avec le PWM matériel (LEDC) à 50 Hz.
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
// Servomoteur (SG90, MG996R…) : fil signal sur un GPIO, alimentation 5 V séparée, GND commun
#define SERVO_PIN 5
#define SERVO_CHANNEL 0
// Un servo attend une impulsion toutes les 20 ms (50 Hz) :
// ~0,5 ms = 0°, ~1,5 ms = 90°, ~2,5 ms = 180° (à ajuster selon le modèle)
const int PULSE_MIN_US = 500;
const int PULSE_MAX_US = 2500;
const int RESOLUTION = 14; // 14 bits : 16384 pas sur 20 ms
void servoWrite(float angle) {
angle = constrain(angle, 0, 180);
float pulseUs = PULSE_MIN_US + (PULSE_MAX_US - PULSE_MIN_US) * angle / 180.0;
uint32_t duty = pulseUs / 20000.0 * ((1 << RESOLUTION) - 1);
ledcWrite(SERVO_CHANNEL, duty);
}
void setup() {
ledcSetup(SERVO_CHANNEL, 50, RESOLUTION); // 50 Hz
ledcAttachPin(SERVO_PIN, SERVO_CHANNEL);
}
void loop() {
// L'angle suit un potentiomètre sur le GPIO 4
servoWrite(map(analogRead(4), 0, 4095, 0, 180));
delay(20);
}
- Alimentez le servo en 5 V séparé (le pic de courant d'un servo fait redémarrer l'ESP32), avec le GND relié à celui de l'ESP32.
- Les butées varient d'un modèle à l'autre : ajustez
PULSE_MIN_USetPULSE_MAX_USpour éviter que le servo force en bout de course. - Servo à rotation continue : 90° = arrêt, 0° et 180° = pleine vitesse dans un sens ou l'autre.
- Plusieurs servos : un canal LEDC par servo (8 canaux).
Buzzer#
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
// Buzzer passif (ou petit haut-parleur avec une résistance) sur un GPIO
#define BUZZER_PIN 5
#define BUZZER_CHANNEL 1
// Une mélodie : fréquences en Hz (0 = silence) et durées en ms
const int melody[][2] = {
{523, 150}, {659, 150}, {784, 150}, {1047, 300}, {0, 100}, {784, 150}, {1047, 450},
};
void playTone(int freq, int durationMs) {
ledcWriteTone(BUZZER_CHANNEL, freq); // fréquence du signal carré (0 = arrêt)
delay(durationMs);
}
void setup() {
ledcSetup(BUZZER_CHANNEL, 2000, 10);
ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
for (auto& note : melody) playTone(note[0], note[1]);
ledcWriteTone(BUZZER_CHANNEL, 0);
}
void loop() {
// Sirène : la fréquence monte et descend
for (int f = 400; f < 1200; f += 10) { ledcWriteTone(BUZZER_CHANNEL, f); delay(3); }
for (int f = 1200; f > 400; f -= 10) { ledcWriteTone(BUZZER_CHANNEL, f); delay(3); }
}
ledcWriteTone(canal, fréquence)génère un signal carré ; 0 l'arrête.- Buzzer passif (sans oscillateur interne) : il joue la fréquence demandée. Un buzzer actif ne sait faire qu'un bip, avec un simple
digitalWrite. - Pour un son plus fort, passez par un transistor ; pour de vrais sons (fichiers WAV), utilisez l'I²S avec un amplificateur MAX98357.
Rubans LED (WS2812B / NeoPixel)#
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
// Ruban WS2812B : DIN sur un GPIO (résistance 330 Ω en série conseillée), 5 V et GND séparés
#define STRIP_PIN 5
#define NUM_LEDS 60
Adafruit_NeoPixel strip(NUM_LEDS, STRIP_PIN, NEO_GRB + NEO_KHZ800);
void setup() {
strip.begin();
strip.setBrightness(60); // limite le courant : ~60 mA par LED en blanc à pleine puissance !
strip.show();
}
uint16_t hueOffset = 0;
void loop() {
int level = map(analogRead(4), 0, 4095, 0, NUM_LEDS); // barre de niveau pilotée par le GPIO 4
for (int i = 0; i < NUM_LEDS; i++) {
if (i < level) {
// arc-en-ciel qui défile : teinte sur 16 bits (0 à 65535)
uint16_t hue = hueOffset + i * 65536L / NUM_LEDS;
strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(hue)));
} else {
strip.setPixelColor(i, 0);
}
}
strip.show(); // envoie tout le ruban d'un coup (pilote RMT : sans bloquer les interruptions)
hueOffset += 256;
delay(16);
}
- Alimentation : jusqu'à 60 mA par LED en blanc plein. 60 LEDs = 3,6 A ! Alimentation 5 V dimensionnée, injectée aux deux bouts pour un long ruban, GND commun avec l'ESP32, et
setBrightnesspour limiter. - Le signal 3,3 V de l'ESP32 suffit en général ; sinon, un adaptateur de niveau (74AHCT125) sur DIN.
ColorHSV+gamma32: couleurs plus naturelles qu'en RGB brut.- Plusieurs rubans : un objet
Adafruit_NeoPixelpar ruban, chacun sur sa broche. Pour des effets avancés : librairie FastLED.
Souris USB#
Avec l'environnement usb-otg, l'ESP32 devient une souris : un capteur pilote le curseur dans n'importe quel logiciel, Godot compris. (Clavier et manette : voir Jour 3.)
Ouvrir ce montage dans Plot Circuits ↗
#include <Arduino.h>
#include "USB.h"
#include "USBHIDMouse.h"
// Environnement usb-otg (ARDUINO_USB_MODE=0)
// Joystick analogique (2 potentiomètres) : X sur GPIO 4, Y sur GPIO 5, bouton sur GPIO 6
#define JOY_X 4
#define JOY_Y 5
#define BUTTON 6
USBHIDMouse Mouse;
int centerX, centerY;
int axis(int pin, int center) {
int v = analogRead(pin) - center;
if (abs(v) < 150) return 0; // zone morte au centre
return constrain(v / 100, -20, 20); // déplacement relatif, en pixels par envoi
}
void setup() {
pinMode(BUTTON, INPUT_PULLUP);
centerX = analogRead(JOY_X); // position de repos = centre
centerY = analogRead(JOY_Y);
Mouse.begin();
USB.begin();
}
bool wasPressed = false;
void loop() {
int dx = axis(JOY_X, centerX);
int dy = axis(JOY_Y, centerY);
if (dx || dy) Mouse.move(dx, dy); // move(x, y, molette, molette horizontale)
bool pressed = digitalRead(BUTTON) == LOW;
if (pressed && !wasPressed) Mouse.press(MOUSE_LEFT);
if (!pressed && wasPressed) Mouse.release(MOUSE_LEFT);
wasPressed = pressed;
delay(10); // ~100 rapports par seconde
}
Mouse.move(dx, dy)est relatif : on envoie un déplacement, pas une position. Zone morte et vitesse se règlent dansaxis().Mouse.move(0, 0, wheel)pour la molette ;MOUSE_RIGHT,MOUSE_MIDDLEpour les autres boutons.- Côté Godot :
InputEventMouseMotion.relative, ouInput.mouse_mode = Input.MOUSE_MODE_CAPTUREDpour un contrôle façon FPS.
Et pour Godot ?#
Trois façons de relier l'ESP32 à un jeu :
| Lien | Latence | Avantages | Page |
|---|---|---|---|
| USB HID (clavier / manette) | ~1 ms | rien à installer côté jeu, marche avec tout logiciel | Jour 3 |
| OSC / UDP par le Wi-Fi | quelques ms | sans fil, bidirectionnel, plusieurs ESP32 | Réseau |
| WebSocket | quelques ms | bidirectionnel, aussi depuis un navigateur | Web |