Aquí voy a documentar mis investigaciones y conclusiones con la ESPRESSIF ESP32-S3-WROOM-1 que según el vendedor es:
ESP32-S3-N16R8 Dev Kit con 16MB Flash y 8MB PSRAM
Estoy especificando que esta versión no tiene cámara porque previamente he hablado sobre otra placa ESP32-S3 N16R8 Cam
La ESP32-S3 WROOM que tengo es la siguiente:

Yo lo he probado y lo que el vendedor anuncia es totalmente correcto:
- Modelo chip: ESP32-S3
- Revision chip: 0
- Cores: 2
- Frecuencia CPU: 240 MHz
- Tamaño flash: 16777216 bytes
- Velocidad flash: 80000000 Hz
- Total heap: 395220 bytes
- Heap libre: 370220 bytes
- Tamaño PSRAM: 8386279 bytes
- PSRAM disponible: 8386035 bytes
Así que veamos algunas cosas con esta tarjeta de desarrollo que tiene una buena cantidad de PSRAM además de todas las otras características.
También dejaré mi conclusión de la reseña al final.
Dos puertos USB
La ESP32-S3 que tengo tiene 2 puertos USB tipo C.
Uno de ellos es el USB-JTAG/Serial (UART virtual) y otro de ellos es el USB-OTG.
En mis pruebas ambos sirven para cargar el código a la tarjeta, pero solo uno de ellos sirve para usar la tarjeta como HID o dispositivo de entrada, así que sí, se puede usar esta placa para simular un teclado o mouse (justamente quiero hacer un teclado TCP)
Solo como referencia: mirando la tarjeta de frente (así como en la foto que acompaña al post), el puerto de la derecha es el puerto JTAG UART y el de la izquierda es el USB-OTG que puede ser usado como HID.
También lo podemos comprobar porque cuando está conectado en el puerto HID y reseteamos la tarjeta con el botón se va a escuchar como cuando desconectamos un dispositivo.
Controladores ESP32 S3
Suelo usar Visual Studio Code con PlatformIO. No necesité instalar otros controladores, aunque previamente he programado la ESP8266 en esta PC con su driver CH340 así que no sé si al ya tener ese driver no es necesario instalar otro.
Si tú tienes problemas instala el CH340.
Creando proyecto
Una vez que esté instalada la extensión de PlatformIO entonces crea un nuevo proyecto. Elige:
- Name: el que tú quieras
- Board: Freenove ESP32-S3 WROOM N8R8 (8MB Flash / 8MB PSRAM)
- Framework: Arduino
Sí, hemos elegido Freenove N8R8 aunque nuestra tarjeta en realidad es una N16R8 pero con esto funcionará bien, no te preocupes, es la placa que más se acerca y que además configura bien los 8MB de PSRAM.
Solo como referencia, mi platformio.ini queda como se ve a continuación. Si quieres puedes ignorar la dependencia de Adafruit porque eso no es estrictamente necesario para programar la placa, solo lo usamos para probar el RGB integrado.
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:freenove_esp32_s3_wroom]
platform = espressif32
board = freenove_esp32_s3_wroom
framework = arduino
lib_deps =
adafruit/Adafruit NeoPixel
monitor_speed = 115200
Probando características
Pasemos a probar algunas cosas cosas:
- El LED RGB que tiene la placa
- Las características de la placa
- Monitor Serial
En el setup voy a imprimir las características
de la ESP32-S3 que tenemos, incluyendo Flash, PSRAM,
RAM, etcétera.
También voy a intentar tomar memoria de la PSRAM para estar totalmente seguros de su capacidad.
Finalmente en dicho setup inicializo el LED RGB, y luego, en
el loop el mismo LED mostrará varios colores de manera simplificada.
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#define PIN_RGB 48
#define NUM_RGB 1
Adafruit_NeoPixel ledRgb(NUM_RGB, PIN_RGB, NEO_GRB + NEO_KHZ800);
void setup()
{
Serial.begin(115200);
Serial.println("Hola mundo");
ledRgb.begin();
ledRgb.setBrightness(255);
Serial.printf("Modelo chip: %s\n", ESP.getChipModel());
Serial.printf("Revision chip: %d\n", ESP.getChipRevision());
Serial.printf("Cores: %d\n", ESP.getChipCores());
Serial.printf("Frecuencia CPU: %d MHz\n", ESP.getCpuFreqMHz());
Serial.printf("Tamaño flash: %u bytes\n", ESP.getFlashChipSize());
Serial.printf("Velocidad flash: %u Hz\n", ESP.getFlashChipSpeed());
Serial.printf("Total heap: %u bytes\n", heap_caps_get_total_size(MALLOC_CAP_INTERNAL));
Serial.printf("Heap libre: %u bytes\n", ESP.getFreeHeap());
Serial.printf("Tamaño PSRAM: %u bytes\n", ESP.getPsramSize());
Serial.printf("PSRAM disponible: %u bytes\n", ESP.getFreePsram());
if (psramFound())
{
Serial.println("Existe PSRAM");
size_t cantidadBytesBuferPsram = 1024 * 1024;
uint8_t *ptr = (uint8_t *)ps_malloc(cantidadBytesBuferPsram);
if (ptr)
{
Serial.printf("Asignados %u bytes en psram\n", cantidadBytesBuferPsram);
memset(ptr, 0xA5, cantidadBytesBuferPsram);
Serial.printf("Primeros bytes: %02X %02X %02X %02X...\n", ptr[0], ptr[1], ptr[2], ptr[3]);
free(ptr);
Serial.println("free psram ok");
}
else
{
Serial.println("no hay psram disponible, pero sí fue detectada");
}
}
else
{
Serial.println("PSRAM no detectada");
}
}
void loop()
{
static uint16_t hue = 0;
ledRgb.setPixelColor(0, ledRgb.gamma32(ledRgb.ColorHSV(hue * 256)));
ledRgb.show();
hue++;
if (hue >= 360)
{
hue = 0;
}
delay(5);
}
Conectando WebSocket
Comencemos a probar conectándonos a la red WiFi, iniciando un servidor, eligiendo un nombre local y conectándonos a un WebSocket.
Primero veamos credenciales.h que contiene el nombre y contraseña de la red:
#define NOMBRE_RED_WIFI "tu red"
#define PASSWORD_RED_WIFI "tu contraseña"
Y ahora el código:
#include <WiFi.h>
#include "Arduino.h"
#include "USB.h"
#include "USBHIDMouse.h"
#include "USBHIDKeyboard.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESPmDNS.h>
#include "credenciales.h"
#define NOMBRE_HOST "esp32_2"
AsyncWebServer servidorHttp(80);
AsyncWebSocket ws("/ws");
USBHIDMouse raton;
USBHIDKeyboard teclado;
USBHID HID;
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
if (type == WS_EVT_CONNECT)
{
Serial.println("Cliente conectado");
client->text("Hola cliente");
}
else if (type == WS_EVT_DATA)
{
String msg = String((char *)data).substring(0, len);
Serial.printf("Mensaje: %s\n", msg.c_str());
client->text("Recibido: " + msg);
}
}
void setup()
{
// HID.begin();
// USB.begin();
Serial.begin(115200);
Serial.println("Setup");
servidorHttp.on("/ping", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "pong"); });
WiFi.begin(NOMBRE_RED_WIFI, PASSWORD_RED_WIFI);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
}
/*
A mí me daba problemas porque la ESP estaba en la 2.4GHZ y mi PC
estaba en la 5Ghz. Solucioné conectando ambos a la 2.4
*/
if (!MDNS.begin(NOMBRE_HOST))
{
Serial.println("Error al iniciar mDNS");
}
else
{
Serial.print("mDNS iniciado. Acceder via: http://");
Serial.print(NOMBRE_HOST);
Serial.println(".local");
MDNS.addService("http", "tcp", 80);
}
Serial.println(WiFi.localIP());
ws.onEvent(onWsEvent);
servidorHttp.addHandler(&ws);
servidorHttp.begin();
}
void loop()
{
Serial.println("Loop");
delay(5000);
/*
raton.move(0, -5);
delay(2000);
teclado.write('A');
*/
}
Utilizo mDNS para no tener que usar la IP de la ESP32 al conectarme al WebSocket, ya que la misma puede cambiar.
Una vez que hayas subido el código debería imprimirse:
Setup
mDNS iniciado. Acceder via: http://esp32_2.local
192.168.0.7
Loop
Hagamos una comprobación rápida: en tu navegador abre http://esp32_2.local/ping para probar si el mDNS hizo su trabajo.
Te debería resolver el host y mostrarte “pong”. En caso de que no, tendrás que acceder a través de la IP, prueba accediendo a http://192.168.0.7/ping (recuerda que esa es mi IP, a ti te pudo imprimir otra)
Ya sea que uses la IP o el nombre del host local vamos a conectarnos
al WebSocket que está expuesto en la raíz /ws. Yo tengo instalado
XAMPP así que abrí http://localhost y ejecuté lo siguiente en la consola de depuración.
const ws = new WebSocket("ws://esp32_2.local/ws");
ws.onopen = () => {
console.log("Conectado");
ws.send("Hola esp32");
};
ws.onmessage = (e) => {
console.log("ESP32 dice:", e.data);
};
No puedes usar HTTPS porque te va a decir mixed-content. Me parece que con file:/// también se puede. Con esto comprobamos que:
- La ESP32-S3 WROOM N16R8 se conecta a la red WiFi
- El servidor web en el puerto 80 funciona bien
- El WebSocket funciona bien
- mDNS ha propagado el nombre de la tarjeta
Cambiando LED RGB
Con estas pruebas ya podemos enviar y recibir cualquier cantidad de datos, pero ahora quiero probar algo que he visto que otras personas hacen: controlar el RGB con un seleccionador de color y ver qué tan rápido se cambia.
Recuerda que estamos usando WebSockets y que estos son muy rápidos. No tanto como usar UDP porque los WebSocket usan TCP, pero yo prefiero TCP con WebSockets para lo que planeo hacer.
Vemos el index.html que solo tiene el input de tipo color
y un párrafo de estado:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Controlar RGB</title>
</head>
<body>
<input type="color" id="inputColor">
<p id="estado"></p>
<script src="script.js"></script>
</body>
</html>
El script se conecta al WebSocket y envía el color en el evento
input (no change, ya que ese se envía solo al terminar de elegir un color y
ocultar el input)
document.addEventListener("DOMContentLoaded", () => {
const $inputColor = document.querySelector("#inputColor");
const $estado = document.querySelector("#estado");
$estado.textContent = "Conectando...";
const ws = new WebSocket("ws://esp32_2.local/ws");
ws.onopen = () => {
$estado.textContent = "WS conectado";
};
ws.onmessage = (e) => {
console.log("ESP32 dice:", e.data);
};
$inputColor.oninput = () => {
if (ws.OPEN) {
ws.send($inputColor.value);
}
console.log($inputColor.value);
}
});
Ahora ya solo falta cambiar el código de la ESP32-S3 para recibir el color como una cadena hexadecimal, parsearlo para que la librería RGB lo entienda y poner ese color.
Recordemos que necesitamos:
#include <Adafruit_NeoPixel.h>
#define PIN_RGB 48
#define NUM_RGB 1
Adafruit_NeoPixel ledRgb(NUM_RGB, PIN_RGB, NEO_GRB + NEO_KHZ800);
Y luego en el setup poner:
ledRgb.begin();
ledRgb.setBrightness(255);
La parte importante será:
else if (type == WS_EVT_DATA)
{
String msg = String((char *)data).substring(0, len);
ledRgb.setPixelColorFromHex(msg);
ledRgb.show();
}
No existe setPixelColorFromHex (o no he visto XD)
pero así sería el código. Al final terminé así:
Primero remuevo el # del color y lo envío:
$inputColor.oninput = () => {
const color = $inputColor.value.substring(1);
ws.send(color);
}
Y luego queda así la recepción:
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
if (type == WS_EVT_CONNECT)
{
Serial.println("Cliente conectado");
client->text("Hola cliente");
}
else if (type == WS_EVT_DATA)
{
String msg = String((char *)data).substring(0, len);
uint32_t color = strtoul(msg.c_str(), NULL, 16);
ledRgb.setPixelColor(0, color);
ledRgb.show();
}
}
El código completo de C++ queda así:
#include <WiFi.h>
#include "Arduino.h"
#include "USB.h"
#include "USBHIDMouse.h"
#include "USBHIDKeyboard.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Adafruit_NeoPixel.h>
#include <ESPmDNS.h>
#include "credenciales.h"
#define NOMBRE_HOST "esp32_2"
#define PIN_RGB 48
#define NUM_RGB 1
Adafruit_NeoPixel ledRgb(NUM_RGB, PIN_RGB, NEO_GRB + NEO_KHZ800);
AsyncWebServer servidorHttp(80);
AsyncWebSocket ws("/ws");
USBHIDMouse raton;
USBHIDKeyboard teclado;
USBHID HID;
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
AwsEventType type, void *arg, uint8_t *data, size_t len)
{
if (type == WS_EVT_CONNECT)
{
Serial.println("Cliente conectado");
client->text("Hola cliente");
}
else if (type == WS_EVT_DATA)
{
String msg = String((char *)data).substring(0, len);
uint32_t color = strtoul(msg.c_str(), NULL, 16);
ledRgb.setPixelColor(0, color);
ledRgb.show();
}
}
void setup()
{
ledRgb.begin();
ledRgb.setBrightness(255);
ledRgb.setPixelColor(0, 0, 0, 0);
ledRgb.show();
// ledRgb.setPixelColor(0, 0);
// HID.begin();
// USB.begin();
Serial.begin(115200);
Serial.println("Setup");
servidorHttp.on("/ping", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "pong"); });
WiFi.begin(NOMBRE_RED_WIFI, PASSWORD_RED_WIFI);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
}
/*
A mí me daba problemas porque la ESP estaba en la 2.4GHZ y mi PC
estaba en la 5Ghz. Solucioné conectando ambos a la 2.4
*/
if (!MDNS.begin(NOMBRE_HOST))
{
Serial.println("Error al iniciar mDNS");
}
else
{
Serial.print("mDNS iniciado. Acceder via: http://");
Serial.print(NOMBRE_HOST);
Serial.println(".local");
MDNS.addService("http", "tcp", 80);
}
Serial.println(WiFi.localIP());
ws.onEvent(onWsEvent);
servidorHttp.addHandler(&ws);
servidorHttp.begin();
}
void loop()
{
Serial.println("Loop");
delay(5000);
/*
raton.move(0, -5);
delay(2000);
teclado.write('A');
*/
}
Si te das cuenta, ya desde hace un momento estoy planeando usar esta tarjeta como Mouse y Teclado, por eso no dejé de incluir las librerías y comenté lo del movimiento.
El código de script.js queda así:
document.addEventListener("DOMContentLoaded", () => {
const $inputColor = document.querySelector("#inputColor");
const $estado = document.querySelector("#estado");
$estado.textContent = "Conectando...";
const ws = new WebSocket("ws://esp32_2.local/ws");
ws.onopen = () => {
$estado.textContent = "WS conectado";
};
ws.onmessage = (e) => {
console.log("ESP32 dice:", e.data);
};
$inputColor.oninput = () => {
const color = $inputColor.value.substring(1);
ws.send(color);
}
});
Mouse y teclado
Lo he conectado a OTG ya que aquí me sirve para usarlo como HID y también permite cargarle el código.
Voy a dejar este apartado pendiente pero sí: la ESP32-S3-WROOM puede convertirse en un ratón y teclado al mismo tiempo. Yo he creado uno que se controla en la web usando TCP y lo publicaré más adelante.
Joystick
En Windows es presionar Win + R, escribir joy.cpl
y presionar Enter. Ahí debe aparecer el Joystick:

