Skip to content

Repository files navigation

espshell

CI

espshell — live over-the-air firmware update

Demo of an over-the-air update. The web page is only a visual cue to make the swap visible — esp-ctl ota upload streams a new firmware over the encrypted link and the device reboots into it in place, no cable, core untouched.

A reusable application-layer firmware on top of ESP-IDF that exposes the full capability surface of any ESP32-family chip to a Linux host over an encrypted TCP shell-style command protocol. Each downstream project builds on top of the core and only adds its own command handlers.

Status: Phases 1–4 implemented (WiFi, TCP, auth, AEAD, OTA, peripherals, filesystem, power). Forward secrecy is not implemented and the /2 handshake design is not final — use /1 only on a trusted LAN or VPN.

Supported targets

  • esp32 (Xtensa LX6, dual-core, BT classic + BLE 4.2, DAC, Hall sensor)
  • esp32s3 (LX7, dual-core, USB-OTG, BLE 5)
  • esp32c3 (RISC-V single-core, BLE 5)
  • esp32c6 (RISC-V, BLE 5 + Thread + Zigbee)

Capability matrix in components/core/include/targets.h.

Security model

  • Transport: TCP over WiFi, single active client per device.
  • Handshake (cleartext): server emits HELLO with a 32-byte random nonce; client responds with AUTH cnonce=… hmac=… where hmac = HMAC-SHA256(token, "espshell-auth-v1" || snonce || cnonce).
  • Session keys: HKDF-SHA256(token, salt = snonce ‖ cnonce, info = "espshell-session-v1").
  • All subsequent frames are binary, length-prefixed, ChaCha20-Poly1305 AEAD. Per-direction monotonic sequence numbers serve as the AEAD nonce → strict anti-replay.
  • Per-connection retry delay: 3 failed authentication attempts on one socket → 10-second cool-down, then drop. Reconnecting resets the counter; this is not a global or per-peer rate limiter.
  • The token never crosses the wire. A captured handshake is nevertheless an offline verifier for token guesses, so use a randomly generated, high-entropy token rather than a human password.
  • /1 has no forward secrecy: disclosure of a token permits derivation of captured historical sessions. Server authentication is established only after the client validates an encrypted record, not by the cleartext OK.
  • The authenticated token grants administrative command access. Disabling MEM_READ/WRITE does not make the remaining commands unprivileged.
  • The handshake timeout applies to individual blocking receives, not to the complete handshake. After authentication, reads are blocking and writes have no explicit deadline. A peer making slow progress or not reading can occupy the single-client server longer than the nominal timeout.
  • SHA-256 verification during OTA detects corruption; it does not authenticate who produced the firmware. Firmware provenance requires a signed-boot/update policy appropriate for the deployment.
  • When enabled, mDNS announces the service, device name, protocol version, chip target, and firmware version without authentication.

Full protocol and limitations: docs/protocol.md.

Built-in commands

System

PING · INFO · STATS · UPTIME · HEAP · TASKS · RESET_REASON · REBOOT [ms] · FACTORY_RESET · CMDS · HELP <cmd>

Config (NVS)

CFG_GET <key> · CFG_SET <key> <value> · CFG_DEL <key> · CFG_LIST [prefix] · CFG_COMMIT (wifi_pass and auth_token are redacted on read.)

Logging

LOG_LEVEL <0..5> · LOG_STREAM ON|OFF

WiFi

WIFI_STATUS · WIFI_SCAN · WIFI_SET <ssid> <pass> · WIFI_RECONNECT

GPIO

GPIO_MODE <pin> <INPUT|OUTPUT|INPUT_PULLUP|INPUT_PULLDOWN> · GPIO_SET <pin> <HIGH|LOW> · GPIO_GET <pin> · GPIO_TOGGLE <pin> · GPIO_WATCH <pin> <RISING|FALLING|ANY> · GPIO_UNWATCH <pin>

Analog

ADC_READ <ch> · ADC_READ_MV <ch> · ADC_STREAM <ch> <ms> · ADC_STREAM_STOP <ch> · DAC_WRITE <ch> <0..255> (ESP32 only)

PWM

PWM_INIT <ch> <pin> <freq> <res> · PWM_SET <ch> <duty> · PWM_STOP <ch>

Buses

I2C_INIT <port> <sda> <scl> <hz> · I2C_SCAN <port> · I2C_READ <port> <addr> <reg> <n> · I2C_WRITE <port> <addr> <reg> <hex>

SPI_INIT <host> <miso> <mosi> <sclk> <cs> <hz> <mode> · SPI_TXRX <host> <hex>

UART_INIT <port> <tx> <rx> <baud> · UART_WRITE <port> <hex> · UART_READ <port> <n> <timeout_ms> · UART_STREAM <port> ON|OFF

