Vuoi controllare un PC da smartphone senza installare programmi sul computer? Con un ESP32-S2 o ESP32-S3 puoi creare una tastiera USB HID ESP32 controllabile via Wi‑Fi: la scheda viene vista da Windows, Linux o macOS come una normale tastiera USB.
Da un telefono, un tablet o un secondo PC è sufficiente aprire la pagina web che gira sull’ESP32 per inviare una frase di testo, utilizzare i tasti funzione, le scorciatoie da tastiera, i comandi di spegnimento e il Wake USB.
Nella versione finale ho implementato anche la libreria di tzapu/WiFiManager: configuri una rete Wi‑Fi una sola volta dal browser e, ai successivi avvii, l’ESP32 torna automaticamente online senza modificare o ricompilare il codice. Veramente comodo!
Come funziona una tastiera USB HID ESP32
HID significa Human Interface Device: è lo standard USB usato da tastiere, mouse, gamepad e molte altre periferiche che il sistema operativo riconosce senza driver speciali.
L’ESP32 non invia comandi al PC tramite un’app remota, Remote Desktop o software da installare. Fa una cosa molto più semplice: si presenta al computer come una tastiera USB reale. Quando premi un tasto nella pagina web, l’ESP32 riceve il comando via Wi‑Fi, lo converte in un report USB HID e il PC lo interpreta come una normale pressione sulla tastiera. In pratica con questo progetto andiamo a creare un vero emulatore di tastiera usb.
Il flusso del progetto è questo:

- Apri dal telefono la pagina web della tastiera.
- Premi un tasto, una scorciatoia o invii un testo.
- Il browser manda il comando all’ESP32 attraverso la rete Wi‑Fi.
- L’ESP32 converte il comando nel relativo codice USB HID.
- Il PC collegato via USB riceve il tasto come se fosse stato premuto su una tastiera fisica.
Il vantaggio è notevole: sul PC da controllare non devi installare nulla. Collegando la scheda via USB, Windows, Linux e macOS la trattano come una periferica HID standard.
Perché realizzare una tastiera USB HID ESP32
L’idea nasce da un’esigenza banale ma reale: avere un PC distante, collegato a una TV, infilato in un mobile, montato in un rack o usato come server domestico, e non voler cercare una tastiera fisica ogni volta che devi digitare qualcosa.
Ecco alcuni utilizzi pratici e legittimi:
- Accendere tramite il Wake USB il proprio PC.
- Gestire un mini-PC o un server headless nel proprio home-lab.
- Inviare comandi rapidi a un PC collegato a una Smart TV.
- Usare shortcut per programmi specifici, presentazioni, media center o automazioni locali.
- Creare una tastiera touch semplificata per esigenze di accessibilità.
- Realizzare una postazione kiosk o una demo didattica.
- Studiare il funzionamento di USB HID.
Non sostituisce Remote Desktop quando devi vedere lo schermo o amministrare un sistema complesso. È invece perfetta quando serve soltanto inviare comandi al computer: una password, una ricerca, un comando rapido, Ctrl+Alt+Canc o una combinazione che normalmente richiederebbe di essere fisicamente davanti al PC.
E sì, può essere molto utile anche per non alzarsi dal divano. L’efficienza energetica del corpo umano è una priorità sottovalutata… 🤣
Hardware per una tastiera USB HID ESP32
L’hardware richiesto è ridotto al minimo:
- Una scheda ESP32-S2 oppure ESP32-S3.
- Un cavo USB dati, se la scheda non ha un connettore USB-A maschio integrato.
- Uno smartphone, tablet o PC da usare per aprire la pagina web.
- Una rete Wi‑Fi esistente per la versione con WiFiManager.
Per questo progetto ho verificato il funzionamento con due dispositivi ESP32-S3 DevKitC e con LOLIN S2 Mini. Entrambe le schede sono adatte.
L’ESP32-S3 è più potente e offre anche il Bluetooth. L’ESP32-S2 è invece una soluzione molto compatta ed economica, con Wi-Fi e USB OTG nativa. La LOLIN S2 Mini, in particolare, mantiene un formato vicino al D1 Mini, quindi è interessante quando lo spazio conta. Se vi ricordate infatti l’avevo usato anche per realizzare il microfono spia Wi-Fi per le sue ridotte dimensioni
Se l’obiettivo è collegare il dispositivo direttamente alla porta USB del PC senza cavi, conviene valutare una board ESP32-S3 in formato dongle USB-A. Il firmware resta concettualmente lo stesso: cambia solo il formato fisico della scheda.
Quale ESP32 scegliere per USB HID
Non tutti gli ESP32 possono funzionare come tastiera USB. La caratteristica decisiva è la presenza del controller USB OTG nativo, cioè un controller programmabile che può comportarsi come dispositivo USB HID.
| Scheda o chip | Tastiera USB HID | Note |
|---|---|---|
| ESP32-S3 DevKitC | Sì | Ottima scelta per sviluppo e test |
| ESP32-S3 Zero | Sì | Piccola e compatta, con USB OTG nativa |
| ESP32-S3 Mini | Sì | Compatibile se usa la USB nativa del chip |
| ESP32-S2 Mini / LOLIN S2 Mini | Sì | Testata nel progetto, compatta ed economica |
| ESP32-S3 T-Dongle | Sì | Interessante in formato chiavetta USB |
| ESP32-C3 | No | Ha USB Serial/JTAG, non USB OTG HID programmabile |
| ESP32 classico | No | Richiede hardware USB aggiuntivo per emulare una tastiera |
ESP32-S2 ed ESP32-S3 supportano USB OTG in modalità Device e sono quindi adatti alla tastiera USB HID ESP32. L’ESP32-C3, nonostante molte schede espongano una porta USB, ha un controller USB Serial/JTAG a funzione fissa: è utile per upload e debug, ma non può trasformarsi in una tastiera HID.
Per questo progetto il nostro caro ESP8266 non va assolutamente bene.
WiFiManager: configurare la rete senza ricompilare
Nelle prime prove che ho realizzato, l’ESP32 creava sempre una rete Wi‑Fi propria, ad esempio ESP32-Keyboard. Funziona, ed è comoda quando non hai un router disponibile, ma obbliga lo smartphone a cambiare rete ogni volta. Dopo il terzo cambio Wi‑Fi per premere Invio, anche il progetto più bello inizia a sembrare un dispetto.
La versione finale usa WiFiManager, una libreria che consente di salvare le credenziali Wi‑Fi direttamente nella memoria della scheda. Non bisogna più scrivere SSID e password nel codice, né ricompilare lo sketch quando cambia la rete. Questa libreria è molto usata nei miei progetti perché rende i progetti più professionali e semplici da utilizzare come il robot o il rilevatore di fumi smart.
Il funzionamento è questo:
- Al primo avvio l’ESP32 crea una rete temporanea chiamata
ESP32-Keyboard-Setup. - Collegati a questa rete dal telefono o dal PC; non richiede password.
- Si apre il portale di configurazione. Se non compare automaticamente, vai su
http://192.168.4.1. - Scegli dall’elenco la rete Wi‑Fi desiderata.
- Inserisci la password e salva.
- L’ESP32 memorizza le credenziali, si riavvia o si riconnette e da quel momento entra automaticamente nella rete scelta.
- Nel Serial Monitor trovi l’indirizzo IP assegnato dal router, da inserire nel browser per aprire la tastiera virtuale.
WiFiManager usa il metodo autoConnect() per provare prima le credenziali già memorizzate; se non riesce a collegarsi, crea un access point con portale di configurazione.
Nella pagina web della tastiera è presente anche il pulsante Reset Wi‑Fi (nuova rete). Serve a cancellare le credenziali salvate e far ripartire il portale di configurazione al successivo avvio. È utile se cambi router, modifichi la password Wi‑Fi o fai quel classico errore di scrivere una password sbagliata e poi dubitare dell’intero universo.
Funzioni della tastiera USB HID ESP32
La pagina web della versione finale non contiene soltanto un campo testo. È una tastiera virtuale pensata per essere usata da smartphone e tablet e include:
- Casella di testo per inviare frasi, comandi o stringhe complete.
- Lettere, numeri e punteggiatura.
- Layout italiano con caratteri come à, è, é, ì, ò, ù, ç, °, § e £.
- Tasti funzione da F1 a F12.
- Tab, Invio, Backspace, Canc, Ins, Home, End, PagSu, PagGiù e Stamp.
- Frecce direzionali.
- Modificatori Ctrl, Alt, Shift, Win e AltGr.
- Combinazioni già pronte: Ctrl+Alt+Canc, Alt+Tab, Alt+F4, Win+D e Win+L.
- Comandi per sospendere, spegnere e risvegliare il PC.
- Pulsanti di emergenza per ripristinare il collegamento USB o riavviare l’ESP32.
- Pulsante per cancellare la configurazione Wi‑Fi e scegliere una nuova rete.
Per ottenere i caratteri italiani corretti, il PC controllato deve avere il layout tastiera impostato su Italiano. L’ESP32 invia i codici fisici HID; è il sistema operativo a tradurli nei caratteri giusti in base al layout attivo.

Sospensione, spegnimento e Wake USB
I tasti rossi della pagina non sono semplici macro di Windows. Lo sketch usa la collezione HID System Control, progettata per comandi come power off, standby e wake. È lo stesso tipo di comando che può essere presente sulle tastiere fisiche dotate dei tasti Power e Sleep.
Il pulsante Sospendi PC invia il comando di standby. Il pulsante Spegni PC invia il comando di spegnimento. Il comportamento dipende comunque dalle impostazioni di alimentazione del sistema operativo: su Windows puoi verificare e modificare l’azione assegnata ai pulsanti nelle Opzioni risparmio energia.
Il pulsante Wake USB è più particolare. Quando il PC entra in sospensione, il bus USB può essere sospeso e un normale tasto HID non arriverebbe al computer. Per questo il firmware dichiara la funzione di Remote Wakeup e usa tud_remote_wakeup(), una funzione TinyUSB che invia il segnale di ripresa USB a basso livello. Il remote wakeup è il meccanismo previsto dallo standard USB per permettere a un dispositivo di risvegliare un host quando il bus è sospeso.
Il Wake USB può funzionare solo se tutte queste condizioni sono soddisfatte:
- Il PC è in sospensione o stand-by, non completamente spento.
- La porta USB continua ad alimentare l’ESP32 durante lo stand-by.
- Windows consente al dispositivo HID di riattivare il computer.
- Il BIOS/UEFI non blocca la riattivazione da USB.
Su Windows, controlla Gestione dispositivi e cerca la scheda Gestione risparmio energia nelle proprietà del dispositivo HID: quando l’opzione è disponibile, abilita “Consenti a questo dispositivo di riattivare il computer”.
Il Wake USB non è però una soluzione garantita per accendere un computer totalmente spento. Per lo stato S5, le alternative affidabili sono Wake-on-LAN oppure un relè/optoisolatore collegato ai pin del pulsante Power della scheda madre. Alcuni BIOS supportano “Power On by USB Keyboard”, ma non è una funzione universale: dipende dal PC, dall’alimentazione standby e dalle impostazioni firmware. Se ti interessa accendere il pc da remoto avevo scritto un articolo usando un relè sul tasto di accensione della scheda madre comandato da Alexa.
Pulsanti di emergenza e gestione USB
Durante i test di Wake USB può accadere che, dopo il resume, la tastiera HID non invii più correttamente nuovi tasti. Non è divertente svegliare il PC e scoprire di non poterlo più controllare. Per questo la versione finale contiene una sezione separata, visivamente diversa dalla tastiera:
la Zona di emergenza — gestione dispositivo. Include tre pulsanti:
- Reset USB (soft): tenta una disconnessione e riconnessione software dell’interfaccia USB tramite TinyUSB.
- Riavvia ESP32: esegue
ESP.restart(), riavvia completamente la scheda e forza una nuova inizializzazione del dispositivo USB dal lato PC. - Reset Wi‑Fi (nuova rete): cancella le credenziali memorizzate da WiFiManager e riporta la scheda alla modalità configurazione Wi‑Fi.
Il reset USB soft è il primo tentativo da fare. Se non ripristina tastiera e comandi, il riavvio completo dell’ESP32 è più efficace: interrompe l’applicazione, resetta le CPU e riavvia il firmware dal bootloader. Dopo alcuni secondi l’ESP32 torna sulla rete Wi‑Fi e il PC rileva di nuovo la tastiera HID.
Come flashare LOLIN S2 Mini con Arduino IDE
A mio avviso, la LOLIN S2 Mini è la scelta migliore per realizzare una tastiera USB HID ESP32 compatta.
Nel menu Arduino IDE il nome della scheda da selezionare è “LOLIN S2 Mini“, non “ESP32 S2 Mini” generico.
Prima di compilare, installa:
- Il core
esp32di Espressif dal Gestore schede di Arduino IDE. - La libreria
WiFiManagerdi tzapu da Sketch > Include Library > Manage Libraries.
Poi imposta Arduino IDE in questo modo:
| Impostazione | Valore consigliato |
|---|---|
| Scheda | LOLIN S2 Mini |
| USB CDC On Boot | Enabled |
| USB DFU On Boot | Disabled |
| USB Firmware MSC On Boot | Disabled |
| Erase All Flash Before Sketch Upload | Disabled |
| Partition Scheme | Default 4MB with SPIFFS |
| Porta | La porta COM che compare collegando la scheda |