Yo tengo un código muy simple. Primero necesitamos añadir la librería de https://github.com/schnoog/Joystick_ESP32S2 en el platformio.ini:
lib_deps =
schnoog/Joystick_ESP32S2
La incluimos. Para esta prueba he conectado un botón pulsador al pin 1 y he inicializado el Joystick:
#include <Joystick_ESP32S2.h>
#define BOTON 1
Joystick_ control;
Luego en el setup iniciamos el push button como
INPUT_PULLUP e inicializamos el joystick con begin.
pinMode(BOTON, INPUT_PULLUP);
control.begin();
Y en el loop leemos el estado con un código de ejemplo que el mismo autor proporciona:
void loop()
{
// Read pin values
int currentButtonState = !digitalRead(BOTON);
if (currentButtonState != lastButtonState)
{
control.setButton(1, currentButtonState);
// Joystick.setButton(0, currentButtonState);
lastButtonState = currentButtonState;
}
if (digitalRead(BOTON))
{
ledRgb.setPixelColor(0, 0, 0, 255);
ledRgb.show();
}
else
{
ledRgb.setPixelColor(0, 0, 255, 255);
ledRgb.show();
}
}
Con esto, mientras tienes abierta la ventana de Dispositivos de juego se te debe mostrar cuando pulses el botón 1.

Conclusión
La ESP32-S3-WROOM-1 es una placa que incluso podría decir viene sobrada, pues tiene muy buenos recursos de hardware y permite posibilidades infinitas.
A mí me costó 276 pesos mexicanos que son aproximadamente 15 USD e incluso trajo su placa de expansión. El vendedor me tuvo paciencia para responder mis dudas y no mintió en ninguna especificación.
Voy a seguir escribiendo sobre la tarjeta más adelante, pues planeo hacer un joystick, publicar el mouse y teclado TCP, hacer un transmisor y receptor de audio, entre otras cosas.