A battery-powered, cable-free SSH/Telnet terminal built on the M5Stack Tab5 (ESP32-P4). It renders a real 80×30 VT100 on the Tab5's 1280×720 MIPI-DSI panel, and — the part that makes it more than a serial console — it speaks Tailscale. Not "WireGuard with a hardcoded endpoint": an actual from-scratch Tailscale client for ESP32. No jump host to rent, no port forwarding, nothing to keep running at home except the box you wanted to reach in the first place.
What it does- 80×30 VT100/ANSI terminal — UTF-8, East-Asian wide characters, alternate screen buffer, DSR responses, and enough CSI coverage to run
vim,htop, and other full-screen TUIs. - SSH (libssh2 1.11.1 + mbedTLS, password or embedded PEM key auth, TOFU host-key pinning in NVS) and Telnet (RFC 854 + NAWS window-size reporting).You can store multiple hosts and select them from the menu.
- USB-serial (FTDI) and UART input — it's a serial console when there's no network.
- Tailscale / WireGuard with DERP relay fallback and DISCO direct-path negotiation.
- Auto-reconnect supervisor: backoff 1 s → 30 s, TCP keepalive (30 s / 10 s / 3 probes) plus SSH keepalive driven once a second, so a dead link surfaces in about a minute instead of hanging forever.
- Provisioning from a phone — on-LCD QR codes, captive portal, HTTP settings page. A factory-fresh unit never touches a PC.
- OTA updates from GitHub Pages with bootloader rollback protection, plus a browser Web Flasher for the initial install.
- Screen lock — idle backlight-off, PIN pad on wake (SHA-256 hash in NVS, escalating lockout after 5 wrong tries).
- Touch, soft keyboard, clip-on Tab5 QWERTY, and USB-A HID keyboards — all four feed the same input sink.
A factory-fresh unit has no credentials, so it can't join anything — the classic chicken-and-egg. The boot path handles it: with no stored STA credentials it powers the C6, initialises Wi-Fi without joining, and brings up a WPA2 softAP directly.
- Power on. The LCD shows a full-screen overlay with a Wi-Fi QR (
WIFI:T:WPA;…— the stock phone camera joins the AP straight from it), a URL QR forhttp://192.168.4.1/, and the credentials in plain text.
- The AP is
Tab5-XXXXXXwith a per-device 10-character password, generated on first boot and persisted in NVS (ambiguous0/o/1/lexcluded). It's also printed in the boot log, anderase-flashregenerates it. - A captive portal — a UDP/53 responder answering every query with 192.168.4.1, plus 302s on
/generate_204,/hotspot-detect.html,/connecttest.txtand unknown paths — pops the settings page automatically on most phones. - Fill in Wi-Fi, the connection profile (SSH/Telnet host, port, user, password), and the Tailscale auth key. Secrets are write-only:
/api/inforeports*_setbooleans and never the values. - Reboot from the page. If Tailscale needs interactive login, a third QR with the login URL appears on the LCD — scan, approve in the browser, done.
The same overlay reappears automatically if a Wi-Fi join later fails, which is exactly when you need it, and can be summoned from the ☰ menu on demand.
Build and installRepository: https://github.com/ciniml/portable_terminal
Path A — Web Flasher (no toolchain). Open https://ciniml.github.io/portable_terminal/ in Chrome or Edge, plug the Tab5 in over USB, pick a release, click Flash. It's esptool-js over WebSerial; the page is built by CI from tagged releases. Artifacts are also on GitHub Releases.
Path B — from source.
git clone --recursive https://github.com/ciniml/portable_terminal.git
cd portable_terminal
source ~/esp-idf/6.0/export.sh
make set-target # idf.py set-target esp32p4, one time
make build
make flash monitorOne extra step, and it matters: the Tab5 ships with M5Stack's C6 Wi-Fi firmware (V1.4.1, esp_hosted 1.4.x), which is incompatible with the 2.x host stack this project uses. c6_updater/ is a self-contained IDF 5.4 project that speaks the factory-compatible protocol over SDIO and pushes the new slave firmware — no cover removal:
./c6_updater/updater.sh /dev/ttyACM0 # wait for "Slave update completed"An ESP-Prog UART fallback is documented in slave_c6_fw/README.md if that fails. Full details and all Kconfig options are in the repo README.
https://github.com/ciniml/portable_terminal
Code written for this project is Boost Software License 1.0. Vendored and submoduled components keep their own licenses: M5GFX / M5Unified (MIT, patched forks), libssh2 (BSD-3-Clause), and the wireguard / tailscale / usb_host_ftdi_sio components originating from ciniml/serial_wifi_logger (BSD-3-Clause), which this project extends substantially — the DISCO rewrite, the HTTPS public-IP probe, CallMeMaybe in both directions, and the direct-path liveness work are new here, and docs/TAILSCALE_PORTING_NOTES.md exists specifically so they can be back-ported to other ESP-IDF projects.









Comments