Per entrare in modalità bootloader e caricare lo sketch:
- Tieni premuto BOOT.
- Contemporaneamente collega la LOLIN S2 Mini al computer con un cavo USB-C dati.
- Dopo uno o due secondi, rilascia BOOT.
- Seleziona la porta COM corretta in Arduino IDE.
- Premi il pulsante Carica.
- Al termine dell’upload, premi RST se la scheda non si riavvia automaticamente.
Durante la configurazione iniziale, l’ESP32 crea un access point senza password. Questa modalità dovrebbe essere utilizzata solo temporaneamente e in un ambiente controllato.
Codice della tastiera USB HID ESP32
Il firmware completo e aggiornato è disponibile anche nel mio repository di GitHub ProgettiConESP32.
Ecco lo sketch, che utilizza WiFiManager, WebServer e le classi USB HID del core Arduino-ESP32:
/*
https://www.lutritech.it/tastiera-usb-hid-esp32-wifi
Tastiera USB HID ESP32: controllare il PC via Wi‑Fi
https://www.youtube.com/@Lutritech
https://www.facebook.com/lutritech.it
https://www.instagram.com/Lutritech
https://github.com/Lutritech
============================================================================
ESP32-S3 DevKitC — Tastiera Virtuale USB HID (Layout ITALIANO)
VERSIONE 5 — Integrazione WiFiManager (connessione a reti Wi-Fi esistenti)
============================================================================
NOVITA' RISPETTO ALLA V4:
- Non crea più un Access Point fisso "ESP32-Keyboard" senza password.
- Al primo avvio (o dopo un reset delle credenziali) crea un AP di
CONFIGURAZIONE temporaneo chiamato "ESP32-Keyboard-Setup": collegati con
telefono/PC, si apre un portale (o vai su http://192.168.4.1), scegli la
tua rete Wi-Fi di casa dall'elenco e inserisci la password.
- Le credenziali vengono salvate in memoria flash (NVS): ai riavvii
successivi l'ESP32 si connette DA SOLO alla rete salvata, senza dover
ricompilare o riconfigurare nulla.
- Aggiunto mDNS: dopo la connessione, la pagina è raggiungibile anche via
http://tastiera.local
(oltre all'IP assegnato dal router, visibile nel Serial Monitor).
- Aggiunto un terzo pulsante nella "Zona di emergenza": "Reset Wi-Fi
(nuova rete)", che cancella le credenziali salvate e riavvia l'ESP32 in
modalità configurazione, utile se vuoi cambiare rete o hai sbagliato
password.
LIBRERIA DA INSTALLARE (obbligatoria):
- In Arduino IDE: Sketch -> Include Library -> Manage Libraries
- Cerca "WiFiManager" di TZAPU (tzapu/WiFiManager) e installala.
Compatibile con ESP32/ESP32-S3.
IMPOSTAZIONI ARDUINO IDE (obbligatorie, invariate):
- Board: "ESP32S3 Dev Module"
- USB Mode: "USB-OTG (TinyUSB)"
- USB CDC On Boot: "Enabled"
- Per il primo flash: tenere BOOT, collegare USB, rilasciare BOOT.
PRIMO UTILIZZO DOPO IL FLASH:
1. Accendi l'ESP32 (collegato via USB al PC).
2. Dal telefono, connettiti alla rete Wi-Fi "ESP32-Keyboard-Setup"
(nessuna password).
3. Si apre automaticamente un portale (o vai su http://192.168.4.1).
4. Scegli la tua rete Wi-Fi di casa, inserisci la password, salva.
5. L'ESP32 si riavvia e si connette da solo alla rete scelta.
6. Controlla il Serial Monitor per l'IP assegnato, oppure vai su
http://tastiera.local dal telefono connesso alla STESSA rete.
Tutte le funzionalità già presenti nella versione 4 restano disponibili: layout italiano, tastiera completa,
Sospendi/Spegni via SystemControl, Wake USB, Reset USB soft, Riavvio ESP32.
============================================================================
*/
#include <WiFi.h>
#include <WiFiManager.h> // libreria tzapu/WiFiManager
#include <ESPmDNS.h>
#include <WebServer.h>
#include "USB.h"
#include "USBHIDKeyboard.h"
#include "USBHIDConsumerControl.h"
#include "USBHIDSystemControl.h"
#include "tusb.h"
// ---------------------------------------------------------------------------
// CONFIGURAZIONE WIFI MANAGER
// ---------------------------------------------------------------------------
const char* AP_SETUP_SSID = "ESP32-Keyboard-Setup"; // AP di configurazione (senza password)
const char* MDNS_NAME = "tastiera"; // -> http://tastiera.local
WiFiManager wm;
WebServer server(80);
USBHIDKeyboard Keyboard;
USBHIDConsumerControl ConsumerControl;
USBHIDSystemControl SystemControl;
volatile bool usbSuspended = false;
// ---------------------------------------------------------------------------
// HID Usage IDs (pagina 0x07) e modificatori "raw" usati da pressRaw()
// ---------------------------------------------------------------------------
#define HID_MOD_LCTRL 0xE0
#define HID_MOD_LSHIFT 0xE1
#define HID_MOD_LALT 0xE2
#define HID_MOD_LGUI 0xE3
#define HID_MOD_RCTRL 0xE4
#define HID_MOD_RSHIFT 0xE5
#define HID_MOD_RALT 0xE6
#define HID_MOD_RGUI 0xE7
#define HID_KEY_MINUS 0x2D
#define HID_KEY_EQUAL 0x2E
#define HID_KEY_LBRACK 0x2F
#define HID_KEY_RBRACK 0x30
#define HID_KEY_NONUS1 0x32
#define HID_KEY_SEMI 0x33
#define HID_KEY_QUOTE 0x34
#define HID_KEY_COMMA 0x36
#define HID_KEY_PERIOD 0x37
#define HID_KEY_SLASH 0x38
#define HID_KEY_1 0x1E
#define HID_KEY_2 0x1F
#define HID_KEY_3 0x20
#define HID_KEY_4 0x21
#define HID_KEY_5 0x22
#define HID_KEY_6 0x23
#define HID_KEY_7 0x24
#define HID_KEY_8 0x25
#define HID_KEY_9 0x26
#define HID_KEY_0 0x27
struct ItKey {
uint32_t codepoint;
uint8_t usage;
bool shift;
bool altgr;
};
const ItKey IT_MAP[] = {
{'!', HID_KEY_1, true, false},
{'"', HID_KEY_2, true, false},
{0x00A3 /* £ */, HID_KEY_3, true, false},
{'$', HID_KEY_4, true, false},
{'%', HID_KEY_5, true, false},
{'&', HID_KEY_6, true, false},
{'/', HID_KEY_7, true, false},
{'(', HID_KEY_8, true, false},
{')', HID_KEY_9, true, false},
{'=', HID_KEY_0, true, false},
{'\'', HID_KEY_MINUS, false, false},
{'?', HID_KEY_MINUS, true, false},
{0x00EC /* ì */, HID_KEY_EQUAL, false, false},
{'^', HID_KEY_EQUAL, true, false},
{0x00E8 /* è */, HID_KEY_LBRACK, false, false},
{0x00E9 /* é */, HID_KEY_LBRACK, true, false},
{'[', HID_KEY_LBRACK, false, true},
{'{', HID_KEY_LBRACK, true, true},
{'+', HID_KEY_RBRACK, false, false},
{'*', HID_KEY_RBRACK, true, false},
{']', HID_KEY_RBRACK, false, true},
{'}', HID_KEY_RBRACK, true, true},
{0x00F9 /* ù */, HID_KEY_NONUS1, false, false},
{0x00A7 /* § */, HID_KEY_NONUS1, true, false},
{0x00F2 /* ò */, HID_KEY_SEMI, false, false},
{0x00E7 /* ç */, HID_KEY_SEMI, true, false},
{'@', HID_KEY_SEMI, false, true},
{0x00E0 /* à */, HID_KEY_QUOTE, false, false},
{0x00B0 /* ° */, HID_KEY_QUOTE, true, false},
{'#', HID_KEY_QUOTE, false, true},
{';', HID_KEY_COMMA, true, false},
{':', HID_KEY_PERIOD, true, false},
{'-', HID_KEY_SLASH, false, false},
{'_', HID_KEY_SLASH, true, false},
};
const int IT_MAP_LEN = sizeof(IT_MAP) / sizeof(IT_MAP[0]);
void pressUnicodeIT(uint32_t cp) {
for (int i = 0; i < IT_MAP_LEN; i++) {
if (IT_MAP[i].codepoint == cp) {
if (IT_MAP[i].shift) Keyboard.pressRaw(HID_MOD_LSHIFT);
if (IT_MAP[i].altgr) Keyboard.pressRaw(HID_MOD_RALT);
Keyboard.pressRaw(IT_MAP[i].usage);
delay(10);
Keyboard.releaseRaw(IT_MAP[i].usage);
if (IT_MAP[i].altgr) Keyboard.releaseRaw(HID_MOD_RALT);
if (IT_MAP[i].shift) Keyboard.releaseRaw(HID_MOD_LSHIFT);
return;
}
}
if (cp < 0x80) {
Keyboard.press((uint8_t)cp);
delay(10);
Keyboard.release((uint8_t)cp);
}
}
void sendUtf8TextIT(const String& text) {
size_t i = 0;
size_t len = text.length();
while (i < len) {
uint8_t b0 = (uint8_t)text[i];
uint32_t cp = 0;
int extra = 0;
if (b0 < 0x80) { cp = b0; extra = 0; }
else if ((b0 & 0xE0) == 0xC0) { cp = b0 & 0x1F; extra = 1; }
else if ((b0 & 0xF0) == 0xE0) { cp = b0 & 0x0F; extra = 2; }
else if ((b0 & 0xF8) == 0xF0) { cp = b0 & 0x07; extra = 3; }
else { i++; continue; }
for (int k = 0; k < extra && (i + 1 + k) < len; k++) {
uint8_t bx = (uint8_t)text[i + 1 + k];
cp = (cp << 6) | (bx & 0x3F);
}
if (cp == '\n') {
Keyboard.press(KEY_RETURN); delay(10); Keyboard.release(KEY_RETURN);
} else if (cp != '\r') {
pressUnicodeIT(cp);
}
i += 1 + extra;
}
}
// ---------------------------------------------------------------------------
// PAGINA WEB
// ---------------------------------------------------------------------------
const char INDEX_HTML[] PROGMEM = R"HTML(
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ESP32-S3 Tastiera Virtuale (IT)</title>
<style>
:root{
--bg:#1b1c1e; --panel:#242528; --key:#33353a; --key-hover:#42454b;
--key-active:#5b8cff; --text:#eaeaea; --muted:#9a9ca1; --danger:#e5484d;
--ok:#2fbf71; --warn:#b57b13; --warn-hover:#d99c1f;
}
*{box-sizing:border-box;}
body{margin:0; background:var(--bg); color:var(--text);
font-family:-apple-system,Segoe UI,Roboto,Arial,sans-serif; padding:16px;}
h1{font-size:18px; font-weight:600; margin:0 0 12px;}
h2{font-size:14px; font-weight:700; margin:0 0 8px; letter-spacing:.04em; text-transform:uppercase;}
.panel{background:var(--panel); border-radius:12px; padding:14px; margin-bottom:14px;}
textarea{width:100%; min-height:70px; border-radius:8px; border:1px solid #3a3c40;
background:#111214; color:var(--text); padding:10px; font-size:15px; resize:vertical;}
.row{display:flex; gap:8px; margin-top:10px; flex-wrap:wrap;}
button{border:none; border-radius:8px; background:var(--key); color:var(--text);
padding:10px 12px; font-size:13px; cursor:pointer; user-select:none; transition:background .1s;}
button:hover{background:var(--key-hover);}
button:active, button.active{background:var(--key-active); color:#fff;}
button.wide{flex:1;}
.send{background:var(--ok); color:#08210f; font-weight:600;}
.send:hover{background:#38d67f;}
.danger{background:var(--danger); color:#fff;}
.danger:hover{background:#ff5b5f;}
.krow{display:flex; gap:6px; flex-wrap:wrap; margin-top:6px;}
.krow button{flex:1 1 auto; min-width:38px;}
.k-wide{flex:2 1 auto !important;}
.k-space{flex:6 1 auto !important;}
.muted{color:var(--muted); font-size:12px; margin-top:6px;}
.status{font-size:12px; color:var(--muted); margin-bottom:10px;}
.emergency{
background: repeating-linear-gradient(135deg, #2b2205, #2b2205 10px, #241b04 10px, #241b04 20px);
border: 2px dashed var(--warn);
border-radius:12px;
padding:14px;
margin-top: 24px;
}
.emergency h2{ color: var(--warn); }
.warn-btn{ background: var(--warn); color:#1a1200; font-weight:700; }
.warn-btn:hover{ background: var(--warn-hover); }
</style>
</head>
<body>
<h1>ESP32-S3 — Tastiera Virtuale USB HID (layout Italiano)</h1>
<div class="status" id="status">Pronta. Imposta il PC su layout tastiera "Italiano".</div>
<div class="panel">
<textarea id="txt" placeholder="Scrivi qui (supporta à è é ì ò ù ç ° § £)..."></textarea>
<div class="row">
<button class="wide send" onclick="sendText()">OK — Invia testo</button>
<button onclick="document.getElementById('txt').value=''">Pulisci</button>
<button onclick="sendKey('ENTER')">Invio</button>
</div>
</div>
<div class="panel">
<div class="krow">
<button onclick="sendKey('ESC')">Esc</button>
<button onclick="sendKey('F1')">F1</button>
<button onclick="sendKey('F2')">F2</button>
<button onclick="sendKey('F3')">F3</button>
<button onclick="sendKey('F4')">F4</button>
<button onclick="sendKey('F5')">F5</button>
<button onclick="sendKey('F6')">F6</button>
<button onclick="sendKey('F7')">F7</button>
<button onclick="sendKey('F8')">F8</button>
<button onclick="sendKey('F9')">F9</button>
<button onclick="sendKey('F10')">F10</button>
<button onclick="sendKey('F11')">F11</button>
<button onclick="sendKey('F12')">F12</button>
</div>
<div class="krow">
<button onclick="sendChar('1')">1</button>
<button onclick="sendChar('2')">2</button>
<button onclick="sendChar('3')">3</button>
<button onclick="sendChar('4')">4</button>
<button onclick="sendChar('5')">5</button>
<button onclick="sendChar('6')">6</button>
<button onclick="sendChar('7')">7</button>
<button onclick="sendChar('8')">8</button>
<button onclick="sendChar('9')">9</button>
<button onclick="sendChar('0')">0</button>
<button onclick="sendChar('\'')">'</button>
<button onclick="sendChar('ì')">ì</button>
<button class="k-wide" onclick="sendKey('BACKSPACE')">← Backspace</button>
</div>
<div class="krow">
<button class="k-wide" onclick="sendKey('TAB')">Tab</button>
<button onclick="sendChar('q')">Q</button>
<button onclick="sendChar('w')">W</button>
<button onclick="sendChar('e')">E</button>
<button onclick="sendChar('r')">R</button>
<button onclick="sendChar('t')">T</button>
<button onclick="sendChar('y')">Y</button>
<button onclick="sendChar('u')">U</button>
<button onclick="sendChar('i')">I</button>
<button onclick="sendChar('o')">O</button>
<button onclick="sendChar('p')">P</button>
<button onclick="sendChar('è')">è</button>
<button onclick="sendChar('+')">+</button>
</div>
<div class="krow">
<button class="k-wide" onclick="sendKey('CAPSLOCK')">Caps Lock</button>
<button onclick="sendChar('a')">A</button>
<button onclick="sendChar('s')">S</button>
<button onclick="sendChar('d')">D</button>
<button onclick="sendChar('f')">F</button>
<button onclick="sendChar('g')">G</button>
<button onclick="sendChar('h')">H</button>
<button onclick="sendChar('j')">J</button>
<button onclick="sendChar('k')">K</button>
<button onclick="sendChar('l')">L</button>
<button onclick="sendChar('ò')">ò</button>
<button onclick="sendChar('à')">à</button>
<button class="k-wide" onclick="sendKey('ENTER')">Invio</button>
</div>
<div class="krow">
<button class="k-wide" onclick="toggleMod('SHIFT')" id="shiftBtn">Shift</button>
<button onclick="sendChar('z')">Z</button>
<button onclick="sendChar('x')">X</button>
<button onclick="sendChar('c')">C</button>
<button onclick="sendChar('v')">V</button>
<button onclick="sendChar('b')">B</button>
<button onclick="sendChar('n')">N</button>
<button onclick="sendChar('m')">M</button>
<button onclick="sendChar(',')">,</button>
<button onclick="sendChar('.')">.</button>
<button onclick="sendChar('-')">-</button>
<button class="k-wide" onclick="toggleMod('SHIFT')" id="shiftBtn2">Shift</button>
</div>
<div class="krow">
<button id="ctrlBtn" onclick="toggleMod('CTRL')">Ctrl</button>
<button id="winBtn" onclick="toggleMod('WIN')">Win</button>
<button id="altBtn" onclick="toggleMod('ALT')">Alt</button>
<button class="k-space" onclick="sendKey('SPACE')">Spazio</button>
<button id="altgrBtn" onclick="toggleMod('ALTGR')">AltGr</button>
<button onclick="sendKey('LEFT')">←</button>
<button onclick="sendKey('UP')">↑</button>
<button onclick="sendKey('DOWN')">↓</button>
<button onclick="sendKey('RIGHT')">→</button>
</div>
<div class="krow">
<button onclick="sendChar('ù')">ù</button>
<button onclick="sendChar('§')">§</button>
<button onclick="sendChar('ç')">ç</button>
<button onclick="sendChar('°')">°</button>
<button onclick="sendChar('£')">£</button>
<button onclick="sendChar('@')">@</button>
<button onclick="sendChar('#')">#</button>
<button onclick="sendChar('[')">[</button>
<button onclick="sendChar(']')">]</button>
</div>
<div class="krow">
<button onclick="sendKey('INSERT')">Ins</button>
<button onclick="sendKey('HOME')">Home</button>
<button onclick="sendKey('PAGEUP')">PgUp</button>
<button onclick="sendKey('DELETE')">Del</button>
<button onclick="sendKey('END')">End</button>
<button onclick="sendKey('PAGEDOWN')">PgDn</button>
<button onclick="sendKey('PRINTSCREEN')">PrtSc</button>
</div>
<div class="muted">
Ctrl/Alt/Shift/Win: attivali (diventano blu), poi premi il tasto da
combinare. AltGr serve per @ # [ ] {} ecc. (già gestiti dai tasti dedicati
qui sopra, che funzionano anche senza attivare AltGr manualmente).
</div>
</div>
<div class="panel">
<div class="row">
<button class="wide danger" onclick="sendCombo(['CTRL','ALT'],'DELETE')">Ctrl+Alt+Canc</button>
<button class="wide" onclick="sendCombo(['ALT'],'TAB')">Alt+Tab</button>
<button class="wide" onclick="sendCombo(['ALT'],'F4')">Alt+F4</button>
<button class="wide" onclick="sendCombo(['WIN'],'D')">Win+D (Desktop)</button>
<button class="wide" onclick="sendCombo(['WIN'],'L')">Win+L (Blocca)</button>
</div>
</div>
<div class="panel">
<div class="row">
<button class="wide danger" onclick="powerAction('sleep')">Sospendi PC</button>
<button class="wide danger" onclick="powerAction('power')">Spegni PC</button>
<button class="wide danger" onclick="powerAction('wake')">Segnale di Wake (USB)</button>
</div>
<div class="muted">
"Sospendi" e "Spegni" funzionano da PC acceso. "Wake" funziona SOLO se il
PC è già in stand-by. Dopo aver usato "Wake", se la tastiera smette di
rispondere, usa i pulsanti ambra qui sotto per sbloccarla.
</div>
</div>
<div class="emergency">
<h2>Zona di emergenza — gestione dispositivo</h2>
<div class="muted" style="margin-top:0; margin-bottom:10px;">
Questi pulsanti NON fanno parte della tastiera: servono a sbloccare la
connessione USB o a riconfigurare la rete Wi-Fi.
</div>
<div class="row">
<button class="wide warn-btn" onclick="usbReset()">⚙ Reset USB (soft)</button>
<button class="wide warn-btn" onclick="restartEsp()">⚠ Riavvia ESP32</button>
</div>
<div class="row">
<button class="wide warn-btn" onclick="wifiReset()">📶 Reset Wi-Fi (nuova rete)</button>
</div>
</div>
<script>
let activeMods = new Set();
function updateModButtons(){
document.querySelectorAll('#ctrlBtn').forEach(b=>b.classList.toggle('active', activeMods.has('CTRL')));
document.querySelectorAll('#altBtn').forEach(b=>b.classList.toggle('active', activeMods.has('ALT')));
document.querySelectorAll('#shiftBtn,#shiftBtn2').forEach(b=>b.classList.toggle('active', activeMods.has('SHIFT')));
document.querySelectorAll('#winBtn').forEach(b=>b.classList.toggle('active', activeMods.has('WIN')));
document.querySelectorAll('#altgrBtn').forEach(b=>b.classList.toggle('active', activeMods.has('ALTGR')));
}
function toggleMod(m){
if(activeMods.has(m)) activeMods.delete(m); else activeMods.add(m);
updateModButtons();
}
function setStatus(msg){ document.getElementById('status').textContent = msg; }
async function post(path, params){
try{
const res = await fetch(path, {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body: new URLSearchParams(params).toString()
});
const txt = await res.text();
setStatus(res.ok ? ('OK: ' + txt) : ('Errore: ' + txt));
}catch(e){ setStatus('Errore di connessione: ' + e); }
}
function sendText(){
const t = document.getElementById('txt').value;
if(!t) return;
post('/api/text', {text:t});
}
function sendChar(ch){ post('/api/text', {text:ch}); }
function sendKey(key){
const mods = Array.from(activeMods).join(',');
post('/api/key', {mods:mods, key:key});
activeMods.clear();
updateModButtons();
}
function sendCombo(mods, key){ post('/api/key', {mods:mods.join(','), key:key}); }
function powerAction(action){ post('/api/power', {action:action}); }
function usbReset(){
if(!confirm("Tentare un reset software della connessione USB?")) return;
post('/api/usbreset', {});
}
function restartEsp(){
if(!confirm("Riavviare completamente l'ESP32? La pagina si disconnetterà per alcuni secondi.")) return;
post('/api/restart', {});
setStatus("Riavvio in corso... attendi 5-10 secondi poi ricarica la pagina.");
}
function wifiReset(){
if(!confirm("Cancellare le credenziali Wi-Fi salvate e tornare alla modalità di configurazione? Dovrai ricollegarti alla rete 'ESP32-Keyboard-Setup' per scegliere una nuova rete.")) return;
post('/api/wifireset', {});
setStatus("Reset Wi-Fi in corso... l'ESP32 si riavvierà in modalità configurazione.");
}
</script>
</body>
</html>
)HTML";
// ---------------------------------------------------------------------------
// UTILITÀ modificatori / tasti nominati
// ---------------------------------------------------------------------------
void applyModifiers(const String& modsCsv, bool press) {
int start = 0;
while (start < (int)modsCsv.length()) {
int comma = modsCsv.indexOf(',', start);
String m = (comma == -1) ? modsCsv.substring(start) : modsCsv.substring(start, comma);
m.trim();
if (m.length() > 0) {
uint8_t code = 0;
if (m == "CTRL") code = HID_MOD_LCTRL;
else if (m == "ALT") code = HID_MOD_LALT;
else if (m == "SHIFT") code = HID_MOD_LSHIFT;
else if (m == "WIN") code = HID_MOD_LGUI;
else if (m == "ALTGR") code = HID_MOD_RALT;
if (code) {
if (press) Keyboard.pressRaw(code);
else Keyboard.releaseRaw(code);
}
}
if (comma == -1) break;
start = comma + 1;
}
}
bool sendNamedKey(const String& key) {
uint8_t code = 0;
if (key == "ENTER") code = KEY_RETURN;
else if (key == "ESC") code = KEY_ESC;
else if (key == "TAB") code = KEY_TAB;
else if (key == "BACKSPACE") code = KEY_BACKSPACE;
else if (key == "DELETE") code = KEY_DELETE;
else if (key == "INSERT") code = KEY_INSERT;
else if (key == "HOME") code = KEY_HOME;
else if (key == "END") code = KEY_END;
else if (key == "PAGEUP") code = KEY_PAGE_UP;
else if (key == "PAGEDOWN") code = KEY_PAGE_DOWN;
else if (key == "LEFT") code = KEY_LEFT_ARROW;
else if (key == "RIGHT") code = KEY_RIGHT_ARROW;
else if (key == "UP") code = KEY_UP_ARROW;
else if (key == "DOWN") code = KEY_DOWN_ARROW;
else if (key == "SPACE") { Keyboard.press(' '); delay(15); Keyboard.release(' '); return true; }
else if (key == "CAPSLOCK") code = KEY_CAPS_LOCK;
else if (key == "PRINTSCREEN") code = KEY_PRINT_SCREEN;
else if (key == "F1") code = KEY_F1;
else if (key == "F2") code = KEY_F2;
else if (key == "F3") code = KEY_F3;
else if (key == "F4") code = KEY_F4;
else if (key == "F5") code = KEY_F5;
else if (key == "F6") code = KEY_F6;
else if (key == "F7") code = KEY_F7;
else if (key == "F8") code = KEY_F8;
else if (key == "F9") code = KEY_F9;
else if (key == "F10") code = KEY_F10;
else if (key == "F11") code = KEY_F11;
else if (key == "F12") code = KEY_F12;
else if (key.length() == 1) { sendUtf8TextIT(key); return true; }
else return false;
Keyboard.press(code);
delay(15);
Keyboard.release(code);
return true;
}
// ---------------------------------------------------------------------------
// HANDLERS principali
// ---------------------------------------------------------------------------
void handleRoot() { server.send_P(200, "text/html", INDEX_HTML); }
void handleText() {
if (!server.hasArg("text")) { server.send(400, "text/plain", "missing text"); return; }
sendUtf8TextIT(server.arg("text"));
server.send(200, "text/plain", "ok");
}
void handleKey() {
String mods = server.hasArg("mods") ? server.arg("mods") : "";
String key = server.hasArg("key") ? server.arg("key") : "";
if (key.length() == 0) { server.send(400, "text/plain", "missing key"); return; }
applyModifiers(mods, true);
delay(10);
bool ok = sendNamedKey(key);
delay(10);
applyModifiers(mods, false);
Keyboard.releaseAll();
server.send(ok ? 200 : 400, "text/plain", ok ? "ok" : "unknown key");
}
void handlePower() {
String action = server.hasArg("action") ? server.arg("action") : "";
if (action == "sleep") {
SystemControl.press(SYSTEM_CONTROL_STANDBY);
delay(100);
SystemControl.release();
server.send(200, "text/plain", "ok - sospensione richiesta");
} else if (action == "power") {
SystemControl.press(SYSTEM_CONTROL_POWER_OFF);
delay(100);
SystemControl.release();
server.send(200, "text/plain", "ok - spegnimento richiesto");
} else if (action == "wake") {
if (usbSuspended) {
tud_remote_wakeup();
delay(50);
server.send(200, "text/plain", "ok - segnale di wakeup inviato");
} else {
SystemControl.press(SYSTEM_CONTROL_WAKE_HOST);
delay(50);
SystemControl.release();
server.send(200, "text/plain", "il dispositivo non risulta sospeso: nessuna azione necessaria");
}
} else {
server.send(400, "text/plain", "unknown action");
}
}
// ---------------------------------------------------------------------------
// HANDLERS di emergenza / gestione dispositivo
// ---------------------------------------------------------------------------
void handleUsbReset() {
server.send(200, "text/plain", "tentativo di reset USB in corso");
delay(100);
tud_disconnect();
delay(500);
tud_connect();
}
void handleRestartEsp() {
server.send(200, "text/plain", "riavvio in corso, attendi qualche secondo");
delay(300);
ESP.restart();
}
void handleWifiReset() {
server.send(200, "text/plain", "credenziali Wi-Fi cancellate, riavvio in modalita' configurazione");
delay(300);
wm.resetSettings();
delay(300);
ESP.restart();
}
void handleNotFound() { server.send(404, "text/plain", "Not found"); }
// ---------------------------------------------------------------------------
// Eventi USB
// ---------------------------------------------------------------------------
void usbEventCallback(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
if (event_base == ARDUINO_USB_EVENTS) {
if (event_id == ARDUINO_USB_SUSPEND_EVENT) {
usbSuspended = true;
Serial.println("[USB] Sospeso.");
} else if (event_id == ARDUINO_USB_RESUME_EVENT) {
usbSuspended = false;
Serial.println("[USB] Ripreso.");
}
}
}
// ---------------------------------------------------------------------------
// SETUP / LOOP
// ---------------------------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(300);
// --- USB HID: inizializzato PRIMA del Wi-Fi, indipendente da esso ---
USB.onEvent(usbEventCallback);
USB.usbAttributes(TUSB_DESC_CONFIG_ATT_SELF_POWERED | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP);
Keyboard.begin();
ConsumerControl.begin();
SystemControl.begin();
USB.begin();
// --- Wi-Fi Manager: connette a rete salvata, o apre il portale di setup ---
Serial.println();
Serial.println("=== ESP32-S3 Tastiera Virtuale HID (layout IT) - v5 ===");
Serial.println("Avvio WiFiManager...");
// Timeout del portale di configurazione: se nessuno lo configura entro
// 180 secondi, l'ESP32 riprova con le credenziali salvate (se esistono)
// o resta in modalita' configurazione.
wm.setConfigPortalTimeout(180);
bool connected = wm.autoConnect(AP_SETUP_SSID); // AP di setup senza password
if (!connected) {
Serial.println("Connessione Wi-Fi non riuscita e portale scaduto. Riavvio...");
delay(3000);
ESP.restart();
}
Serial.println("Connesso alla rete Wi-Fi!");
Serial.print("Indirizzo IP: "); Serial.println(WiFi.localIP());
if (MDNS.begin(MDNS_NAME)) {
Serial.print("mDNS avviato: http://");
Serial.print(MDNS_NAME);
Serial.println(".local");
} else {
Serial.println("Avvio mDNS non riuscito (non bloccante, si può usare l'IP).");
}
// --- Web server ---
server.on("/", HTTP_GET, handleRoot);
server.on("/api/text", HTTP_POST, handleText);
server.on("/api/key", HTTP_POST, handleKey);
server.on("/api/power", HTTP_POST, handlePower);
server.on("/api/usbreset", HTTP_POST, handleUsbReset);
server.on("/api/restart", HTTP_POST, handleRestartEsp);
server.on("/api/wifireset", HTTP_POST, handleWifiReset);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("Server web avviato.");
}
void loop() {
server.handleClient();
}
Lo sketch contiene la pagina HTML completa, la gestione del layout italiano, i tasti funzione, le combinazioni, le API HTTP per l’invio dei tasti, la gestione del remote wakeup e i pulsanti di ripristino. Il principio resta comunque questo: Wi‑Fi per ricevere il comando, USB HID per inviarlo al PC.
La pagina web dell’ESP32 utilizza HTTP e non HTTPS. Il progetto è quindi pensato per essere utilizzato all’interno di una rete Wi-Fi considerata affidabile. Non dovrebbe essere esposto direttamente a Internet. Per realizzare questo sketch ho utilizzato Claude SONNET 5 e tanta pazienza.
Usi legali della tastiera USB HID ESP32
Una tastiera USB HID ESP32 è utile in molti contesti perfettamente legittimi, purché venga usata solo su dispositivi propri o per i quali hai ricevuto autorizzazione esplicita:
- Gestione di home-lab, server domestici, mini-PC e postazioni senza tastiera fissa.
- Comandi rapidi su un media center o un computer collegato alla TV.
- Automazioni personali con scorciatoie da tastiera.
- Progetti di elettronica e formazione su USB, Wi‑Fi e microcontrollori.
- Test di accessibilità con tastiere touch personalizzate.
- Kiosk, demo e postazioni tecniche controllate.
BadUSB e keylogging: conoscere il rischio per difendersi
Un dispositivo capace di emulare una tastiera USB è anche un promemoria utile: una porta USB non è solo un posto dove ricaricare il telefono. La stessa tecnologia HID può essere usata in attacchi chiamati BadUSB, nei quali una periferica apparentemente innocua viene collegata a un PC e invia comandi senza consenso.
Il sistema operativo tende a fidarsi di una tastiera perché una tastiera, per definizione, deve poter digitare. Questo è il motivo per cui in ambienti aziendali, sanitari e con dati sensibili è importante limitare le periferiche USB non autorizzate, applicare policy di endpoint security e non collegare dispositivi sconosciuti “solo per vedere cosa fanno”. Spoiler: di solito non fanno niente di buono.
Il keylogging è un argomento distinto ma collegato alla sicurezza delle periferiche: registrare ciò che un utente digita o monitorare un PC senza autorizzazione viola privacy e norme penali. Studiare queste tecniche è utile per difendersi e per progettare ambienti più sicuri, non per usarle su computer altrui.
Disclaimer
Questo progetto e il codice associato sono destinati esclusivamente a uso personale, didattico, di laboratorio e su dispositivi di propria proprietà o per i quali si dispone di autorizzazione esplicita.
Non utilizzare questa soluzione per accedere a computer di terzi, eseguire comandi non autorizzati, intercettare dati, installare software, aggirare misure di sicurezza o compromettere la riservatezza di altre persone. L’uso improprio di dispositivi HID può violare norme su accesso abusivo a sistemi informatici, privacy e trattamento dei dati.
Lutritech e l’autore non si assumono responsabilità per usi illegali o non autorizzati delle informazioni e del firmware descritti nell’articolo.
FAQ sulla tastiera USB HID ESP32
Quale ESP32 può funzionare come tastiera USB?
Per questo progetto puoi usare ESP32-S2 o ESP32-S3, perché dispongono di USB OTG nativa e possono funzionare come dispositivi USB HID. L’ESP32-C3 non è adatto: la sua USB integrata è Serial/JTAG e non può essere programmata come tastiera USB HID.
La tastiera USB HID ESP32 funziona con Windows 11?
Sì. Windows 11 riconosce ESP32-S2 ed ESP32-S3 come tastiere HID standard senza richiedere driver aggiuntivi. Nel firmware devi però abilitare la modalità USB OTG/TinyUSB e usare la porta USB nativa della scheda.
Posso usare LOLIN S2 Mini per questo progetto?
Sì. La LOLIN S2 Mini monta un ESP32-S2 con Wi‑Fi e USB OTG nativa, quindi è compatibile con la tastiera USB HID ESP32. In Arduino IDE seleziona la board LOLIN S2 Mini.
Posso cambiare rete Wi‑Fi senza ricompilare?
Sì. WiFiManager salva SSID e password nella memoria dell’ESP32. Se vuoi cambiare rete usa il pulsante Reset Wi‑Fi, poi riconnettiti alla rete di setup e scegli la nuova rete dal captive portal.
Il Wake USB può accendere un PC spento?
Il Wake USB è pensato per risvegliare un PC in sospensione. L’accensione da computer completamente spento dipende da BIOS/UEFI, alimentazione della porta USB e supporto specifico della scheda madre. Per un risultato affidabile da PC spento usa Wake-on-LAN o un relè/optoisolatore sui pin del pulsante Power.
Perché dopo Wake USB la tastiera può smettere di funzionare?
Dopo un ciclo di sospensione e resume, l’interfaccia HID può occasionalmente restare in uno stato non ottimale. Per questo il progetto include Reset USB soft e Riavvia ESP32, che permettono di ripristinare la connessione senza scollegare fisicamente il dispositivo.
Conclusioni
Realizzare una tastiera USB HID ESP32 controllabile via Wi‑Fi è un progetto semplice nelle dimensioni ma molto versatile. Con una LOLIN S2 Mini o un ESP32-S3, WiFiManager e una pagina web ottieni una periferica USB intelligente: non richiede programmi sul PC, si controlla dal browser e può inviare testo, tasti funzione, shortcut e comandi di alimentazione.
La versione finale con WiFiManager rende il progetto davvero utilizzabile tutti i giorni: configuri il Wi‑Fi una volta, poi l’ESP32 torna automaticamente nella tua rete. È utile per home-lab, mini-PC, automazioni locali, test USB e postazioni remote. E, soprattutto, rende possibile premere Ctrl+Alt+Canc senza iniziare una caccia al tesoro sotto la scrivania. Sì, anche questo si chiama progresso tecnologico 😄.