An out-of-band remote serial console using the ESP32-C3 to provide emergency access via WiFi to a headless server or embedded system when primary network services fail.
The ESP32-C3 acts as a network-to-serial bridge: it connects to your target device’s serial console via its internal USB CDC interface and exposes it securely over a password-protected Telnet server on a static WiFi IP.
Suitable for ESP32-C3 boards with native USB-Serial bridge (Super Mini or Waveshare ESP32-C3-Zero).
- Local Management: Use UART0 for device configuration & debugging. Requires physical access to the device and external USB to Serial adapter (maybe a second ESP32C3: ESP32-C3 UART Bridge).
- Static IP: configurable IP and port.
- Telnet negotiation: suppress local echo and parse IAC/DO/WILL commands to keep terminal clean. Multiple telnet newline format are supported (LF, CR-LF, CR-NUL).
- Session authentication: Optional password protection with delay after 3 failed login attempts.
- Low level USB CDC I/O to avoid
usb_serial_jtagdriver. See USB Serial JTAG Read Bug - Background USB reader prevents host console buffer stalls and retains the last 64 KB of output for immediate viewing on connect.
Server ESP32-C3 Client
┌────────┐ ┌───────┬────────┐ ┌────────┐
│ │ USB │ │ │ TCP │ │
│Console │◄───►│ CDC │ WiFi │◄───►│ Telnet │
│ │ │ │ │ │ │
└────────┘ └───────┴────────┘ └────────┘
┌─────┐
│UART0│ ◄──────► Initial provisioning & debug
└─────┘
Tested on ESP-IDF version: v5.5.2
idf.py set-target esp32c3
idf.py menuconfig
idf.py build
idf.py flash
Options for menuconfig:
- Component config → ESP System Settings → Channel for console output: Default: UART0
- Component config → ESP System Settings → Channel for console secondary output: No secondary console
Right after flashing, connect to UART0: 115200 bauds.
W (302) main: Device is not configured yet.
=== ESP32 Console Ready ===
=== Configuration Status ===
Device is INCOMPLETELY CONFIGURED.
=== Configuration Values ===
ssid: (not set)
pass: (not set)
ip: (not set)
mask: 255.255.255.0
gw: 192.168.1.1
port: 23
sespass: *** (set)
Please configure the device. Reboot when ready.
Type 'help' to see available commands.
esp>
Configure options:
esp> ssid "My wifi AP"
OK
esp> pass mypass
OK
esp> ip 192.168.1.4
OK
esp> sespass secret
OK
esp> reboot
Rebooting...
Default password is secret. Set sespass to "" to disable it.
After reboot, check WiFi connection logs, etc.
Look for IP confirmation:
I (1000) wifi_manager: Got IP: 192.168.1.4
You can reconfigure options at any moment. To show current configuration:
esp> conf
=== Configuration Status ===
Device is FULLY CONFIGURED and ready to connect.
=== Configuration Values ===
ssid: "My wifi AP"
pass: *** (set)
ip: 192.168.1.4
mask: 255.255.255.0
gw: 192.168.1.1
port: 23
sespass: *** (set)
Telnet to configured IP and port. Using a telnet client: Putty, Telnet, etc.
telnet 192.168.1.4 23Example session:
Remote Console Telnet Server
Password: ******
--- Remote console open ---
(Telnet EOF or disconnect to close session)
Ubuntu 18.04.2 LTS cuadrado ttyACM0
cuadrado login: reinoso
Password:
Last login: Fri Jan 2 12:47:07 CET 2026 on ttyACM0
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-55-generic x86_64)
reinoso@cuadrado:~$Error when you try to send bytes to Linux when ESP32 is not connected, or agetty is not running at the other end:
--- Remote console open ---
(Telnet EOF or disconnect to close session)
--- Remote end not listening ---
Plug the board to the Linux box.
Identify the tty assigned to the ESP32 (could be different in each reboot).
# readlink /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_*
../../ttyACM0Get the device name:
# basename ../../ttyACM0
ttyACM0Spawn a serial console:
# setsid \
agetty -L \
$(basename $(readlink /dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_*))\
linuxConnect your device and check its USB properties in syslog:
kernel: usb 1-4: new full-speed USB device number 4 using xhci_hcd
kernel: usb 1-4: New USB device found, idVendor=303a, idProduct=1001, bcdDevice= 1.01
kernel: usb 1-4: Product: USB JTAG/serial debug unit
kernel: usb 1-4: Manufacturer: Espressif
kernel: usb 1-4: SerialNumber: AC:A7:04:BA:3A:38Or list the full info with this command:
# udevadm info -a -n /dev/ttyACM0
...
ATTRS{idProduct}=="1001"
ATTRS{idVendor}=="303a"
ATTRS{manufacturer}=="Espressif"
ATTRS{product}=="USB JTAG/serial debug unit"
ATTRS{serial}=="AC:A7:04:BA:3A:38"
...Choose the most relevant for your case and create a new udev rule.
Since this is the only "Espressif"'s "USB JTAG/serial debug unit" I will have in my box, I need only idProduct and idVendor values. Feel free to add ATTRS{serial} if you require it.
# cat /etc/udev/rules.d/99-esp32-console.rules
KERNEL=="ttyACM*", \
SUBSYSTEMS=="usb", \
ATTRS{idVendor}=="303a", \
ATTRS{idProduct}=="1001", \
SYMLINK+="ttyESP32"This rule will create a symlink called /dev/ttyESP32 to /dev/ttyACMx each time the kernel detects your ESP32:
lrwxrwxrwx 1 root root 7 Jan 4 13:55 /dev/ttyESP32 -> ttyACM0
crw-rw---- 1 root dialout 166, 0 Jan 4 13:55 /dev/ttyACM0
Best path: custom compiled kernel with cdc_acm driver built-in.
Second best path: add cdc_acm module to the Init Ram Disk:
# cat /etc/initramfs-tools/modules
cdc_acmNow run update-initramfs.
Activate a new terminal using systemd tty generator:
systemctl enable --now getty@ttyESP32.serviceThe kernel console= parameter will not work if the driver is compiled as a module.
So a true console is not possible unless you compile a custom kernel.
You cannot add an additional console once the system is running and ttyESP32 is available.
But you can setup syslog to forward kernel messages to ttyESP32 (in Ubuntu):
# usermod -aG dialout syslog
# echo 'kern.* /dev/ttyESP32' | tee /etc/rsyslog.d/99-ttyESP32.conf
# systemctl restart rsyslogThe incoming kernel messages will be kept until new login in a 64kb ring buffer.
- Telnet traffic is unencrypted.
- Session requires password, but credentials are sent in plaintext.
Reinoso Guzman (https://www.electronicayciencia.com).
The MIT License (MIT).