Document commands and electrical test procedures, delittered README,

overall restructure of docs.
This commit is contained in:
2026-08-25 15:16:08 +02:00
parent d779fb046e
commit bbcdf37a3b
4 changed files with 314 additions and 633 deletions
+19 -416
View File
@@ -2,7 +2,7 @@
![ESP32 Serial Swiss Army Knife logo](/images/logo.png "ESP32 Serial Swiss Army Knife")
Universal wireless serial adaptor firmware for the ESP32-S3.
ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one MAX3243-backed UART1 serial port and safely shares it between native USB CDC-ACM, an HTTPS/WebSocket browser terminal, and SSH: one connected client can write while others observe. The firmware also provides persistent serial and Wi-Fi configuration, a UART0 recovery console, and hardware diagnostics; it is not a general-purpose router, captive portal, or unauthenticated TCP serial server.
## Initial hardware target
@@ -10,13 +10,17 @@ Universal wireless serial adaptor firmware for the ESP32-S3.
- ESP32-S3-WROOM-1-N16R8 module
- 16 MB flash
- 8 MB octal PSRAM
- Adafruit MAX3243 full-pinout RS-232 breakout, product 5988
- [Adafruit MAX3243 full-pinout male RS-232 breakout](https://www.adafruit.com/product/6253)
The firmware has completed **Phase 0 hardware characterization**, the **Phase 1 serial-core foundation**, the **Phase 2 transport-neutral session broker**, native USB CDC-ACM, the **Phase 4 Wi-Fi foundation**, the **Phase 5 authenticated HTTPS web terminal**, and the **Phase 6 authenticated SSH serial transport**. Phase 5B provides an offline xterm.js interface and bounded WebSocket transport; Phase 6 adds a bounded, password-authenticated, shell-only SSH path with an independent persistent host identity. The MAX3243 diagnostics and recovery consoles remain available. No electrical test starts automatically; UART1 starts when requested explicitly or when native USB, an authenticated web terminal, or an authenticated SSH session opens.
## Development status
## Hardware wiring
Hardware characterization, the serial core, session broker, and native USB CDC-ACM are complete. The Wi-Fi foundation is implemented and substantially validated, with several smoke tests still pending. The project is currently entering the **HTTPS/web-console phase**; later work includes interface hardware, security hardening, OTA, BLE, enterprise Wi-Fi, WireGuard evaluation, and optional storage features. See the [implementation plan](GPT-logs/Implementation%20plan.md) for validation details and roadmap.
See [`wiring.md`](wiring.md) for the hardware profile, GPIO assignments, loopback diagrams, safety notes, and the recommended test sequence. The initial profile covers the ESP32-S3-DevKitC-1 N16R8 and the Adafruit MAX3243 full-pinout RS-232 breakout.
## Documentation
- [Hardware wiring](wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
- [Electrical tests](docs/electrical_tests.md): MAX3243, UART loopback, and session-broker verification procedures.
- [Command reference](docs/command_reference.md): UART0 administration, serial, broker, USB, Wi-Fi, web, SSH, and diagnostic commands.
## Flash partition layout
@@ -31,15 +35,15 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
| `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps |
| `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot |
| `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot |
| `storage` | `0x8B0000` | 7488 KiB | Future LittleFS web assets, logs, and files |
| `storage` | `0x8B0000` | 7488 KiB | Reserved for future LittleFS web assets, logs, and files |
Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
The partition table reserves OTA and LittleFS space but does not by itself implement OTA downloads, rollback confirmation, core-dump handling, NVS encryption, or filesystem mounting. Those features will be enabled deliberately in later phases.
The table reserves OTA and storage space; it does not implement OTA downloads, rollback confirmation, core-dump handling, NVS encryption, or filesystem mounting.
### One-time migration from the default partition table
The previous 1 MiB factory application began at `0x10000`, which is now inside the enlarged NVS address range. A normal upload does not erase all stale bytes there. Perform a full flash erase once when first switching to this layout:
The previous 1 MiB factory application began at `0x10000`, which now lies inside the enlarged NVS range. A normal upload does not erase stale data in that range. When first switching to this layout, erase the flash completely:
```sh
pio run --target erase
@@ -47,9 +51,7 @@ pio run --target upload
pio device monitor -b 115200
```
This erases the currently saved serial configuration and all other flash contents. The firmware will boot with safe serial defaults and recreate NVS. Subsequent ordinary uploads do not require another full erase.
PlatformIO's application-size report should now use the 4 MiB `ota_0` slot instead of the previous 1 MiB factory partition.
This removes saved serial configuration and all other flash contents. The firmware recreates NVS with safe defaults. Subsequent ordinary uploads do not need a full erase.
## Build
@@ -66,415 +68,16 @@ pio run --target upload
pio device monitor -b 115200
```
The firmware starts an interactive console on UART0 with the prompt `serial-tool>`. Type `help` to display concise root-command descriptions. This USB-to-UART device normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport described below.
The firmware provides an interactive UART0 console at `serial-tool>`. Run `help` for available commands. The USB-to-UART bridge normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport, which normally appears as `/dev/ttyACM*`.
The console provides line editing, history for the current session, terminal-aware cursor movement, and Tab completion. ESP-IDF supplies completion for root commands; the project extends it to nested `debug`, `serial`, `broker`, `usb`, `wifi`, `web`, and `ssh` subcommands plus safe fixed values such as AP policy and serial framing. Password values are intentionally never completion candidates.
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
ESP-IDF normally probes terminal cursor support once while constructing the UART REPL. If the board boots without a terminal attached, that probe times out and selects dumb mode. The DevKit's USB-to-UART bridge exposes no host-open signal that firmware can monitor, and entering enhanced mode before a terminal exists would block linenoise while it waits for a cursor-position response.
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Retrieve generated web and SSH credentials only from the physical UART0 console with `web credentials show`.
The project therefore preserves safe dumb mode until the first real UART byte arrives. Attach picoterm, picocom, minicom, PuTTY, or another ANSI-capable terminal and press Enter once. That empty line is consumed by the basic reader and promotes the next prompt to enhanced mode, enabling history, Delete, cursor keys, and completion without rebooting. This detects terminal activity rather than electrical USB attachment; a genuinely non-ANSI terminal is not automatically distinguishable on this hardware profile.
## Security notes
The root-level system commands are:
```text
memory
reboot
```
`memory` reports current free space, a conservative aggregate of each matching region's lifetime minimum, and the largest contiguous block for internal 8-bit heap, the internal DMA-capable subset, and external PSRAM. It is intended for comparing idle state with concurrent USB, WebSocket, and SSH sessions. `reboot` acknowledges the request, waits briefly for UART output to drain, and calls the ESP-IDF software restart. RAM-only serial or Wi-Fi changes are lost unless they were persisted first with `serial save` or `wifi save`.
### Phase 1 serial service
The `serial` command manages the working configuration and UART1 service:
```text
serial status
serial start
serial stop
serial set <baud|data-bits|parity|stop-bits|flow|dtr|rts-threshold> <value>
serial save
serial load
serial defaults
serial reset
serial counters
serial clear-counters
```
Safe defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control, and inactive DTR. Supported configuration values are:
| Parameter | Values |
|---|---|
| `baud` | 1101000000 |
| `data-bits` | `7`, `8` |
| `parity` | `none`, `even`, `odd` |
| `stop-bits` | `1`, `2` |
| `flow` | `none`, `rts-cts` |
| `dtr` | `inactive`, `active`, `on-connect` |
| `rts-threshold` | 1127 bytes |
`serial set` changes the working configuration and safely restarts UART1 if the service is running. It does not write flash; use `serial save` to commit the current configuration to NVS. `serial defaults` changes RAM only, while `serial reset` applies and persists defaults. The firmware never erases the shared NVS partition automatically when storage is incompatible or unavailable.
The service uses independent software RX and TX streams. Calls into those streams are nonblocking, and a deasserted CTS cannot block service shutdown. UART data access is intentionally reserved for the session broker; the `serial` command controls configuration and lifecycle only.
UART1 has exclusive ownership while the service runs. Phase 0 commands will refuse to touch the port until `serial stop` releases it.
### Phase 2 session broker
The broker is initialized at boot and continuously drains the serial service whenever UART1 is running. It is transport-neutral: console test clients, native USB CDC, authenticated WebSocket terminals, and authenticated SSH sessions all use the same API.
```text
broker status
broker clients
broker counters
broker clear-counters
broker connect <name>
broker disconnect <client-id>
broker request-writer <client-id>
broker release-writer <client-id>
broker force-writer <client-id|none>
broker send-hex <client-id> <hex-bytes>
broker read <client-id> [maximum-bytes]
broker events <client-id>
```
Each connection receives a generation-safe numeric ID. Stale IDs from disconnected clients cannot address a newly reused slot. Up to eight clients may connect, each with a bounded 4096-byte output queue and a 16-entry event queue. The queue control structures remain in internal RAM, while their approximately 35 KiB of payload storage prefers PSRAM with internal fallback; queue semantics and bounds are unchanged.
UART RX is copied to every connected client. A full observer queue drops bytes only for that observer and records the loss; it never blocks UART reception or another client. With no clients, the broker still drains UART data and records it as unobserved.
Exactly one client may hold the writer lease. Competing requests are denied and generate events. Administrative forced reassignment atomically revokes the old writer and grants the new one. Bytes already accepted before revocation remain queued for transmission; revocation prevents future admission rather than purging the UART TX stream.
Connect, disconnect, writer grant, release, revoke, and denial events have a broker-global sequence number. Event queues are intentionally bounded, so future transports should reconcile sequence gaps against broker snapshots. The first and last broker connection also drive the Phase 1 `DTR=on-connect` policy.
### Native USB CDC-ACM transport
The ESP32-S3's native USB OTG peripheral presents one CDC-ACM serial interface through the development board's connector labelled **USB**. It uses GPIO19 (`USB D-`) and GPIO20 (`USB D+`) and normally appears on Linux as `/dev/ttyACM*`. It is not the USB-to-UART bridge used for upload and logs.
The UART0 development console provides these diagnostics and controls:
```text
usb
usb help
usb status
usb counters
usb clear-counters
usb request-writer
usb release-writer
```
Both `usb` and `usb help` print the same multi-line command summary and return successfully; runtime state is shown explicitly with `usb status`.
Opening the CDC port with DTR asserted automatically starts UART1, connects a broker client named `usb-cdc`, and requests the writer lease. If another client already owns the lease, USB remains connected as a read-only observer; `usb status` reports its current role. Closing the port or unplugging native USB disconnects that broker client and discards transport-local pending data. The serial service itself remains running until it is stopped explicitly with `serial stop`.
The data path is binary-transparent. UTF-8 bytes, NUL bytes, terminal escape sequences, and color sequences are passed unchanged; interpretation remains the terminal application's responsibility. USB output is bounded and nonblocking, so a host that stops reading can lose only its own observer data rather than stall UART1 or another client.
Host line coding is accepted for baud rates 1101000000 with 7 or 8 data bits, none/odd/even parity, and 1 or 2 stop bits. USB's 1.5 stop bits and mark/space parity are rejected. Supported settings are applied to the working UART configuration only when USB owns the writer lease and queued UART TX has drained. They are not saved to NVS automatically; use `serial save` deliberately if the setting should survive reboot. USB RTS is reported as host status only. It does not drive the physical RS-232 RTS line, which remains controlled by UART1's configured RTS/CTS flow control.
The development VID/PID comes from Espressif's TinyUSB defaults. The USB serial-number string is derived from the ESP32-S3 station MAC so multiple adapters can be distinguished consistently.
#### Linux loopback validation
Keep the USB-to-UART cable connected for logs and commands, and connect a second data-capable cable to the native **USB** connector. On the host, identify the new CDC device:
```sh
dmesg
ls -l /dev/ttyACM*
```
With power removed and no external RS-232 peer attached, connect only DE-9 pin 3 (`TX`) to pin 2 (`RX`), then power the board. Open the native port with a serial terminal such as:
```sh
picocom -b 115200 /dev/ttyACM0
```
Use the actual device path assigned by the host. Typed data should return through USB → broker → UART1 → MAX3243 loopback → broker → USB. On the UART0 console, verify `usb status`, `usb counters`, `broker clients`, and `serial status`. The USB client should normally be the writer and counters should increase without drops.
For a binary check, install PySerial on the host and send all byte values:
```python
import serial
payload = bytes(range(256))
with serial.Serial("/dev/ttyACM0", 115200, timeout=2) as port:
port.reset_input_buffer()
port.write(payload)
echoed = port.read(len(payload))
assert echoed == payload, (len(echoed), echoed.hex())
print("256-byte binary USB/RS-232 loopback passed")
```
Close the terminal and check `usb status` and `broker clients`; DTR-aware applications should cause the USB broker client to disconnect. Physically unplugging the native USB cable is the definitive detach test. To test observer mode, assign a console test client as writer before opening `/dev/ttyACM0`; USB should connect as an observer, receive UART output, and discard host-originated input until ownership is granted.
Power down and remove the DE-9 pin 3-to-2 jumper before connecting an external serial peer.
### Wi-Fi foundation
Wi-Fi is managed independently of the serial-session broker. It provides network connectivity and recovery access-point policy for the HTTPS and SSH services. The firmware does not run plaintext HTTP, DNS interception, a captive portal, NAPT, or any unauthenticated/plaintext TCP serial listener.
Configuration uses four fixed station-profile slots. Lower numeric priority values are tried first, with slot number breaking ties. Profiles support WPA2/WPA3 mixed operation or require WPA3-SAE. ESP-IDF's station threshold can express “WPA2 or stronger” but not a strict WPA2-only maximum, so the configuration does not pretend to offer a distinct WPA2-only mode. Each profile attempt has a 12-second association/DHCP deadline. After all enabled profiles fail, the manager uses exponential retry delays from 2 to 60 seconds.
AP policy is independent of the station profiles:
| Policy | Behavior |
|---|---|
| `off` | Station only; never start the fallback AP |
| `fallback` | Start the AP immediately when no profiles exist, or after one failed profile cycle; disable it after station connectivity has remained stable for 30 seconds |
| `always` | Keep AP and station active concurrently |
Fresh defaults enable Wi-Fi with `fallback` policy, AP channel 6, a MAC-suffixed SSID such as `ESP32-SAK-A1B2C3`, and a randomly generated 16-character password. The initial random credential is saved to NVS automatically when possible so it remains stable across reboot. Retrieve it deliberately from the physical UART0 administration console with `wifi ap show-secret`.
The `wifi` command provides:
```text
wifi status
wifi profiles
wifi start|stop|reconnect
wifi profile set <slot> <priority> <mixed|wpa3> <ssid>
wifi profile secret <slot>
wifi profile enable|disable|delete <slot>
wifi ap policy <off|fallback|always>
wifi ap ssid <ssid>
wifi ap channel <1..11>
wifi ap secret|show-secret
wifi save|load|defaults|reset
wifi counters|clear-counters
wifi ping <host> [count]
wifi nslookup <host>
wifi traceroute <host> [max-hops]
```
The network diagnostics are also registered as root aliases, so `ping`, `nslookup`, and `traceroute` are equivalent to their `wifi`-prefixed forms. `ping` accepts 120 probes and supports IPv4 or IPv6. `nslookup` prints unique numeric IPv4/IPv6 results. `traceroute` is currently IPv4-only, sends one ICMP Echo probe per hop, accepts 130 hops, and uses a one-second timeout per hop; routers that suppress ICMP replies appear as `*`.
Ordinary status and profile output never displays passwords. `wifi profile secret` and `wifi ap secret` read through a dedicated no-echo UART0 prompt, keeping credentials out of the command line and its history. SSIDs containing spaces can be quoted. Profile and AP edits apply to the working RAM configuration and restart Wi-Fi asynchronously if it is running; use `wifi save` explicitly to persist them. `wifi start` and `wifi stop` also change the working `enabled-at-boot` setting, which becomes persistent only after `wifi save`.
A typical station setup is:
```text
wifi profile set 0 10 mixed "your SSID"
wifi profile secret 0
wifi profile enable 0
wifi save
wifi reconnect
wifi status
```
The fallback AP uses Espressif's default `192.168.4.1/24` network for now. AP clients receive addresses through its DHCP server but are not routed to the station network. ESP32-S3 has one 2.4 GHz radio, so in AP+STA mode the AP follows the connected station's channel. Station connection attempts and scans can temporarily increase AP latency, and clients can briefly reconnect when the channel moves.
Wi-Fi credentials currently reside as plaintext in the application-owned `wifi_app/config` NVS blob. Selecting `WIFI_STORAGE_RAM` prevents the ESP-IDF driver from creating a second persistent credential copy, but it does not encrypt the application's blob. The reserved `nvs_key` partition alone does not enable encryption. NVS encryption, secure boot, flash encryption, and core-dump credential exposure require a deliberate later security phase.
#### Wi-Fi validation
1. Boot with no station profiles. `wifi status` should report `ap-only`, and the generated SSID should be visible from another device.
2. Use `wifi ap show-secret`, join the AP, confirm a `192.168.4.x` lease, and run `wifi ping 192.168.4.1`. The authenticated HTTPS page should be reachable at `https://192.168.4.1/`.
3. Configure and enable a WPA2/WPA3 station profile using the example above. `wifi status` should progress through `connecting`, `waiting-ip`, and `online` and display the acquired address, channel, RSSI, and negotiated authentication.
4. Reboot and verify profile and AP credential persistence.
5. Configure two profiles with different priorities, make the first unavailable, and verify failover to the second after its timeout.
6. Make all profiles unavailable and verify fallback AP startup plus increasing retry delays in `wifi status`/`wifi counters`.
7. Test `wifi ap policy always` while online and confirm both interfaces remain available; expect the AP channel to follow the station.
8. Test `wifi stop`, `wifi start`, and `wifi reconnect` while confirming UART0 and native USB serial operation remain unaffected.
9. If available, test a WPA3-only profile and a wrong password, then inspect the disconnect reason and counters.
### Phase 5 authenticated HTTPS web terminal
One ESP-IDF HTTPS server listens on TCP port 443 across whichever AP and station interfaces are active. There is no plaintext port 80 listener. Phase 5A established persistent authentication, certificate management, and recovery; Phase 5B adds these local-only browser resources and transport endpoints:
```text
GET /
GET /api/status
POST /api/ws-ticket
WSS /ws/serial?ticket=<one-time-ticket>
GET /assets/xterm.css
GET /assets/xterm.js
GET /assets/addon-fit.js
GET /assets/app.js
```
The page, status API, assets, and ticket endpoint require HTTP Basic authentication over TLS. `/` is now a responsive xterm.js serial workspace; `/api/status` returns JSON containing uptime plus non-secret Wi-Fi, serial-service, broker, native-USB, HTTPS, and WebSocket state/counters. xterm.js and FitAddon are pinned, vendored, compressed, and served by the ESP32 itself, so the terminal works while connected only to the fallback AP and never depends on a CDN. The terminal is constrained to the browser viewport, and fitting only resizes xterm when the host dimensions and resulting row/column count actually change; terminal rendering therefore cannot create a self-amplifying page-height loop.
The document CSP permits scripts only from the device itself. xterm.js requires inline styles for its DOM renderer, but inline scripts remain prohibited. A data-URL empty favicon avoids an unnecessary authenticated `/favicon.ico` request. A CSP warning naming a UUID or browser extension despite no inline script in the served document is browser-injected content and is intentionally not hash-whitelisted by the firmware.
On first boot, the device generates and persists:
- username `admin` and a random 24-character Base64URL-safe password shared by HTTPS and SSH;
- an ECDSA P-256 HTTPS private key;
- a device-specific self-signed SHA-256 certificate valid from 2025-01-01 through 2049-12-31;
- certificate SANs for `192.168.4.1` and a MAC-suffixed name such as `esp32-sak-a1b2c3.local`;
- a separate ECDSA P-256 SSH host key and OpenSSH-compatible SHA-256 fingerprint.
The HTTPS certificate and shared administrative credentials remain stable across ordinary reboot and OTA-slot changes until explicitly rotated. The `.local` name is included for future hostname discovery, but this phase does not yet advertise mDNS; use the AP address or the station address reported by `wifi status`.
The physical UART0 administration console provides:
```text
web
web help
web status
web start|stop
web counters|clear-counters
web credentials show
web credentials rotate --force
web certificate info
web certificate rotate --force
web reset --force
```
`web` and `web help` print the same usage summary. `web credentials show` is the intended first-boot credential-retrieval path for both HTTPS and SSH. Rotation and reset operations write NVS immediately rather than creating RAM-only secrets. Credential rotation revokes active WebSocket and SSH sessions. Destructive operations require a literal `--force`; `web reset --force` is also the recovery path for an incompatible or damaged `web_sec/material` blob and starts HTTPS with the recovered material. Invalid stored material is never overwritten automatically, and HTTPS failure never disables UART0, native USB, the serial core, or Wi-Fi recovery.
The self-signed certificate is not trusted by browsers or host tools by default. After retrieving the password, validate from a host connected to the fallback AP with:
```sh
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' https://192.168.4.1/
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' https://192.168.4.1/api/status
```
A request without credentials should return `401 Unauthorized` and a `WWW-Authenticate` challenge:
```sh
curl -k -i https://192.168.4.1/api/status
```
Inspect and compare the live certificate with `web certificate info`:
```sh
openssl s_client -connect 192.168.4.1:443 -servername esp32-sak-device.local </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -fingerprint -sha256
```
Replace the example SNI name with the DNS SAN printed by `web certificate info`. SNI is not required for this single-certificate server, but supplying the device name makes the test representative of future hostname use. Repeat the fingerprint check after reboot to confirm persistence, then optionally test each explicit rotation command and verify that only the requested material changes.
#### WebSocket authentication and broker behavior
Browser JavaScript cannot reliably attach a Basic `Authorization` header to a WebSocket constructor. The authenticated page therefore obtains a 192-bit random, RAM-only ticket with `POST /api/ws-ticket`, then presents that ticket once in the WSS URL. The server stores only its SHA-256 digest, accepts it once within 30 seconds, binds it to the current credential generation, and creates no broker client until validation succeeds. Credential rotation invalidates outstanding tickets and closes active web-terminal and SSH sessions.
ESP-IDF 5.5 sends the RFC 6455 `101 Switching Protocols` response before invoking the application WebSocket handler. Consequently, an invalid ticket receives the protocol upgrade and is then closed immediately rather than receiving an HTTP `401`; it never gains a broker session, serial output, or writer access. Strict rejection before `101` would require a framework-level pre-handshake authorization hook that ESP-IDF 5.5 does not provide.
Each accepted browser becomes a normal `SESSION_BROKER_CLIENT_WEB`. Opening the first terminal starts UART1 if necessary and automatically requests the writer lease. A competing web or USB client remains a read-only observer when another client owns the lease. The page clearly reports its role and provides **Request control**, **Release control**, and **Reconnect** actions. Broker ownership remains authoritative even if a browser is stale or malicious.
Serial traffic uses binary WebSocket frames. Browser input is UTF-8 encoded and split into at most 1024-byte frames; output is drained in at most 512-byte frames. Each web session permits only one queued/in-flight TLS frame. If a browser stops reading, its own 4096-byte broker observer queue eventually drops data without blocking UART reception, USB, or another broker observer. ESP-IDF performs TLS sends on one shared HTTP task, so a slow TLS peer can delay other HTTPS work for at most the configured one-second socket timeout; this is bounded rather than absolute per-socket isolation.
A direct ticket diagnostic is available without exposing the ticket in firmware logs:
```sh
curl -k -u 'admin:YOUR_24_CHARACTER_PASSWORD' -X POST https://192.168.4.1/api/ws-ticket
```
For end-to-end validation:
1. Open `https://192.168.4.1/`, accept the device certificate warning, and authenticate as `admin`.
2. Confirm xterm.js loads without Internet access and the page reaches **Connected / Writer** when no other writer exists.
3. Send text, terminal escape sequences, UTF-8, and pasted input through an RS-232 loopback or peer; verify exact traffic through `web counters`, `broker clients`, and serial counters.
4. Open a second browser/private session. It should connect as an observer, receive the same UART output, and keep terminal input disabled.
5. Release control in the first browser, request it in the second, and verify the role badges, broker writer ID, and actual serial input ownership change together.
6. Open native USB while a web writer exists, then repeat with USB owning the lease. Confirm each losing transport remains an observer and cannot inject bytes.
7. Close/reload a browser and verify its broker client disappears, the writer lease is released when applicable, and reconnect uses a fresh ticket.
8. Run `web credentials rotate --force`; existing browsers should disconnect and old credentials must no longer mint tickets.
9. Exercise `web stop`, `web start`, and reboot while confirming UART0/native USB recovery remains available and no stale web broker clients survive.
Vendored browser sources, versions, hashes/provenance, deterministic gzip artifacts, and MIT license notices are recorded under [`web_assets/`](web_assets/SOURCES.md). Third-party code remains under its upstream license; project firmware code remains GPL-3.0-only.
HTTPS permits up to six simultaneous client sockets: two bounded persistent WebSocket terminals plus parallel browser asset, ticket, and status requests. ESP-IDF documents approximately 40 KiB per active TLS socket, so this is a concurrency ceiling rather than preallocated per-socket memory. WebSocket serial sessions themselves remain fixed at two. To preserve internal DRAM during concurrent handshakes, the firmware configures ESP-IDF's mbedTLS allocator for the board's external PSRAM. Basic authentication is acceptable here only because plaintext HTTP is disabled. It is an initial administration mechanism, not the final authorization design.
**Current security limitation:** the shared admin password and HTTPS ECDSA private key are stored as plaintext in the application-owned `web_sec/material` NVS blob, the SSH ECDSA private key is plaintext in `ssh_sec/material`, and Wi-Fi credentials are plaintext in `wifi_app/config`. The reserved `nvs_key` partition does not activate NVS encryption. ESP-IDF 5.5 keeps active mbedTLS allocations in external PSRAM under this memory configuration; wolfSSL/wolfSSH allocations also prefer PSRAM with internal-memory fallback. Neither framework guarantees complete zeroization of released session allocations, and PSRAM encryption is not enabled. Do not treat the current firmware as resistant to physical flash or RAM extraction; NVS encryption, flash/PSRAM encryption, secure boot, protected OTA, secret-aware core-dump handling, and framework-level key zeroization belong to the later hardening phase.
### Phase 6 authenticated SSH serial transport
A wolfSSH server listens on TCP port 22 across whichever Wi-Fi interfaces are active. It uses the same `admin` username and 24-character password as HTTPS, but has a separate persistent ECDSA P-256 host key in `ssh_sec/material`. The SSH identity is deliberately independent of the HTTPS certificate key so certificate rotation does not unexpectedly invalidate SSH `known_hosts` entries.
The UART0 administration console provides:
```text
ssh
ssh help
ssh status
ssh start|stop
ssh sessions
ssh disconnect <session-id>
ssh counters|clear-counters
ssh host-key info
ssh host-key rotate --force
ssh reset --force
```
`ssh` and `ssh help` print the same usage summary. `ssh host-key info` prints the OpenSSH-style `SHA256:...` fingerprint over the standard `ecdsa-sha2-nistp256` public-key wire blob. `ssh host-key rotate --force` replaces a valid key; `ssh reset --force` explicitly replaces missing, incompatible, or corrupt SSH material. Both operations persist immediately, stop active SSH sessions before changing identity, and restart the service when appropriate. After deliberate rotation, remove the old host entry on the client with `ssh-keygen -R <address>` and verify the new fingerprint before reconnecting.
Only password authentication and an interactive shell/PTY channel are accepted. Public-key user authentication, arbitrary `exec`, subsystem requests, SFTP, SCP, agent forwarding, and TCP forwarding are not provided. Authentication is bounded to three password attempts per connection and a 15-second handshake deadline. At most two total SSH sessions or in-progress handshakes are admitted at once. Two deliberately stalled clients can occupy both fixed slots until their deadlines, so these bounds limit resource consumption but do not prevent availability attacks. This is useful embedded admission control, not a substitute for network filtering, strong unique credentials, monitoring, or mature brute-force protection.
After authentication and shell negotiation, the channel becomes a binary-transparent serial byte stream. The firmware does not inject prompts, status messages, writer-control escapes, line handling, or terminal emulation into channel data. SSH framing and encryption terminate at the ESP32; bytes between the SSH channel and session broker are retained across partial nonblocking transfers.
Each admitted shell becomes a `SESSION_BROKER_CLIENT_SSH`. The first session opportunistically requests the writer lease; a later session remains an observer if USB, WebSocket, a test client, or another SSH session already owns it. Observers receive UART output but their input is discarded. SSH deliberately has no in-band writer-control sequence because any reserved byte sequence would break binary transparency. Inspect ownership with `ssh sessions` or `broker clients`, and use the physical UART0 `broker force-writer <client-id|none>` recovery command when administrative reassignment is required.
Before connecting for the first time:
1. Run `web credentials show` on UART0 and keep the password private.
2. Run `ssh host-key info` and record the exact `SHA256:...` fingerprint.
3. Obtain the AP or station address from `wifi status`.
Then connect with OpenSSH, forcing the implemented authentication method if the host has unrelated keys configured:
```sh
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no admin@192.168.4.1
```
Compare OpenSSH's first-connection ECDSA fingerprint with `ssh host-key info` before accepting it. Once connected, all terminal bytes go directly to the physical RS-232 session. OpenSSH may print a one-time pre-authentication device banner; this is SSH protocol metadata and is not inserted into the serial stream.
The wolfSSH runtime has one owner task and two fixed session slots. A single owner serializes the managed component's `SINGLE_THREADED` build, avoids one large crypto stack per client, and services both nonblocking sessions fairly. The task is pinned to CPU 1 so SSH key exchange and stream processing cannot monopolize the CPU 0 Wi-Fi/web path. It retains a 20 KiB internal stack because external task stacks are unsafe during flash/NVS cache-disable windows; `ssh status` reports its measured minimum-free stack space so this can be reduced later from hardware evidence rather than guesswork. wolfSSL/wolfSSH dynamic allocations prefer external PSRAM and fall back to internal 8-bit memory. Wi-Fi/lwIP payload allocations and broker observer/event payload queues also prefer PSRAM, preserving internal memory for DMA, task stacks, and control structures. WolfCrypt's seed callback is installed after `wolfSSH_Init()` and routes all SSH RNG seeding through the project's serialized pre-radio CTR_DRBG instead of the registry component's default post-radio `esp_random()` path. HTTPS remains on ESP-IDF mbedTLS and is not switched to wolfSSL.
The official Espressif registry baseline is pinned in `src/idf_component.yml` and `dependencies.lock`:
```text
wolfssl/wolfssl 5.8.2~1
wolfssl/wolfssh 1.4.20
```
The build enables `CONFIG_ESP_ENABLE_WOLFSSH`, keeps `CONFIG_ESP_TLS_USING_MBEDTLS`, and raises `CONFIG_LWIP_MAX_SOCKETS` to 16 for HTTPS/WebSocket and SSH concurrency. Adding the linked SSH implementation increased the measured release image from approximately 29.3% to 33.9% of the 4 MiB OTA slot and static RAM from 17.5% to 18.5% of 320 KiB. Runtime task stacks, Wi-Fi/TLS state, and PSRAM allocations are not fully represented by the static RAM percentage.
#### SSH validation
1. Boot and record `memory` plus `ssh status`; confirm SSH reports `running=yes`, TCP port 22, password authentication, no sessions, CPU 1 ownership, and a plausible nonzero stack minimum-free value.
2. Compare the first OpenSSH host-key prompt exactly with `ssh host-key info`; disconnect if it differs.
3. Verify a wrong password fails and the shared password from `web credentials show` succeeds.
4. Exercise text, NUL-containing/binary payloads, UTF-8, ANSI color, cursor-control sequences, and a full-screen terminal application through an RS-232 loopback or peer.
5. Confirm `ssh sessions`, `broker clients`, `ssh counters`, and serial counters reflect traffic and cleanup.
6. Connect USB CDC as writer, then one WebSocket observer and one SSH observer. Re-run `memory` and `ssh status`; all transports should remain responsive, internal minimum-free/largest-block values should remain nonzero, and the SSH stack minimum-free value should show whether its 20 KiB allocation can later be reduced safely.
7. Test ownership against native USB and WebSocket clients, using UART0 `broker force-writer` for deterministic reassignment.
8. Run `ssh disconnect <session-id>` and verify the socket and broker client disappear without affecting another observer.
9. Leave an unauthenticated TCP/SSH client stalled and verify its slot closes after 15 seconds while an established session continues.
10. Run `web credentials rotate --force`; active SSH clients should disconnect and the old password should fail.
11. Run `ssh host-key rotate --force`, remove the old client entry, and verify the newly printed fingerprint persists across reboot.
12. Exercise `ssh stop`, `ssh start`, Wi-Fi reconnect, and reboot while confirming UART0/native USB recovery remains available and no stale SSH broker clients survive.
### Phase 0 diagnostics
The top-level `status` command retains quick MAX3243 signal-state inspection. Potentially disruptive hardware-characterization operations are grouped below `debug` so the primary help page stays concise:
```text
status
debug transceiver <enable|disable>
debug drivers <tx 0|1> <dtr 0|1> <rts 0|1>
debug loopback-a
debug loopback-b
debug valid-test
debug uart-loopback <baud> [8N1|8E1|8O1|8N2|7E1|7O1] [bytes]
debug uart-suite
debug cts-flow-test
debug rts-flow-test
```
Run `debug` without a subcommand for its usage summary. `debug uart-loopback` defaults to `8N1` and 256 bytes. Its accepted payload range is 1512 bytes. `debug uart-suite` covers 300 through 250000 baud and all supported frame formats. `debug cts-flow-test` verifies transmit gating and exact resumption, while `debug rts-flow-test` uses UART2 as an internal traffic generator to verify automatic receive backpressure. Follow the command-specific loopback wiring in [`wiring.md`](wiring.md) before invoking any test.
A mutex-protected port lease prevents diagnostics, UART1 service startup, and future clients from reconfiguring the same GPIOs concurrently. If a UART driver cannot be removed during cleanup, the firmware keeps the MAX3243 shut down and marks the port faulted until reboot rather than exposing an ambiguous hardware state.
The onboard RGB LED reports the most recent test-harness state:
| Color | Meaning |
|---|---|
| Blue | Idle; waiting for a command |
| Yellow/orange | Test running |
| Green | Last test passed |
| Red | Last test failed |
This hardware profile uses the onboard RGB LED on GPIO48. Official ESP32-S3-DevKitC-1 v1.1 boards commonly use GPIO38 instead, and compatible boards or clones may vary. A different board revision requires an adjusted board pin profile before running this firmware.
The HTTPS interface uses a device-specific self-signed certificate and HTTP Basic authentication over TLS; there is no plaintext HTTP or TCP serial listener. SSH supports password-authenticated interactive shell sessions only. The shared admin password, HTTPS private key, SSH private key, and Wi-Fi credentials are currently stored in plaintext application-owned NVS blobs. The reserved `nvs_key` partition does not enable encryption. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete.
## License
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Phase 6 pins the official Espressif registry components `wolfssl/wolfssl` 5.8.2~1 and `wolfssl/wolfssh` 1.4.20. Their GPL releases are compatible with this project; third-party components remain subject to their respective licenses. The wolfSSH 1.4.20 managed archive contains stale GPLv2-or-later wording in `LICENSE.txt` while upstream wolfSSH source headers identify GPLv3-or-later. Both are GPLv3-compatible, but distributors should preserve the upstream notices and re-check the registry package's license metadata. These registry versions are an integration baseline rather than a permanent security pin and must be reviewed against current upstream wolfSSL/wolfSSH releases during production hardening.
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `wolfssl/wolfssl` `5.8.2~1` and `wolfssl/wolfssh` `1.4.20`; review upstream security releases before production use.
+116
View File
@@ -0,0 +1,116 @@
# Command reference
Use these commands from the UART0 `serial-tool>` administration console. Run `help` for the available root commands and `<group> help` for a group summary. Configuration changes are RAM-only unless explicitly saved.
## System
| Command | Description |
|---|---|
| `memory` | Show free memory, minimum free memory, and largest blocks for internal RAM, DMA-capable RAM, and PSRAM. |
| `reboot` | Drain console output briefly and restart the ESP32. |
| `status` | Show quick MAX3243 signal state. |
## Serial service
| Command | Description |
|---|---|
| `serial status` | Show UART1 state, configuration, modem signals, and ownership. |
| `serial start` / `serial stop` | Start or release the physical UART1 service. |
| `serial set <baud|data-bits|parity|stop-bits|flow|dtr|rts-threshold> <value>` | Change the working serial configuration and safely restart a running service. |
| `serial save` / `serial load` | Save the working configuration to NVS or load it. |
| `serial defaults` / `serial reset` | Apply defaults in RAM, or apply and persist them. |
| `serial counters` / `serial clear-counters` | Show or clear serial counters. |
Defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control, and inactive DTR. Supported values: baud `110``1000000`; data bits `7` or `8`; parity `none`, `even`, or `odd`; stop bits `1` or `2`; flow `none` or `rts-cts`; DTR `inactive`, `active`, or `on-connect`; and RTS threshold `1``127` bytes. The broker exclusively owns serial data access.
## Session broker
| Command | Description |
|---|---|
| `broker status` / `broker clients` | Show broker state or connected clients. |
| `broker counters` / `broker clear-counters` | Show or clear broker counters. |
| `broker connect <name>` / `broker disconnect <client-id>` | Create or remove a console test client. |
| `broker request-writer <client-id>` / `broker release-writer <client-id>` | Request or relinquish the single writer lease. |
| `broker force-writer <client-id|none>` | Administratively assign or clear the writer lease. |
| `broker send-hex <client-id> <hex-bytes>` | Send hexadecimal bytes through a writer client. |
| `broker read <client-id> [maximum-bytes]` | Read queued serial output for a client. |
| `broker events <client-id>` | Show ownership and connection events for a client. |
Each client has a generation-safe ID. There can be one writer and multiple observers; a slow observer loses only its own queued output.
## Native USB CDC-ACM
| Command | Description |
|---|---|
| `usb` / `usb help` | Show USB command usage. |
| `usb status` | Show CDC connection, broker role, and runtime state. |
| `usb counters` / `usb clear-counters` | Show or clear USB counters. |
| `usb request-writer` / `usb release-writer` | Request or release USB writer ownership. |
Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, starts UART1 if needed, and requests writer ownership. It becomes an observer if another client is writer. USB data is binary-transparent. Supported host line coding is 1101000000 baud, 7/8 data bits, none/odd/even parity, and 1/2 stop bits; settings apply only while USB owns the writer lease and are not saved automatically.
## Wi-Fi
| Command | Description |
|---|---|
| `wifi status` / `wifi profiles` | Show Wi-Fi state or configured station profiles. |
| `wifi start` / `wifi stop` / `wifi reconnect` | Start, stop, or reconnect Wi-Fi. |
| `wifi profile set <slot> <priority> <mixed|wpa3> <ssid>` | Set a station profile. |
| `wifi profile secret <slot>` | Set a profile password through a no-echo prompt. |
| `wifi profile enable|disable|delete <slot>` | Manage a station-profile slot. |
| `wifi ap policy <off|fallback|always>` | Configure fallback AP behavior. |
| `wifi ap ssid <ssid>` / `wifi ap channel <1..11>` | Set the AP name or channel. |
| `wifi ap secret` / `wifi ap show-secret` | Set or reveal the AP password. |
| `wifi save|load|defaults|reset` | Persist, restore, reset in RAM, or reset and persist configuration. |
| `wifi counters|clear-counters` | Show or clear Wi-Fi counters. |
| `wifi ping <host> [count]` | Send 120 IPv4 or IPv6 ICMP probes. |
| `wifi nslookup <host>` | Resolve and display unique IPv4/IPv6 addresses. |
| `wifi traceroute <host> [max-hops]` | Run IPv4 ICMP traceroute with up to 30 hops. |
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Passwords are not displayed by ordinary status output.
## HTTPS web terminal
| Command | Description |
|---|---|
| `web` / `web help` | Show web-service command usage. |
| `web status` | Show HTTPS and WebSocket state. |
| `web start` / `web stop` | Start or stop HTTPS service. |
| `web counters` / `web clear-counters` | Show or clear web counters. |
| `web credentials show` | Display the physical-console-only shared admin credentials. |
| `web credentials rotate --force` | Replace credentials and disconnect web and SSH sessions. |
| `web certificate info` | Display certificate identity and fingerprint. |
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
| `web reset --force` | Explicitly replace missing, incompatible, or damaged web material. |
HTTPS listens on port 443 only. Authenticate to `https://<device-address>/` as `admin` with the credential shown on UART0. The device serves its vendored xterm.js terminal without Internet access. Browser sessions use binary WebSocket frames and follow the broker's one-writer rule.
## SSH serial transport
| Command | Description |
|---|---|
| `ssh` / `ssh help` | Show SSH command usage. |
| `ssh status` | Show service state and resource information. |
| `ssh start` / `ssh stop` | Start or stop the SSH server. |
| `ssh sessions` | List active SSH sessions and broker roles. |
| `ssh disconnect <session-id>` | Disconnect one SSH session. |
| `ssh counters` / `ssh clear-counters` | Show or clear SSH counters. |
| `ssh host-key info` | Display the OpenSSH host-key fingerprint. |
| `ssh host-key rotate --force` | Replace the persistent SSH host key. |
| `ssh reset --force` | Explicitly replace invalid or missing SSH material. |
SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a separate host key. It accepts password-authenticated interactive shell/PTY sessions only; it does not provide public-key authentication, `exec`, SFTP, SCP, forwarding, or subsystems. Verify the fingerprint from `ssh host-key info` before accepting an SSH host key.
## Hardware diagnostics
| Command | Description |
|---|---|
| `debug transceiver <enable|disable>` | Enable or shut down the MAX3243. |
| `debug drivers <tx 0|1> <dtr 0|1> <rts 0|1>` | Drive static TX, DTR, and RTS logic levels for measurement. |
| `debug loopback-a` / `debug loopback-b` | Test MAX3243 driver/receiver loopback configurations. |
| `debug valid-test` | Verify valid RS-232 voltage detection. |
| `debug uart-loopback <baud> [8N1|8E1|8O1|8N2|7E1|7O1] [bytes]` | Run a parameterized UART loopback test. |
| `debug uart-suite` | Test supported baud rates and frame formats. |
| `debug cts-flow-test` / `debug rts-flow-test` | Verify hardware transmit gating or receive backpressure. |
Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. Diagnostics refuse to use UART1 until `serial stop` releases it. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed.
+149
View File
@@ -0,0 +1,149 @@
# Electrical tests
These procedures verify the MAX3243 breakout, UART1 data path, hardware flow control, and session broker. They are manual tests: the firmware never starts one automatically.
> **Safety:** With power removed, install only the wiring required by the selected test. DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are driven outputs. Never connect one of these outputs to another driven output. Keep temporary Dupont wiring short and secure.
## Configuration A: data and handshake pairs
Connect the following pairs:
| Driven output | Receiver input | Expected ESP32 logic |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 2, `RX` | `RX == TX` |
| DE-9 pin 4, `DTR` | DE-9 pin 6, `DSR` | `DSR == DTR` |
| DE-9 pin 7, `RTS` | DE-9 pin 8, `CTS` | `CTS == RTS` |
```text
DE-9 pin 3 TX ─────> pin 2 RX
DE-9 pin 4 DTR ─────> pin 6 DSR
DE-9 pin 7 RTS ─────> pin 8 CTS
```
Run `debug loopback-a`. The test cycles all TX/DTR/RTS states and verifies the three receivers and `VLD`. Driver and receiver inversions cancel, so the resulting ESP32 logic levels must match.
This configuration also supports `debug valid-test`, `debug uart-loopback <baud> [format] [bytes]`, and `debug uart-suite`; the UART commands need only the pin 3-to-2 connection.
## Configuration B: remaining receivers
Remove every configuration A jumper, then connect:
| Driven output | Receiver input | Expected ESP32 logic |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 1, `DCD` | `DCD == TX` |
| DE-9 pin 4, `DTR` | DE-9 pin 9, `RI` | `RI == DTR` |
| DE-9 pin 7, `RTS` | DE-9 pin 2, `RX` | `RX == RTS` |
```text
DE-9 pin 3 TX ─────> pin 1 DCD
DE-9 pin 4 DTR ─────> pin 9 RI
DE-9 pin 7 RTS ─────> pin 2 RX
```
Run `debug loopback-b`. Configurations A and B together exercise all three drivers and five receivers.
## CTS transmit gating
Disconnect all external DE-9 equipment, power down, and remove previous jumpers. Install only:
| Driven output | Receiver input | Purpose |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 2, `RX` | Return UART1 data for comparison |
| DE-9 pin 4, `DTR` | DE-9 pin 8, `CTS` | Present inactive and active CTS states |
```text
DE-9 pin 3 TX ─────> pin 2 RX
DE-9 pin 4 DTR ─────> pin 8 CTS
```
Run `debug cts-flow-test`. At 115200 baud with hardware CTS enabled, the firmware first queues 512 bytes while CTS is inactive and confirms that no data is sent for 250 ms. It then activates CTS and verifies automatic, exact transmission and reception of all bytes.
## RTS receive backpressure
Disconnect all external DE-9 equipment, power down, and remove previous jumpers. Install only:
| Driven output | Receiver input | Purpose |
|---|---|---|
| DE-9 pin 4, `DTR` | DE-9 pin 2, `RX` | Carry UART2-generated data to UART1 RX |
| DE-9 pin 7, `RTS` | DE-9 pin 1, `DCD` | Return UART1 RTS to UART2 CTS |
```text
UART2 TX / DE-9 pin 4 DTR ─────> pin 2 RX / UART1 RX
UART1 RTS / DE-9 pin 7 RTS ─────> pin 1 DCD / UART2 CTS
```
Run `debug rts-flow-test`. UART2 queues 4096 bytes while UART1 is initially unread. Once UART1 reaches its receive threshold, it deasserts RTS and stops UART2 through CTS. Draining UART1 must reassert RTS and let UART2 finish. `UART_BUFFER_FULL` is expected; FIFO overflows, framing/parity errors, breaks, or any data mismatch are failures.
## Voltage and polarity checks
With no DE-9 loopback jumpers installed, measure pins 3, 4, and 7 relative to DE-9 pin 5 (`GND`):
```text
debug drivers 0 0 0
```
Each output should be at a positive RS-232 voltage. Then run:
```text
debug drivers 1 1 1
```
Each output should be negative. Exact voltages depend on supply, load, meter, and charge-pump behavior; polarity is the important result.
## Recommended order
1. With no DE-9 jumpers, run `status`.
2. Measure output polarity with `debug drivers 0 0 0` and `debug drivers 1 1 1`.
3. Power down, install configuration A, power up, and run `debug loopback-a`.
4. Run `debug valid-test` with configuration A still installed.
5. Keep only pin 3-to-2 and run `debug uart-loopback 9600 8N1 256`, then `debug uart-suite`.
6. Power down, install configuration B, power up, and run `debug loopback-b`.
7. Power down, install CTS wiring, power up, and run `debug cts-flow-test`.
8. Power down, install RTS wiring, power up, and run `debug rts-flow-test`.
RTS and CTS remain GPIO signals during static and basic UART tests. Only the flow-control tests assign them to UART peripherals. The firmware shuts down the MAX3243 while changing GPIO-matrix routing and restores outputs to static logic 1 after each test.
## Session-broker loopback
This test verifies broker fan-out and writer ownership independently of the diagnostics. Disconnect external peers, power down, remove previous jumpers, and connect only DE-9 pin 3 (`TX`) to pin 2 (`RX`). Power up and run:
```text
serial start
broker connect writer
broker connect observer
broker clients
```
Use the client IDs printed by the device:
```text
broker request-writer <writer-id>
broker send-hex <writer-id> 0055aaff1b5b33316d
broker read <writer-id> 64
broker read <observer-id> 64
```
Both clients should receive `0055aaff1b5b33316d`. A read may return zero if UART1 has not returned the bytes yet; repeat it shortly afterward.
Verify writer exclusion and administrative reassignment:
```text
broker request-writer <observer-id>
broker send-hex <observer-id> dead
broker force-writer <observer-id>
broker send-hex <observer-id> 112233
broker read <writer-id> 64
broker read <observer-id> 64
broker events <writer-id>
broker events <observer-id>
```
The competing request and its initial send should fail. After reassignment, both clients should receive `112233`; events should show denial, revocation, and grant. Clean up:
```text
broker disconnect <observer-id>
broker disconnect <writer-id>
serial stop
```
Remove the loopback jumper with power off before connecting an external serial device.
+30 -217
View File
@@ -1,15 +1,17 @@
# Hardware wiring
This document records hardware-specific pin assignments. Keeping each assignment tied to a named board combination allows additional wiring profiles to be added later without treating one GPIO map as universal.
This document records the GPIO assignments for the initial hardware profile. Additional profiles should use separate sections rather than treating this GPIO map as universal.
## Profile: ESP32-S3-DevKitC-1 N16R8 with Adafruit MAX3243
This initial profile is for:
This profile supports:
- An ESP32-S3-DevKitC-1-compatible development board with an ESP32-S3-WROOM-1-N16R8 module, 16 MB flash, 8 MB octal PSRAM, two USB-C ports, and its onboard RGB LED on GPIO48.
- The [Adafruit RS-232 Full Pinout Level-Shifter Breakout, product 5988](https://www.adafruit.com/product/5988), using a MAX3243 and a DE-9 female connector.
- An ESP32-S3-DevKitC-1-compatible board with an ESP32-S3-WROOM-1-N16R8 module, 16 MB flash, 8 MB octal PSRAM, two USB-C ports, and an onboard RGB LED on GPIO48.
- The [Adafruit RS-232 Full Pinout Level-Shifter Breakout](https://www.adafruit.com/product/5988), which uses a MAX3243 and has a DE-9 connector.
The Adafruit board provides three logic-to-RS-232 drivers (`TX`, `RTS`, and `DTR`), five RS-232-to-logic receivers (`RX`, `CTS`, `DSR`, `DCD`, and `RI`), valid-signal detection, and shutdown control. It is a level shifter, not a galvanic isolator.
Both the male and female Adafruit breakout versions can be used. Prefer the male breakout because it is the cleaner arrangement for the intended cable topology. A female breakout usually needs a gender changer; use a straight-through changer only to change connector gender, not to cross signals.
The breakout provides three logic-to-RS-232 drivers (`TX`, `RTS`, and `DTR`), five RS-232-to-logic receivers (`RX`, `CTS`, `DSR`, `DCD`, and `RI`), valid-signal detection, and shutdown control. It is a level shifter, not a galvanic isolator.
### Wiring table
@@ -23,16 +25,16 @@ The Adafruit board provides three logic-to-RS-232 drivers (`TX`, `RTS`, and `DTR
| `CTS` | Breakout → ESP32 | GPIO16 | J1-9 | UART1 hardware transmit-flow-control input |
| `DTR` | ESP32 → breakout | GPIO7 | J1-7 | Software-controlled data-terminal-ready output |
| `DSR` | Breakout → ESP32 | GPIO5 | J1-5 | Data-set-ready input |
| `DCD` | Breakout → ESP32 | GPIO4 | J1-4 | Carrier/connect-state input |
| `DCD` | Breakout → ESP32 | GPIO4 | J1-4 | Carrier-detect input |
| `RI` | Breakout → ESP32 | GPIO6 | J1-6 | Ring-indicator input |
| `VLD` | Breakout → ESP32 | GPIO8 | J1-12 | Valid RS-232 voltage indication |
| `OFF` | ESP32 → breakout | GPIO9 | J1-15 | Optional active-low transceiver shutdown |
Use the GPIO labels printed on the development board when wiring a compatible clone; its physical header numbering may differ from the official DevKitC-1 documentation.
Use the GPIO labels printed on a compatible development board; physical header numbering may differ from the official DevKitC-1 documentation.
### Wiring diagram
The arrows show signal direction at the breakout's 3.3 V logic header.
Arrows show signal direction at the breakout's 3.3 V logic header.
```text
ESP32-S3-DevKitC-1 N16R8 Adafruit MAX3243 breakout
@@ -55,242 +57,53 @@ GPIO8 <──────────── VLD
GPIO9 ────────────> OFF
```
### UART and flow-control pins
### UART and flow control
UART1 is used for the external RS-232 data path. GPIO17 and GPIO18 are the ESP32-S3's conventional `U1TXD` and `U1RXD` pins. GPIO15 and GPIO16 are physically adjacent to them on header J1 and are routed to UART1 RTS and CTS through the ESP32-S3 GPIO matrix.
UART1 is the external RS-232 data path. GPIO17 and GPIO18 are the ESP32-S3's conventional `U1TXD` and `U1RXD` pins. GPIO15 and GPIO16 are routed to UART1 RTS and CTS through the GPIO matrix, so their fixed-function labels do not constrain this use.
The GPIO matrix means GPIO15 and GPIO16 can serve UART1 even though their fixed-function labels mention UART0 RTS and CTS. UART0 logging remains on GPIO43 and GPIO44 through the board's USB-to-UART bridge. GPIO19 and GPIO20 are reserved for the current native USB CDC-ACM transport.
RTS/CTS flow control will be configurable. When enabled, the UART peripheral can handle it in hardware so backpressure does not depend on application-task scheduling. When disabled, firmware must place the output signals in compatible idle states.
UART0 logging remains on GPIO43 and GPIO44 through the board's USB-to-UART bridge. GPIO19 and GPIO20 are reserved for native USB CDC-ACM. When RTS/CTS flow control is enabled, the UART peripheral handles it in hardware; when disabled, firmware places the signals in compatible idle states.
### Modem-control signals
GPIO4 through GPIO7 form a compact group for the remaining modem-control lines:
GPIO4 through GPIO7 provide modem-control lines:
- GPIO4 reads `DCD`.
- GPIO5 reads `DSR`.
- GPIO6 reads `RI`.
- GPIO7 drives `DTR`.
DCD, DSR, and RI are ordinary GPIO inputs rather than hardware-managed UART signals. Firmware will monitor them and make their state available to the serial session broker and user interfaces. DTR is a software-controlled GPIO output.
`DCD`, `DSR`, and `RI` are GPIO inputs monitored by the firmware. `DTR` is a software-controlled GPIO output. `VLD` on GPIO8 indicates that the MAX3243 sees a valid RS-232 voltage on at least one receiver; it is a connection hint, not proof of correct cabling or serial settings.
`VLD` on GPIO8 reports whether the MAX3243 detects a valid RS-232 voltage on at least one receiver. It is a useful connection hint but does not prove that the cable, signal direction, or serial parameters are correct.
The breakout's `OFF` pin is connected to the MAX3243 active-low `!FORCEOFF` input. High or unconnected keeps the transceiver enabled; low forces shutdown. Its onboard pull-up makes it safe to leave unconnected during initial testing. If firmware uses GPIO9 later, it should preferably configure it as an open-drain output that is released to enable the transceiver and driven low to disable it.
The breakout's `OFF` pin drives the MAX3243 active-low `!FORCEOFF` input. High or unconnected enables the transceiver; low forces shutdown. Its onboard pull-up makes it safe to leave unconnected during initial testing. If controlled by firmware, GPIO9 should be open-drain: released to enable and driven low to disable.
### Power and cable notes
- Connect breakout `Vin` to `3V3`, not `5V`. ESP32-S3 GPIOs are not 5 V tolerant.
- The MAX3243 generates the positive and negative RS-232 voltages required on the DE-9 side.
- Connect breakout `Vin` to `3V3`, never `5V`; ESP32-S3 GPIOs are not 5 V tolerant.
- The MAX3243 generates the positive and negative RS-232 voltages on the DE-9 side.
- The breakout is not isolated: ESP32 ground, breakout ground, USB ground, and DE-9 pin 5 are electrically connected.
- The breakout is electrically DTE-style: `TX`, `RTS`, and `DTR` drive the DE-9, while `RX`, `CTS`, `DSR`, `DCD`, and `RI` receive from it.
- A straight-through male-to-male gender changer changes connector gender only. Use a straight-through cable for a DCE peer and an appropriate null-modem cable for another DTE peer.
- The current female Adafruit breakout was validated in a temporary DTE-to-DTE lab chain using a straight-through male-to-male gender changer, a null-modem cable, and a USB RS-232 adapter. The gender changer corrected only the physical connector while the null-modem cable crossed the signal directions. A male version of the Adafruit breakout is planned as the cleaner permanent connector arrangement.
- The breakout is DTE-style: `TX`, `RTS`, and `DTR` drive the DE-9; `RX`, `CTS`, `DSR`, `DCD`, and `RI` receive from it.
- Use a straight-through cable for a DCE peer and a suitable null-modem cable for another DTE peer.
- A female breakout can be used with a male-to-male straight-through gender changer. The changer changes physical gender only; it does not replace a null-modem cable where signal crossing is required.
GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels instead of counting header positions.
GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels rather than counting header positions.
### Development and native USB connectors
This hardware profile uses both USB controllers for different purposes:
| DevKit connector | ESP32-S3 path | Typical Linux device | Purpose |
|---|---|---|---|
| `USB-to-UART` | UART0 on GPIO43/44 through the board's bridge chip | `/dev/ttyUSB*` | Firmware upload, logs, and `serial-tool>` development console |
| `USB` | Native USB OTG, GPIO19 `D-` and GPIO20 `D+` | `/dev/ttyACM*` | CDC-ACM client connected to the serial-session broker |
| `USB-to-UART` | UART0 on GPIO43/44 through the bridge chip | `/dev/ttyUSB*` | Firmware upload, logs, and `serial-tool>` console |
| `USB` | Native USB OTG, GPIO19 `D-` and GPIO20 `D+` | `/dev/ttyACM*` | CDC-ACM client connected to the session broker |
GPIO19 and GPIO20 are connected to the native USB connector and must not be assigned to the MAX3243, display, buttons, or other peripherals while USB CDC is enabled. Both connectors may be attached to the development host during testing so UART0 logs remain available while native USB carries broker data. Compatible DevKit clones can differ in connector labels and power-path design; verify the board schematic before assuming that two attached cables cannot back-power one another.
GPIO19 and GPIO20 must not be assigned to the MAX3243 or other peripherals while USB CDC is enabled. Both connectors may be attached during testing, but compatible clones can differ in connector labels and power-path design; consult the board schematic to avoid back-powering.
The USB-to-UART bridge's host DTR/RTS controls are used for automatic boot/reset circuitry and do not provide firmware with a reliable host-open indication. Consequently, attachment cannot be detected like native CDC DTR. After an unattended boot, the UART0 REPL remains in safe basic mode until it receives its first byte; pressing Enter once after attaching an ANSI terminal enables full line editing for the next prompt.
The USB-to-UART bridge's DTR/RTS controls serve automatic boot/reset and do not reveal whether a terminal is open. After an unattended boot, press Enter once in an ANSI-capable terminal to enable enhanced console editing.
CDC DTR indicates that the host application has opened the native serial port. It controls the lifetime of the `usb-cdc` broker client but is not directly forwarded to the physical DE-9 DTR output. Physical DTR follows the firmware's `serial` configuration. Likewise, host CDC RTS is status information only; GPIO15/DE-9 RTS remains dedicated to UART1 receive flow control when `flow=rts-cts` is enabled.
Native USB CDC DTR controls the lifetime of the `usb-cdc` broker client but is not forwarded to physical DE-9 DTR. Physical DTR follows the `serial` configuration. CDC RTS is status information only; GPIO15/DE-9 RTS remains UART1 receive flow control when `flow=rts-cts` is enabled.
## Phase 0 loopback tests
## Electrical verification
The hardware-characterization firmware never starts a test automatically. Wire exactly one configuration below while the board is powered down, inspect the connections, power it again, and then invoke the corresponding console command.
> **Important:** DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are all driven outputs. Never connect any of these three pins to another one of these output pins. Connect each output only to the receiver input specified by the selected test.
The temporary Dupont-wire breakout is mechanically fragile. Keep wires short, make all changes with power removed, and prevent loose conductors from touching neighboring pins.
### Configuration A: primary data and handshake pairs
Connect:
| From driven output | To receiver input | Expected ESP32 logic |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 2, `RX` | `RX == TX` |
| DE-9 pin 4, `DTR` | DE-9 pin 6, `DSR` | `DSR == DTR` |
| DE-9 pin 7, `RTS` | DE-9 pin 8, `CTS` | `CTS == RTS` |
```text
DE-9 pin 3 TX ─────> pin 2 RX
DE-9 pin 4 DTR ─────> pin 6 DSR
DE-9 pin 7 RTS ─────> pin 8 CTS
```
Run `debug loopback-a`. The firmware cycles all eight TX/DTR/RTS logic combinations, waits for the MAX3243 outputs and receivers to settle, and verifies all three receiver states plus `VLD`. Two inversions occur—once in the driver and once in the receiver—so the final ESP32 logic levels must match.
Configuration A can also be used for:
- `debug valid-test`, with no external RS-232 peer connected.
- `debug uart-loopback <baud> [format] [bytes]`, although only the pin 3 to pin 2 link is needed by that command.
- `debug uart-suite`, again using only the pin 3 to pin 2 data link.
### Configuration B: remaining receivers
Remove all configuration A jumpers, then connect:
| From driven output | To receiver input | Expected ESP32 logic |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 1, `DCD` | `DCD == TX` |
| DE-9 pin 4, `DTR` | DE-9 pin 9, `RI` | `RI == DTR` |
| DE-9 pin 7, `RTS` | DE-9 pin 2, `RX` | `RX == RTS` |
```text
DE-9 pin 3 TX ─────> pin 1 DCD
DE-9 pin 4 DTR ─────> pin 9 RI
DE-9 pin 7 RTS ─────> pin 2 RX
```
Run `debug loopback-b`. Together, configurations A and B exercise all three MAX3243 drivers and all five receivers.
### Configuration C: CTS transmit gating
Disconnect every external DE-9 cable or peer, power the ESP32 and breakout down, and remove every previous jumper. Confirm that the two jumpers below are the only connections to these RS-232 pins before powering up again.
| From driven output | To receiver input | Test purpose |
|---|---|---|
| DE-9 pin 3, `TX` | DE-9 pin 2, `RX` | Return UART1 transmitted data for exact comparison |
| DE-9 pin 4, `DTR` | DE-9 pin 8, `CTS` | Let software-controlled DTR present inactive and active CTS states |
```text
DE-9 pin 3 TX ─────> pin 2 RX
DE-9 pin 4 DTR ─────> pin 8 CTS
```
Run `debug cts-flow-test`. The test operates UART1 at 115200 baud with hardware CTS enabled and performs two phases:
1. DTR logic 1 produces CTS logic 1, the inactive/high state. The firmware queues 512 bytes and verifies that transmission does not complete and no byte reaches RX during a 250 ms observation period.
2. DTR changes to logic 0, producing active/low CTS. The queued transmission must resume automatically, and all 512 bytes must return through RX without missing, extra, or corrupted data and without UART errors.
A buffered UART transmitter is used so the console command itself cannot deadlock while CTS is blocking transmission. The firmware does not manipulate the UART transmit queue between the blocked and resumed phases.
### Configuration D: RTS receive backpressure
Disconnect every external DE-9 cable or peer, power the ESP32 and breakout down, and remove every previous jumper. Confirm that the two jumpers below are the only connections to these RS-232 pins before powering up again.
| From driven output | To receiver input | Test purpose |
|---|---|---|
| DE-9 pin 4, `DTR` | DE-9 pin 2, `RX` | Carry UART2-generated test data into UART1 RX |
| DE-9 pin 7, `RTS` | DE-9 pin 1, `DCD` | Return UART1 RTS through DCD to UART2 CTS |
```text
UART2 TX / DE-9 pin 4 DTR ─────> pin 2 RX / UART1 RX
UART1 RTS / DE-9 pin 7 RTS ─────> pin 1 DCD / UART2 CTS
```
Run `debug rts-flow-test`. GPIO7 is temporarily routed from UART2 TX through the MAX3243 DTR driver. GPIO4 receives UART1 RTS through the DCD receiver and is simultaneously routed to UART2 CTS. This creates a complete hardware-controlled flow loop without an external serial peer.
The test queues 4096 bytes from UART2 but initially does not read UART1. UART1's receive ring eventually fills, its hardware FIFO crosses the configured threshold, and UART1 automatically deasserts RTS. That state passes through the physical pin 7 to pin 1 jumper and blocks UART2 through CTS. The firmware then drains UART1, which must automatically reassert RTS and allow UART2 to finish.
`UART_BUFFER_FULL` is expected during this deliberate pressure test. It means the UART ISR could not fit its current received-data batch into the software RX ring, confirming receive-side resource pressure. The driver preserves that batch while hardware RTS stops the sender. `UART_FIFO_OVF`, framing errors, parity errors, breaks, missing bytes, extra bytes, or data mismatches are failures.
### Manual voltage and polarity checks
With no DE-9 loopback jumpers installed, use the static `debug drivers` command and measure each driven output relative to DE-9 pin 5 (`GND`):
```text
debug drivers 0 0 0
```
Each of pins 3, 4, and 7 should be at a positive RS-232 voltage. Then run:
```text
debug drivers 1 1 1
```
Each output should be at a negative RS-232 voltage. Exact values vary with supply, load, meter, and charge-pump behavior; polarity is the primary check. The firmware leaves all outputs at logic 1 after automated tests.
### Recommended test order
1. Start with no DE-9 jumpers and run `status`.
2. Use `debug drivers 0 0 0` and `debug drivers 1 1 1` for the three output-polarity measurements.
3. Power down, install configuration A, power up, and run `debug loopback-a`.
4. With configuration A still installed and no external peer, run `debug valid-test`.
5. Keep only the pin 3 to pin 2 jumper and run a basic test such as `debug uart-loopback 9600 8N1 256`.
6. If that passes, run `debug uart-suite`.
7. Power down, replace the jumpers with configuration B, power up, and run `debug loopback-b`.
8. Power down, install configuration C, power up, and run `debug cts-flow-test`.
9. Power down, install configuration D, power up, and run `debug rts-flow-test`.
RTS and CTS remain ordinary GPIO signals during static and basic UART loopback tests. Only the two dedicated flow-control commands hand them to UART peripherals. Every test shuts the MAX3243 down while changing GPIO-matrix routing and restores all outputs to static logic 1 afterward.
## Phase 2 broker loopback
The Phase 2 broker can be checked independently of the Phase 0 UART test implementation. Disconnect external peers, power down, remove all previous jumpers, and connect only:
```text
DE-9 pin 3 TX -> pin 2 RX
```
Power up and use the default 115200 8N1 configuration:
```text
serial status
serial start
broker connect writer
broker connect observer
broker clients
```
On a fresh boot the returned IDs are normally 8 and 9, but always use the IDs printed by your device. In the commands below, substitute those values if they differ:
```text
broker request-writer 8
broker send-hex 8 0055aaff1b5b33316d
broker read 8 64
broker read 9 64
```
Both clients are observers, including the active writer, so both reads should return the exact nine bytes:
```text
Client 8 read 9 bytes: 0055aaff1b5b33316d
Client 9 read 9 bytes: 0055aaff1b5b33316d
```
If a read occurs before UART1 has returned the bytes, it may report zero; run it again. Next verify writer exclusion and forced reassignment:
```text
broker request-writer 9
broker send-hex 9 dead
broker force-writer 9
broker send-hex 9 112233
broker read 8 64
broker read 9 64
broker events 8
broker events 9
broker counters
```
The competing writer request and pre-reassignment send are expected to fail. After forced reassignment, both clients must receive `112233`. Events should describe the denied request, revocation of client 8, and grant to client 9, with the forced-change events reporting final writer 9.
Clean up in this order:
```text
broker disconnect 9
broker events 8
broker disconnect 8
broker status
serial counters
serial stop
serial status
```
Disconnecting client 9 automatically releases its writer lease. Client 8 receives the corresponding release and disconnect events; draining them before its own disconnect avoids intentionally counting them as discarded events. After the final disconnect, the broker should report zero clients and writer 0. `serial stop` must return the RS-232 port owner to `idle`, after which Phase 0 commands are available again.
Remove the loopback jumper with power off before connecting an external serial peer.
See [Electrical tests](docs/electrical_tests.md) for safe loopback wiring, polarity checks, UART flow-control verification, and session-broker loopback testing.
## Future hardware profiles
Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers will receive separate profiles here. GPIO assignments must be reviewed for each board's display, buttons, USB connection, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals.
Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers require separate profiles. Review display, buttons, USB, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals before assigning GPIOs.