Filesystem (SPIFFS @ storage partition)

FS_INFO · FS_LIST [path] · FS_READ <path> · FS_WRITE <path> <hex> · FS_DEL <path> · FS_FORMAT

Time

TIME_GET · TIME_SET <unix_ts> · SNTP_SYNC [server]

Power

SLEEP_LIGHT <ms> · SLEEP_DEEP <ms> · CPU_FREQ <80|160|240>

OTA (chunked, SHA-256 verified)

OTA_BEGIN <size> <sha256> · OTA_DATA <hex> · OTA_END · OTA_ABORT · OTA_ROLLBACK

Diagnostics

CHIP_TEMP · HALL_READ (ESP32 only) · MEM_READ <addr> <n> / MEM_WRITE <addr> <hex> (Kconfig-gated, off by default)

BLE

BLE_SCAN <s> · BLE_ADVERTISE <name> · BLE_STOP — stubs in v1; full NimBLE integration in v2 (adds ~150 KB flash, needs partition rebalance).

Async events (EVT <type> <data>)

LOG · HEALTH · GPIO · ADC · UART, plus arbitrary EVT PROJECT <data> emitted by downstream code via net_send_event().

Project extension

#include "core.h"

static bool cmd_read_bme(int argc, char **argv, char *resp, size_t sz) {
    /* ... */
    snprintf(resp, sz, "t=%.2f h=%.2f", t, h);
    return true;
}

void project_init(void) {
    cmd_register("READ_BME", cmd_read_bme, "Read BME280 sensor");
}

Tutorial

Full step-by-step walkthrough in docs/TUTORIAL.md — from git clone to running your own custom command over OTA.

Build & flash

git clone https://github.com/AdrianRodriguezM/espshell && cd espshell
. $HOME/esp/esp-idf/export.sh  # activate ESP-IDF environment (once per shell session)
idf.py set-target esp32        # or esp32s3 / esp32c3 / esp32c6
idf.py build flash monitor

If you use the get_idf alias (recommended by Espressif), run get_idf instead of sourcing export.sh directly — both activate the same environment.

Port selection: pass -p <port> explicitly if the device is not on /dev/ttyUSB0. On Linux, USB-serial adapters usually appear as /dev/ttyUSB0 (CP210x/CH340) or /dev/ttyACM0 (CDC-ACM). Run ls /dev/ttyUSB* /dev/ttyACM* to find the right port. If you get "permission denied", add yourself to the dialout group: sudo usermod -aG dialout $USER (requires re-login).

Stale build directory: if you move or rename the project folder, the cached build/ directory will refuse to build with a path mismatch. Fix with:

idf.py fullclean
idf.py build

First-boot provisioning (no UART required after flash)

On first boot without WiFi credentials, the device starts a temporary open AP:

*** espshell first-boot token ***
    <64-hex-chars>                    ← copy this

╔═══════════════════════════════════════╗
║     espshell — provisioning mode     ║
║  1. Connect to WiFi: espshell-XXYY   ║
║  2. esp-ctl --host 192.168.4.1 shell ║
║  3. WIFI_SET <ssid> <pass>           ║
║  4. REBOOT                           ║
╚═══════════════════════════════════════╝

The full encrypted shell runs on 192.168.4.1:9000 — same protocol, same token auth. After REBOOT the device comes up in STA mode and announces itself via mDNS (esp-ctl discover). No UART cable needed again.

To skip provisioning mode, set the WiFi SSID/password via idf.py menuconfig before flashing (espshell core → Default WiFi SSID/Password).

On the very first boot the device prints a generated hex token over UART. Copy it — it is only printed once. Its strength depends on the target's active entropy sources during first boot. If lost, erase the NVS partition to regenerate:

python3 -m esptool --chip esp32 -p /dev/ttyUSB0 -b 115200 \
    erase-region 0x9000 0x6000

Build the CLI

cd tools/esp-ctl
make
cp devices.example.toml ~/.config/esp-ctl/devices.toml
$EDITOR ~/.config/esp-ctl/devices.toml
chmod 600 ~/.config/esp-ctl/devices.toml
./esp-ctl --device default shell

Find devices without a fixed IP

The firmware advertises _espshell._tcp over mDNS (Kconfig: ESPSHELL_ENABLE_MDNS, hostname from device_name). One shot:

./esp-ctl discover     # prints a ready-to-paste devices.toml section

host = "name.local" also works in profiles if your system resolves mDNS names (nss-mdns); discover itself has no such dependency.

License

Licensed under GNU GPL v3.0 or later (GPL-3.0-or-later, SPDX). See LICENSE.

About

ESP-IDF secure shell firmware + host CLI for ESP32 family with encrypted command protocol

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages