Restructure project documentation

Separate operator guides from development contracts, relocate
implementation notes under `docs/devel/`, and update cross-references
and
hardware tooling paths. Add getting-started, user administration, and
firmware update guides while preserving firmware behavior.
This commit is contained in:
2026-09-21 15:24:42 +02:00
parent 655f26a611
commit ab6a8010ba
43 changed files with 959 additions and 364 deletions
+88
View File
@@ -0,0 +1,88 @@
# Developer documentation
This directory collects implementation contracts, research, design decisions,
regression procedures, and historical acceptance evidence. It is not the operator
manual: start with the [project README](../../README.md) for user documentation,
the [command reference](../command_reference.md) for commands, and the
[security operations checklist](../security_operations.md) for safe operation.
[Wiring](../wiring.md) and [electrical tests](../electrical_tests.md) remain beside
the user documentation because hardware safety and bring-up need that context.
## Firmware contracts and evidence
- [Flash partition layout](flash_layout.md): storage allocation, reserved partitions, and update boundaries.
- [Roadmap and acceptance record](roadmap.md): completed phases, retained scope,
historical evidence, and limits of acceptance.
- [Web administration contracts](web_administration.md): contributor-facing API,
ownership, lifecycle, and recovery details.
- [User/administration regression procedures](user_administration_tests.md):
reusable checks, including historical baselines explicitly marked as superseded.
- [HTTPS idle cleanup](https_idle_cleanup.md): HTTPD owner, SDK, and lifecycle contract.
- [Web admission diagnostics](web_admission_diagnostics.md): instrumentation and interpretation limits.
- [Web throughput diagnostics](web_throughput_diagnostics.md): counters, capture procedures, and evidence limits.
- Display design references: [Phase 7 display mockup](phase7_display_mockup.svg)
and [Phase 7C icon mockup](phase7c_icon_mockup.svg), not promises of current UI appearance.
Source code is authoritative for current behavior. A checklist is not a test result;
host doubles do not prove target timing, flash durability, power-loss recovery, or
physical electrical safety. Preserve dates, revisions, workload details, and explicit
limitations when recording new evidence. Overall user acceptance does not establish
that every proposed regression procedure was executed.
Markdown links are document-relative. Unless explicitly stated otherwise, source,
build, and test paths in code spans and commands are repository-root-relative.
Hardware notes identify retained artifact paths and any different command directory.
## Tests and asset provenance stay with their artifacts
Test fixtures, scripts, and contextual READMEs remain under [tests](../../tests/).
Examples include the [HTTPD idle suite](../../tests/web_httpd_idle/README.md),
[admission diagnostics suite](../../tests/web_diagnostics/README.md),
[web administration suite](../../tests/web_admin_transport/README.md), and
[browser UI suite](../../tests/web_ui_session/README.md). Read each suite's scope,
prerequisites, and execution instructions rather than assuming a single host-test command.
These links are navigation, not claims that tests ran during documentation cleanup.
[Web asset provenance and generation policy](../../web_assets/SOURCES.md) remains
with the web assets. Do not hand-edit generated embedded assets or regenerate them
as a side effect of documentation changes; preserve upstream licenses and attribution.
## Hardware research and design notes
The [contextual PCB README](../../hardware/PCB/README.md) stays with the editable
KiCad project, CAD, libraries, exports, and scripts. The board remains a draft,
not fabrication-ready. Development notes preserve the hardware-relative substructure:
- [Adafruit MAX3243 research](hardware/PCB/adafruit-research.md)
- [Button selection and evidence](hardware/PCB/button-notes.md)
- [DE-9 mechanical research](hardware/PCB/db9-mechanical-notes.md)
- [Module footprint notes](hardware/PCB/footprint-notes.md)
- [RS-232 reference footprint notes](hardware/PCB/rs232-footprint-notes.md)
- [PCB draft design and release gates](hardware/PCB/pcb-draft-notes.md)
- [Routing validation and historical evidence](hardware/PCB/validation/pcb-routing-summary.md)
Original reference/provenance READMEs remain beside their sources:
[Adafruit Eagle reference](../../hardware/PCB/reference/README.md) and
[DE-9 CAD reference](../../hardware/PCB/reference/db9-cad/README.md).
Relocation does not change licenses, validate provisional male-module assumptions,
or qualify the board for manufacture. Do not casually rerun destructive generators;
use the retained hardware README and each script's documented safeguards.
## Security maintenance and evidence
Contributor maintenance includes reviewing relevant ESP-IDF, wolfSSL/wolfSSH, and
bundled web-library upstream release/security notes, especially before an update.
A relevant issue needs a separate upgrade/mitigation decision and scoped validation;
this is not an ongoing CVE audit or a claim that the firmware is vulnerability-free.
Dependency patches, crypto upgrades, secure boot, encrypted NVS, and eFuse changes
are outside the accepted lightweight operating-checklist scope, not implicit work
items or properties of a deployed device. See the [Phase 9 record](roadmap.md#phase-9--security-and-production-hardening).
The original security checklist's commands and limits were source-reviewed against
application sources and the command reference at `f40c09c`. That provenance is not
a new build, device test, security audit, or verification of flash/eFuse settings;
verify current implementation when behavior changes. Keep the operator checklist
focused on actionable use and recovery rather than contributor dependency work.
Never put passwords, cookies, tickets, Wi-Fi secrets, private keys, or verifier
material in routine logs, documentation captures, or status output.
+22
View File
@@ -0,0 +1,22 @@
# Flash partition layout
The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the custom [`partitions.csv`](../../partitions.csv) layout:
| Partition | Offset | Size | Purpose |
|---|---:|---:|---|
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, mDNS hostname, local-display, role-based user, HTTPS identity, and SSH host-key data |
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
| `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 | Reserved; not mounted or used for session recording |
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.
Admin HTTPS application upload uses the inactive OTA slot and `otadata`; NVS and other data partitions are untouched by the updater. It does not provide remote downloads, automatic rollback/health confirmation, core-dump handling, NVS encryption or filesystem mounting. Image validation does not guarantee a working application; compatible storage schemas and wired recovery remain the owner's responsibility.
Partition reservations do not enable their named features. In particular, `nvs_key` does not enable encryption, and the default firmware does not capture core dumps. Existing generated SDK configurations can override `sdkconfig.defaults`; inspect the effective configuration when verifying a build's security properties.
See [first installation and destructive migration](../getting_started.md#partition-migration-and-erasing) and the [firmware update guide](../firmware_update.md) for operator procedures. Do not change partition offsets as part of an ordinary application update.
@@ -0,0 +1,145 @@
# Adafruit MAX3243 breakout: authoritative CAD header research
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Research date: 2026-09-20. Scope: source retrieval and pad/geometry extraction only; no project schematic or layout modified.
## Critical product distinction
**Product 5988 is the DE-9 female version. The male version is product 6253.** The product-5988 page's male variant link goes to `/product/6253`; each product description explicitly identifies its connector gender. Both link to the same Learn guide. Do not describe the downloaded CAD as verified male-specific CAD: its connector is `X2`, value `DE9-F`, package `F09HP`, and the repository README identifies product 5988.
The exact header map below is proven for the published 5988 board. It is useful reference for the family, but identical male-board geometry/pad numbering has **not** been established by a male-specific CAD file or physical measurement. Confirm the actual 6253 module before releasing a male carrier footprint. No inference from connector gender or a generic DE-9 footprint is substituted for that check.
## Provenance and downloaded originals
Authoritative chain:
1. [Adafruit Learn downloads](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads) links to the Eagle repository below, a TI datasheet, fab print and 3D models.
2. [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB).
3. Retrieved `main` commit **`63c6200bb3ef17d491089cfa6441b1810d79bb20`**, committed 2024-08-12T22:42:27Z. All four local originals were downloaded from this immutable revision, not a moving branch.
4. [Product 5988, female](https://www.adafruit.com/product/5988), [product 6253, male](https://www.adafruit.com/product/6253), and [Learn pinouts](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/pinouts) corroborate identity and signal names.
Local files in `../../../../hardware/PCB/reference`, unchanged upstream contents:
| File | SHA-256 |
|---|---|
| `Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd` | `6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870` |
| `Adafruit RS-232 Full Pinout Level-Shifter Breakout.sch` | `4efbbe0ad14194e4e4563dd5f7dac007dfd52eba636f7390ffb822f3e81cd896` |
| `../../../../hardware/PCB/reference/README.md` | `2825ba741a8ce83abdaf2c7cb9170641473357630ccbee40a0016dcdb5cd80bb` |
| `license.txt` | `075dad5e5fc96c27014fabc269f4f5732909cffd178a486f546d982b6cf86b74` |
Raw URL prefix for each filename (URL-encode spaces):
`https://raw.githubusercontent.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB/63c6200bb3ef17d491089cfa6441b1810d79bb20/`
**License:** upstream `license.txt` is **Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)**. README attribution: “Designed by Limor Fried/Ladyada for Adafruit Industries.” README explicitly requires all its preceding text in redistribution; the full original README and license are therefore retained. Preserve these notices when redistributing CAD and comply with applicable attribution/share-alike obligations for adaptations. The CAD-derived content of this report is attributed to that design and provided under CC BY-SA 3.0.
The guide also links [5988 3D CAD](https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter). Its directory listing contains `.f3d`, `.step`, `.stl`, and `.jpg`; these were **not downloaded or used for measurements**, and their licensing was not independently assessed. No male-specific 3D model was verified in this research.
## Exact header numbering and net map
Source: Eagle XML `drawing/board/elements/element[@name='JP2']`, embedded `microbuilder` package `1X12_ROUND_76MIL`, and `drawing/board/signals/signal/contactref`. Cross-checked against schematic `JP2`, deviceset `HEADER-1X12`, selected device `76MIL`: symbol pin N maps directly to physical pad N.
**Viewing convention:** component/top view, DE-9 pointing toward the top of the page (+Y), logic header along the bottom. X increases rightward, Y upward. The origin is the lower-left bounding-box corner of the bare PCB (the corner itself is rounded). These are Eagle board coordinates in **millimetres**, not a mating-face view and not a bottom view.
```text
DE-9 / +Y
+-----------------------+
| |
| o o | module mounting holes
| |
| 1 2 3 ... 12 | JP2, y = 2.667 mm
+-----------------------+
Vin OFF
+X ->
```
| JP2 physical pad | Label / function | CAD net | X (mm) | Y (mm) | MAX3243 IC1 pad | DE-9 X2 pad via level shifter |
|---:|---|---|---:|---:|---:|---:|
| 1 | Vin | VCC | 1.905 | 2.667 | 26 | — |
| 2 | GND | GND | 4.445 | 2.667 | 25 | 5 (direct ground) |
| 3 | DCD | DCD | 6.985 | 2.667 | 15 | 1 |
| 4 | RX | RXD | 9.525 | 2.667 | 16 | 2 |
| 5 | TX | TXD | 12.065 | 2.667 | 13 | 3 |
| 6 | DTR | DTR | 14.605 | 2.667 | 14 | 4 |
| 7 | DSR | DSR | 17.145 | 2.667 | 17 | 6 |
| 8 | RTS | RTS | 19.685 | 2.667 | 12 | 7 |
| 9 | CTS | CTS | 22.225 | 2.667 | 19 | 8 |
| 10 | RI | RI | 24.765 | 2.667 | 18 | 9 |
| 11 | VLD | !INVALID | 27.305 | 2.667 | 21 | — |
| 12 | OFF | !FORCEOFF | 29.845 | 2.667 | 22 | — |
**Left-to-right order: `Vin GND DCD RX TX DTR DSR RTS CTS RI VLD OFF`.** This is physical pad order, not the order of rows in the project's wiring table.
At the logic header, TX/DTR/RTS are inputs to the MAX3243; RX/DCD/DSR/CTS/RI are outputs from it. Schematic gates explicitly map TX to T2IN, DTR to T1IN, RTS to T3IN, RX to R4OUT, DCD to R5OUT, DSR to R3OUT, CTS to R1OUT and RI to R2OUT. RS-232 and logic signal nets are separated by the chip; matching signal names do not mean direct continuity.
OFF is **active low** (`!FORCEOFF`, IC1 pad 22) with 10 kΩ pull-up R1 to VCC. The Learn pinout page agrees. The upstream README and shop prose incorrectly say high shuts it down; do not propagate that prose error. VLD is `!INVALID`, high on valid input detection. Use 3.3 V Vin for this project's ESP32 interface, as already required by `docs/wiring.md`.
## Exact published PCB geometry
All figures below are nominal CAD dimensions, not measured tolerances.
### Logic header JP2
- Single row, **12 plated through-hole pads at 2.54 mm (0.100 inch) pitch**.
- Element origin `(15.875, 2.667)`; no element rotation or mirroring (R0).
- Package local pad centres: `x = -13.970 + 2.540 × (N - 1)`, `y = 0`, N = 1…12.
- Board coordinates: `x = 1.905 + 2.540 × (N - 1)`, `y = 2.667`.
- First-to-last centre span **27.940 mm (1.100 inch)**.
- All holes: **1.000 mm drill**, explicit **1.9304 mm (76 mil) round copper diameter**. Each pad has `rot="R90"`; that does not rotate the header row or change a round pad's outline.
- Both end pad centres are **1.905 mm** from the corresponding vertical board edge.
- Row is **2.667 mm (0.105 inch)** above the lower bounding-box edge.
- No square pad-1 marker is defined by this package: identify pad 1 using Vin and the stated view, not assumed square copper.
### Bare-board outline and mounting holes
Source: `drawing/board/plain/wire[@layer='20']` and mounting-hole element/package definitions.
- Bare PCB bounding box: **31.750 mm X × 29.337 mm Y** (1.250 × 1.155 inches), X = 0…31.750, Y = 0…29.337.
- Straight top edge `(0,29.337)` to `(31.750,29.337)`; square top corners.
- Bottom corners: **2.540 mm radius**, defined by 90° arcs `(0,2.540)` → `(2.540,0)` and `(29.210,0)` → `(31.750,2.540)`.
- Bottom straight edge: X = 2.540…29.210 at Y = 0.
- Module mounting holes: `U$2` at **(3.810, 11.430)** and `U$3` at **(27.940, 11.430)**.
- Both use `MOUNTINGHOLE_3.0_PLATEDTHIN`, pad `P$1`, **3.000 mm drill / 3.600 mm copper diameter**. They are plated pads, not anonymous NPTH holes, and neither appears in the board's signal contactrefs.
- Hole centre spacing **24.130 mm (0.950 inch)**; hole row is **8.763 mm** above the header row.
For a carrier footprint using header pad 1 as `(0,0)` with Y still upward:
- Header pads: `(2.540 × (N - 1), 0)`.
- Mounting hole centres: **(1.905, 8.763)** and **(26.035, 8.763)**.
- Bare-board bounding box: X = **−1.905…29.845**, Y = **−2.667…26.670**; retain corner arcs described above.
If the destination CAD uses screen coordinates with Y downward, transform Y deliberately; do not silently mirror physical pad numbering. The placement side and whether the module faces up or down must be fixed before generating a carrier footprint.
### DE-9 on the downloaded female CAD (not male qualification)
`X2` origin `(15.875,17.907)`, R0, package `F09HP`, value `DE9-F`.
| X2 pad | Board X (mm) | Board Y (mm) | CAD net |
|---|---:|---:|---|
| 1 | 10.3886 | 16.637 | RS_DCD |
| 2 | 13.1318 | 16.637 | RS_RXD |
| 3 | 15.8750 | 16.637 | RS_TXD |
| 4 | 18.6182 | 16.637 | RS_DTR |
| 5 | 21.3614 | 16.637 | GND |
| 6 | 11.7602 | 19.177 | RS_DSR |
| 7 | 14.5034 | 19.177 | RS_RTS |
| 8 | 17.2466 | 19.177 | RS_CTS |
| 9 | 19.9898 | 19.177 | RS_RI |
| G1 | 3.3528 | 17.907 | no signal contactref |
| G2 | 28.3972 | 17.907 | no signal contactref |
Signal holes have 1.016 mm drill and 1.778 mm octagonal copper; shell/mechanical pads G1/G2 have 3.302 mm drill and 5.080 mm copper, spaced 25.0444 mm. Shell pads are not shown tied to GND by signal contactrefs; DE-9 pin 5 is grounded. These are **PCB solder-tail coordinates**, not front-face pin coordinates.
The connector package's layer-21 drawing reaches local Y = 17.907, hence board Y = 35.814, beyond the board's Y = 29.337. This is library drawing evidence, **not a validated mechanical envelope**. Do not size an enclosure from silkscreen. Product pages list assembled dimensions of 33.6 × 32.0 × 14.1 mm (5988) and 33.7 × 31.8 × 14.2 mm (6253), with no coordinate/tolerance definition; these do not replace a male-specific mechanical drawing, and differ from the female library drawing extent.
## Validation and next-use boundary
- Parsed the actual Eagle board and schematic XML successfully using Python's standard-library XML parser.
- Joined package pad names to board signal contactrefs, applied JP2's unrotated element translation, and cross-checked schematic header numbering/net assignments.
- Extracted outline arcs, mounting-hole package dimensions, and connector pad data directly; no dimensions inferred from product photos.
- Recorded SHA-256 hashes of the downloaded originals.
- No firmware build or hardware tests performed: this task changed research/reference files only.
- **Ready:** exact published 5988 header map, pad centres, board outline and hole coordinates for CAD reference.
- **Still required for a male-module carrier release:** establish that the purchased module is 6253, verify its header order and mounting geometry against male-specific CAD or the physical module, and verify connector overhang, mating clearance, module height and mounting orientation. The shared guide alone is insufficient proof of identical male geometry.
+110
View File
@@ -0,0 +1,110 @@
# Button selection: Omron B3F-1000
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Verified 2026-09-20 against the Omron datasheet and installed KiCad libraries. This research covers three active-low GPIO-to-GND buttons.
**Integration decision:** the A0 schematic now implements B3F-1000 switches with one **2.2 kΩ, 1%, at least 0.125 W external pull-up** per input. The lower-current/internal-pull-up-only discussion below records the selection rationale, not an unselected schematic option. Firmware is unchanged. Exact resistor MPN, supply tolerance/budget and stock remain release checks.
## Schematic-ready recommendation
**B3F-1000 fits the installed footprint mechanically and its normally-open contact topology matches the circuit. Low-current operation with internal pull-ups needs the qualification below.**
For each of the three buttons:
- Value / manufacturer part number: **B3F-1000**, Omron; standard silver-plated contacts, ivory flat plunger, no separate ground terminal.
- Symbol: **`Switch:SW_Push`** (normally open, two passive pins).
- Explicitly assign footprint: **`Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x`**. The installed symbol's default footprint field is empty.
- Symbol pin **1 → GPIO**, symbol pin **2 → GND**. The switch is nonpolar; swapping these two nets is electrically equivalent.
| Function | Pin 1 net | Pin 2 net |
|---|---|---|
| Previous/back | GPIO10 | GND |
| Select/confirm | GPIO13 | GND |
| Next | GPIO14 | GND |
GPIO assignments verified in `src/board_pins.h`. The PCB README specifies normally-open buttons to GND with internal pull-ups. Released = high; pressed = low. No voltage source connects directly through the switch: any external pull-up goes through a resistor to 3.3 V.
## Exact terminal and pad mapping
The installed footprint has **four physical holes but only two distinct electrical pad numbers: `1,1,2,2`**, not four uniquely numbered pads. The installed `SW_Push` symbol has pin 1 at the left and pin 2 at the right in its default orientation; pin numbers are hidden by the symbol.
Unrotated footprint, front/top view; local coordinates in mm, positive Y downward:
```text
6.5 mm
KiCad pad 1 (0,0) ----------- KiCad pad 1 (6.5,0)
Omron terminal 4 Omron terminal 3
NO
contact 4.5 mm
Omron terminal 2 Omron terminal 1
KiCad pad 2 (0,4.5) --------- KiCad pad 2 (6.5,4.5)
```
The horizontal connections above are permanent internal connections; pressing joins the two rows. Omron datasheet **page 4**, upper-left **“Terminal Arrangement/Internal Connections (Top View)”**, explicitly shows terminals **4–3 common** and **2–1 common**. This maps to KiCad's upper pair numbered 1 and lower pair numbered 2 when aligned as shown. **Omron terminal 1 is not KiCad pad 1 in this orientation.** The manufacturer's separate bottom-view numbering sketch must not be mistaken for a top view. Omron states terminal numbers are not marked on the switches. A 180° insertion swaps the two electrical sides without affecting this nonpolar circuit.
Both pads numbered 1 receive the symbol pin-1 net; both pads numbered 2 receive pin-2's net. Do not attach a four-unique-pin symbol to this footprint without an explicit remapping. Connecting GPIO and GND to two terminals within the same common pair would permanently ground the GPIO.
The footprint sets `duplicate_pad_numbers_are_jumpers no`; do not assume PCB routing software will treat the internal metal as a routing jumper. Route/check the duplicate-pad nets normally.
## Manufacturer dimensions and ratings
Source: Omron B3F datasheet, ordering table p. 1, ratings/operating characteristics p. 3, B3F-1000 drawing p. 4. Dimensions below are mm.
| Item | Datasheet specification |
|---|---|
| Body | 6 ±0.2 × 6 ±0.2 |
| Overall height above seating plane | 4.3 ±0.2 |
| Plunger diameter | 3.5 |
| Terminal row spacing | 4.5 ±0.2 |
| Formed terminal span at hole-entry region | 6.5 ±0.5 |
| Maximum-width dimension shown across formed leads | 7.7 ±0.5 |
| Lead projection below seating plane | 3.5 |
| Lead thickness / width callouts | 0.3 / 0.7 |
| Recommended PCB hole-centre grid | 6.5 ±0.1 × 4.5 ±0.1 |
| Recommended PCB holes | Four, diameter 1 ±0.1; reference drawing for PCB thickness 1.6 |
| General dimensional tolerance | ±0.4 unless otherwise specified |
| Contact form | SPST-NO |
| Rated resistive load, standard silver version | **1–50 mA at 3–24 VDC** |
| Minimum applicable load | **10 µA at 1 VDC, reference value** |
| Initial contact resistance | 100 mΩ maximum |
| Bounce | 5 ms maximum |
| Operating force | 0.98 ±0.29 N (100 ±30 gf) |
| Release force | 0.2 N minimum |
| Pretravel | 0.25 +0.2/−0.1 |
| Durability for B3F-1000, 0.98 N | 1,000,000 operations minimum |
| Protection / washing | IP00 / washing not possible |
### Installed footprint comparison
Inspected `/usr/share/kicad/footprints/Button_Switch_THT.pretty/SW_TH_Tactile_Omron_B3F-100x.kicad_mod` (file format version `20260206`, generator version `10.0`) and `/usr/share/kicad/symbols/Switch.kicad_sym`.
- Hole centres: `(0,0)`, `(6.5,0)`, `(0,4.5)`, `(6.5,4.5)` — match Omron's nominal PCB grid.
- All four plated through-hole pads: circular copper diameter **1.7**, drill **1.0**, front/back copper and mask — nominal drill matches Omron's reference hole diameter. Confirm the fabricator's finished-hole tolerance when ordering.
- Fab body: `(0.25,-0.75)` to `(6.25,5.25)`, **6 × 6**, matching nominal body dimensions.
- Courtyard: `(-1.1,-1.1)` to `(7.6,5.6)`, **8.7 × 6.7**. This is a library placement boundary, not an enclosure or finger-access clearance specification.
- Footprint description specifies H4.3 mm. No physical sample fit, enclosure-height check, or independent 3D-model measurement was performed.
## Low-current qualification / selection decision
**Do not claim internal-pull-up operation meets the standard part's full rated-load range.** At 3.3 V, pull-up resistance greater than 3.3 kΩ gives less than 1 mA contact current. For illustration only, 45 kΩ would give about 73 µA; that is not a verified resistance specification for this board's GPIOs.
Omron's 10 µA / 1 V minimum is explicitly a **reference value**, not an unconditional low-current reliability guarantee. B3F-1000 is a reasonable prototype choice, but internal-pull-up-only contact reliability should be qualified in the intended environment and over life.
For a conservative silver-contact design within the stated rated load, consider **one external 2.2 kΩ pull-up to 3.3 V per button** (about 1.5 mA while pressed); verify supply/resistor tolerances and GPIO limits before adopting. This recommendation was subsequently adopted in the A0 carrier schematic; see the integration decision above. If low current is important, the datasheet also lists **B3F-1002-G** with gold contacts in the same p. 4 dimensional group, rated 100 µA–50 mA at 3–24 VDC, but with a different operating force (1.76 ±0.49 N); weak internal pull-ups are not automatically within that rated range either. It has not been selected or stock-checked here.
Retain firmware debouncing; a mechanical switch is not bounce-free. The device is unsealed (IP00), and the datasheet prohibits washing.
## Supply evidence
**Live stock not verified; no supply assurance.** On 2026-09-20 a request to the [DigiKey B3F-1000 product URL](https://www.digikey.com/en/products/detail/omron-electronics-inc-emc-div/B3F-1000/33150) returned HTTP 403 / an anti-bot challenge. No quantity, price, lead time, or lifecycle status could be established from that response. Confirm the exact MPN and available quantity with a distributor before BOM release; datasheet availability is not stock evidence.
## Evidence and remaining checks
- [Authoritative Omron datasheet](https://omronfs.omron.com/en_US/ecb/products/pdf/en-b3f.pdf), retrieved 2026-09-20.
- [Local PDF](../../../../hardware/PCB/reference/button-omron-b3f.pdf).
- [Extracted text](../../../../hardware/PCB/reference/button-omron-b3f.txt) for searching; use the PDF for drawing interpretation.
- [Rendered page 4](../../../../hardware/PCB/reference/button-omron-b3f-page4.png), visually checked for the internal connections and dimensions.
Before manufacturing: validate the selected pull-up strategy against supply tolerances/budget; check ERC/net assignment and PCB DRC in the actual design; confirm finished holes, enclosure/actuator access and a sample's fit/continuity; confirm procurement. This review verifies datasheet-to-library compatibility, not a completed-board electrical or mechanical test.
@@ -0,0 +1,136 @@
# DE-9 mechanical reference — pinned Adafruit 5988 STEP
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Research: 2026-09-21. **Nominal CAD geometry, not a toleranced connector drawing or physical fit qualification.** No board, schematic, footprint, application dependency or main README changes.
**User-confirmed assumption:** male and female modules have identical external dimensions; use the external geometry below for mechanical planning of the male module on that basis. This is not independent male-CAD verification and **does not verify the male pin map**. The source explicitly identifies `DE9-F` / `F09HP` (5988). Do not infer contact numbering from gender, the model, or the front view.
## Sources, attribution and licensing
- User's link: https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter
- Pinned repository revision: **`5b8a21fb5e2e48478faf8901077be812f5224066`**, 2024-09-05, Noe Ruiz, “Adding 5988 RS-232 Level Shifter”. GitHub path history returned this as the latest change to that directory at retrieval.
- Immutable directory: https://github.com/adafruit/Adafruit_CAD_Parts/tree/5b8a21fb5e2e48478faf8901077be812f5224066/5988%20RS-232%20Level%20Shifter
- Unmodified originals retained in `../../../../hardware/PCB/reference/db9-cad`: STEP, upstream repository-root `../../../../hardware/PCB/reference/db9-cad/README.md`, upstream repository-root `../../../../hardware/PCB/reference/db9-cad/LICENSE`. `../../../../hardware/PCB/reference/db9-cad/sources.json` records **full immutable raw URLs and SHA-256 hashes**.
| Original | SHA-256 |
|---|---|
| `../../../../hardware/PCB/reference/db9-cad/5988 RS-232 Level Shifter.step` | `8eab9dc591a985fc987ceec2d7534e5e259263c73bebc64c0c3cd847e135906f` |
| `../../../../hardware/PCB/reference/db9-cad/LICENSE` | `f60436644e066301d4c2e8f1f0674a157b1f60e4fd5586717b99dde255cc18cd` |
| `../../../../hardware/PCB/reference/db9-cad/README.md` | `9ed9beac7600623b7b9bd0d02e1fdcd86610ce22966723179e6c6b618e399dce` |
The CAD repository root license is **MIT, Copyright (c) 2016 Adafruit Industries**; root README merely describes STEP/Fusion/STL parts. No part-specific license file is present in the four-file directory. Retain the original MIT notice and warranty disclaimer with redistribution. This assessment follows the repository's stated license, not an independent audit of any imported vendor geometry. F3D, STL and JPG were listed but not downloaded or used; no image-derived dimensions or mesh fallback were needed.
The separate Eagle board reference is from `Adafruit-RS-232-Level-Shifter-Breakout-PCB`, commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`; its local `.brd` hash was rechecked (`6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`). Eagle-derived registration/hole information is attributed to **Limor Fried/Ladyada for Adafruit Industries**, under **CC BY-SA 3.0 Unported**; retain `../../../../hardware/PCB/reference/README.md` and `../../../../hardware/PCB/reference/license.txt`. The Eagle-derived portions of these notes use that license. Do not silently substitute the CAD-parts repository's MIT license for the Eagle design license.
## Extraction method and confidence
Used Python/OpenCascade (`cadquery-ocp==7.9.3.1.1`) in the isolated project `.cache/cad-tools/venv`; no application dependencies changed. The initially attempted older OCP wheel was unavailable for this Python; the pinned installed version succeeded.
- `../../../../hardware/PCB/reference/db9-cad/extract.py`: STEPControl import with assembly placements applied; `BRepBndLib.AddOptimal` with triangulation and shape-tolerance enlargement disabled; analytic planes/cylinders via `BRepAdaptor_Surface`. No tessellation used for measurement.
- `../../../../hardware/PCB/reference/db9-cad/geometry.json`: all 15 solids and their faces, bounds, validity, analytic cylinder radii/axes, plane normals. All 15 pass `BRepCheck_Analyzer`.
- `../../../../hardware/PCB/reference/db9-cad/validate.py`: checks original hashes, all 12 header centres against actual Eagle XML, board bounds, mounting holes and selected front-face geometry; writes `../../../../hardware/PCB/reference/db9-cad/measurements.json` containing explicit face selections and their bounds.
- For unique PCB/connector bodies, STEP `CLOSED_SHELL` face order is associated with import face order, checking face counts, every surface type and every cylinder radius. Appearance assignments corroborate feature identification; **colour is not a material certification**.
- Imported solid 12 = STEP `#101` / `Body2`, the main connector. It fuses housing/insert, contacts, flange, shell and front hex posts; its whole-solid bounds are **not metal-only dimensions**. Solids 13/14 = `#102`/`#103` (`Body3`/`Body4`), separate anchor brackets.
- Front shell/flange/hex-post faces have the shared `Opaque(245,245,246)` appearance and the expected geometric continuity. The report measures those **face subsets**, excluding plastic, contacts, PCB and ICs. There is no separate hollow metal shell solid from which to certify wall thickness.
**High confidence:** numerical nominal positions/bounds/radii of identified modeled features and STEP-to-Eagle board registration. **Moderate confidence:** physical fidelity of the simplified connector model and material boundaries; the contact/tail geometry differs from Eagle. **Unverified:** manufacturing tolerances, actual purchased-part fit, threads, male electrical mapping, cable/backshell envelope and final enclosure cutout. Extra decimal places below support reproducibility, not manufacturing accuracy.
## Coordinate system and orientation
All dimensions are **mm**. Use `(X,Y,Z)` in the imported STEP assembly:
- X rightward in component/top view; Y toward the connector/cable, Z above the PCB.
- Bare breakout PCB: **X 0…31.750, Y 0…29.337, Z 0…1.570**. PCB front edge is **Y = 29.337**; underside is Z = 0, component-side surface Z = 1.570.
- Logic header hole row: **Y = 2.667**, X = `1.905 + 2.540 × (N−1)`, N = 1…12. No installed header/socket body is modeled; this is the hole-axis datum, not a plastic-header edge.
- STEP XY is already Eagle board XY: **identity transform**, no scale, rotation or translation. Verified by all 12 header holes, both mounting holes and PCB bounds. STEP connector symmetry alone would not establish this registration.
- Existing carrier footprint coordinates (header pad 1 origin, Y downward): **`x = X − 1.905; y = 2.667 − Y`**. Height above module PCB top: **`h = Z − 1.570`**. Add the chosen module-top elevation later to get case/carrier height; socket/standoff stack is not yet selected.
- Connector front/mating approach is along Y, **parallel to the PCB**, not down into it. A cable approaches from +Y and inserts toward −Y. A front view looks along −Y with +Z up; +X then appears to the viewer's **left**, unlike component/top view. Do not reuse top-view left/right pin numbering as front-view numbering.
## Metal feature dimensions — selected subsets, not assembly bounds
The Z centre of the D profile and front screw-lock axes is **7.84762024**, or **6.27762024 above the PCB top**. Connector centre X = **15.875** (carrier-local x = **13.970**).
| Feature | X bounds | Y bounds | Z bounds | Nominal dimensions / evidence |
|---|---|---|---|---|
| Front metal flange, exposed faces | 0.475…31.275 | 27.007…27.407 | 1.572620…14.122620 | **30.800 wide × 12.550 high × 0.400 axial thickness**; solid 12 faces 8, 11–14 |
| D-shell straight outer wall region | 8.035981…23.714019 | 28.007…32.607 | 3.897620…11.797620 | **15.678038 wide × 7.900 high**, straight axial length **4.600**; faces 18–25 |
| D-shell outer surface including root blend and rounded front lip | 7.435981…24.314019 | 27.407…33.207 | 3.297620…12.397620 | **16.878038 wide × 9.100 high × 5.800 axial extent**; faces 18–25, 81–88, 103–110 |
| Left front hex screw-lock post | 0.719189…6.030811 | 27.407…32.207 | 5.547620…10.147620 | **4.600 across flats**, **5.311622 across corners**, **4.800 long**; faces 53–61, 89 |
| Right front hex screw-lock post | 25.719189…31.030811 | 27.407…32.207 | 5.547620…10.147620 | Same dimensions; faces 62–70, 90 |
The flange is centred laterally on the PCB, inset **0.475** from each side. Its lower edge is only **0.002620** above the modeled PCB top, upper edge **12.552620** above it. Preserve this tiny source offset in registration; do not interpret it as a designed assembly clearance.
The D-shell has sloping side planes at **10° from vertical** in the front cross-section (`|normal X| = cos 10°`, `|normal Z| = sin 10°`), corner-cylinder radii **1.700**, root/lip rounding radius **0.600**. The wider side is upper (+Z). The maximum 16.878038 × 9.100 envelope occurs at the **root blend**, not uniformly along the projecting nose. These are bounding dimensions of a D shape, not a rectangular cutout prescription. Shell wall thickness / actual hollow cavity geometry is **not verified** by this fused representation.
### Front cable screw-lock holes: actual modeled cylinders, not thread specifications
- Axes **parallel to Y**, at `(X,Z) = (3.375,7.84762024)` and `(28.375,7.84762024)`.
- **Axis spacing = 25.000**, symmetric ±12.500 about X = 15.875.
- Carrier-local axis x = **1.470 and 26.470**; post front plane y = **−29.540**.
- Solid 12 faces **60 / 69**, raw STEP faces **#13822 / #13831**, are inward-facing cylindrical hole surfaces: **radius 1.500, diameter 3.000**, Y = **27.407…32.007** (straight cylindrical length **4.600**).
- Mouth cones (faces **89 / 90**) run Y = **32.007…32.207**, growing to **diameter 3.400** at the front: **0.200 axial × 0.200 radial** chamfer. Front mouth plane **Y = 32.207**.
- Flat modeled bottoms at Y = **27.407** (faces 61/70): total modeled mouth-to-bottom depth **4.800** including chamfer.
- **No helical threads modeled. Do not call this M3, 4-40, a tap drill, minor/major thread diameter, or usable engagement depth.** Actual screw standard, pitch, engagement and tolerances require a connector drawing or physical check.
## Front planes, PCB overhang and header offsets
There is no single interchangeable “front plane”. The flange, hex-post mouth and shell lip differ. The shell's foremost geometric plane is a useful **mating-face reference**, but the source does not establish a manufacturer's functional fully-mated datum or panel seating specification.
| Reference plane | STEP/Eagle Y | Forward of PCB front edge Y=29.337 | Forward of header row Y=2.667 | Carrier-local y |
|---|---:|---:|---:|---:|
| Flange rear face | 27.007 | −2.330 | 24.340 | −24.340 |
| Flange front face / shell root | **27.407** | **−1.930** | **24.740** | **−24.740** |
| Straight shell begins | 28.007 | −1.330 | 25.340 | −25.340 |
| Breakout PCB front edge | **29.337** | 0 | **26.670** | **−26.670** |
| Front hex-post mouth faces | **32.207** | **+2.870** | **29.540** | **−29.540** |
| Straight shell ends / rounded lip begins | 32.607 | +3.270 | 29.940 | −29.940 |
| Foremost shell lip / modeled insert face | **33.207** | **+3.870** | **30.540** | **−30.540** |
Thus the modeled shell nose projects **3.870 beyond the breakout PCB**; the hex posts project **2.870**, ending **1.000 behind the shell nose**. The flange itself is **1.930 behind the PCB front edge**. Do not position a case wall at the flange while ignoring that the PCB extends 1.930 beyond it.
For later unrotated, component-up carrier placement with header pad 1 at `(x0,y0)`:
- Breakout front edge: `y0 − 26.670`.
- Flange front: `y0 − 24.740`.
- Hex mouths: `y0 − 29.540`.
- Shell nose: `y0 − 30.540`.
- If the **breakout PCB front edge** is aligned with carrier top edge `ytop`, choose `y0 = ytop + 26.670`; shell nose is then at `ytop − 3.870`, hex mouths at `ytop − 2.870`.
- If instead the **shell nose** is aligned with that edge, choose `y0 = ytop + 30.540`; breakout front edge is then 3.870 inward. These are different choices, not a recommended final layout.
## Three distinct mechanical systems — do not interchange
| System | Axes / coordinates | Spacing | Diameter or geometry | Purpose |
|---|---|---:|---|---|
| Front cable screw-lock holes | Y axes; X 3.375 / 28.375, Z 7.847620 | **25.000** | Modeled bore Ø3.000, mouth Ø3.400; no thread qualification | Retain mating cable at connector front |
| Breakout PCB mounting holes | Z axes; `(3.810,11.430)` / `(27.940,11.430)` | **24.130** | Eagle drill Ø3.000 / plated copper Ø3.600; STEP hole cylinders Ø3.000 | Mount module to carrier/standoffs |
| Connector shell/anchor PCB holes G1/G2 | Z axes; Eagle `(3.3528,17.907)` / `(28.3972,17.907)` | **25.0444** | Eagle drill Ø3.302 / copper Ø5.080; STEP rounds X to 3.353 / 28.397 | Receive connector anchor legs, **not** carrier mounting or cable screws |
### Separate metal anchor brackets and underside clearance
Solids **13/14** represent the bent anchor brackets/legs, not the front hex posts. Per bracket whole-solid bounds (leg-only components, not whole assembly):
- Right: **X 25.875…30.875, Y 17.907…24.307, Z −2.230…10.122620**.
- Left: **X 0.875…5.875**, same Y and Z.
- Each bracket envelope: **5.000 wide × 6.400 deep × 12.352620 high**. Bent sheet regions are **0.500 thick**; the lower two fingers occupy Y = **17.907…18.407**, not a circular screw shank.
- Tips reach **2.230 below the module underside**, i.e. **3.800 below the module top**. This needs carrier/case underside clearance; do not turn these bounds into a mounting-hole drill recommendation.
- Bracket symmetry centres X = **3.375 / 28.375**, lower sheet midplane Y = **18.157**. Relative to Eagle G1/G2, centre offsets are respectively **+0.0222 / −0.0222 in X** and **+0.250 in Y**. These are source-model differences, not a reason to move the Eagle pads.
- Main fused body's signal tails separately reach Z = **−1.607380**. Their modeled rows are Y = **16.467 / 19.307**, versus Eagle **16.637 / 19.177**; horizontal pitch also differs (model 2.770 versus Eagle 2.7432). The 3D model is **not suitable as electrical pin/drill authority**.
No complete all-metal rear housing reconstruction is claimed: only the explicitly selected front surfaces and separate anchor brackets above. Main solid 12's 30.800 × 18.200 × 15.730 bounds include plastic/contact tails and must not be relabeled as a shell-only envelope.
## Validation, reproduction and release boundary
From project root:
```sh
.cache/cad-tools/venv/bin/python -B hardware/PCB/reference/db9-cad/extract.py
python3 -B hardware/PCB/reference/db9-cad/validate.py
```
Both successfully ran; validation reported PASS for original hashes, 15 valid solids, all 12 Eagle header centres, PCB bounds, mounting holes, metal-appearance subsets and front lock cylinders. Extraction artifacts retain unrounded floating-point values. No firmware build, board DRC, hardware measurement or physical mating test was performed (research-only task).
**Ready for nominal placement planning:** metal flange dimensions, shell outer envelope, front hex-post dimensions and bore geometry, their distinct front planes, 25.000 front-axis spacing, PCB/header registration and overhang, anchor-leg underside extent. The A1 PCB now aligns the breakout front edge with the carrier top edge. Nominal metal bounding projections and screw-lock axes appear on Dwgs.User; companion layout/footprint notes have been updated. Neither projection nor nominal bound is an enclosure cutout or clearance qualification.
**Before fabrication / final case design:** verify actual male pin map; check physical connector profile and overhang against these nominal values; select header/socket and standoff heights; establish manufacturing/assembly clearances, cable backshell/tool access, wall thickness and cutout shape; verify thread standard and retention; perform a 1:1 fit/mating check. In particular, do not use exact-fit nominal bounds as an enclosure clearance or assume the CAD's Ø3.000 lock bore specifies a real screw thread.
+108
View File
@@ -0,0 +1,108 @@
# Provisional carrier module footprints
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
These project-local footprints are in `../../../../hardware/PCB/Carrier.pretty`. They are mechanical
starting points, **not fabrication-ready connector selections**. Both are now
assigned to their matching symbols in the native schematic through the project
library tables and used in the routed PCB draft. The assembled module 3D models
are not provided. See `pcb-draft-notes.md` for placement/routing and
`rs232-footprint-notes.md` for the additional provisional RS-232 reference footprint.
## Shared pad/drill provenance and limits
Reference inspected locally: installed KiCad library footprint
`Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical`, file
`/usr/share/kicad/footprints/Connector_PinHeader_2.54mm.pretty/PinHeader_1x04_P2.54mm_Vertical.kicad_mod`
(format version `20260206`; local KiCad `10.0.6-1.fc44`).
Both footprints use that reference's **1.70 × 1.70 mm plated through-hole pads,
1.00 mm drills, rectangular pad 1, and circular remaining pads**, on
`*.Cu` and `*.Mask`, with unused copper layers retained. Nominal pitch is 2.54 mm.
Only the reference's pad/drill convention is reused, not its connector body,
courtyard, or 3D model. HW678 pad 23 is circular: only overall pad 1 is rectangular.
**PROVISIONAL connector fit — awaiting the selected actual pin/socket datasheet.**
Breadboard fit supports centre spacing, not drill size, finished-hole tolerance,
annular-ring suitability, socket body clearance, mating height, or solderability.
Validate those against the actual final headers and beta-test sockets and the PCB
fabricator's process before release. No electrical nets or ESP32 GPIO assignments
are inferred. No courtyards are supplied: module/connector assembly clearances
remain incomplete, so automated courtyard checks cannot establish fit.
Coordinates below are millimetres, unrotated on the carrier front side, X right
and Y down. Views are not mirrored. Placement on the back side or viewing the
module from its solder side requires the appropriate KiCad transform; do not
silently reverse the numbering.
## `Carrier:OLED_26mm_I2C_Provisional`
Origin: top-left corner of the module PCB, viewed from the front/display side,
header at the top. User-verified geometry from the current request and the later
mechanical-evidence section of `../../../../hardware/PCB/README.md`:
| Pad | Signal | X | Y |
|---|---|---:|---:|
| 1 | GND | 9.19 | 1.50 |
| 2 | VCC | 11.73 | 1.50 |
| 3 | SCL | 14.27 | 1.50 |
| 4 | SDA | 16.81 | 1.50 |
- PCB boundary: (0, 0) to (26, 26), solid rectangle on `F.Fab`.
This depicts the module, not the carrier's routed edge; no `Edge.Cuts` is used.
- Four unnumbered, non-plated through holes, diameter 2.00 mm, at
(1.75, 1.75), (24.25, 1.75), (1.75, 24.25), (24.25, 24.25).
Hole centres are 1.75 mm from adjacent edges, with 22.50 mm spacing in both axes.
These transfer the verified module hole geometry to the carrier; they do not
select screws, inserts, bosses, or hardware clearance envelopes.
- Display boundary: (1, 4.5) to (25.5, 21), 24.5 × 16.5 mm, dashed on `F.Fab`.
This is the supplied display boundary, not a verified active-pixel area or case cutout.
- Pad-to-signal mapping is documented here and in the footprint description;
footprint pads themselves have numbers, not assigned nets.
The approved geometry supersedes the earlier 22.16 mm hole-spacing measurement:
22.50 mm hole spacing, Y=1.50 mm header centres, and the centred header coordinates
above apply. See README for the user-reported successful template fit check.
## `Carrier:HW678_2x22_Provisional`
Component/top view: antenna up, USB connectors down. Origin is the top-left header
centre (pad 1), **not a PCB corner**.
- Left row: pads 1 through 22, top to bottom, X=0.
- Right row: pads 23 through 44, top to bottom, X=25.40.
- Y=0 through 53.34 in 2.54 mm increments for each row (21 intervals).
- User breadboard fit verifies nominal 2.54 mm pitch and 25.40 mm row spacing;
these supersede the earlier raw caliper dimensions in README.
- Numbering follows the project's explicit convention, not a generic dual-row
connector's alternating or counterclockwise numbering. Physical numbers do
**not** establish GPIO, supply, or ground assignments.
- Only pads and annotation text are present. **No PCB outline, antenna keepout,
courtyard, USB envelope, mounting holes, or 3D model is guessed.** Text positions
are annotations, not dimensional evidence. Outline registration, antenna extent,
connector offsets/overhang, and assembly height remain unknown.
## Validation and release gates
Validation performed with KiCad 10.0.6 `pcbnew.FootprintLoad`: both footprints
parsed successfully. Assertions passed for pad counts, all numbered pad positions,
pad shapes/types/sizes/drills, OLED NPTH positions and diameters, both OLED `F.Fab`
rectangle bounds, and absence of courtyards, `Edge.Cuts`, and keepout zones.
HW678 was also checked to contain no graphical shapes. The local Python binding
emitted three startup `PROPERTY_ENUM` assertions (also seen on a version-only
import); these did not prevent loading or the checks from passing.
Repeat the retained footprint regression checks from the repository root with:
```sh
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
```
This requires the system Python with KiCad's `pcbnew` binding. It checks the
native-loaded pad counts, coordinates, pad types, sizes, drills and geometry. Parsing is
not DRC, electrical verification, connector fit verification, or fabrication approval.
Print both footprints at 1:1 and check the real assemblies, especially HW678 row
numbering/orientation. Before layout release, select and verify connectors, map
HW678 pins from reliable board evidence, and establish module envelopes,
antenna restrictions, USB access, mounting hardware, and courtyards.
+246
View File
@@ -0,0 +1,246 @@
# Compact two-layer PCB — A1 draft
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Open **`../../../../hardware/PCB/serial-carrier.kicad_pcb`** in KiCad 10. The routed board belongs to the
existing `../../../../hardware/PCB/serial-carrier.kicad_pro` / `../../../../hardware/PCB/serial-carrier.kicad_sch` project. **Not for fabrication.**
No Gerbers, production drills or manufacturing archive were generated.
## Size and placement
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
- **Two copper layers**, F.Cu and B.Cu. All components mount on the front.
- Proposed **1.6 mm** carrier thickness; fabrication stackup/process not selected.
- Stepped outline: a **36 × 19 mm top-left antenna cutaway** fits within a simple
rectangular/square case. The lower edge is now straight, with ESP32 USB toward
the bottom and RS-232 toward the top. U1 moved down by 19 mm; its exact USB/body
registration remains provisional pending the user's measured ESP32 geometry.
- Module and mating-plug overhangs are not included in the carrier dimensions.
The final complete assembly/enclosure envelope still needs measurement.
Coordinates in the following table are local millimetres from the carrier's
upper-left corner, X right/Y down, viewed from the component side. In the KiCad
file add (100, 100) mm to obtain absolute board coordinates. All footprints are
unrotated; their origins are defined in the footprint notes, not necessarily
component body corners.
| Reference | Local origin (X, Y) | Placement |
|---|---|---|
| U1 | (8, 23) | ESP32 left header pin 1; antenna toward top-left cutaway, USB toward bottom edge |
| U2 | (43, 26.670) | MAX3243 header pin 1; bare module PCB front exactly flush with carrier top edge |
| DS1 | (43, 36) | OLED module upper-left PCB corner; display faces up |
| R1 / R2 / R3 | (43 / 54 / 65, 34) | Axial pull-ups in the gap between RS-232 and OLED |
| SW1 / SW2 / SW3 | (43 / 55 / 67, 68) | Back, select, next below display; switch pad 1 origins |
| H1 | (3.5, 22.5) | Proposed case mounting hole below antenna notch |
| H2 | (3.5, 75) | Proposed case mounting hole near bottom-left edge |
| H3 | (78, 4) | Proposed case mounting hole |
| H4 | (78, 76) | Proposed case mounting hole |
Outline vertices: (36,0), (82,0), (82,80), (0,80), (0,19), (36,19), closed.
The previous A0 board is retained in `../../../../hardware/PCB/validation/pcb-a0-before-reposition.kicad_pcb`.
The board area is **58.76 cm²**; its bounding rectangle is 65.6 cm².
H1–H4 are proposed **Ø3.2 mm NPTH M3 clearance holes**, not a previously agreed
case specification. They are marked board-only and excluded from the electrical
BOM. The OLED and RS-232 module mounting holes are additional, separate holes.
Screw heads, spacers, heat-set inserts, tool access and underside lead clearance
must be checked against the final case and assembly heights. No insert dimensions
or module stand-off heights are assumed.
## Routing and electrical status
The PCB contains actual copper, not just ratsnest placement:
- All **38 required physical-pad spanning connections** across **17 connected
nets** routed, including both duplicated pads of each switch contact.
- **565 segments**, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
- **22 through vias**, **0.70 mm copper / 0.30 mm drill**.
- **No copper pours**; ground is explicitly routed. This is an initial routing
solution, not a reviewed return-current/EMI or power-integrity design.
- Proposed minimum different-net clearance **0.25 mm** and copper-edge clearance
**0.50 mm**. These are selected draft design rules, not fabrication qualification.
- Project netclasses align interactive routing defaults with the draft: Default
uses 0.25 mm tracks / 0.25 mm clearance, while Power matches `/+3V3` and `/GND`
with 0.50 mm tracks. Both use 0.70 / 0.30 mm vias.
- Schematic pad-net assignments, component values and UUID linkage are retained.
U1's intentionally unused GPIO/USB/UART0/5 V header pads remain unused.
The bounded draft router prioritizes connectivity and conservative geometric
clearance, not optimal trace topology. Do not treat DRC passage as evidence of
current-carrying capacity, supply stability, I²C signal quality or EMC compliance.
## Explicit provisional mechanical choices
### ESP32 and RF
A two-layer **no-tracks/no-vias/no-pads/no-zone-fill rule area** reserves local
X=10…31 mm, Y=19…32 mm beneath the antenna-side end of U1, adjacent to the notch. This is a deliberate carrier design
allocation, **not a measured antenna envelope or a manufacturer RF keepout**.
The module itself may span the rule area; footprint placement is not forbidden.
No carrier copper enters the reserve, but copper runs close to its boundary.
U1's exact PCB/antenna/USB offsets are still unmeasured. The intended antenna
overhang and USB access must be checked with the actual board and a printed
placement template. Move U1, change the edge or enlarge the reserve if required.
Missing RF clearance cannot be signed off from the current footprint alone.
### RS-232
U2 uses the new **`Carrier:MAX3243_Reference_Provisional`** footprint **on the PCB
only**. Its bare-module outline/header/holes are adapted from published female
5988 CAD for the intended male 6253 module. The user explicitly confirms identical
male/female external dimensions. The linked Adafruit STEP has now been measured
with OpenCascade; its nominal metal geometry is used on that basis. Male header
mapping, actual fit, tolerances and the mounting stack still need qualification.
See `rs232-footprint-notes.md` and `db9-mechanical-notes.md` for evidence/licenses.
The module's **bare PCB front is flush with carrier Y=0**, as requested. The
connector has distinct front planes, not one universal case-wall datum:
| Feature | Position relative to carrier top edge | Meaning |
|---|---:|---|
| Flange front / shell root | 1.930 mm inside | Behind the PCB edge |
| Breakout PCB front | 0 | Flush with the carrier edge |
| Hex screw-lock mouth faces | 2.870 mm outside | Front cable-retention hole mouths |
| Shell nose | 3.870 mm outside | Foremost modeled shell surface |
The front screw-lock axes are **25.000 mm apart**, with centres **6.277620 mm
above the module PCB top**. They are not the two 24.130 mm-spaced PCB mounting
holes. The source models Ø3.000 mm bores but **no threads**; this does not establish
M3 or 4-40 hardware. The main metal flange is 30.800 × 12.550 × 0.400 mm; the
shell envelope including its root/lip is 16.878 × 9.100 × 5.800 mm.
`../../../../hardware/PCB/add_db9_envelope.py` places the measured metal bounding projections and screw-lock
axes on **Dwgs.User**, relative to U2. They are not silkscreen, a courtyard or a
case cutout. Run the helper after moving U2; it preserves copper and project
settings. The original STEP and its extraction/validation scripts are retained.
**Case-wall positioning remains a design decision:** PCB-edge alignment does not
put the flange or screw-lock mouths at that same plane. For example, making the
hex mouths flush with the case exterior would place that exterior 2.870 mm beyond
the carrier edge. No wall thickness, cable-backshell clearance or sealing scheme
has been assumed. The model's anchor legs extend **2.230 mm below the module
underside**; select a stand-off/header stack or clearance solution before assembly.
The schematic footprint remains intentionally blank, preserving its caution
and avoiding edits to the schematic that was open in KiCad. Consequently there
is **one known schematic-parity warning**: U2 footprint differs from the empty
schematic assignment. This is not excluded/suppressed, and is not a net mismatch.
Once the male footprint is qualified, explicitly assign it in the schematic
before relying on Update PCB from Schematic to manage U2's footprint.
### OLED and controls
OLED geometry, including its display boundary, is the user-fit-verified geometry.
The module schematic/library files are unchanged. The new overall placement still
needs an assembly fit check: adjacent resistor bodies, module underside parts,
header/socket bodies, button actuation and finger/plunger clearance are not
represented by complete assembly courtyards. In particular the axial resistors
are tightly packed between the module outlines; confirm clearance and height.
## Device outlines on front silkscreen
The PCB now has **0.12 mm front-silkscreen outlines** for the OLED PCB and the
published RS-232 reference module, plus a dashed OLED display boundary. The
RS-232's visible rounded corners retain true arcs. Small gaps clear pad/mask
openings, vias and adjacent resistor markings; the original full F.Fab geometry
remains unchanged.
U1 instead has **header-row guides**, labelled `HEADER GUIDES`: guide edges are
1.20 mm outside the outer pin centres. These are graphic alignment aids, **not
claimed socket-body dimensions or an inferred ESP32 PCB outline**. The actual
U1 body/antenna/USB outline still requires measured offsets. Existing button and
resistor silkscreen remains unchanged.
These are board-local drawings, not edits to the footprint libraries. If the
module placement or routing changes, close PCB Editor and refresh them with:
```sh
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
```
The script requires pcbnew and Shapely, replaces only its own generated graphics,
and refuses an open PCB or concurrent board change. It leaves project settings,
copper, pads and source F.Fab outlines unchanged. Run it **after** placement or
routing regeneration; moving a footprint does not move these board-local drawings.
It currently supports the draft's unrotated module placements.
The original A0 silkscreen-only change left its routing unchanged. A1 deliberately
repositions U1/U2 and reroutes all connections, then refreshes these outlines.
Silkscreen is clipped away from the new flush U2 front board edge. Physical DRC
remains **0 violations**, **0 unconnected**, with the same U2 footprint-parity
warning. PDF and both previews have been refreshed.
## Review outputs
- `../../../../hardware/PCB/validation/pcb-draft.svg` / `../../../../hardware/PCB/validation/pcb-draft.png`: combined copper/outline/fab review.
- `../../../../hardware/PCB/validation/pcb-draft-3d.png`: KiCad render of the carrier and available standard
component models. **ESP32, OLED and RS-232 assembled module models are absent**;
the render is not an assembled-device or enclosure fit check.
- `../../../../hardware/PCB/pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at
100%, disable fit-to-page, and confirm the outer width is 82 mm before use.
It includes the known module outlines and nominal STEP-derived DE-9 metal
bounding projections; no guessed U1 body or case-wall geometry.
## Validation and limits
Run from the repository root with the system Python containing KiCad bindings:
```sh
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
```
The first script exports a **fresh saved-schematic netlist** and checks the actual
PCB pad assignments, component linkage, dimensions, layer count, nominal thickness,
mounting holes, track/via sizes and KiCad DRC. It never rewrites the PCB or project.
It reports the single expected U2 parity warning explicitly, and fails on any
other parity issue, DRC violation or unconnected item. Reports are retained in
`../../../../hardware/PCB/validation/pcb-parity-drc.json` and `../../../../hardware/PCB/validation/pcb-validation-summary.json`.
The second independently checks continuous copper/hole/edge geometry and original
placement/net/UUID preservation against the initial unrouted snapshot. It needs
locally installed NumPy and Shapely in addition to pcbnew. It is not a substitute
for current-schematic net validation and is specific to this initial layout.
Results with KiCad **10.0.6**:
- **0 physical DRC errors/warnings; 0 unconnected items.**
- All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**.
- Independent minimum different-net/copper-to-NPTH gap **0.270544 mm**; minimum
copper/NPTH-to-board-edge gap **1.900 mm**.
- All 9 electrical footprints and 4 board-only mounts retained; positions, outline
and RF area revised as documented, with electrical linkage preserved.
- Startup `PROPERTY_ENUM` assertions from the local pcbnew binding are still
emitted, but the checks complete successfully.
DRC uses the existing default ignored checks: `missing_courtyard`,
`track_not_centered_on_via`, `tuning_profile_track_geometries`,
`footprint_filters_mismatch`, `footprint_type_mismatch`. No new exclusions were
added to hide problems. **Missing module courtyards therefore remain an explicit
manual release gate**. The draft rules do not represent a chosen fabricator's
complete capability set. No electrical/hardware/fabrication tests were performed.
## Regeneration and project settings
Normal use is to **edit the native PCB in KiCad**, not regenerate it. The optional
`../../../../hardware/PCB/generate_pcb_draft.py` recreates the *unrouted* A1 placement and discards manual
PCB work only with explicit `--overwrite`; it refuses an open-project lock. A1 was
made by mutating the existing board, not running that generator. All A1 saves use
`SaveBoard(..., True)` to preserve user project settings. The original A0 creation
populated the minimal project with rules; this revision leaves those settings and
the schematic unchanged. Refresh device silkscreen and CAD envelope helpers after
any intentional regeneration or placement/routing changes.
`../../../../hardware/PCB/route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
existing tracks requires explicit `--route --overwrite --replace-routes`. Do not
run that over manual routing without a backup. It is a bounded initial-draft
router, **not** a general-purpose autorouter. See `validation/pcb-routing-summary.md`
for algorithm limits and commands. These scripts do not need downloaded packages
or services in the current environment and create no manufacturing outputs.
Before fabrication: qualify all module pin maps/footprints and connectors,
complete assembly courtyards and RF/USB/DE-9 checks, review power/return paths,
verify the existing USB power circuit and OLED pull-ups, confirm procurement and
case hardware, rerun ERC/DRC, and perform an independent hardware design review.
@@ -0,0 +1,122 @@
# MAX3243 carrier — PROVISIONAL female CAD reference / VERIFY MALE
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Footprint: `Carrier:MAX3243_Reference_Provisional` in
`../../../../hardware/PCB/Carrier.pretty/MAX3243_Reference_Provisional.kicad_mod`.
Intended only for a <=100 × 100 mm, two-copper-layer carrier PCB draft.
No schematic assignment or edits are made. This footprint does not establish
whole-board fit, connector access, DRC compliance, or fabrication readiness.
## Evidence and scope
The user reports that the male **6253** module has the same dimensions. This is
not confirmation of its pad map. Geometry below is taken from published **female
5988** Eagle CAD, not verified male-specific CAD. Confirm the purchased module's
identity, orientation, header numbering, outline and mounting geometry before
release. Never infer header order from DE-9 gender or mating-face numbering.
Source: [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB),
commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`.
Local original: `../../../../hardware/PCB/reference/Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd`;
SHA-256 `6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`
was rechecked against the file. See `adafruit-research.md` for provenance and
female/male distinctions.
CAD-derived geometry adapted from the design by **Limor Fried/Ladyada for Adafruit
Industries**, under **CC BY-SA 3.0 Unported**:
https://creativecommons.org/licenses/by-sa/3.0/ . The CAD-derived footprint and
these CAD-derived notes use that license. Preserve the full upstream notices in
`../../../../hardware/PCB/reference/README.md` and `../../../../hardware/PCB/reference/license.txt` when redistributing this adaptation.
Changes: coordinate transformation, carrier pad convention, NPTH carrier holes,
and provisional annotations; no module circuitry or connector package copied.
## View, origin and exact nominal geometry
Front/component top view, module facing up, DE-9 toward the top (**negative Y**).
Origin is header JP2 pin 1, not a PCB corner. X increases rightward; Y downward.
Transform Eagle board coordinates in mm with:
`x = X - 1.905; y = 2.667 - Y`.
- Pads 1…12: `(2.54 × (N - 1), 0)`, first-to-last span 27.94 mm.
- Full bare-module outline is on `F.Fab`, **not carrier `Edge.Cuts`**.
Bounds: X = -1.905…29.845, Y = -26.670…2.667 (31.750 × 29.337 mm).
Top corners are square. Bottom corners retain the source 2.540 mm radius arcs.
- Left arc: (-1.905, 0.127) to (0.635, 2.667), centre (0.635, 0.127).
Right arc: (27.305, 2.667) to (29.845, 0.127), centre (27.305, 0.127).
Midpoints use radius/sqrt(2), rounded to nine decimal places in the file;
the arcs are not chamfers or a bounding-box approximation.
- Two unnumbered **3.00 mm NPTH carrier holes**: (1.905, -8.763) and
(26.035, -8.763), spacing 24.130 mm. These transfer module-hole centres and
drill diameter only. The source module holes are actually plated, 3.60 mm
copper / 3.00 mm drill, unconnected in the source signal map. Carrier NPTH
deliberately does not copy that plating or assign electrical connections.
No screw, washer, standoff or tool-access clearance is implied.
## Provisional carrier pads and female reference map
Same convention documented in `footprint-notes.md` for the other carrier modules:
**1.70 × 1.70 mm PTH pads / 1.00 mm drills**, rectangular pad 1, circular pads
2…12, `*.Cu` and `*.Mask`, unused copper layers retained. That convention derives
from the installed KiCad `PinHeader_1x04_P2.54mm_Vertical`; it is not a selected
header/socket qualification. The source module itself uses 1.9304 mm round pads.
Confirm actual pins/sockets, finished-hole tolerances, annular rings and assembly
height before fabrication. Square pad 1 is a carrier marker, not source evidence.
| Pad | X (mm), Y=0 | Female label | Female CAD net |
|---:|---:|---|---|
| 1 | 0.00 | Vin | VCC |
| 2 | 2.54 | GND | GND |
| 3 | 5.08 | DCD | DCD |
| 4 | 7.62 | RX | RXD |
| 5 | 10.16 | TX | TXD |
| 6 | 12.70 | DTR | DTR |
| 7 | 15.24 | DSR | DSR |
| 8 | 17.78 | RTS | RTS |
| 9 | 20.32 | CTS | CTS |
| 10 | 22.86 | RI | RI |
| 11 | 25.40 | VLD | !INVALID |
| 12 | 27.94 | OFF | !FORCEOFF |
**This is the female reference map, NOT a confirmed male map.** Footprint pads
have numbers only, no assigned nets. OFF is active low; VLD indicates valid input
when high. This project's logic interface requires 3.3 V Vin.
## Intentionally omitted / release gates
The footprint F.Fab outline still describes only the bare module PCB. Subsequent
STEP B-rep measurements now establish nominal shell/flange/hex-post envelopes and
registration; see `db9-mechanical-notes.md`. The user explicitly confirms identical
male/female external dimensions. A1 board-local Dwgs.User projections show those
nominal bounds, without changing this footprint's pads or deriving drills from
the 3D model. Mating-plug/cable, case wall/cutout, assembly height and actual fit
remain unqualified. No guessed complete courtyard or 3D mounting offset is added.
There is **no fake courtyard**; courtyard checks cannot establish assembly fit.
Short silkscreen labels identify PROVISIONAL / FEMALE CAD REF / VERIFY MALE.
Before layout release: verify male pad map and orientation against the actual
module or authoritative male CAD; perform a 1:1 fit check; select headers and
mounting hardware; verify assembled and mating envelopes; then establish real
clearances/courtyard and check the complete <=100 × 100 mm two-layer layout.
## Validation performed
KiCad **10.0.6** `pcbnew.FootprintLoad` successfully loaded the footprint using
system Python (`/usr/bin/python3 -B`) in a read-only inline check. Assertions passed
for all 12 numbered pad positions against source JP2 package/element coordinates,
female signal-contactref order, pad sizes/drills/shapes and copper/mask presence,
no assigned header nets, both NPTH centres/sizes/drills, and all six transformed
source outline segments including both 90-degree, 2.540 mm radius arcs and centres.
Also checked absence of courtyard, Edge.Cuts, zones and 3D models.
The first check assumed arc endpoint order was preserved; KiCad normalizes arc
orientation. The corrected check matches endpoint pairs independent of order and
verifies radius, angle and centre; it passed without changing geometry. The
binding emitted the same three startup `PROPERTY_ENUM` assertions documented for
other module validation; these did not prevent loading or successful checks.
No hardware fit test, male electrical verification, board DRC or firmware build
was performed. Existing `../../../../hardware/PCB/validate_footprints.py` was not changed and does not
cover this new footprint. Only this notes file and the new footprint were written;
schematic, project and lock files were left untouched.
@@ -0,0 +1,256 @@
# A1 top-left-notch / bottom-USB routing — current result
Paths in prose point to the retained hardware artifacts or neighboring development notes.
Commands keep their stated working directory; repository-root paths remain root-relative.
Date: 2026-09-21. Actual revised board: `../../../../../hardware/PCB/serial-carrier.kicad_pcb`.
**38/38 connections across 17 multi-pad nets; 0 physical DRC violations,
0 unconnected items; exactly 1 expected U2 footprint parity warning.**
The warning is the board-only `Carrier:MAX3243_Reference_Provisional` assignment
versus the intentionally blank schematic footprint, not a net mismatch.
## Geometry and preservation
Coordinates below are local to absolute origin (100,100) mm; all dimensions mm.
- Carrier: 82 x 80, outline `[(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)]`.
- U1 origin: (8,23), formerly (8,4). Header end-pad centres now Y=76.340;
USB guide Y=83 is 3 mm below the bottom edge. No ESP32 body was invented.
- U2 origin: (43,26.670), formerly (43,30). Actual reference F.Fab bare-PCB
front edge is absolute Y=100.000, flush with carrier local Y=0.
- Nominal male metal external dimensions are user-confirmed: shell overhang
3.870, hex-mouth overhang 2.870, flange front 1.930 behind the carrier edge.
With this placement their local Y positions are -3.870, -2.870 and +1.930.
Integration added seven board-local Dwgs.User graphics for the CAD metal
projections and screw-lock axes.
- H1: (3.5,22.5), H2: (3.5,75). H3/H4 unchanged at (78,4)/(78,76).
- OLED, resistors and switches unchanged.
- RF reserve: local X=10..31 / Y=19..32, absolute X=110..131 / Y=119..132,
both layers; no tracks, vias or fills inside it. Boundary remains provisional.
The existing A0 board was mutated, **not regenerated**. The one-time
`../../../../../hardware/PCB/validation/pcb-a0-before-reposition.kicad_pcb` backup remains byte-for-byte intact.
`../../../../../hardware/PCB/validation/pcb-routing-input.kicad_pcb` is now the A1 unrouted audit input, not A0.
`../../../../../hardware/PCB/validation/pcb-routing-preservation.py` verified all 13 complete serialized footprints
identical except the four authorized top-level placements, including schematic
fields, pad/net definitions, electrical UUID paths and all footprint graphics.
Non-owned board-graphic UUIDs and unrelated graphics are preserved. ESP/RS/RF
annotations moved with placement; the post-routing silkscreen refresh replaced
91 owned graphics. Its edge clipping avoids silk across U2's flush front edge.
All board saves used `SaveBoard(..., True)`. Project `.pro`, `.prl`, schematic,
symbol/footprint libraries and library tables retain their original hashes.
README and pcb-draft-notes were intentionally updated during integration. The generator's constants/text were updated but it was **not
run**; its automatic project/netclass writes were removed. PCB Editor lock was
absent at migration, routing, silkscreen refresh and final preservation check.
## Routing and independent measurements
- F.Cu/B.Cu; **565 segments, 22 through vias**.
- **451 signal segments at 0.25 mm; 114 +3V3/GND segments at 0.5 mm**.
- Vias: 0.7 mm copper / 0.3 mm drill. No copper pours needed.
- Continuous measured minimum different-net/NPTH clearance: **0.270544 mm**.
- Minimum copper/NPTH-to-edge gap: **1.900 mm**.
- Minimum copper/NPTH-to-RF-reservation gap: **0.025 mm**.
- Minimum via-drill edge to other drilled-hole edge: **0.820544 mm**.
Two bounded passes, each with 240 seconds / 1,200,000 expansions per search:
1. 31.18 seconds, 38/38, 674 segments / 16 vias, physical DRC clean. One GND
connection required the router's 0.25 mm fallback; strict width validation
correctly failed. Retained as `../../../../../hardware/PCB/validation/pcb-routing-a1-pass1.kicad_pcb` for evidence.
2. 32.44 seconds, 38/38, 565 segments / 22 vias. Short GND connections (<=15 mm)
now route before longer signal fanout; long GND links remain last. All power
and ground now use 0.5 mm; no fallback. This is the saved current result.
The grid remains 0.05 mm, physical clearance 0.25 mm plus 0.02 mm routing margin.
No rip-up/unbounded retry or external network router was used.
## Validation / rerun commands
From `hardware/PCB`:
```sh
/usr/bin/python3 -B validate_pcb_draft.py
/usr/bin/python3 -B validation/pcb-routing-check.py
/usr/bin/python3 -B validation/pcb-routing-preservation.py
kicad-cli pcb drc --format json --all-track-errors --severity-all --exit-code-violations -o validation/pcb-routing-all-drc.json serial-carrier.kicad_pcb
```
All passed on A1. Validators perform fresh KiCad DRC, fresh schematic-netlist
comparison, geometry checks, continuous copper-distance checks, and routing
save-refusal tests. DRC was read-only to preserve project settings; no copper
zones exist to refill. Existing ignored DRC checks were not changed; see reports.
The preservation audit includes the intentional documentation updates and seven
CAD projection graphics while retaining electrical and project preservation checks.
Rerouting remains explicitly destructive to existing/manual copper and requires:
```sh
/usr/bin/python3 -B route_pcb_draft.py --route --overwrite --replace-routes --seconds 240 --max-expansions 1200000
/usr/bin/python3 -B add_device_silkscreen.py
```
Then rerun the validators. The one-shot `pcb-routing-reposition.py --apply`
refuses anything except the original A0 snapshot and must **not** be rerun on A1.
Current evidence: `../../../../../hardware/PCB/validation/pcb-routing-a1-pass2.log`, `../../../../../hardware/PCB/validation/pcb-routing-run.json`,
`../../../../../hardware/PCB/validation/pcb-routing-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-all-drc.json`, `../../../../../hardware/PCB/validation/pcb-parity-drc.json`,
`../../../../../hardware/PCB/validation/pcb-validation-summary.json`, `../../../../../hardware/PCB/validation/pcb-routing-check.json`,
`../../../../../hardware/PCB/validation/pcb-routing-reposition.json`, and `../../../../../hardware/PCB/validation/pcb-routing-preservation.json`.
The current post-integration board SHA-256 is recorded in
`../../../../../hardware/PCB/validation/pcb-routing-check.json`; the CAD projection additions do not change copper.
**Still a draft, not fabrication-ready.** ESP32 geometry/antenna/USB access,
module pinout/header fit, full assembly/height clearances, power integrity and
hardware operation remain unqualified. Nominal RS-232 metal dimensions alone
are not assembly qualification. Placement PDF, SVG/PNG and carrier-only 3D
preview were refreshed after CAD envelope integration. No Gerbers generated.
---
# Historical A0 routing result — superseded by A1 above
The following records the earlier A0 routing/integration. Its dimensions, counts,
measurements and no-save reproduction do not describe current A1. A0 `pass1`,
`pass2`, `pass3`, `baseline` and `dry-run` files are historical; use the A1 and
current reports listed above.
Date: 2026-09-20. KiCad 10.0.6; system `/usr/bin/python3`.
## Saved result
Actual copper routing is saved in `../../../../../hardware/PCB/serial-carrier.kicad_pcb`, not just a preview
or an exported proposal. **38/38 required spanning-tree connections across 17
multi-pad nets routed; KiCad reports 0 unconnected items, 0 errors, 0 warnings.**
Intentional single-pad `unconnected-(...)` nets retain their original names and
are not connected to anything else.
- Two copper layers, F.Cu and B.Cu.
- 569 straight track segments: 425 at 0.25 mm for signals and 144 at 0.5 mm for
+3V3/GND. No narrow power-track fallback was necessary.
- 20 through vias, 0.7 mm copper diameter / 0.3 mm drill.
- No copper pours: all ground connections were routed explicitly.
- Original RF rule area, x110..131 / y100..113 mm on both copper layers, preserved.
No tracks, vias or fills enter the reservation. Minimum copper-to-reservation
gap measured independently is 0.025 mm; this is geometric compliance with the
specified provisional boundary, **not validation of an antenna's RF needs**.
- All 9 electrical footprints and 4 standalone mounting footprints preserved.
Pad positions, shapes, sizes, drills, types, nets, functions, pin types and UUIDs;
footprint placement, orientation, library IDs, values, UUIDs and schematic UUID
paths; net table, outline and RF rule-area geometry/flags match the input.
- Generator, README, schematic, project and libraries were not edited by routing.
No external network dependency or manufacturing exports were used.
## Validation performed
From `hardware/PCB`:
```sh
kicad-cli pcb drc --format json --refill-zones --save-board -o validation/pcb-routing-drc.json serial-carrier.kicad_pcb
kicad-cli pcb drc --format json --all-track-errors --severity-all --exit-code-violations -o validation/pcb-routing-all-drc.json serial-carrier.kicad_pcb
/usr/bin/python3 -B validation/pcb-routing-check.py
```
Both DRC runs: **0 violations and 0 unconnected items**. No DRC settings or
exclusions were added or changed. Existing project rules specify 0.25 mm minimum
clearance, 0.5 mm copper-edge clearance, 0.25 mm minimum track width and 0.7 mm
minimum via diameter.
Independent continuous-geometry audit (`../../../../../hardware/PCB/validation/pcb-routing-check.json`):
| Check | Measured minimum | Requirement |
|---|---:|---:|
| Different-net copper / copper-to-NPTH clearance | 0.275 mm | 0.25 mm |
| Copper or NPTH edge to board edge | 0.525 mm | 0.5 mm |
| Copper / NPTH edge to RF reserve | 0.025 mm | No intersection |
| Via drill edge to another drilled hole edge | 0.840175 mm | 0.25 mm |
The audit uses exact straight-line/point/rectangle distance calculations with
copper radii, rather than the router's raster masks. It also verifies dimensions,
source-preservation invariants, and three overwrite-refusal cases. Requires
locally installed `pcbnew`, NumPy and Shapely; the router itself only needs
`pcbnew` and NumPy.
DRC still honors the project's pre-existing ignored checks:
`missing_courtyard`, `track_not_centered_on_via`,
`tuning_profile_track_geometries`, `footprint_filters_mismatch`, and
`footprint_type_mismatch`. `--severity-all` does not enable ignored checks.
The routing subtask compared electrical UUID linkage and pad-net assignments
with the input board. Subsequent integration also ran fresh schematic-parity DRC
and added missing symbol metadata fields to the PCB without changing copper.
That check reports exactly one expected U2 footprint warning (provisional PCB
footprint versus intentionally blank schematic assignment); no net mismatch.
See `../../../../../hardware/PCB/validation/pcb-parity-drc.json` and `../../../../../hardware/PCB/validation/pcb-validation-summary.json`.
## Method and bounded passes
`../../../../../hardware/PCB/route_pcb_draft.py` uses a two-layer 8-direction raster A* search, an explicit
via cost, exact-pad-centre terminal stubs, conservative foreign-copper and hole
obstacles, and only collinear path compression. It preserves the L-shaped edge
and RF reservation. Per-net minimum spanning trees connect every physical pad,
including duplicate switch contacts; short links precede longer links and GND is
last. It does not optimize return-current paths, topology or total via count.
1. Initial 0.1 mm raster / 0.04 mm safety margin / 250,000 expansions per search:
29/38 connections; DRC 0 violations, 9 unconnected.
2. Increased search allowance to 1,200,000 expansions: 30/38; DRC 0 violations,
8 unconnected. Some raster header passages were blocked, not just expensive.
3. Final 0.05 mm raster / 0.02 mm additional safety margin, with 0.25 mm physical
clearance unchanged: 38/38 in approximately 26.5 seconds; DRC fully connected.
Each pass had a 240-second routing deadline. No unresolved routing remains.
A KiCad Python binding ownership problem in the first replacement attempt caused
an exception/segfault before any board save. Replacement now uses `board.Delete`
instead of `board.Remove`; the subsequent save and no-save rerun both passed.
The known startup PROPERTY_ENUM assertions do not prevent validation.
## Rerun safety
By default the script never saves. The existing routed board is protected even
from a preview unless `--replace-routes` is given. A preview can be run with:
```sh
/usr/bin/python3 -B route_pcb_draft.py --replace-routes --seconds 240
```
This exact no-save workflow was tested: it again found 38/38 connections, 569
segments and 20 vias, and left the board byte-for-byte unchanged.
**The following command discards all existing/manual tracks and vias**, then
replaces them with fresh draft routing. Back up manual work first:
```sh
/usr/bin/python3 -B route_pcb_draft.py --route --overwrite --replace-routes --seconds 240 --max-expansions 1200000
```
The script refuses a PCB-editor lock, unexpected outline/RF/board structure,
unsupported pad orientation/layers, unapproved existing routes, or detected board
changes before save. Candidate serialization must preserve immutable geometry
and linkage before replacing the board. It deliberately does not run DRC itself:
rerun the validation commands after any routing change. Runtime/expansion limits
can produce a partial board; inspect both the run report and actual DRC, not just
the script exit status.
## Evidence and remaining limitations
- `../../../../../hardware/PCB/validation/pcb-routing-input.kicad_pcb`: unrouted input snapshot, after baseline DRC save.
- `../../../../../hardware/PCB/validation/pcb-routing-baseline.json`: 38 unconnected / no violations before routing.
- `../../../../../hardware/PCB/validation/pcb-routing-pass1-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-pass2-drc.json`: intermediate DRC.
- `../../../../../hardware/PCB/validation/pcb-routing-pass2.log`, `../../../../../hardware/PCB/validation/pcb-routing-pass3.log`: bounded routing progress.
- `../../../../../hardware/PCB/validation/pcb-routing-run.json`: final saved-route metrics and connection list.
- `../../../../../hardware/PCB/validation/pcb-routing-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-all-drc.json`: final KiCad DRC evidence.
- `../../../../../hardware/PCB/validation/pcb-routing-check.py`, `../../../../../hardware/PCB/validation/pcb-routing-check.json`: repeatable independent audit.
- `../../../../../hardware/PCB/validation/pcb-routing-dry-run.log`: successful final no-save reproduction.
Routing-only DRC-saved snapshot SHA-256 (before integration copied schematic
metadata fields): `bf94b6c9b00c43a97f933054758a2c9d6541981ae09f2f285dcdb567bd73d9b1`.
The current post-integration hash is recorded by `../../../../../hardware/PCB/validation/pcb-routing-check.json`;
geometry, pad-net linkage and copper are unchanged by the metadata copy.
**Still DRAFT / NOT FOR FABRICATION.** Provisional module/connector geometry,
antenna envelope, assembly/courtyard clearances, USB access, male RS-232 connector
fit, physical pin mapping, power integrity, EMI and hardware operation are not
qualified by routing or DRC. All previously documented mechanical/electrical
release gates remain. No physical-device or manufacturing validation performed.
+68
View File
@@ -0,0 +1,68 @@
# Bounded ordinary HTTPS idle retention
Source/build/test paths in code spans and commands are relative to the repository root
unless a different working directory is stated; Markdown links are relative to this document.
Current owner/SDK contract. The user accepted idle cleanup as working; [overall acceptance](roadmap.md#phase8-acceptance-evidence) records current status and evidence limits. No individual unreported soak, fault or reserve result is inferred.
## Evidence and scope
User-authorized response to the admission capture: post-TLS occupancy **6/6, ordinary4 / serial2 / admin0**, ordinary connection ages **50–74 seconds**; admin ticket returned in **14 ms**, no subsequent TLS/upgrade observed, ticket unconsumed. These ages measure connection lifetime, not time since the last request. They support investigating retained ordinary sockets, not claiming every captured socket was idle. Installed HTTPD excludes its listening fd from `select` while full with LRU disabled. New connections can therefore wait before TLS or upgrade even when ticket issuance was fast.
This policy releases **expired idle ordinary HTTPS connections**, not arbitrary ordinary requests to make room. The policy uses the existing six sockets, LRU-disabled operation, one-second receive/send and five-second TLS-handshake timeouts without expanding task stacks or queue capacities. Current overall handler capacity is 39. No new task. UART0 recovery, independent USB access, serial broker ownership, tickets, authentication and both WebSocket transports are unchanged.
## Usage and timeout semantics
- Automatically enabled for every successfully started HTTPS server, including when diagnostic tracing is disabled and either optional transport is unavailable. No new console command, setting or credential migration.
- **15 seconds of observed ordinary idle retention**, checked by a **one-second ESP timer** that requests work on HTTPD. The three existing five-second browser status-poll intervals leave room for normal keepalive reuse instead of a TLS handshake per request.
- A new post-TLS connection that has sent no request gets the same full idle window. Time spent establishing TLS does not consume it.
- The first owner sweep observing a new connection or a changed successful-request completion marker starts a fresh window. The marker covers every ordinary route, including login, assets, status, typed settings, tickets and keepalive errors that return successfully—not just diagnostic wrappers. Failed requests are deleted by the normal SDK path instead.
- HTTPD pending bytes, TLS pending bytes or a readable TCP fd reset the observation window. Negative TLS pending results and `select` errors conservatively reset it too. No bytes are read or discarded by the probe.
- At the threshold, after current SDK classification and zero-time readiness checks, HTTPD calls `shutdown(current_fd, SHUT_RDWR)`. A successful shutdown is latched; a failed call retries on the next probe. The SDK's subsequent read/delete path owns socket close, TLS destruction, diagnostic close notification and freeing the slot. The probe never calls `close`, overrides TLS cleanup, or queues a session-close pointer.
- **WebSockets (serial and admin), closing WebSockets and async requests are exempt.** The check uses actual SDK flags, not diagnostic metadata. Successful explicit 101/classification and request cleanup finish before the owner can sweep, so there is no ordinary-idle interval during upgrade admission.
- Under an available owner and timely successful work delivery, expect 15 seconds plus the initial observation delay (nominally up to one second), the next probe delay (nominally up to one second), and SDK read/cleanup latency. This is a conservative sampled idle policy, **not a strict wall-clock timeout or admission SLA**.
### Deliberate limitations
1. Parsing an incomplete request, synchronous response sends, leftover-body purge and TLS handshakes serialize on HTTPD. The probe cannot interrupt them. Existing per-read/per-send timeouts remain; a peer trickling input can extend overall processing beyond one timeout. This change does not provide a slowloris deadline or solve TLS/memory/global-socket pressure.
2. Four continuously active ordinary connections plus two WebSockets can still fill all six slots. They are not evicted. Likewise, an admission attempt immediately after fresh saturation can hit the browser's existing 15-second timeout before the conservative window expires; the user may still need to retry. Older idle retained sockets are eligible on the next delivered probe.
3. As with any HTTP keepalive timeout, bytes arriving **after** the last readiness check can race a shutdown. Already executing HTTPD requests/responses are protected; future client intent cannot be predicted. A client may need a new connection. No new application-level mutation retry/replay is added. Response completion here means synchronous HTTPD completion, not proof of peer receipt/TCP acknowledgement.
4. `httpd_queue_work` with `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` disabled uses loopback UDP. A reported queue error releases the reservation and retries next tick. **A successful send is not an execution acknowledgement**: an accepted-but-lost control message leaves one reservation pending, disables further probes, and requires successful HTTPS stop/restart to restore probing. There is no speculative reservation timeout: it could accumulate delayed callbacks and violate the one-probe bound. This is explicitly regression-tested, not hidden behind a hard retention guarantee. HTTPD blockage or queue loss also cannot be repaired by raising sockets/LRU/timeouts here.
5. A failed stop leaves cleanup detached and ownership retained for a later stop retry; it does not restart probes on a partially stopped server. Timer allocation/start failure gates HTTPS start with its error rather than silently starting without the policy. UART0/USB recovery is unaffected.
## Exact installed SDK audit
All SDK references below are under `~/.platformio/packages/framework-espidf/components/`; installed framework is ESP-IDF **5.5.0** (`3.50500.0`). Production private access remains solely in `src/web_httpd_adapter.c`, with the existing compile-time version guard extended to require idle-lifecycle re-audit.
- `esp_http_server/src/httpd_main.c`: `httpd_server` selects the listener only with capacity or LRU enabled. Control work runs **before** current data sessions, then accept. `httpd_process_session` skips async sessions and synchronously runs `httpd_sess_process`; errors delete the session before any subsequent owner work. Accept invokes TLS synchronously through `open_fn`.
- `esp_http_server/src/httpd_sess.c`: `httpd_sess_process` calls `httpd_req_new`, then `httpd_req_delete`, and only after both return success assigns `session->lru_counter = ++hd->lru_counter`. This happens even with LRU disabled. New sessions zero the slot, including the marker; the global counter resets when all sessions are deleted. Application calls to `httpd_sess_update_lru_counter` are confined to verified serial-WebSocket send work (`web_serial_transport.c`), which the sweep exempts. This marker is not a timestamp and not an fd-generation token.
- `esp_http_server/src/httpd_parse.c`: `httpd_req_new` synchronously parses and invokes the URI handler. `httpd_req_delete` drains any remaining body; cleanup clears `hd_req_aux.sd` and request pointers. A return from a diagnostic handler wrapper or a response-send call is earlier than this boundary. The sweep requires HTTPD's thread identity and no current `hd_req_aux.sd`, and skips `for_async_req`. Current ordinary handlers do not use async requests, out-of-owner sends, or unfinished chunked responses. Re-audit that contract if introduced.
- `esp_http_server/include/esp_http_server.h` has no global synchronous post-request-cleanup hook. Its event notifications are not such a hook: `esp_http_server_dispatch_event` posts to the event loop. `HTTP_SERVER_EVENT_SENT_DATA` is emitted by `httpd_resp_send` and per `httpd_resp_send_chunk`, **before** handler return/body purge/cleanup. Send/receive overrides belong to HTTPS and do not expose a safe completion hook. URI matching and error handlers likewise cannot supply an all-route post-cleanup boundary.
- `esp_https_server/src/https_server.c`: successful `httpd_ssl_open` stores the transport context and installs TLS send/recv/pending functions before synchronous `HTTPD_SSL_USER_CB_SESS_CREATE`. The application callback invalidates any old row for that fd even if the TLS pointer, socket-slot address and counter value were reused. If fd lookup unexpectedly fails, all observations reset conservatively. The close callback remains the diagnostic observer; SDK destruction remains intact. `httpd_ssl_pending` calls `esp_tls_get_bytes_avail` without consuming data; errors can post an existing SDK error event.
- `httpd_sess_trigger_close` resolves fd to a raw reusable `sock_db *`, then queues `httpd_sess_close`. Its zero-counter/LRU guard does not prove the same connection still occupies that slot. **Not used by this policy.** Direct owner shutdown retains the slot until SDK read cleanup and has no deferred fd/pointer argument that could later close a replacement.
- `httpd_queue_work` uses `cs_send_to_ctrl_sock` / `sendto` in `esp_http_server/src/util/ctrl_sock.c`. The actual generated config leaves `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` undefined/off, selecting the nonblocking queue mode. The idle initializer explicitly rejects builds with that blocking option on. Successful `httpd_stop` waits for `THREAD_STOPPED`, frees HTTPD and ends possible old callback execution; failed stop is not a retirement boundary.
## Ownership and bounded storage
`src/web_httpd_idle.{c,h}` owns one persistent ESP timer, six static observation rows, lifecycle gate/generation and queued/submitting flags. It uses no request data, secret, dynamic per-connection allocation, payload buffer, new task or additional socket. Timer callback performs only short metadata locking and at most one queue submission; all private session access, readiness and shutdown run on HTTPD.
`src/web_server.c` serializes lifecycle. Prepare initializes rows **before** SSL startup; the TLS callback and sweep thereafter share the same owner. Attach publishes a nonzero, nonwrapping `uintptr_t` server generation, passed by value as opaque work argument—not a mutable shared descriptor or raw fd. At most one queued/executing probe is reserved; a separate submitting flag remains set until `httpd_queue_work` returns even if work already finished. That closes the callback-before-submit-return race.
Detach first prevents submissions, then waits at most one second for any submitting call to return. Fence timeout forbids SSL destruction, retaining the handle for retry. An already executing sweep may finish safely while stop waits for HTTPD. Only successful SDK stop retires a discarded queued reservation; restart gets a new generation even if the server handle is reused. A stale generation cannot sweep or clear a newer reservation. No counter wrap or generic off-owner session-list query is accepted.
`src/web_httpd_adapter.{c,h}` defines the six-row bound and 15-second policy and performs the version-pinned owner sweep. `src/CMakeLists.txt` adds only the new module. Diagnostics remains unchanged internally; server composes idle identity reset followed by existing diagnostic publication.
## Regression and resource limits
`tests/web_httpd_idle/run.py` exercises production lifecycle/sweep paths, installed SDK request cleanup, host socketpair/readiness/shutdown/fd reuse and deterministic TLS/timer/queue doubles. `tests/web_admin_transport/server_lifecycle.py` checks server composition. Historical host validation passed; these references do not claim a new run or real target scheduler/TLS timing.
Bounded storage: six observation rows (144 bytes), one persistent timer (installed non-profiled layout 32 bytes before allocator overhead), lifecycle/generation flags and one reserved probe. SDK control UDP/mailbox allocation is transient and separate. No per-connection payload/task/stack/socket growth. Actual HTTPD/timer margins and runtime overhead/reserve floors require target measurement, not host sizes. See [latest firmware resources](roadmap.md#phase8-acceptance-evidence).
## Regression procedure — not execution evidence
1. Start with diagnostic capture disabled: establish two serial WebSockets and ordinary HTTPS fetches. Verify idle ordinary sockets disappear after the observation window while both serial clients/lease remain unchanged. Enable capture only as needed to compare close/open occupancy; ages remain connection ages, not idle timestamps.
2. Reproduce the original ordinary4/serial2 full-slot case; wait beyond the idle window, then issue/open admin. Record client ticket/TLS/upgrade timings and occupancy without recording tickets/cookies. Verify no repeat reload loop is needed for already-old idle saturation. Separately test fresh saturation and acknowledge the existing 15-second browser timeout limit.
3. Leave status polling active for several minutes: no five-second TLS reconnect churn; both WebSockets and binary serial data/broker isolation survive. Repeat Settings/account-key operations, large assets, login/logout and two browser contexts. Compare with polling paused to distinguish genuinely idle slots.
4. Slow incomplete headers/bodies, pipelined requests, slow response readers and slow/failed TLS handshakes: no probe-driven close of an executing response or admitted WebSocket. Record owner delays; do not infer an overall request deadline from the unchanged one-second receive timeout.
5. Repeated close/reopen/fd reuse, full-mix stop/start and certificate rotation via supported UART0/SSH/browser lifecycle paths. Confirm UART0 and USB remain available, queued work never affects replacement connections, failed-stop retries retain ownership, and no start allocates a second server.
6. Capture settled/loaded/post-cleanup internal/DMA/PSRAM values and HTTPD/ESP-timer minimum-free stack. Soak at the accepted full client mix. Investigate control queue loss separately if probing appears stuck; successful stop/start is the safe recovery, not an eviction/capacity increase.
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="620" viewBox="0 0 960 620" role="img" aria-labelledby="title description">
<title id="title">Phase 7 dual-color OLED overview-screen mockup</title>
<desc id="description">An enlarged mockup of the verified 128 by 64 pixel local status display. The upper sixteen yellow rows and lower forty-eight blue rows are separate rendered panels, divided by a narrow physical black separator. The yellow panel shows service, Wi-Fi, client, writer, and alert status. The blue panel shows IP address, RS-232 configuration, modem signals, traffic counters, and page navigation. Three buttons below are labeled previous, OK, and next.</desc>
<defs>
<filter id="yellowGlow" x="-20%" y="-40%" width="140%" height="180%">
<feGaussianBlur stdDeviation="1.7" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="blueGlow" x="-20%" y="-40%" width="140%" height="180%">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<linearGradient id="bezel" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#253747"/>
<stop offset="0.5" stop-color="#14212c"/>
<stop offset="1" stop-color="#091017"/>
</linearGradient>
<radialGradient id="button" cx="42%" cy="35%" r="70%">
<stop offset="0" stop-color="#566777"/>
<stop offset="0.65" stop-color="#273440"/>
<stop offset="1" stop-color="#111920"/>
</radialGradient>
<pattern id="pixels" width="5" height="5" patternUnits="userSpaceOnUse">
<path d="M5 0H0V5" fill="none" stroke="#ffffff" stroke-opacity="0.022" stroke-width="0.45"/>
</pattern>
</defs>
<rect width="960" height="620" fill="#0b1118"/>
<text x="480" y="35" fill="#d7e3ed" font-family="sans-serif" font-size="20" font-weight="600" text-anchor="middle">Phase 7 — 128×64 local status display</text>
<rect x="55" y="55" width="850" height="470" rx="25" fill="url(#bezel)" stroke="#456074" stroke-width="3"/>
<circle cx="82" cy="82" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="878" cy="82" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="82" cy="498" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="878" cy="498" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<!-- The screen is exactly 2:1. Its 320-pixel height represents 64 rows at 5x scale. -->
<rect x="160" y="95" width="640" height="320" rx="3" fill="#010406" stroke="#718696" stroke-width="2"/>
<rect x="160" y="95" width="640" height="80" fill="#1a1600"/>
<rect x="160" y="175" width="640" height="240" fill="#00121a"/>
<rect x="160" y="95" width="640" height="320" fill="url(#pixels)"/>
<line x1="160" y1="175" x2="800" y2="175" stroke="#77848c" stroke-opacity="0.38"/>
<g font-family="DejaVu Sans Mono, Liberation Mono, Consolas, monospace" font-size="27" font-weight="600" letter-spacing="0.2">
<g fill="#ffd52e" filter="url(#yellowGlow)">
<text x="177" y="131">SER● WIFI▮▮▮ U1 W1 S1</text>
<text x="177" y="168">WR:SSH CLIENTS:3 !:0</text>
</g>
<g fill="#20c9ff" filter="url(#blueGlow)">
<text x="177" y="211">IP 192.168.122.42</text>
<text x="177" y="251">RS232 230400 8N1</text>
<text x="177" y="291">FLOW:NONE VLD:1</text>
<text x="177" y="331">DCD0 DSR1 CTS1 RI0</text>
<text x="177" y="371">RX:1.23M TX:633</text>
<text x="177" y="408">‹ OVERVIEW 1/4 ›</text>
</g>
</g>
<g font-family="sans-serif" font-size="14" fill="#9fb0bd">
<text x="812" y="112">row 0</text>
<text x="812" y="171">row 15</text>
<text x="812" y="190">row 16</text>
<text x="812" y="414">row 63</text>
<text x="148" y="445" text-anchor="end">128 px</text>
<text x="480" y="445" text-anchor="middle">Verified panels: yellow rows 0–15 · black divider · blue rows 16–63</text>
</g>
<g>
<circle cx="325" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<circle cx="480" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<circle cx="635" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<g fill="#d7e3ed" font-family="sans-serif" font-size="25" font-weight="700" text-anchor="middle">
<text x="325" y="502">◀</text>
<text x="480" y="501" font-size="18">OK</text>
<text x="635" y="502">▶</text>
</g>
<g fill="#91a4b2" font-family="sans-serif" font-size="14" text-anchor="middle">
<text x="325" y="551">previous / hold: back</text>
<text x="480" y="551">select / hold: confirm</text>
<text x="635" y="551">next</text>
</g>
</g>
<text x="480" y="593" fill="#718594" font-family="sans-serif" font-size="13" text-anchor="middle">Mockup only — glyph metrics remain to be finalized; the physical color boundary is hardware-verified.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1080" height="720" viewBox="0 0 1080 720" role="img" aria-labelledby="title description">
<title id="title">Phase 7C compact icon status UI mockup</title>
<desc id="description">Four 128 by 64 OLED page mockups with the same fixed-position yellow status icons and changing blue page content. Active icons are solid yellow with values beneath; inactive SSH and alert slots are blank.</desc>
<defs>
<style>
.heading{fill:#d8e4ec;font:600 20px sans-serif}.note{fill:#93a8b6;font:14px sans-serif}.screen{fill:#010609;stroke:#668092;stroke-width:2}.yellowbg{fill:#191600}.divider{stroke:#77858d;stroke-opacity:.5;stroke-width:2}.active{fill:#ffd52e;stroke:#ffd52e}.blue{fill:#27c9ff;stroke:#27c9ff}.content{fill:#27c9ff;font:600 18px 'DejaVu Sans Mono',monospace}.value{fill:#ffd52e;font:600 14px 'DejaVu Sans Mono',monospace}.page{fill:#d8e4ec;font:600 17px sans-serif}
</style>
<!-- Official Material Design Icons usb path, @mdi/svg 7.4.47. -->
<symbol id="usb" viewBox="0 0 24 24"><path d="M15,7V11H16V13H13V5H15L12,1L9,5H11V13H8V10.93C8.7,10.56 9.2,9.85 9.2,9C9.2,7.78 8.21,6.8 7,6.8C5.78,6.8 4.8,7.78 4.8,9C4.8,9.85 5.3,10.56 6,10.93V13A2,2 0 0,0 8,15H11V18.05C10.29,18.41 9.8,19.15 9.8,20A2.2,2.2 0 0,0 12,22.2A2.2,2.2 0 0,0 14.2,20C14.2,19.15 13.71,18.41 13,18.05V15H16A2,2 0 0,0 18,13V11H19V7H15Z"/></symbol>
<symbol id="serial" viewBox="0 0 24 24"><path d="M5 3h14v5h3v13H2V8h3zm2 2v3h10V5zM5 11v7h14v-7zm2 2h2v2H7zm4 0h2v2h-2zm4 0h2v2h-2z"/></symbol>
<symbol id="wifi" viewBox="0 0 24 24"><path d="M1 8q11-9 22 0l-2 3q-9-7-18 0zm5 6q6-5 12 0l-2 3q-4-3-8 0zm4 6a2 2 0 1 1 4 0z"/></symbol>
<symbol id="web" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20M4.3 9h3.1A16 16 0 0 1 9 4.7 8.1 8.1 0 0 0 4.3 9m-.2 6h3.3a16 16 0 0 1 0-6H4.3a8 8 0 0 0-.2 6m4.9 4.3A16 16 0 0 1 7.4 15H4.3A8.1 8.1 0 0 0 9 19.3M10 15h4a14 14 0 0 0 0-6h-4a14 14 0 0 0 0 6m1 4.9v-3h2v3a8 8 0 0 1-2 0m0-15.8v3h2v-3a8 8 0 0 0-2 0m4 15.2a16 16 0 0 0 1.6-4.3h3.1a8.1 8.1 0 0 1-4.7 4.3m1.6-10.3h3.1A8.1 8.1 0 0 0 15 4.7 16 16 0 0 1 16.6 9m.1 2a14 14 0 0 1 0 4h3.2a8 8 0 0 0 0-4z"/></symbol>
<symbol id="ssh" viewBox="0 0 24 24"><path d="M2 3h20v18H2zm3 4 4 4-4 4 2 2 6-6-6-6zm8 8v2h6v-2z"/></symbol>
<symbol id="clients" viewBox="0 0 24 24"><path d="M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8m7-1a3 3 0 1 0 0-6 3 3 0 0 0 0 6M2 21v-3c0-3 3-5 7-5s7 2 7 5v3zm14-8c4 0 6 2 6 5v3h-4v-3c0-2-1-4-3-5z"/></symbol>
<symbol id="alert" viewBox="0 0 24 24"><path d="M12 2 1 22h22zm-1 7h2v7h-2zm0 9h2v2h-2z"/></symbol>
<g id="status">
<g class="active"><use href="#serial" x="12" y="4" width="30" height="30"/><use href="#wifi" x="80" y="4" width="30" height="30"/><use href="#usb" x="150" y="4" width="30" height="30"/><use href="#web" x="220" y="4" width="30" height="30"/><use href="#clients" x="360" y="4" width="30" height="30"/></g>
<text x="17" y="57" class="value">On</text><text x="91" y="57" class="value">6</text><text x="154" y="57" class="value">Wr</text><text x="231" y="57" class="value">1</text><text x="371" y="57" class="value">3</text>
</g>
</defs>
<rect width="1080" height="720" fill="#0b1118"/><text x="540" y="34" text-anchor="middle" class="heading">Phase 7C — fixed icon dashboard and compact status pages</text><text x="540" y="57" text-anchor="middle" class="note">Yellow strip is identical on every page · inactive icon slots and their supplementary values remain blank</text>
<g transform="translate(62 98)"><text x="256" y="-16" text-anchor="middle" class="page">Overview</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#serial" x="8" y="72" width="30" height="30"/><use href="#clients" x="8" y="104" width="30" height="30"/><use href="#wifi" x="8" y="136" width="30" height="30"/><use href="#web" x="8" y="200" width="30" height="30"/></g><text x="45" y="96" class="content">Serial:230400 8N1</text><text x="45" y="128" class="content">Clients:3 Wr:USB</text><text x="45" y="160" class="content">WiFi:LabNet Ch:6</text><text x="45" y="192" class="content">IP:192.168.1.42</text><text x="45" y="224" class="content">Web:1 SSH:0 USB:1</text></g>
<g transform="translate(592 98)"><text x="256" y="-16" text-anchor="middle" class="page">RS-232 / modem</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#serial" x="8" y="72" width="30" height="30"/></g><text x="45" y="96" class="content">Mode:230400 8N1</text><text x="45" y="128" class="content">Flow:rts-cts</text><text x="45" y="160" class="content">DCD:0 DSR:1 CTS:1</text><text x="45" y="192" class="content">Valid:Yes RX:1.2M</text><text x="45" y="224" class="content">TX:633 Err:0</text></g>
<g transform="translate(62 438)"><text x="256" y="-16" text-anchor="middle" class="page">Broker clients</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#usb" x="8" y="72" width="30" height="30"/><use href="#web" x="8" y="104" width="30" height="30"/><use href="#ssh" x="8" y="136" width="30" height="30"/></g><text x="45" y="96" class="content">Wr:USB Q:0</text><text x="45" y="128" class="content">Ob:web Q:0</text><text x="45" y="160" class="content">Ob:SSH Q:0</text><text x="45" y="224" class="content">Drop:0 Events:0</text></g>
<g transform="translate(592 438)"><text x="256" y="-16" text-anchor="middle" class="page">Network / services</text><rect width="512" height="256" class="screen"/><rect width="512" height="64" class="yellowbg"/><line x1="0" y1="64" x2="512" y2="64" class="divider"/><use href="#status"/>
<g class="blue"><use href="#wifi" x="8" y="72" width="30" height="30"/><use href="#web" x="8" y="168" width="30" height="30"/><use href="#ssh" x="8" y="200" width="30" height="30"/></g><text x="45" y="96" class="content">WiFi:LabNet Ch:6</text><text x="45" y="128" class="content">IP:192.168.1.42</text><text x="45" y="160" class="content">AP:Off Clients:0</text><text x="45" y="192" class="content">HTTPS:On Web:1</text><text x="45" y="224" class="content">SSH:On Sessions:0</text></g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

+397
View File
@@ -0,0 +1,397 @@
# Implementation roadmap
Source/build/test paths in code spans and commands are relative to the repository root
unless a different working directory is stated; Markdown links are relative to this document.
This document tracks the implementation and hardware-validation plan for the ESP32 Serial Swiss Army Knife. It describes intended sequencing rather than a release schedule. Completion requires implementation, documentation and build validation, plus relevant hardware tests or an explicitly recorded user waiver. A waiver is not a hardware-test pass.
## Status legend
- **Complete** — implemented and accepted, with target-hardware validation or an explicit phase-specific user waiver recorded below.
- **In progress** — implementation or validation is actively underway, but the overall phase is not complete.
- **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks.
- **Planned** — accepted project direction, not yet implemented.
- **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment.
## Project-wide design constraints
These constraints apply across all phases:
- UART0 remains the trusted physical administration and recovery console.
- UART1 and the MAX3243 provide the single physical RS-232 data port.
- All serial transports use the central session broker: one writer, multiple observers.
- A slow or failed client must not block UART reception or unrelated clients.
- Serial data paths remain binary-transparent; transports must not reserve in-band control sequences.
- Network serial access must be authenticated and encrypted. No unauthenticated plaintext TCP serial service is planned.
- Secrets must not be exposed by routine status output, command completion, or logs.
- Resource use must remain bounded and observable on the ESP32-S3 N16R8 target.
- New work must preserve USB CDC and UART0 recovery when Wi-Fi or a network service fails.
- Project code remains `GPL-3.0-only`; third-party license notices and compatibility must be preserved.
## Phase overview
| Phase | Scope | Status |
|---|---|---|
| 0 | RS-232 hardware characterization and diagnostics | **Complete** |
| 1 | Persistent UART1 serial-service foundation | **Complete** |
| 2 | Transport-neutral session broker | **Complete** |
| 3 | Native USB CDC-ACM serial transport | **Complete** |
| 4 | Wi-Fi station/AP foundation and network diagnostics | **Complete** |
| 5A | Authenticated HTTPS administration foundation | **Complete** |
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
| 6 | Authenticated SSH serial transport | **Complete** |
| 7 | Local display and button interface | **Complete** |
| 8 | Role-based users and administrative access | **Complete** |
| 9 | Small intermittent-use security baseline | **Complete (user signoff 2026-09-18; new hardware check waived)** |
| 10 | Simple admin web firmware upload | **Complete (explicit user acceptance 2026-09-18; upload and normal operation verified)** |
| 12 | Dual-stack networking and local service discovery | **Complete (explicit user validation; fresh boot and full client mix at 230400 baud)** |
## Completed phases
### Phase 0 — RS-232 hardware characterization
Implemented and hardware-validated:
- Central board pin profile for the ESP32-S3 N16R8 and MAX3243 breakout.
- MAX3243 enable, static driver, receiver, valid-voltage, and modem-signal checks.
- UART loopback across supported framing and baud-rate combinations.
- Hardware CTS transmit gating and RTS receive-backpressure tests.
- Exclusive RS-232 port ownership and fault-safe cleanup.
- RGB status indication for diagnostic state.
- Phase 0 hardware diagnostics, including signal status, are isolated under the `debug` CLI submenu.
Detailed procedures are in [Electrical tests](../electrical_tests.md) and [Hardware wiring](../wiring.md).
### Phase 1 — Serial-service foundation
Implemented and hardware-validated:
- Persistent, versioned UART configuration with safe defaults.
- UART1 lifecycle management and exclusive ownership.
- Nonblocking RX and TX software streams.
- Baud rate, data bits, parity, stop bits, RTS/CTS, DTR policy, and RTS threshold controls.
- Runtime counters and safe restart after working-configuration changes.
- Explicit save/load/default/reset behavior; edits are not silently persisted.
### Phase 2 — Session broker
Implemented and hardware-validated:
- Up to eight generation-safe broker clients.
- Exactly one writer lease with multiple observers.
- Bounded per-client output and event queues.
- Slow-observer isolation and drop accounting.
- Writer grant, release, denial, revocation, and forced administrative reassignment.
- DTR `on-connect` integration.
- Partial-transfer and retryable no-progress semantics for transports.
- Broker payload storage placed preferentially in PSRAM while FreeRTOS control structures remain in internal RAM.
### Phase 3 — Native USB CDC-ACM
Implemented and hardware-validated:
- Native ESP32-S3 USB CDC-ACM serial transport.
- Automatic broker connection when the host opens the port with DTR asserted.
- Opportunistic writer acquisition with observer fallback.
- Binary-transparent bidirectional serial data.
- Host line-coding visibility for diagnostics; UART1 remains under explicit serial configuration control.
- Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls.
### Phase 4 — Wi-Fi foundation
Implemented and hardware-validated:
- Four persistent station profiles with priority ordering.
- WPA2/WPA3 mixed mode and WPA3-SAE-required mode.
- Explicit no-echo secret entry through UART0.
- AP policies `off`, `fallback`, and `always`.
- Random persistent fallback-AP password and MAC-derived default SSID.
- Bounded profile attempts, DHCP deadlines, failover, and retry backoff.
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
- Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`.
Post-validation enhancements implemented; regression validation pending:
- Edits to disabled station profiles are staged without restarting the active radio; enable-state changes and edits affecting enabled radio policy apply asynchronously.
- Configurable STA-only `sak-<suffix>.local` mDNS hostname with an independent versioned NVS record, live hostname updates, automatic withdrawal/restoration across STA IPv4 transitions, and nonfatal failure isolation. The responder initializes at most once and keeps its task stack internal while general metadata prefers PSRAM.
A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP.
### Phase 5A — Authenticated HTTPS foundation
Implemented and hardware-validated:
- HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS.
- Persistent random shared `admin` credential, later retained only as migration/recovery material by Phase 8.
- Persistent device-specific ECDSA P-256 certificate and private key.
- Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery.
- Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths.
- Credential rotation revocation hooks shared with WebSocket and SSH sessions.
### Phase 5B — Offline browser terminal
Implemented and hardware-validated:
- Vendored xterm.js and FitAddon assets; no Internet or CDN dependency.
- CSP-compatible page structure with scripts served as separate resources.
- Authenticated status and one-time WebSocket-ticket APIs.
- Binary WebSocket serial frames and broker writer/observer integration.
- Two bounded browser-terminal sessions.
- Writer request/release controls and role indication.
- Bounded TLS/socket behavior and slow-observer isolation.
- Viewport-constrained terminal fitting without recursive page growth.
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected.
### Phase 6 — Authenticated SSH serial transport
Implemented and target-hardware validated:
- wolfSSH on TCP port 22 with a separate persistent ECDSA P-256 host key.
- Two bounded interactive shell/PTY sessions; no `exec`, file transfer, forwarding, or subsystems.
- Authenticated, binary-transparent broker forwarding with opportunistic writer acquisition and observer fallback.
- Bounded handshake attempts/deadlines, receive work, buffers, and a single wolfSSH owner task pinned to CPU 1.
- UART0 lifecycle, session, counter, and host-key administration.
- PSRAM-preferred allocations with internal fallback and internal task stacks.
- Bounded broker/WebSocket work, software mbedTLS AES, and software wolfCrypt AES/SHA to avoid the validated shared-hardware-crypto/PSRAM watchdog failure while retaining ESP-IDF SHA/MPI acceleration.
- Concurrent USB, WebSocket, and SSH operation validated with USB and SSH alternately holding the writer lease.
### Phase 7 — Local display and buttons
Implemented and target-hardware validated:
- Optional 128×64 SSD1315-compatible I²C OLED on GPIO11/12 and active-low buttons on GPIO10/13/14; assignments remain centralized in `board_pins.h`.
- Display service with a static framebuffer, bounded dirty-page commits, and separate yellow status rows 0–15 plus blue content rows 16–63.
- Low-priority status/control task built from copied public snapshots; no service lock is held across I²C and the UI never becomes a broker client.
- Overview, RS-232, broker, and network/service pages with no credential or key material.
- Confirmation-protected local controls for service lifecycle, Wi-Fi reconnect/profile rotation, writer release, display off, and reboot; the UI cannot assign a writer or edit secrets/configuration.
- Persistent dim/off settings, bounded boot animation, consumed wake presses, stuck-button quarantine, and one-shot confirmations.
- Missing, disconnected, or unresponsive display hardware remains nonfatal and can recover through one bounded reprobe.
- Concurrent serial, UART0, USB, WebSocket, SSH, and injected display/button fault behavior validated. See [Electrical tests](../electrical_tests.md) and [Hardware wiring](../wiring.md).
### Phase 8 — Role-based users and administrative access — Complete
Implemented and accepted: a bounded, persistent user system replaces the single shared network credential. Both roles can use the authenticated web serial/status interface. Over SSH, `user` routes to the broker-backed serial stream while `admin` routes exclusively to the administration shell and receives no broker client or writer lease. UART0 remains the physical recovery and bootstrap authority.
Completed implementation (8A–8C target-hardware validated; 8D explicitly accepted at 8D.22 on 2026-09-13):
1. **Phase 8A — User database and UART0 administration — Complete**
- Versioned NVS database for up to eight `user`/`admin` accounts, random account IDs and authentication generations, final-admin protection, and three Ed25519/P-256 keys per account.
- Salted PBKDF2-HMAC-SHA256 password verifiers, bounded no-echo entry, one-time generated passwords, and secret-free account/key status.
- Historical migration/bootstrap supported the initial cutover. Current firmware instead commits missing storage empty, provisions the first admin with normal UART0 `user add`, and provides UART0-only unavailable-database recovery; legacy credential/bootstrap commands are removed.
- Migration, bootstrap, CRUD, persistence, and command basics target-hardware validated; the full fault-injection matrix remains regression coverage. NVS remains unencrypted and offline guessing remains possible.
2. **Phase 8B — Role-aware HTTPS and SSH authentication — Complete**
- HTTPS and SSH authenticate through the common database and copied secret-free principals; unavailable user storage fails closed. The initial HTTPS Basic path was replaced by cookie authentication in 8D.
- One-time principal-bound WebSocket tickets and ongoing principal-currentness checks prevent stale admission or input.
- Account mutations request targeted WebSocket/SSH revocation; authentication generations provide fail-safe invalidation while unrelated accounts remain connected.
- Password/key login, ticket behavior, targeted revocation, recovery, and concurrent transport operation are target-hardware validated. Legacy credentials were subsequently removed without changing established TLS identity or valid users.
3. **Phase 8C — SSH administrative shell — Complete**
- Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream.
- UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed.
- The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs.
- Transport-neutral bounded prompts now support interactive user passwords/keys and Wi-Fi secrets over admin SSH without exposing hidden input or allowing another command while a prompt is active. Ping callbacks enqueue typed bounded events and the dispatcher alone formats their output. Four-entry per-session history and whole-line Tab completion are RAM-only and wiped on disconnect.
- Authenticated administrators receive the operational registry, including HTTPS material rotation/reset, reboot, ping, and SSH lifecycle/session/host-key mutation. Self-terminating reboot and SSH actions are deferred until acknowledgement output drains, block further shell input, and execute through existing synchronous owner APIs from a separate bounded control task. First-admin provisioning and explicit `user recover --force` remain physical-UART0 operations (`user bootstrap` was later removed); admin SSH also rejects generating a replacement password for its own account.
- `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort application-buffer draining. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
- Target-hardware validation passed for route separation, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation. Stress at 460800 baud with SSH and WebSocket clients caused substantial expected packet drops and slower display controls, but did not exhaust memory or require lowering the supported baud-rate range.
4. **Phase 8D — Integrated web administration — Complete**
- User explicitly signed off tested firmware at **8D.22 on 2026-09-13**, superseding earlier per-slice pending acceptance/review gates. Cookie login/logout replaces Basic; bounded digest-only sessions, same-origin/CSRF checks, principal currentness and targeted revocation protect serial and admin routes.
- Admin-only Serial/Admin selection and Settings preserve connected terminal observation and serial writer ownership. The browser shell shares the serialized UART0/admin-SSH dispatcher, with bounded output, explicit admission and retained frontend restrictions—not unrestricted shell parity.
- Typed Serial, Accounts/password/authorized-key, Network/Wi-Fi/mDNS, Display, Broker and SSH settings; confirmed HTTPS stop/restart/reboot and HTTPS/SSH identity rotation. Canonical owners compare reserved service/identity/target generations, preserve commit/failure semantics and expose no stored secrets. Admitted work is not cancelled by later logout/timeout; uncertain mutations are never automatically replayed.
- Accessible Serial/Wi-Fi/client/writer contextual controls reuse existing controllers. Writer transfer is explicit and generation-safe; one writer, isolated observers, binary transparency, UART0 administrative recovery and network-independent native USB UART1 remain invariants.
- Scope removed by user: **8D.15** dedicated typed network diagnostics (shell diagnostics retained) and the unimplemented **8D.19** ordinary browser-session/native-USB controls (SSH controls retained). Browser identity reset/recovery/export is excluded; canonical recovery remains available.
- Acceptance evidence and legacy storage compatibility are condensed below. [Web administration](web_administration.md) retains current API/owner/recovery contracts; [regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) retain practical checks, not claims of execution.
#### Phase8 acceptance evidence
**8D.22 signoff, 2026-09-13:** the user explicitly accepted the tested firmware, superseding earlier per-slice target, review and integration gates for the retained scope above. Earlier scoped acceptance covered browser login/logout, shared administration, Serial/account/Network presentation and legacy cleanup. Prior host regression suites and independent reviews were reported passing; this consolidation asserts no new build or test execution.
Prior final Phase 8 build: **PASS, 100,556 B linked RAM / 1,828,573 B flash, CPU 160 MHz**. The combined binary WebSocket-send fix was separately accepted at **160 MHz / 230400 baud with the full client mix including browser admin**; retain combined send and bounded failed-send isolation, not the earlier frequency-only experiment. Ordinary HTTPS idle cleanup was accepted, not every possible admission failure or owner delay.
Latest reported Phase 8 telemetry (bytes):
| Sample / capability | Free | Minimum free | Largest block |
|---|---:|---:|---:|
| Boot internal 8-bit | 59,808 | 58,840 | 31,744 |
| Boot internal DMA | 52,052 | 51,084 | 31,744 |
| Boot PSRAM | 8,196,968 | 8,183,972 | 8,126,464 |
| Loaded after burst, internal 8-bit | 31,508 | 2,052 | 18,432 |
| Loaded after burst, internal DMA | 23,752 | 460 | 18,432 |
| Loaded after burst, PSRAM | 8,136,624 | 8,065,972 | 7,995,392 |
Loaded SSH minimum-free stack: **15,028 B**. This sample had two SSH sessions (serial/admin), two serial WebSockets and USB, with SSH as writer. Browser admin had been used then closed; it was **not active in this capture**. Web send/queue/protocol and SSH IO errors were zero; one SSH handshake failure and one session revocation were reported without an established cause.
**Limits, not reopened acceptance gates:** missing latest broker/serial counters prevent an exact zero-drop or byte-integrity inference. No latest browser-admin-active full-mix, individual fault-injection, exact duration, soak, cleanup-cycle or reserve-floor result is inferred. Internal/DMA lifetime minima remain a nonblocking transient-headroom follow-up, not approved reserves or proof of allocation failure: capability pools overlap and per-region minima may be non-simultaneous, so do not sum them. HTTPD/dispatcher stack margins, correlated peaks and allocation-reserve policy remain unmeasured/unapproved here. SDK TLS `-0x004C` means NET_RECV_FAILED, not an OOM diagnosis; stale cookies as the cause of two boot authentication failures remain unconfirmed.
#### Phase8 legacy credential compatibility
Cleanup was accepted **2026-09-08**, with unchanged certificate fingerprint and usable preexisting test users. This is not an unreported provisioning, recovery, power-loss or all-key pass.
- **Users:** missing `user_db/database` is committed as an empty v1 database, with no legacy account/password import. First-admin provisioning is physical UART0 `user add <username> admin` (optional `--generate`); entered passwords use hidden confirmation and generated passwords appear once. UART0-only `user recover --force` rebuilds only an unavailable database empty, refuses healthy databases (including empty ones), and leaves other configuration and TLS/SSH identities untouched; follow with normal `user add`.
- Valid existing v1 user bytes load without rewrite or changes to accounts, roles, IDs, authentication generations, verifiers or authorized keys; migrated `user` accounts are not promoted. Private `v1_admin_marker` preserves the old wire byte and is derived from admin count on mutation, not a new role/schema/public bootstrap state. Public bootstrap and legacy migration/synchronization APIs are removed; final-admin protection, conditional mutations, principal currentness and targeted revocation remain.
- **TLS:** at unchanged `web_sec/material`, a private reader validates the **1,392-byte v1** layout and migrates to **1,340-byte TLS-only v2**, preserving exact private-key DER, certificate DER, SHA-256 fingerprint and material generation—not rotating identity. The candidate is validated and committed before publication; temporary credential-bearing input is wiped. Malformed/unknown material, read/crypto failures and migration write/commit failures fail closed without regeneration or overwrite; truly missing material may be generated normally.
- `web credentials show`, `web credentials rotate --force` and `user bootstrap` are removed. `web certificate info` / `web certificate rotate --force` retain frontend policy. `web reset --force` replaces TLS identity only, not accounts/passwords/verifiers; replacement/restart may close browser sessions and require renewed certificate trust/login, without revoking unrelated SSH sessions. No public legacy credential reader, display, rotation or authentication path remains.
- **Downgrade/security:** older v1-only firmware cannot read v2 HTTPS storage; user-database v1 compatibility does not make HTTPS downgrade safe or restore removed credentials. Logical NVS replacement is **not secure flash wiping**: historical plaintext legacy credentials may remain, Wi-Fi secrets and TLS/SSH private keys remain unencrypted, and PBKDF2 verifiers permit offline guessing. No erase is required or authorized by cleanup.
- Prior host tests used NVS fault doubles and a generated identity wrapped in the historical layout, **not an independently captured old-device fixture**; they do not prove flash durability or power-loss behavior. Focused runners: `tests/web_security/run.py`, `tests/admin_console_boundary/accounts.py`, `tests/admin_ssh_policy/run.py`. See [legacy-removal regressions](user_administration_tests.md#current-legacy-removal-regression-procedure).
### Phase 9 — Security and production hardening
**Complete — user signoff 2026-09-18; new hardware check explicitly waived.** Reduced scope: an intermittently powered wireless serial cable on a trusted network, not a permanently exposed Internet service. The earlier extensive Phase 9 implementation was abandoned in favor of the accepted pre-Phase-9 baseline. Short exposure reduces risk; it does not make upstream defects harmless or certify this firmware secure.
Small implementation:
- Keep the baseline PlatformIO/ESP-IDF and managed-library versions and upstream sources. No generated dependency patches, custom SSH state machine, crypto-policy replacement, allocator hooks, or SDK migration in this phase.
- Explicitly disable core dumps and select silent panic reboot through standard `sdkconfig.defaults` options. Keep ordinary logging; reduced crash diagnostics are a deliberate trade-off. Existing generated configurations must be checked because defaults do not override saved selections.
- Follow the [operational checklist](../security_operations.md): unique credentials, trusted identity verification, no Internet forwarding, networking off/power off after use, and existing rotation/recovery procedures.
- Review upstream maintenance releases occasionally and before planned upgrades. Evaluate reachable issues against actual use; any stock dependency upgrade is a separate build/device-tested task, not a reason to accumulate local backports. Preserve existing license notices.
**Excluded:** secure boot, NVS/flash/PSRAM encryption, eFuse changes, partition changes, exhaustive CVE/zeroization audits, new authentication throttling, and production certification. Existing authentication, session isolation and recovery behavior remain unchanged. There is no protection against physical extraction or replacement of firmware. Phase 10 uses administrator-selected firmware, without a separate image-signing system.
**Host/build evidence (2026-09-18):** normal `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the original 20241119 toolchains: **94,196 B linked RAM / 1,828,565 B flash**. Generated configuration confirms no dumps and silent panic reboot; compilation inputs contain none of the abandoned Phase 9 source overlays or wolf crypto guards. No device operation or target validation was performed.
**Acceptance (2026-09-18):** the user signed off this small Phase 9 and explicitly waived a new whole-phase hardware check of boot, UART0 recovery, native USB serial, HTTPS/SSH login and normal serial sharing. Application code is unchanged, **but crash-diagnostic defaults changed**; this is a waiver, not evidence of target or panic-path validation. The prior build/configuration evidence above remains the validation record; no new hardware test was performed. A separately controlled panic check with synthetic secrets remains optional, not a completion gate; do not add a production panic endpoint or erase flash. Disabling dumps does not erase old contents.
### Phase 10 — Simple admin web firmware upload
**Complete — explicit user acceptance on 2026-09-18: firmware upload works and normal operation is verified.** The admin **Settings → HTTPS / Reboot** firmware card uploads the locally built `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. Install this OTA-enabled firmware by wire first, then use web application uploads. There is no release server, automatic download or update service. [Firmware update and recovery](#phase10-update-and-recovery) below covers operation; [regression guidance](#phase10-regression-guidance) covers future checks.
Implemented scope:
- Raw `POST /api/firmware` requires exact `Content-Type: application/octet-stream`, known exact `Content-Length`, cookie, same-origin Origin and `X-CSRF-Token`; no multipart/JSON, Basic authentication or ordinary `user` access. Reject queries, ambiguous headers, chunked transfer and `Expect`; UI code leaves browser-managed headers alone. Admission precedes body/flash work; session/principal currentness is rechecked before boot selection. The card confirms upload/reboot and shows progress/results without automatic retries. HTTPD capacity is 40 method/path handlers.
- Standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) stream one upload through a 4 KiB buffer into the inactive slot (two 4 MiB app slots). A reboot task with a 2 KiB stack is preallocated before erase. Lifecycle/identity reservations and ordinary software-reboot gating exclude competing work; no custom flash protocol or whole-image RAM buffer.
- Accepts an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Browser hints require a nonempty `.bin` of at most 4 MiB; the server bounds raw length against actual destination capacity before trusting HTTPD's narrowed length. Checks completeness, target/header and exact SDK image length/integrity, including mandatory appended SHA-256, before selection. The digest is not publisher authentication; neither it nor filename checks prove board compatibility or a working application.
- Before selection, rejected/incomplete uploads and receive/write/validation failures abort any live OTA handle without selecting the candidate; the inactive slot may be erased/partially written. Boot-metadata failure (`firmware_commit_failed`) needs inspection and carries SDK transactional uncertainty. `200 {"ok":true,"rebooting":true}` means validation/selection succeeded; successful synchronous send schedules restart after 500 ms, retaining reservations, but proves neither browser receipt nor boot success. Response failure after selection schedules no restart, releases reservations for manual reboot and latches further admissible uploads to 409 `firmware_selected_reboot_required`; the latch survives HTTPS stop/start until device reset.
- Synchronous HTTPD receive/flash blocks other HTTPD work: browser sessions can stall/drop. The 120-second total receive-loop and ten-second stall checks use the existing one-second socket timeout, not an absolute deadline: synchronous SDK erase/write/validation and scheduling are not preempted. The browser's 180-second timeout cannot cancel committed work. UART0/native USB remain independent paths, not guarantees of uninterrupted serial timing during flash; reboot interrupts all transports. Software exclusion cannot prevent physical reset, power loss or panic.
- **Preserve NVS:** write only the inactive application slot and the OTA selection metadata. Do not erase the chip, rewrite the partition table/bootloader, or touch `nvs`, `nvs_key`, PHY, storage or other data partitions. Existing users, passwords, Wi-Fi/serial settings and HTTPS/SSH identities remain stored, as with an application-only wired update without erase. This preserves stored bytes; the uploaded firmware must still understand the existing schemas and must not itself erase/migrate them incompatibly.
- Keep wired USB-to-UART flashing documented as recovery if the uploaded application does not boot or no longer serves the web UI. Basic image validation cannot prevent a valid but broken application from requiring wired recovery.
**Not in scope:** secure boot, image signatures/signing-key management, anti-rollback/version-downgrade enforcement, automatic rollback/post-boot health-confirmation machinery, remote release discovery, partition migration or NVS backup/restore. Use upstream OTA support without dependency patches. The administrator is responsible for selecting trusted, compatible firmware, including when deliberately installing an older build.
**Historical integration build and host evidence (parent-reported):** `pio run` **PASS**, **94,220 B linked RAM / 1,847,645 B flash**. Against Phase 9's 94,196 B / 1,828,565 B, this is **+24 B RAM / +19,080 B flash**. Final backend 88 cases plus the pinned SDK begin/abort failure-contract test, UI 169 groups, server lifecycle 44, admin transport 25, console lifecycle, SSH runtime and cookie lifecycle checks passed. Additional cookie base/admin/display/lifecycle, HTTPD idle cleanup 18, and SSH management/runtime/security checks passed; the extra cookie `--admin` run initially lacked a reboot symbol in its fixture, corrected in test-only code before passing. Host doubles are not target evidence. The later concise-UI fix was copy-only; its latest reported UI regression passed all **169 groups**, with no rebuild after the text change. The build figures above remain historical; this documentation update ran no build or tests.
**Acceptance (2026-09-18):** after the firmware upload implementation and concise-UI fix, the user confirmed: “That works perfectly. And the usual operation is also verified.” Phase 10 is complete by this explicit acceptance of working upload and verified normal operation. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or wired-recovery passes. The [regression guidance](#phase10-regression-guidance) below is reusable future guidance, not an acceptance blocker.
#### Phase10 update and recovery
1. Keep **USB-to-UART** available for UART0 administration/flashing; native USB CDC is network-independent UART1 access, not administration. Install the updater by wire first: older firmware without the route cannot install its own first web updater. The custom OTA layout is required; [one-time old-layout migration](../getting_started.md#partition-migration-and-erasing) is separate and destructive.
2. Build trusted, schema-compatible `esp32-s3-devkitc-1-n16r8` firmware with `pio run`; choose **`.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`**, never a merged image or a renamed non-application file. Observe the [downgrade warning](#phase8-legacy-credential-compatibility).
3. Save desired RAM-only settings and record nonsecret configuration/public HTTPS/SSH fingerprints. Arrange a maintenance window and stable power/network; sign in as `admin` over trusted HTTPS, open **Settings → HTTPS / Reboot → Firmware update**, choose the file and confirm **Upload and reboot…**.
4. Wait for validation/reboot, not just 100% transmitted bytes. Restore network reachability, reload and explicitly sign in; verify the running application, saved configuration and identities before another upload.
5. A lost response, timeout, page close, sign-out or browser abort proves neither cancellation nor failure. The UI locks uncertain outcomes without replay; reload clears only browser locks, not the device latch. Inspect through UART0 and deliberately reboot if appropriate: any later reset can boot an already selected image.
6. For initial wired installation or recovery when boot/HTTPS fails, connect USB-to-UART, select a known-good compatible checkout, run `pio run --target upload`, then `pio device monitor -b 115200`. **Do not erase for ordinary updates or recovery**; there is no automatic recovery flashing/rollback.
Replies: **400** malformed/incompatible/incomplete input; **401/403** authentication/Origin/CSRF or `admin_required`; **408** `firmware_timeout`; **409** selected-image latch; **413/415** destination capacity/content type; **500** `firmware_write_failed`/`firmware_commit_failed`; **503** busy/unavailable/resources. Unread rejected bodies close rather than drain; bounded secret-free JSON may never reach a disconnected browser.
#### Phase10 regression guidance
These are reusable checks, **not recorded passes or outstanding acceptance gates**. Record only performed outcomes; host flash/network/scheduling doubles are not hardware evidence. Focused host suites: `python3 tests/web_firmware_update/run.py` and `python3 tests/web_ui_session/run.py` (the SDK begin/abort case covers a live handle published on erase failure).
- Exercise wired-first install, known-good upload/reboot/new build and explicit reconnect; compare saved users/roles, password/key login, Wi-Fi/serial/display/hostname settings and public HTTPS/SSH fingerprints without recording secrets.
- Interrupt/cancel receive; reject corrupt, truncated, wrong-target, non-application and oversized inputs without unintended selection/restart, then deliberately perform a valid update. Reject unauthenticated/ordinary-user, wrong-Origin and missing/wrong-CSRF requests before flash. Never attempt power interruption at commit without wired recovery ready.
- Exercise competing upload, HTTPS lifecycle/identity and ordinary reboot exclusion; HTTPD requests may wait rather than promptly return busy. With controlled post-selection response-failure injection, verify no automatic restart, subsequent 409 (also after HTTPS restart), then manual reboot; an arbitrary disconnect does not establish this fault case.
- Observe web stalls/drops and serial/network recovery, single-writer isolation, UART0 recovery and native USB UART1 access without networking; demonstrate known-good wired recovery without erase and recheck configuration/identities. Do not claim uninterrupted traffic.
## Current and planned phases
**Phases 8, 9, 10 and 12 are complete** for their accepted scopes. Phase 9 includes the explicit new-hardware-check waiver above; Phase 10 includes explicit user acceptance of upload and normal operation. Phase 12 includes explicit user validation and the fresh-boot/full-client evidence below. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak, recovery or reserve measurement was performed for completed phases.
### Phase 12 — Advanced network integration
**Complete by explicit user validation:** “I just validated Phase 12 successfully.” Accepted baseline:
- Dual-stack local access and DNS-SD discovery: advertise available HTTPS and SSH services using the shared `sak-<suffix>.local` hostname, with appropriate IPv4 `A` and IPv6 `AAAA` records. Preserve IPv4 access and verify IPv6 support throughout HTTPS, WebSocket and SSH. Clients choose address-family preference and fallback; DNS-SD cannot mandate IPv6 preference. Certificate-name integration and trust remain separate concerns.
- Send the configured device hostname (`sak-<suffix>`, without `.local`) through DHCPv4 Host Name option 12 so a suitably configured DHCP/DNS server can publish the lease address in its own DNS zone. Set the STA netif hostname before DHCP starts, define how hostname changes reach subsequent DHCP exchanges, and verify the transmitted option and server-side DNS registration. DNS publication and the DNS domain remain server policy; mDNS does not provide this integration.
- IPv6 baseline: link-local addressing and SLAAC-provided ULA/global addresses, with mDNS/DNS-SD for local discovery; no DHCPv6 requirement. Preserve link-local interface scope and keep advertisements synchronized with usable addresses and service availability. Registration of SLAAC addresses in infrastructure DNS is a separate optional feature, outside this baseline; SLAAC does not provide automatic hostname registration.
- Validate IPv4-only, IPv6-only and dual-stack operation, including reconnects, address changes and service lifecycle changes. Discovery remains local-link by default; preserve UART0 and native USB recovery regardless of network state.
The device is not intended to become a general-purpose router. Captive-portal interception, unauthenticated DNS redirection, NAPT, and a plaintext serial listener remain out of scope unless the project requirements are explicitly revised.
#### Implementation and operational boundaries
- The Wi-Fi owner installs the full configured hostname before STA connection and updates it after hostname edits. DHCPv4 option 12 uses `sak-<suffix>`, not `.local`. Rename does not force DHCP restart: the next DHCP exchange carries the new name, and DNS registration/cache cleanup remain server policy. Existing hostname limits and NVS formats are unchanged.
- STA enables SLAAC and IPv6 link-local creation; either IPv4 or a preferred IPv6 address establishes `ONLINE`. **Link-local-only counts as online**, prevents IPv4-only timeout/failover, and can retire the fallback AP after the existing stability interval. This indicates local address availability, not Internet reachability or a default route. Web, CLI and OLED distinguish absent IPv4 from IPv6 availability. A diagnostic follow-up adds up to three preferred numeric IPv6 addresses to the snapshot, copied and cleared with the same observation; `wifi status` and browser Network settings print them in link-local/ULA/GUA groups; overview/OLED status retains availability flags. The settings JSON is bounded to 2304 bytes, with no extra netif calls on HTTPD.
- HTTPS/WebSocket use IDF's existing dual-stack listener. SSH explicitly uses one dual-stack listener, retaining the existing two-session limit. Bracketed IPv6 HTTPS authorities are canonicalized and bound to the existing Host/Origin/session checks; scoped IPv6 literal URLs are rejected. Prefer the `.local` hostname for link-local browser access, subject to client resolver support. No certificate regeneration, new trust mechanism, or automatic IPv6 preference is introduced.
- STA-only mDNS advertises `_https._tcp:443` and `_ssh._tcp:22` according to listener availability, without TXT metadata. Record and address-family changes converge through the existing Wi-Fi owner, including offline reconciliation; service setters do no component work. The responder is not restarted for ordinary changes. Initialization failure remains latched/nonfatal.
- mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently.
- `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set.
- `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added.
- The accepted baseline initially retained IPv4-only traceroute and limited family/scope selection. The later requested [dual-stack diagnostics follow-up](../command_reference.md#dual-stack-network-diagnostics) adds `-4`/`-6` to ping/traceroute/nslookup across UART0, admin SSH and browser Admin, scoped IPv6 probes and explicit A+AAAA lookup. This follow-up is built/host-tested, not covered retroactively by the acceptance capture below. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning remain outside scope.
#### Validation evidence and acceptance
`pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the guarded mDNS source compiled: **94,396 B linked RAM / 1,853,025 B flash**, +176 B RAM / +5,380 B flash versus the recorded Phase 10 build. These are static link sizes, not runtime heap or stack headroom. Initial target compilation rejected lock-free bool atomics; the implementation now uses short portMUX sections. Existing SDK Kconfig notes remain.
Host checks passed: Wi-Fi/address/hostname reconciliation, mDNS lifecycle/family repair, actual patched multicast-membership functions and CMake guards, SSH dual-stack/management/runtime/security, HTTPS authority parser (689 cases on host and actual installed lwIP parser), cookie authorization variants, Network settings, browser UI/CSP (171 groups), HTTPS lifecycle (45 groups and eight status projections), firmware upload (88 cases plus SDK contract), broker diagnostics, and session-store/serial integration. Network-settings host fakes were updated after the new netif dependency exposed a compilation failure; the rerun passed. These are not packet-level or hardware evidence.
Follow-up after the user's live status report: both preferred-address flags were `yes`, while plain client `ping` selected IPv4. Source verification confirms the flags require actual preferred addresses; this is not evidence of failed SLAAC or verified IPv6 reachability. `wifi status` now prints the addresses for diagnosis. Latest follow-up `pio run` passed: **94,444 B linked RAM / 1,853,685 B flash** (+48 / +660 versus the initial Phase 12 build). Browser Admin console now permits canonical Wi-Fi commands, including disruptive edits and hidden prompts, as explicitly requested. Focused policy/browser Wi-Fi prompt, address snapshot/rendering, Network/cookie and UI regressions passed. The broader console-boundary suite passes its console/certificate stages but has a pre-existing SSH-adapter fixture compilation failure for missing `web_firmware_update_reserve_reboot`; no production change was made to hide it.
Latest address-display build: `pio run` **94,444 B linked RAM / 1,854,485 B flash**; Network API/owner checks and **174 browser groups plus CSP** passed. These remain build/host results, separate from the user's device evidence below.
##### User-reported device acceptance
User explicitly validated Phase 12 after confirming numeric IPv6 ping/HTTPS, Avahi AAAA resolution and normal IPv6 hostname access following the Fedora `authselect` mDNS correction. The earlier hostname failure was a client resolver configuration issue, not missing device IPv6 addresses. The acceptance capture adds:
- **Fresh boot:** HTTPS, SSH and mDNS running without reported startup failures; zero SSH sessions, serial stopped, no broker clients, USB attached but not host-open. The configured UART profile is **230400 baud, 8N1, RTS/CTS, DTR active, RTS threshold 96**.
- **Full client mix:** two authenticated public-key SSH sessions with IPv6 ULA peers (one serial observer, one admin console); two serial WebSockets (one writer, one observer); one active browser admin WebSocket; native USB host-open as an observer. Four broker clients, exactly one writer, all reported pending/event counts zero. Serial running with RX-available/TX-pending zero, CTS asserted and valid RS-232 voltage at observation time.
- SSH: two successful handshakes, zero handshake/authentication failures or timeouts, zero stream I/O failures and zero session revocations. Stream counters **RX 68 / accepted 65 / rejected 3 / TX 161,209 bytes**; two broker writer revocations were reported. The rejected bytes are retained as evidence, not silently described as zero loss or assigned an unverified cause.
- Web serial: two connections, **27 accepted RX frames/bytes**, zero rejected RX frames/bytes; **782 binary TX frames / 318,230 bytes**, 11 control frames / 948 bytes. Send/queue/protocol/close failure counters zero. Two writer requests were denied; the final broker snapshot still shows one writer. Browser admin: one connection, **106 RX / 3,721 TX bytes**, zero send/queue/protocol/authorization/backpressure failures. No response errors reported.
- HTTPS and SSH both report running, not transitioning, `ESP_OK`; mDNS reports expected announcement and `ESP_OK`. Expected announcement alone is not packet-level service-record proof; the earlier successful Avahi lookup separately establishes the reported AAAA lookup.
All heap figures are bytes, copied from the user's observations:
| Observation | Heap capability | Free | Lifetime minimum-free | Largest block |
|---|---|---:|---:|---:|
| Fresh boot | Internal 8-bit | 65,880 | 64,912 | 31,744 |
| Fresh boot | Internal DMA | 58,124 | 57,156 | 31,744 |
| Fresh boot | External PSRAM | 8,196,732 | 8,187,980 | 8,126,464 |
| Full client mix | Internal 8-bit | 35,408 | 8,528 | 22,528 |
| Full client mix | Internal DMA | 27,652 | 772 | 22,528 |
| Full client mix | External PSRAM | 8,111,952 | 8,072,612 | 7,995,392 |
SSH owner stack: **20,480 B** configured, **18,476 B minimum-free** at fresh-boot observation and **16,284 B minimum-free** under the full mix.
**Evidence limits:** minimum-free is the conservative sum of matching heap regions' lifetime minima, not a simultaneous free-space measurement or guaranteed allocation reserve. Internal 8-bit and DMA capabilities overlap and must not be added as independent pools. The **772 B DMA lifetime minimum remains a headroom watch item**, not an observed allocation failure or acceptance blocker. Boot output contains two TLS `-0x004C` receive errors and unauthenticated-request failures; the capture does not establish their cause or connect them to memory exhaustion. Command snapshots are non-atomic; differing cumulative request/authentication counts must not be treated as one aligned measurement interval. No UART overflow/per-observer drop counters, soak duration, byte-for-byte capture integrity, exhaustive fault injection, DHCP packet capture/server-zone update, or every address-family/service transition is established by this excerpt. User acceptance is recorded without inventing those passes.
##### Reusable regression guidance
The following procedures are retained for future regression testing, **not outstanding acceptance blockers or a claim that every item was executed**. Keep UART0 and USB recovery available:
1. On IPv4, IPv6-only and dual-stack STA networks, exercise HTTPS login, serial/admin WebSockets, SSH user/admin access and one-writer/observer isolation. Test hostname access plus IPv4 and unscoped ULA/GUA IPv6 literals; verify SSH host keys and HTTPS identity rather than trusting discovery.
2. Capture DHCP DISCOVER/REQUEST option 12 at boot, rename/renew and reconnect, including a maximum-length suffix. With a configured DHCP/DNS server, verify the resulting zone entry and its server-controlled update/removal behavior.
3. Browse `_https._tcp` and `_ssh._tcp` and inspect A/AAAA/SRV answers over both families. Stop/restart each service; rename; drop/reacquire DHCP while IPv6 survives; change/deprecate/expire RA prefixes; disconnect/reconnect. Confirm eventual withdrawal/restoration, valid-address AAAA semantics and no persistent zero-A response. Exercise repeated family transitions and, where practical, missed-event/action repair.
4. Check link-local-only readiness and the fallback-AP transition explicitly. Confirm the client preserves interface scope; do not infer browser link-local success from `ONLINE` alone. Test multicast filtering/AP isolation separately from firmware address readiness.
5. Repeat the previously accepted 230400-baud mixed USB/WebSocket/SSH workload with discovery and address changes. Capture current/minimum internal, DMA and PSRAM availability, task stack margins, UART overflow and per-client drop counters. No new runtime memory reserve or maximum-baud performance guarantee is claimed.
Phase 12 is complete by the explicit user validation above. This acceptance update changes documentation only; the agent did not run a new build, test, upload, erase or device operation.
## Cross-phase release gates
Every phase should satisfy the following before being marked complete, with any user-waived check explicitly recorded in that phase rather than reported as passed:
1. A clean release build for the ESP32-S3 N16R8 target.
2. No new compiler or project diagnostics attributable to the change.
3. Focused automated tests where practical and documented hardware tests where hardware behavior is involved.
4. UART0, native USB CDC, and previously completed network transports still operate.
5. Broker writer/observer semantics and binary transparency remain intact.
6. Failure, disconnect, stop/start, reboot, and stale-session cleanup paths are exercised.
7. Runtime memory, stack, socket, and queue bounds are measured under maximum supported concurrency.
8. Secrets remain absent from ordinary status output and logs.
9. [Command reference](../command_reference.md), [Hardware wiring](../wiring.md), electrical tests, and this roadmap are updated as applicable.
10. New dependencies receive security, maintenance, and GPL-3.0 compatibility review.
## Explicitly deferred work
The following are not implemented merely because flash partitions or library support exist:
- NVS, flash, or PSRAM encryption.
- Secure boot or production eFuse provisioning.
- Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is complete in Phase 10; these advanced update features remain deferred.
- Core-dump collection or secret-safe core-dump processing.
- Filesystem mounting.
- SFTP, SCP, SSH `exec`, forwarding, or subsystems.
- General routing, NAPT, captive-portal interception, or unauthenticated serial access.
Deferring these features is intentional: each changes the security model, recovery behavior, or bounded-resource assumptions and therefore requires its own design and validation gate.
+242
View File
@@ -0,0 +1,242 @@
# User administration and authentication tests
Source/build/test paths in code spans and commands are relative to the repository root
unless a different working directory is stated; Markdown links are relative to this document.
Reusable procedures, **not execution evidence**. [Phase 8 is accepted](roadmap.md#phase8-acceptance-evidence); overall 8D.22 user sign-off does not assert that every checklist item below ran. Historical 8A/B sections describe their original baselines only; current cookie authentication, removed bootstrap/credentials and UART0-only recovery override them. Use current contracts and the integrated regression section for present behavior. Never log generated/entered passwords, cookies/tickets or private/verifier material.
## Current Broker management regression procedure — 8D.16
The [Broker contract](web_administration.md#broker-and-contextual-controls) defines current API, generations, wrap safety and bounds. Retain these checks for future regressions; acceptance is recorded separately.
- Verify admin-only detailed client rows against non-consuming UART0 snapshots/counters; direct normal-user management GET/POST denied. Observe zero/one/full client mix without reading UART payloads as a probe.
- Opening, selecting, cancelling, refreshing and navigating must leave writer and both terminals unchanged. Explicit full-page Refresh clears selection; contextual refresh retains selected target/version and sticky stale/absence latches until deliberate reselection. Native confirmation must identify exact target and snapshot writer.
- Assign among USB/SSH/two browser serial clients with both admins connected. Disconnect/reuse target while confirmation is open; race writer release/reacquire, competing requests, shell force and local release. Stale confirmation must fail without changing the intervening lease; Refresh/reselect/confirm explicitly.
- Test separate-login result isolation, same-login tab replacement, pending capacity, lost ACK/result, bounded polling/manual recovery, logout/expiry/revocation and HTTPS stop/restart. Never infer cancellation from connection loss or replay automatically.
- At signed-off 160 MHz/230400 baud, check full-mix binary traffic/drop isolation, UART0/USB recovery and optional-route failure isolation. Capture exact revision/mix and internal/DMA/PSRAM plus HTTPD/dispatcher margins; host tests/build do not approve reserves or target throughput for an unmeasured workload.
## Current Network settings regression procedure — 8D.12/8D.13
The [Network contract](web_administration.md#network) defines the current byte codec, owner/persistence semantics and uncertainty. Retain these regression procedures without inferring individual execution from overall sign-off.
- Verify admin-only Settings/Network and direct-route normal-user denial, current cookie/principal, body/query/framing/Origin/CSRF checks, unavailable/contended snapshots and generation races against CLI/local controls.
- Round-trip UTF-8 and arbitrary SSID bytes through text/hex, including NUL/BOM/non-UTF-8 and 32-byte boundaries. No saved PSK or length may appear in responses, status/logs/completion/local display. Keep omits credentials; Replace never accepts blank; disabled-STA Clear (including disable+clear) works; enabled-STA/AP clear is denied even when AP policy is off. Check transient-input expiry and context/session clearing.
- Distinguish RAM Apply, explicit Save, stored-only Wi-Fi Load and reboot persistence. Missing/invalid/failing Wi-Fi storage must not install generated defaults or new AP secrets. Exercise stale generations, queue failure/drop accounting and NVS failures without secret logging. No Wi-Fi reset/default/export action exists.
- Prepare UART0 and USB before confirming disruptive actions. Test Start/Stop (including RAM boot policy), Reconnect/Next, AP policies and stopped no-ops. The selected profile is an edit target, not explicit connection selection; Next uses canonical priority/wrap. Cancel confirmations and exercise lost ACK/401/disconnect, manual Check Result/Refresh, another-tab result replacement and no automatic replay. `accepted` is not online; delivery before disconnection is not guaranteed. Reconnect via STA/AP; UART0 administers recovery, USB preserves independent UART1 access.
- Test mDNS generation/Set/Save/Load/Defaults, live/offline reannouncement, next STA IP, init/live failure isolation and `applied_not_queued`. Verify actual client DNS and changed-hostname browser trust/login, not merely `announced`. Confirm no unintentional Wi-Fi secret reset.
- With USB/two web serial/SSH serial and both admin routes, verify hidden output draining and writer/observer preservation through Settings navigation. Separate actual network-disruption losses from serial/broker regressions. Exercise optional Network route/timer failure and stop/restart without taking down unrelated routes. Browser-shell restrictions remain unchanged.
- Capture boot/full-mix internal/DMA/PSRAM free/minimum/largest blocks and memory floors during TLS/Network operations; timer heap/slot costs, repeated-operation cleanup/soak and **HTTPD/dispatcher stack margins** remain required. Record exact revision/client mix and nonsecret counters, including broker and manager queue drops. Host tests do not establish target reserve or hard scheduling/cancellation guarantees.
## Current legacy-removal regression procedure
The legacy bootstrap/credential/reconciliation instructions in the phase baselines below are **historical only**, superseded by [legacy credential removal](roadmap.md#phase8-legacy-credential-compatibility) and the current [command reference](../command_reference.md). Basic authentication is also historical; current HTTPS uses cookie login. The current overrides apply to later Phase 8C/browser procedures too: no `user bootstrap` or `web credentials` command remains, first-admin creation uses normal UART0 `user add`, and recovery rebuilds empty. Never treat the checklist below as evidence of execution.
1. On a disposable controlled NVS image with only `user_db/database` missing, boot and confirm an empty database is persisted with zero accounts/admins and no imported credential. Reboot and confirm it remains empty. Keep physical UART0 attached; do not factory-erase the device for this test.
2. Run `user add maint admin` on UART0, check hidden password confirmation and cancellation, then confirm account/password persistence after reboot. Separately exercise `user add operator user --generate` and secure one-time display. Final-administrator delete/demotion must still fail. Existing SSH own-password generation restrictions and typed browser generated-password support remain unchanged.
3. Load a valid existing v1 user image and verify accounts, roles, IDs, auth generations, verifiers and keys are unchanged, including a formerly migrated role-`user` account. No bootstrap status should appear and no account should be silently promoted. No web material change may synchronize a verifier.
4. On a disposable malformed user image, confirm authentication fails closed without automatically overwriting storage. On UART0 run `user recover --force`, confirm empty storage, then `user add maint admin`. Recovery must refuse healthy databases, including healthy empty storage, and be unavailable through SSH/browser. Verify serial/Wi-Fi configuration and TLS/SSH identities remain intact. Inject read/write/commit failures where available and check failure isolation and complete committed records, not partial live mutations.
5. Upgrade valid 1,392-byte v1 `web_sec/material`; verify persisted 1,340-byte TLS-only v2 and exact certificate/key DER, fingerprint and generation retention in a controlled fixture without logging private data. Reboot and confirm identity continuity. Malformed/unknown/cryptographically invalid records and migration read/write/commit failures must fail closed without fallback replacement. Do not interpret a failed commit as proof that no flash write occurred.
6. Confirm help/completion/status expose no legacy credential/bootstrap operation or secret. Removed commands must reject without mutation. Exercise retained user generation and `web certificate rotate --force` through supported frontends. `web reset --force` must change TLS only, require new certificate trust/login after HTTPS restart, leave user credentials/generations unchanged, and not revoke unrelated SSH sessions.
7. Recheck UART0/native USB availability and broker one-writer/isolated-observer behavior with network authentication unavailable. Record only nonsecret counters and telemetry. Older v1-only firmware cannot read HTTPS v2; logical NVS replacement is not secure flash erasure and no factory erase is required.
Acceptance and prior evidence limits are recorded in [legacy compatibility](roadmap.md#phase8-legacy-credential-compatibility) and [overall acceptance](roadmap.md#phase8-acceptance-evidence); the preceding procedure is not an execution log.
## Historical Phase 8A baseline — role-based database and UART0 administration
Keep UART0 physically attached throughout these tests. At the Phase 8A baseline, HTTPS and SSH still used the legacy shared credential while the new database and public keys were prepared for the Phase 8B cutover.
### 1. First-boot migration and persistence
Upgrade a device that already has working HTTPS/SSH credentials and no `user_db` blob. After boot:
1. Run `user status` and `user show admin`.
2. Confirm exactly one `admin`-named account exists with role `user`, `bootstrapped=no`, no displayed password/salt/verifier, and no public keys.
3. Confirm the pre-upgrade credential still authenticates to HTTPS and SSH and that serial terminal behavior is unchanged.
4. Reboot and confirm `user status` reports the same account ID and authentication generation.
5. Confirm routine boot logs, `web status`, and `ssh status` contain no user password, verifier, salt, or raw public-key blob.
A transient failure reading the legacy web credential must not create or commit an empty user database. If a valid `user_db` blob already exists, it must still load without the legacy credential being readable.
### 2. Administrator bootstrap
Run `user bootstrap`, verify that typed password characters are not echoed, enter a 12–64 character printable password twice, and confirm `user show admin` now reports role `admin` and a higher authentication generation. Reboot and confirm `bootstrapped=yes` and one administrator persist. On a separate resettable test image, exercise `user bootstrap --generate`; record the generated password securely for the test, verify it is displayed once, then clear the terminal scrollback after use.
During Phase 8A, verify the newly bootstrapped password does **not** replace the legacy HTTPS/SSH login yet and the existing network credential continues to work. This separation is intentional until Phase 8B. A second `user bootstrap` must be rejected without changing the account.
### 3. Account CRUD, capacity, and invariants
Use generated test passwords where practical:
```text
user add operator user --generate
user add maint admin --generate
user list
user show operator
user role operator admin --force
user role operator user --force
user password operator --generate
```
Confirm unique valid usernames are accepted; uppercase, empty, oversized, or punctuation-invalid usernames are rejected. Confirm duplicate users are rejected. Fill all eight slots and verify a ninth account is rejected without altering existing records; delete a non-final account and confirm its slot can be reused with a new account ID.
With two administrators, demote or delete one and confirm the operation persists. Then attempt both deletion and demotion of the final administrator and confirm both are rejected. Before bootstrap on a separate migration run, verify `user delete admin --force` is rejected so legacy synchronization remains recoverable. `Ctrl-C` during either hidden password prompt must cancel without changing the account or leaving stale input in the next console command.
### 4. SSH public-key import and fingerprints
Generate disposable Ed25519 and ECDSA P-256 OpenSSH keys on a trusted host. For each supported type, run `user key add <username>`, paste the single `.pub` line, and confirm `user show <username>` reports the type, stable slot index, and expected `SHA256:` fingerprint without displaying the Base64 blob or comment.
Verify all of the following fail without changing the account generation or key list:
- Unsupported RSA, certificate, or unknown key types.
- Truncated/invalid Base64, mismatched outer and embedded types, trailing blob data, malformed Ed25519 lengths, and an invalid/off-curve ECDSA point.
- Adding the same key again to the same account.
- Adding a fourth key to an account that already has three.
- Deleting an empty/out-of-range slot.
Assign the same public key to a second account and confirm it is accepted, receives an independent slot, and authenticates as the username selected by the SSH client.
Then exercise `user key delete <username> <index> --force` and `user key clear <username> --force`, reboot, and confirm the exact remaining fingerprints persist. Phase 8A stores these keys but does not yet accept SSH key login.
### 5. Legacy credential rotation boundary and reboot reconciliation
Before administrator bootstrap on a resettable test image:
1. Run `web credentials rotate --force`.
2. Confirm the console reports that the pre-bootstrap migrated verifier was synchronized.
3. Verify the new legacy credential works for HTTPS and SSH, reboot, and confirm `user show admin` retains the synchronized authentication generation.
4. Where NVS fault injection or controlled power interruption is available, interrupt after the web credential commits but before the user blob commits. On the next boot, confirm reconciliation updates the migrated verifier rather than committing an empty database or losing network recovery.
After bootstrap, rotate the legacy credential again. Confirm the console explicitly says it remains separate from user passwords, existing network sessions are revoked, the new legacy credential works, and the bootstrapped administrator generation/password are not silently replaced. Apply the same checks to `web reset --force`, additionally confirming HTTPS certificate recovery/restart behavior remains intact.
### 6. Explicit database recovery and storage failures
Using a disposable device or test NVS image, inject a wrong-size/version or malformed `user_db` blob and reboot. Confirm normal database initialization fails without erasing shared NVS, but the `user` command is still registered. `user status` must report the database unavailable. Run:
```text
user recover --force
user status
user bootstrap --generate
```
Confirm recovery explicitly replaces only `user_db`, imports the current legacy credential as role `user`, and leaves serial, Wi-Fi, HTTPS identity, SSH host key, and legacy network login intact. Confirm recovery is rejected while a valid database is initialized. Simulate NVS write/commit exhaustion or failure during create, password, role, key, and recovery operations; after reboot, each operation must expose either the complete old blob or complete new blob, never a partially live mutation.
Because NVS is not yet encrypted, this test validates logical verifier storage rather than resistance to physical extraction. PBKDF2 verifiers remain susceptible to offline guessing, and append-oriented NVS may retain historical plaintext legacy credentials until a full partition/flash erase.
### 7. Phase 8A regression
With the maximum supported mix of USB CDC, WebSocket, and SSH serial clients active, repeat user list/show and several UART0 mutations. Confirm password prompts remain physical-UART-only, no user operation acquires a broker client or writer lease, existing Phase 8A network authentication and revocation behavior remains functional, and serial/broker counters show no unexplained loss. Record `memory`, `web counters`, and `ssh counters` before and after the run.
## Historical Phase 8B baseline — role-aware HTTPS and SSH authentication
These procedures preserve the Phase 8B regression baseline. Complete the Phase 8A bootstrap first and retain two disposable accounts, one `user` and one `admin`. At that baseline both roles received the serial/status interface; current Phase 8C firmware instead routes SSH administrators to the administration shell.
### 1. HTTPS passwords and fail-closed behavior
For both roles, authenticate to `/`, `/api/status`, and the browser terminal with the account password. Confirm the full 16-character username and a 64-character password work, while absent, malformed, oversized, wrong-user, and wrong-password Basic headers fail without exposing which component was wrong. After bootstrap, confirm the value from `web credentials show` no longer authenticates unless it independently equals a current user password.
On a disposable malformed-database image, confirm HTTPS returns authentication-service unavailable and never falls back to the legacy credential. Recover through physical UART0 only. Verify `web status` reports user-database authentication and active WebSocket sessions show account, user role, and password method without secrets.
### 2. SSH password and public-key login
For both `user` and `admin`, verify password SSH login reaches the same broker-backed serial stream as before. Confirm `ssh sessions` shows the account, user role, password method, and writer/observer role. An administrator must not receive UART0 commands yet.
Import one Ed25519 key and one ECDSA P-256 key through `user key add`, then verify each matching private key authenticates without a password and `ssh sessions` reports `public-key`. Confirm an unregistered key, wrong username, unsupported RSA key, certificate key, and three failed completed attempts are rejected. Normal client probe-then-sign behavior must consume one completed attempt, not two; unsigned probes alone must never create an authenticated session or broker client.
### 3. WebSocket ticket binding
Authenticate separately as two users and request tickets. Confirm each ticket is accepted exactly once, expires after 30 seconds, and cannot be reused or altered. Change one account after ticket issuance but before WebSocket connection; its ticket must fail closed without creating a broker client, while the other account's ticket still works. Repeat with delete followed by recreation of the same username and confirm the old ticket remains invalid.
### 4. Targeted session revocation
Connect simultaneous WebSocket and SSH sessions for two different users and place one affected session in writer state. One mutation at a time, exercise password change, role change, SSH-key add/delete/clear, and account deletion. Required results:
1. Outstanding tickets and active sessions for the changed username are revoked promptly and any writer lease is released.
2. The stale session cannot inject additional serial bytes or reacquire writer ownership.
3. The unrelated user's WebSocket/SSH sessions remain connected and retain their expected broker roles.
4. New authentication succeeds only with the updated password/key/role state.
5. Deleting and recreating the same username does not revive old tickets or sessions.
Run `web status`, `ssh sessions`, `web counters`, `ssh counters`, and `broker clients` around each mutation. Also rotate `web credentials rotate --force` after administrator bootstrap and confirm it changes only recovery material: role-based HTTPS/SSH sessions remain connected and user passwords are unchanged. `web reset --force` restarts HTTPS because the TLS identity changes, but must not revoke unrelated SSH sessions.
### 5. Concurrency regression
With USB CDC, two role-based network users, one WebSocket terminal, one SSH terminal, and UART1 traffic active, alternate writer ownership and mutate one account. Confirm binary transparency, observer isolation, bounded authentication/handshake behavior, UART0 responsiveness, and no unexpected disconnect of the unaffected user. Record memory, broker, web, SSH, and serial counters before and after. Repeat after reboot to verify passwords, roles, keys, and authentication methods persist.
## Phase 8C SSH administrative shell
**Validation status: passed on target hardware.** The full matrix below passed, including concurrent USB/WebSocket/user-SSH/admin-SSH operation. At 460800 baud with SSH and WebSocket clients in parallel, substantial packet drops and slower display controls were observed under load, but memory exhaustion could not be provoked and no lower baud-rate cap is planned.
Use one disposable `admin` and one disposable `user`. Keep UART0 attached throughout. The SSH server still accepts only shell sessions: `exec`, subsystem/SFTP/SCP, forwarding, and unauthenticated connections must remain rejected.
### 1. Route separation and normal shells
1. Connect as the normal user and confirm the existing broker-backed UART1 serial stream, broker client, and writer/observer behavior are unchanged.
2. Connect as the administrator and confirm the `admin@serial-tool>` prompt appears. Run `help`, `memory`, `serial status`, `wifi status`, `web status`, `broker status`, and `broker clients`. Compare representative output with UART0 and confirm both routes execute the same registered command implementations.
3. From UART0 run `ssh sessions` and `broker clients`. The user session must show `route=broker`; the admin session must show `route=admin-console`, `broker=0`, `broker-role=n/a`, and no writer lease. The admin session must not start UART1 or alter broker client/writer counts merely by connecting.
### 2. Bounded command processing
Exercise printable input, backspace, Ctrl-C, CR/LF, an empty line, and a line longer than the documented limit. Confirm the command line is bounded, overflow is discarded through a clear diagnostic, and a new prompt remains usable. Run `help`, `user list`, and `broker clients` in a normal ANSI terminal and confirm every line starts in column zero: canonical LF output must be normalized to CRLF without doubling handlers that already emit CRLF.
Run at least five distinct commands, then use Up/Down to navigate the four-entry per-session history, return to a saved draft with Down, and confirm older entries are bounded out. Verify history does not survive reconnect and is not shared with a second administrator. Exercise Tab on root and nested prefixes such as `us`, `user l`, `wifi ap sh`, `wifi next`, and `ssh host-key i`; confirm the same hints are offered by UART0 and SSH. For an ambiguous prefix, confirm both routes print the same matching candidates and redraw the unchanged input line instead of cycling candidates; unique/common prefixes must redraw cleanly without inserting escape-sequence bytes into the command. Type a command, use Left/Right to insert and delete characters in its middle, then use Home/End and the terminal's Pos1/Ende keys; verify cursor placement, Backspace, and Delete change exactly the intended byte before submission.
Run an unsupported command and confirm it is rejected without affecting UART0 or the serial broker. Run the full root `help` output to exercise output-ring draining. With the SSH client temporarily unable to read output, confirm the worker applies input backpressure rather than accepting an unbounded command/output backlog; inspect `ssh counters` for admin-console admission and input-backpressure values.
### 3. Remote account administration
Run `user list`, `user show <name>`, `user add <name> user --generate`, `user password <name> --generate`, `user role <name> admin --force`, and the key delete/clear operations from the administrative shell. Confirm generated passwords appear once only on that authenticated channel, affected account sessions are revoked, and unrelated sessions remain connected.
Import both supported key types through the remote form:
```text
user key add <username> ssh-ed25519 <base64-blob>
user key add <username> ecdsa-sha2-nistp256 <base64-blob>
```
Confirm the full ECDSA P-256 command is accepted, fingerprints appear in `user show`, a duplicate on the same account is rejected, and the same key can be imported for a second account. Verify subsequent private-key SSH login uses the selected SSH username.
### 4. Interactive administration, lifecycle actions, and revocation
Confirm `user bootstrap` and `user recover --force` remain unavailable from SSH and continue to work through physical UART0. Also confirm that an administrator cannot run `user password <own-username> --generate` over its own SSH session; entered replacement passwords and generated passwords for other accounts remain available. From admin SSH, exercise manually entered user passwords and public keys, Wi-Fi station/AP secret entry, AP secret display, legacy web recovery credential display/rotation, HTTPS certificate rotation/reset, and both `ping` and `wifi ping`. Hidden characters must not echo or enter command history; visible key input must support Backspace and Ctrl-C; ping lines must remain ordered and correctly attributed to the invoking SSH session.
Exercise `ssh disconnect` for another session and the current session. For the other-session case, confirm the acknowledgement drains, the target closes, and the source shell returns. Separately test `reboot`, `ssh stop`, `ssh host-key rotate --force`, and `ssh reset --force`: each must acknowledge scheduling, stop accepting another command, drain output, and then close/reboot as appropriate. Reconnect after key replacement and verify the new fingerprint. Simulate an unread SSH output window and confirm the destructive action cancels after its bounded drain timeout rather than remaining pending forever.
While an administrative command is queued or running, use UART0 to change that admin's role/password/key or delete it. Confirm no second remote administrative command runs after the mutation, the SSH session is revoked promptly, and queued output is not delivered to a reused SSH slot. Repeat with a different account mutation and confirm the administrator remains connected.
Finally, issue commands concurrently from UART0 and admin SSH, including `user list`, long `help` output, and one UART0 interactive password or key prompt while an SSH command waits. Confirm the single dispatcher serializes all `esp_console_run()` calls, UART0 retains its line editing/history/completion, prompt input is consumed only from UART0, outputs are not mixed between transports, and there is no stack overflow, corrupted argument parsing, database damage, or broker disruption.
## Integrated web administration regression procedure
Current retained-scope checks for future changes. [8D.22 sign-off](roadmap.md#phase8-acceptance-evidence) closes the phase; these are **not additional acceptance conditions or claims of individual execution**. Record exact revision, workload/client mix, duration, counters and heap/stack evidence for any new run. Do not treat host doubles as target timing/power-loss proof or reserve approval. Prepare UART0 recovery/native USB before disruptive tests and do not erase persisted data without explicit approval.
### 1. Integrated login and authorization
Authenticate as both roles through the same-origin login page, explicitly log out, and switch accounts without clearing browser state or relying on an HTTP Basic credential cache. Verify bounded session capacity, expiry, login throttling, malformed cookies, CSRF rejection, Origin rejection, password/role/key invalidation, deletion/recreation, and unrelated-account isolation. A normal user must be unable to discover through navigation or invoke directly any admin ticket, admin WebSocket, typed mutation API, quick setting, detailed client list, or writer-transfer operation.
### 2. Serial/admin terminal switching and lease preservation
As an administrator, connect the browser serial terminal, acquire the writer lease, send and observe serial data, then switch repeatedly between **Serial terminal** and **Admin shell**. The visible terminal contents and input route must change, but `broker clients`, the displayed browser client ID, and active writer ID must remain unchanged. Request control/Release control and writer/observer state must stay visible in both modes. While Admin shell is selected, have a normal user request the writer lease and confirm the retained browser lease prevents unintended takeover. Only explicit Release control, confirmed writer transfer, Disconnect, logout, revocation, expiry, or connection failure may release it.
Verify the browser admin shell executes the canonical registry through the single dispatcher, preserves bounded history/completion/prompts and backpressure, and does not itself become a second broker client. First-admin provisioning remains UART0-only, remote recovery is rejected, and the removed bootstrap command is unavailable everywhere. Closing only the admin-console route must leave the browser serial client and its lease intact.
### 3. Quick settings and client popovers
For an administrator, open the **Serial** and **Wi-Fi** card popovers by hover, keyboard focus, and click/tap. Confirm each path exposes the same controls, opening alone performs no mutation, Escape/outside click closes cleanly, current values refresh safely, common edits have clear apply-versus-save behavior, and links reach the full Settings section. Saved Wi-Fi secrets must not appear by default or enter routine DOM text, logs, URLs, or status responses.
Open **Broker clients** and confirm its secret-free list matches authoritative broker snapshots: client ID, transport/name, writer/observer role, and bounded queue/drop state. Open **Active writer**, choose another currently connected eligible client, confirm the transfer explicitly, and verify exactly one writer remains. Disconnect or recycle the target before confirmation and confirm its stale generation/ID is rejected without releasing or reassigning the current writer. Hover/focus alone must never transfer ownership.
### 4. Typed settings and destructive operations
Exercise user/password/role/key management, serial settings and persistence, Wi-Fi profiles/AP policy/secrets, retained SSH service/session controls, display settings, HTTPS lifecycle/reboot and HTTPS/SSH rotation through typed bounded APIs. Dedicated typed network diagnostics and ordinary browser-session/native-USB management are excluded; exercise existing shell diagnostics only through permitted frontends. Compare resulting subsystem state with the equivalent canonical CLI behavior without routing API requests through command strings. Generated passwords appear once in no-store responses; destructive or self-terminating HTTPS/SSH/reboot/security actions require explicit confirmation and explain the expected connection loss.
### 5. Concurrency and failure isolation
Run USB, browser serial, browser admin shell, user SSH, admin SSH, UART0, and active UART1 traffic concurrently. Alternate explicit writer transfers while issuing administrative commands and opening/closing popovers. Verify one writer, isolated observers, bounded memory/queues, principal revocation, no mixed admin output, no hidden lease loss during terminal switching, and continued UART0/native-USB recovery if web-session or admin-console initialization fails.
### 6. Service identity, lost acknowledgement and recovery
Confirm public fingerprints and both service/identity versions before HTTPS/SSH rotation. Race canonical CLI changes with open confirmations; stale operations must reject before mutation. For HTTPS, exercise send-return/ID-callback admission, accepted-but-lost work and failed destruction without accumulating callbacks; restart must retain owner reservation while deliberately invalidating logins. For SSH, failed stop must skip mutation/start and retained context must survive until every slot retires. Inspect precommit/postcommit partial effects without assuming an error means no change; never replay automatically. Verify new trust via UART0, fresh HTTPS login after restart, stopped-rotate versus CLI-reset behavior, and continued unrelated-service recovery. Browser identity Reset/recovery/export is not part of the workflow.
### 7. Display, API bounds and optional failures
Exercise Display generation conflicts, explicit RAM/Save/Load/Defaults/Reset, absent panel with available UI task and concurrent button activity. Reset storage failure must leave RAM unchanged. Across all typed domains check body/receive/schema limits, optional registration failures, one-slot/login isolation, queue expiry versus admitted execution, saturation and stale IDs. Secret timers cancel/wipe only non-executing work; logout/deadline cannot recall admitted commits. Compare Serial service discards with preserved broker clients/lease/output, rather than claiming uninterrupted data through explicit reconfiguration.
+138
View File
@@ -0,0 +1,138 @@
# Web administration contracts
Current implementation contracts; [Phase 10 firmware upload is complete by explicit user acceptance on 2026-09-18](roadmap.md#phase-10--simple-admin-web-firmware-upload), confirming upload and normal operation, not specific fault or recovery checks. Phase status and executed-evidence limits belong in the [roadmap](roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance record](roadmap.md#phase8-acceptance-evidence), not in implementation timelines. [Regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) describe checks, not results. Source is authoritative; start with the [code map](../agent/code-map.md).
## Authentication and admission
- HTTPS-only, same-origin login replaces Basic authentication entirely. Four digest-only opaque sessions have absolute one-hour expiry; four pre-login challenges last 120 seconds. Login is globally bounded to five credential attempts per 60 seconds. Live sessions/challenges/tickets are not evicted to admit another client.
- Raw tokens travel only in the host-only `__Host-` cookie with Secure, HttpOnly, SameSite=Strict, Path=/ and explicit lifetime; no Domain. Session records retain copied secret-free principals, CSRF state and nonzero, nonreused originating-session IDs. Passwords, raw cookies/tickets, verifiers and private keys never enter routine snapshots/logs/completion/display.
- Mutation admission requires current cookie/principal, strict Origin and CSRF validation; administration additionally requires current `admin`. Normal users retain serial/status but cannot invoke administration directly. Authentication POST fetches use CORS mode with fixed same-origin URLs and same-origin credentials: do not accept Origin `null` to compensate for browser no-referrer behavior.
- Session-store initialization is part of admitted HTTPS start; authentication failure gates HTTPS. Failed start/accepted stop disables and wipes session state. Logout invalidates only the originating session before socket cleanup; account mutation invalidates that account's sessions/tickets, including deletion/recreation, without revoking unrelated accounts. Currentness checks remain authoritative if best-effort notifications fail.
- Four serial tickets and two admin tickets are digest-only, single-use, 30-second, session/principal-bound records. Cookie/Origin/ticket/currentness and transport admission precede explicit WebSocket 101. Store RNG/SHA/database calls run outside short spinlocks; IDs/expiry/epochs fence stale publication without nested store/transport locks.
- HTTPD remains bounded to six sockets, two serial WebSockets, one admin WebSocket and 40 method/path handlers; LRU eviction is disabled. Sessions, sockets, tickets and the two shared remote-console slots are separate capacity limits. Optional settings/admin failures preserve unrelated routes where their initialization contract permits; UART0 and native USB remain independent of web readiness.
`web_httpd_adapter` alone accesses private IDF 5.5.0 HTTPD state. It rejects duplicate/ambiguous headers, postpones 101 until admission, and wipes consumed header scratch while preserving right-aligned unread bytes. Optional Settings registration stages descriptor/name allocations before publishing either, avoiding the pinned public registration failure path. Re-audit these private boundaries on SDK upgrades and same-version SDK patches: the version guard does not detect patches that retain the same version number. HTTPD response headers are pointer-backed, not copied; both `Set-Cookie` value buffers must remain valid and distinct through response send. Do not reuse or wipe those buffers before sending completes. Do not enable header/ticket debug logging. Auth documents, scripts and sensitive responses are no-store with CSP/no-referrer/frame-denial protections; authored loader changes require matching CSP hashes. Generated assets are not a normal documentation/build output.
## Terminal and console ownership
Switching Serial/Admin/Settings never creates a second serial client, requests/releases the writer lease or reconnects serial. Hidden connected terminals keep draining; only selected terminal input is sent. Both browser terminals have separate 5,000-line scrollback and 64 KiB callback-accounted pending-output bounds with visible browser-drop counts. Admin input is bounded to 4 KiB admission and 512-byte frames. Admin open/reopen is explicit; admin close is isolated. Disconnect pauses serial reconnect but retains login; logout/expiry/revocation closes affected sockets normally.
The document binds terminal state to its first validated username/role/session-stable CSRF tuple. A different identity requires a clean document before showing retained output. Pagehide fences sockets/work and hides scrollback until same-session revalidation; admin is not automatically reopened. Fit readiness caches only success and uses at most three generation-fenced animation-frame retries per external request.
`admin_ssh_console` is the sole `esp_console_run()` caller for UART0, admin SSH and browser admin. Two fixed remote-console slots are shared across SSH/browser, not two per transport. Queue records carry copied principals and transport-qualified generation tokens. Owner currentness is checked outside console locks before commands and during prompts (250 ms polling plus validation/scheduling latency), then identity is rechecked. This is operation admission, not cancellation or rollback of arbitrary executing handlers. Consumed output and retired prompt/history state are wiped.
HTTPD alone owns browser-admin socket IO and its 1,552-byte PSRAM-only payload. A 20 ms timer queues at most one owner poll, not a new task. Close uses current-owner shutdown, not queued reusable `sock_db *` pointers. Detach fences submitters; queued state retires only after successful HTTPD destruction. Failed destruction retains ownership and prevents unsafe restart/reuse.
### Browser-shell policy
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
- Browser `wifi` uses the canonical command handler, including bare `wifi` (status), profile/AP settings, persistence, lifecycle and network diagnostics. Profile/AP secret entry uses the shared hidden prompt: no input echo, history or completion of secret bytes; cancellation, disconnect and failed currentness discard the input. Explicit `wifi ap show-secret` reveals the AP password only in the invoking admin terminal; routine status and completion do not reveal credentials. Browser `mdns` still permits only exact `mdns status`. Root and `wifi`-prefixed ping/traceroute/nslookup support the same `-4`/`-6` and device-local IPv6 scope syntax as UART0/admin SSH; see [dual-stack diagnostics](../command_reference.md#dual-stack-network-diagnostics). They run on the shared dispatcher, not HTTPD or the client browser; no extra endpoint or permission expansion is involved.
- Browser `user` permits status/list/show and interactive add/password plus forced role/delete for **other accounts only**. Self changes, generated passwords, key commands and recovery are denied there; typed Accounts supports the separately bounded self/generated/key workflows.
- Browser SSH stop/disconnect/reset and host-key mutation are denied; typed SSH Settings has its own safe owner path. Do not claim full browser-shell parity.
- Browser `reboot` and owner-relative `exit` are supported. First-admin provisioning uses normal `user add` on UART0; unavailable-database recovery is UART0-only. The legacy `user bootstrap` and web credential commands no longer exist.
Wi-Fi changes use the existing canonical manager path, not deferred browser drain/acknowledgement. Stop, reconnect, profile/AP edits, load/defaults/reset can disconnect network clients before output arrives. A lost result does not cancel admitted work: reconnect and inspect before retrying; use UART0 if networking is unavailable. Changes are RAM-only until `wifi save`, except `wifi reset`, which also persists defaults. Native USB remains independent UART1 access, not an administration console.
Deferred self-affecting shell actions use the existing bounded drain/control path (up to ten seconds plus a short delay), not guaranteed peer delivery. Browser certificate rotation hands a typed action after drain/200 ms to the existing 12 KiB dispatcher, never crypto/NVS on the 4 KiB control stack. Pending input is discarded through execution, and an executing slot remains reserved across self-detach. UART0/admin SSH retain canonical recovery actions.
## Typed settings API and operation lifetime
All routes below are under `/api/settings/`. Each domain has bodyless GET snapshot plus GET/POST operation unless noted. Requests reject queries, malformed/oversized/duplicate/unknown fields and inappropriate bodies; mutation routes require bounded JSON with current admin/Origin/CSRF. GET allows absent Origin but rejects mismatch. Snapshot contention/unavailability is not an empty successful projection. HTTPD performs bounded admission/encoding only; canonical mutations run on the existing four-entry administration dispatcher using **IDs only**, not command strings, credentials, request pointers or socket handles.
| Domain and source | Snapshot / operation suffix | Request / snapshot / result buffer bounds (bytes) |
|---|---|---|
| `web_serial_settings`, serial service | `serial` / `serial-operation` | 256 / 256 / 96 |
| `web_account_settings`, user database | `accounts` / `account-operation` | 768 / 1024 (accounts), 512 (keys) / 96 |
| `web_network_settings`, Wi-Fi + mDNS | `network` / `network-operation` | 768 / 2304 / 128 |
| `web_display_settings`, local status UI | `display` / `display-operation` | 256 / 128 / 96 |
| `web_broker_settings`, session broker | `broker` / `broker-operation` | 256 / 2048 / 96 |
| `web_ssh_settings`, SSH owner/security | `ssh` / `ssh-operation` | 256 / 768 / 96 |
| `web_lifecycle_settings`, HTTPS owner/security | `lifecycle` / `lifecycle-operation` | 256 / 320 / 96 |
Mutation bodies use at most four receive attempts. Each domain retains one original-login-bound pending/result slot, nonreused operation IDs and an executing reservation. Results are replaceable, login-isolated observations, not durable history or idempotency keys. Another tab can replace a completed result. Dispatcher currentness and the 30-second dequeue deadline precede canonical owner admission. **Admitted work may finish after logout, revocation, timeout or navigation.** A failed response, 401 or disappearance of a result proves neither success nor cancellation; inspect current state before any deliberate retry.
Accounts and Network each use a one-second timer to cancel/wipe queued, non-executing secrets after 30 seconds plus scheduling latency. Shared input wipes on dequeue/rejection; executing locals wipe on return. This is not a hard wall-clock erasure/execution guarantee. Other domain deadlines are dequeue checks, not new cancellation timers. UI fences late/session-changed responses, bounds request/result checking to 15 seconds and never automatically replays/restores mutations. Serial/Accounts/Network/Display/Broker use bounded automatic checks where implemented (at most ten one-second GETs); SSH and HTTPS/Reboot require manual Check Result/Refresh.
### Serial and Display
Serial Apply/Defaults change RAM; Save persists working device state, not browser drafts. Load follows canonical defaults/fallback behavior. Reset uses canonical apply/persist/best-effort rollback. Reconfiguration/stop discards serial-service RX/TX and task-local pending data but preserves broker clients, lease and already-fanned output. USB with DTR may restart a stopped service. `/api/status` uses a zero-wait consistent serial snapshot (`running:null` if unavailable).
Display accepts dim/off timeouts 0–86400 seconds and an expected nonzero configuration generation. All writers, including CLI/legacy Apply, share a zero-wait owner reservation; NVS runs outside critical sections. Save stabilizes selected RAM; Load retains canonical fallback without rewriting NVS; Reset commits defaults **before** RAM publication, leaving RAM unchanged on failure. Buttons/diagnostic holds update activity, not configuration generation. Configuration needs an available UI task, not a physically present panel, and never takes over I2C.
### Accounts and authorized keys
The database owns eight accounts, final-admin protection and conditional target username/account-ID/auth-generation checks inside its mutation lock. Create/password/role/delete and key mutations share canonical commit/invariant logic. HTTPD uses zero-wait secret-free projections, not blocking CLI snapshots. Successful mutations request target-only web/SSH revocation; self mutation can revoke access before result retrieval.
Separate bodyless POST `accounts/generate-password` returns one 24-character value without mutation or retained retrieval. The browser uses a 60-second context-bound saved acknowledgement before separate submission; this is UX, not delivery proof, and JavaScript strings cannot be securely wiped. POST `accounts/keys` returns only slot/type/SHA-256 fingerprints for the selected identity. Key add/delete/clear share the operation slot; public-key import is at most 384 decoded text bytes in the 768-byte request and uses canonical Ed25519/P-256 validation. Three stable key indices may be sparse; never interpret response-array position as an index. No stored key blob, verifier or password is exported.
### Network
Wi-Fi config/runtime is one zero-wait consistent projection; mDNS is a separate projection, not cross-domain atomic authorization. Four stable profiles carry enabled/priority/security/SSID/password-configured metadata. `mixed` means WPA2-or-stronger, not open. `announced` is expected STA announcement, not verified DNS. Runtime `ipv6_addresses` contains up to three preferred addresses from the same snapshot as the availability flags, encoded as eight lowercase four-digit hextets without a zone. Network settings display separate link-local, ULA and GUA lists; absent groups show `none`. Link-local access needs the client's interface scope, and address presence does not assert a route or Internet reachability. Encoding uses the existing snapshot only, not netif/driver calls on HTTPD; the 2304-byte response buffer adds 256 bytes of bounded stack storage.
SSID wire values are reversible **bytes**, maximum 32: printable ASCII, standard single-character JSON escapes and `\u00HH`, with no raw non-ASCII, non-byte Unicode or surrogates. NUL/non-UTF-8 round-trip. UI text is UTF-8-encoded before byte serialization; exact reversible text or literal hex preserves existing bytes and BOM, with no silent replacement/truncation.
Flat operations select one domain/target:
- `wifi-patch` + Wi-Fi generation: optional boot policy, AP policy/channel/SSID/password/clear; `profile-patch` additionally selects stable profile 0–3 and optional enabled/priority/security.
- `wifi-save|wifi-load` require Wi-Fi generation. `start|stop|reconnect|next-profile` use canonical connection controls, not explicit selected-profile connection.
- `mdns-set` requires its generation and suffix; `mdns-save|mdns-load|mdns-defaults` require its generation. Suffix is 1–55 lowercase ASCII letters/digits/hyphens, no leading/trailing hyphen, producing `sak-<suffix>`.
Omitted fields preserve current bytes under the Wi-Fi mutex. Password Keep omits, Replace is 8–63 printable ASCII bytes, disabled-STA Clear is explicit; replacement and clear cannot coexist. AP clear is always denied, even while off. No saved PSK or length is returned. Wi-Fi compare/merge/whole-candidate validation and required queue admission precede RAM publication. Save stabilizes selected bytes; Load reads stored configuration only and cannot generate fallback secrets. No browser Wi-Fi reset/default-secret/export route exists.
Edits require explicit Save. Disabled-profile-only edits do not restart active radio; enabled-policy/AP changes follow canonical asynchronous restart. Start/Stop change RAM enabled-at-boot; Reconnect/Next are no-ops while stopped. Next follows enabled priority order with wrap; editor selection is not connection selection. mDNS independently owns generation/persistence; the Wi-Fi manager owns radio/reannouncement. mDNS RAM change followed by queue failure is reported without rollback; its Load may choose deterministic MAC-derived defaults.
Network result fields are `id/action/state/error`: `pending`, `accepted`, `ok`, `failed`, `cancelled`, `stale`, `invalid`, `loaded_defaults`, `applied_not_queued`, or login-isolated `idle`. `accepted` means RAM/owner-queue admission, **not** association/DHCP/DNS completion. `ok` denotes explicit Save. Loss can precede response; recover via STA/AP and inspect before retrying. UART0 administers recovery; native USB provides network-independent UART1, not an admin console.
### Broker and contextual controls
Broker management atomically copies eight compact client rows, writer and lease generation under one zero-wait mutex acquisition, without reading UART payloads or consuming events. Conditional assignment compares the selected nonzero target ID and lease generation under the same force-writer lock before any effects. Stale/absent targets conflict; same-current-target assignment is a no-op only after validation. Accepted serial TX is not recalled by transfer.
IDs have three slot bits and 29 generation bits; exhausted client slots retire until reboot rather than wrap. The separate 32-bit lease version saturates at UINT32_MAX, survives counter clear and advances on grant/release/revoke before advisory event delivery. Forced transfer can advance twice; it is not a count. Saturation blocks typed assignment but leaves ordinary request/release/disconnect and canonical recovery force available. Reboot invalidates old browser sessions.
Serial/Wi-Fi/Broker quick views reuse one nonmodal settings host and existing controllers/drafts, not parallel editors. Hover/focus/click/tap never mutate. Network quick mode excludes password controls. Full-page drafts are protected; promotion preserves controller/selection, departure fences reads/timers without cancelling admitted work. Dismissal restores the prior terminal without stealing unrelated focus; explicit Escape/Close returns focus to its trigger.
Broker contextual reads use one timer five seconds after successful completion and a five-second whole-read deadline, stopping on errors/uncertainty/departure. Explicit target/lease selection is not rebased by refresh. Sticky stale/absence latches require deliberate reselection even if later snapshots match again. Confirmation captures exact versions before session revalidation; guarded aria-disabled controls preserve focus during updates.
## Service lifecycle and identity rotation
Service generations are saturated, distinct from identity and session generations, and advance on admitted canonical lifecycle attempts including failures; counter clear/reinit cannot revive stale versions. Public zero-wait metadata authorizes nothing. Conditional controls compare under the canonical owner reservation; no snapshot-check/unconditional-mutate gap is allowed. Reboot invalidates originating logins and boot-local reservation IDs.
### HTTPS and reboot
GET `lifecycle` has seven fields: `generation`, `running`, `transitioning`, `controllable`, `identity_generation`, `fingerprint`, `rotatable`. Stored HTTPS fingerprint is 64 lowercase hex SHA-256 digits. Stop/restart/reboot require exactly action + service generation; rotate also requires identity generation. Unknown/duplicate/escaped/coerced fields and zero/saturated versions reject. Unavailable identity yields generation zero/empty fingerprint/not rotatable without removing ordinary service controls.
Self-cutting HTTP actions use **successful synchronous send return → one nonreused-ID HTTPD callback → existing dispatcher**. Send return is not browser receipt. The callback never waits or performs lifecycle work, and captures no request/fd/reusable operation pointer. A two-second ACK admission deadline and post-validation 30-second dequeue deadline are not execution bounds. Accepted-but-lost HTTPD work retains one reservation until callback arrival or successful HTTPD destruction; failed destruction cannot release it or accumulate more callbacks. The original login/current admin is revalidated before owner admission.
`web_server_stop_current()`/`web_server_restart_current()` compare and reserve under the server mutex. Restart retains transition ownership through stop/start; failed stop skips start, failed cleanup retains handles and canonical recovery, and reinit preserves the error. `web_server_reboot_current()` reserves the HTTPS generation then uses canonical `esp_restart()` outside locks, not an HTTPD stop wait or self-console cleanup.
`web_server_replace_identity(service_generation, identity_generation, reset, &committed)` reserves **service before identity**, before any crypto/storage. Both nonzero generations mean conditional healthy-running rotation; both zero retain CLI semantics; conditional Reset is invalid. Canonical CLI/browser-shell rotation and direct security rotate/reset share nonreused task-owner-bound identity reservation tokens. Only the owner can replace once/release; exhaustion fails closed until reboot. Crypto/NVS run outside service/security mutexes and spinlocks; commit precedes live publication and old-key wipe. Reservation remains held while reserved start copies the committed pair.
HTTPS ordering is **commit → stop → restart**. Precommit generation/RNG/storage failure leaves identity, HTTPD and logins unchanged (service version may advance after admission). Postcommit lifecycle failure never rolls back identity; failed stop can leave old served and new stored fingerprints different. Running replacement restarts HTTPS; canonical stopped rotation stays stopped, while CLI TLS-only reset can recover unavailable identity and starts a stopped service. Browser has no Reset/recovery/export action.
Save drafts; rotation/restart invalidates all web logins and closes both browser routes. Inspect `web certificate info` through trusted UART0, verify fingerprint before renewing trust, then reload/sign in freshly. Accepting a warning alone is not trusted verification. Use canonical UART0/admin SSH `web stop` / `web start` for retained-server recovery. Network/SSH/USB are not stopped by HTTPS-only operations; whole-device reboot interrupts all transports and loses unsaved RAM.
### Application firmware upload
The admin-only firmware card is in **Settings → HTTPS / Reboot**. It sends a raw `POST /api/firmware` with `application/octet-stream`, known length, session cookie, same-origin Origin and `X-CSRF-Token`; it is not a JSON Settings operation or dispatcher/result-slot workflow. Standard SDK OTA APIs stream through a 4 KiB buffer to the inactive application slot and select it only after validation/currentness checks. Only that slot and `otadata` are written; NVS/data partitions are untouched.
One upload reserves HTTPS lifecycle/identity and excludes ordinary software reboot. The synchronous HTTPD handler can stall/drop browser serial/admin sessions. Its 120-second total receive-loop and ten-second stall checks are not preemptive flash deadlines or uninterrupted-traffic guarantees. Successful response send schedules a delayed reboot, not proof of peer receipt. Failed response after commit leaves the image selected, schedules no automatic reboot, releases reservations for manual reboot, and latches subsequent admissible uploads to 409 `firmware_selected_reboot_required` until reset. Never automatically retry an uncertain outcome.
See [firmware update](roadmap.md#phase-10--simple-admin-web-firmware-upload) for wired-first installation, exact image selection, manual recovery, acceptance limits and reusable future hardware regression checks. Phase 10 is accepted; there are no signature/version policies or automatic rollback.
### SSH
GET `ssh` supplies service/session state plus identity generation, fixed P-256 algorithm, unpadded OpenSSH `SHA256:` base64 fingerprint and rotatable flag. Service actions use exactly `action`, `generation`, `target`; rotate adds `identity_generation` and requires target zero. Start/stop and exact-session disconnect use published state, saturated service generation and the command mutex; exhausted SSH session slots retire rather than wrap. Disconnect success is an owner close request, not completed teardown. HTTPD never calls wolfSSH or waits for the SSH task.
`ssh_transport_replace_identity()` compares/reserves **service then identity before stop or storage**, holding the existing command mutex across **stop → generate/commit/publish → conditional restart**. Canonical UART0/deferred admin-SSH rotation/reset and direct security mutations share the task-bound nonreused identity reservation. Crypto/NVS run outside security locks/spinlocks. Both nonzero versions select conditional rotation (also while stopped); both zero preserve canonical semantics, with no conditional Reset.
Failed stop/timeout skips mutation and never starts again; pending owner work is not cancelled. Persistence failure after successful stop may already have disconnected clients and attempts old-identity restart if previously running. Committed replacement remains committed if restart fails. Stopped rotation stays stopped; canonical reset can recover unavailable material/start stopped SSH. Only the SSH owner frees runtime context after all slots retire, before clearing cleanup admission; start rejects orphan handles. wolfSSH copies DER into its context; caller/candidate/superseded private bytes are wiped.
SSH changes leave invoking HTTPS available, so they use the ordinary ID-dispatcher/result path, not HTTPS's self-cutting ACK gate. UI confirms both versions and all-SSH/session scope, retains stale selection and manual 15-second/no-replay flow. A failed result may represent partial effects. Verify new trust through UART0 `ssh host-key info` before updating known_hosts; no HTTPS relogin is inherently required. Browser Reset/recovery/key export and user authorized-key changes are separate, excluded from host rotation.
## Diagnostics, recovery and scope boundaries
See [admission diagnostics](web_admission_diagnostics.md), [ordinary HTTPS idle cleanup](https_idle_cleanup.md), [throughput diagnostics](web_throughput_diagnostics.md) and [legacy storage compatibility](roadmap.md#phase8-legacy-credential-compatibility). Broker read means transport handoff, not peer receipt; capture non-consuming counters before disconnect. TLS `-0x004C` is generic NET_RECV_FAILED, not evidence of OOM. Resource minima and counter observations require attribution, not inferred causes.
Phase 8D.15's dedicated typed network-diagnostics UI/API was removed: diagnostics remain shell-based, subject to frontend policy. The unimplemented 8D.19 ordinary browser-session/native-USB control expansion was removed; existing SSH controls remain. No full shell parity, browser identity recovery/reset/export, encryption or secure boot is implied by Phase 8 acceptance. Phase 10 application upload is complete by explicit user acceptance of upload and normal operation; unreported fault, NVS-comparison, power-loss and recovery checks are not implied. UART0 is the administrative recovery authority; native USB is binary-transparent, network-independent UART1 access. Neither permits bypassing the broker's single writer or recalling already-admitted work.
+35
View File
@@ -0,0 +1,35 @@
# Web admission diagnostics
Source/build/test paths in code spans and commands are relative to the repository root
unless a different working directory is stated; Markdown links are relative to this document.
Current default-disabled instrumentation, not an admission fix or an allocation-failure detector. `src/web_diagnostics.{c,h}` observes public synchronous HTTPS create/close callbacks and the four serial/admin ticket/upgrade handlers. It adds no owner task, queued probe or socket-capacity change. See [ordinary idle cleanup](https_idle_cleanup.md) for the independent cleanup policy and [acceptance evidence](roadmap.md#phase8-acceptance-evidence) for user reports.
## Usage and interpretation
Use UART0 (preferred during network stalls) or authenticated admin SSH:
```text
web diagnostics clear
web diagnostics enable
web diagnostics show
```
Enable before reproducing the failed third connection. At failure, run `web diagnostics show` promptly, alongside existing `memory`/`web counters`/browser Network timing evidence; repeat the snapshot if admission remains stalled. Then `web diagnostics disable` freezes event retention (live occupancy continues updating); `web diagnostics show` prints retained history, and `web diagnostics clear` erases history/counters without changing live sockets or enable state. The setting is RAM-only and defaults off after boot. Existing browser-shell policy deliberately denies these commands, even though shared completion offers the fixed forms. There is no HTTP diagnostic endpoint.
- Six connection records are maintained from boot even with capture disabled, so enabling on an already loaded server does not mislabel existing sockets. Each successful TLS connection gets a monotonically increasing, non-wrapping, firmware-lifetime 64-bit `conn` sequence independent of fd, cookie, ticket, broker or user identity. Clear, disable and HTTPS restart do not reset it. Internal TLS object identity is used only during synchronous cleanup; no pointer is exported or dereferenced by the console.
- Snapshot output gives its boot-relative `snapshot_us`, post-TLS occupancy split ordinary/serial-WS/admin-WS and each live fd/connection sequence/open time/age. `kind=0/1/2` means ordinary/serial/admin. Ordinary includes every successful TLS socket not yet observed as upgraded, including idle keep-alives, assets/login/settings and sockets not yet used for HTTP. Classification is actual public `httpd_ws_get_fd_info()` state after upgrade-handler return, **not inferred from `ESP_OK`**. No URI string is inspected.
- The fixed 32-entry overwrite ring records successful TLS open, TLS transport-context close, and enter/result for serial/admin ticket and upgrade handlers. `t_us` is boot-relative observation time; result `dt_us` measures the underlying handler only (including its work/IO, excluding entry resource sampling); close `dt_us` is successful-TLS connection lifetime. Open/enter durations are zero. Open/result occupancy includes the connection; close occupancy excludes it. Event IDs survive clear; `overwritten` counts evicted retained records since clear. `unmatched` counts duplicate create/unmatched close; `lost` counts untrackable creates (getter failure, metadata/sequence exhaustion). Nonzero anomaly counters mean occupancy is not trustworthy as complete evidence. Counters saturate.
- `rc` is the exact handler return, **not HTTP status or ticket issuance outcome**: sending a 401/403/503 can return `ESP_OK`. Match browser HTTP status and existing rejection/issuance counters; there is no ticket-value correlation, request ID, authentication identity, header/body/query logging, or allocation-failure attribution. Upgrade success is visible in the occupancy classification. Ring overwrite or clear/toggle during an operation can leave unpaired enter/result records; do not invent a duration for a missing result.
- Each retained event samples free/largest bytes for internal 8-bit, internal DMA and PSRAM 8-bit, plus the current HTTPD task's minimum-free stack **in ESP-IDF bytes**. Capability scans run outside the diagnostic lock. Samples are sequential, not an atomic heap snapshot; overlapping internal/DMA pools must not be added. Stack watermark includes diagnostic call overhead and is not dispatcher margin. `show` does not query a live task handle: resources are historical event samples, not fresh heap values at show time. Use `memory` for current system heap.
- Console snapshots copy only local diagnostic metadata under a short portMUX, never inspect HTTPD session internals or wait for its owner. Printing occurs outside the lock. At most 32 ID-qualified rows and six live records are printed, even during churn; concurrent clear/overwrite is reported as “no longer retained”. Capture epochs reject samples crossing enable/disable/clear, and sequence checks prevent a stale upgrade result from reclassifying a reused fd. No queued diagnostic work exists, hence no outstanding probe or stale queue lifetime to retire on restart. Minimal connection bookkeeping and upgrade classification remain active while disabled; heap scans and event recording do not. Instrumentation still has CPU/static RAM cost, not zero perturbation.
## SDK audit and explicit blind spots
Audited installed PlatformIO ESP-IDF **5.5.0**, `components/esp_https_server/src/https_server.c` (`httpd_ssl_open`, `httpd_ssl_close`), `components/esp_http_server/src/httpd_sess.c` (`httpd_sess_delete`) and `httpd_main.c`. HTTPS performs synchronous TLS creation before the configured `open_fn` and `user_cb` create callback. It installs a transport-context destructor; that destructor invokes the public close callback before deleting TLS/freeing the context. HTTPD's default close closes the fd, then clears contexts, then frees its session slot. The diagnostic close therefore marks a cleanup observation, not a FIN timestamp or causal close reason. It uses the stored fd, not a getter on an already-closed socket. **Neither `open_fn` nor `close_fn` is replaced**, preserving all existing HTTPS cleanup ownership and failure behavior. Callbacks execute synchronously under the existing HTTPD lifecycle; successful stop finishes cleanup before restart, while failed/partial stop retains remaining live metadata. No asynchronous fd-only events are consumed, avoiding event-delay/fd-reuse ambiguity.
This deliberately bounded first slice does **not** measure TCP connect/accept/listen backlog, pending clients when IDF stops accepting at capacity, handshake begin/duration/failure, TLS allocation failure or aggregate lwIP socket pressure. The public configured open hook is post-TLS too; adding it would not fix these blind spots. No owner-queued client-list probe is added. Occupancy is an owner-published **successful-TLS lower bound**, not the complete HTTPD session table while a handshake is in progress. A clean **6/6** snapshot supports established-connection saturation at that instant (three WS + three ordinary is directly distinguishable); fewer than six does not exonerate admission/TLS/global socket pressure. Correlate time with browser evidence and existing secret-free TLS errors; do not call this pre-TLS tracing or claim the root cause is proven. Existing capacities, receive/send/handshake timeouts and accepted admission issue remain unchanged.
## Regression entry points and limits
`python3 tests/web_diagnostics/run.py` covers bounded metadata/ring behavior and SDK guards; `python3 tests/web_admin_transport/server_lifecycle.py` covers lifecycle composition. These are reusable host commands, not a new execution claim. Doubled TLS/scheduling and host tests cannot establish target admission latency, reserve floors or fault causes. Capture on UART0 alongside `memory`, `web counters`, browser timings and non-consuming broker/serial counters, without cookies/tickets/headers. Generic TLS NET_RECV_FAILED (`-0x004C`) and NET_CONN_RESET (`-0x0050`) are not OOM diagnoses.
+80
View File
@@ -0,0 +1,80 @@
# Web serial throughput diagnostics
Source/build/test paths in code spans and commands are relative to the repository root
unless a different working directory is stated; Markdown links are relative to this document.
## Current contract and accepted fix
Default-disabled instrumentation measures broker fan-out and binary WebSocket TX, not CPU usage, peer receipt or pure scheduler latency. The user separately accepted the combined binary header/payload-send fix at **160 MHz / 230400 baud with full mix including browser admin**. [Acceptance evidence](roadmap.md#phase8-acceptance-evidence) distinguishes that report from the latest capture and unmeasured reserves. The old frequency-only experiment is not the retained solution.
`web_httpd_ws_send_binary` is an IDF-5.5.0-pinned owner-only session-override send with a bounded 516-byte header/payload scratch copy and 512-byte payload. Generation validation and one outstanding item per slot remain. A non-full send installs a reject-only override before close/shutdown, preventing SDK automatic PONG/CLOSE from reentering TLS with different arguments after incomplete output, even if shutdown fails. No retry/replay; HTTPD owns TLS destruction. Text/control/admin retain the SDK sender. One API call does not promise one TLS record/packet or peer receipt. Keep CPU160, priorities, scheduling and 4096-byte broker/512-byte web payload bounds unless separately changing them with evidence.
## Broker accounting
`broker counters` retains global totals and adds one row per active client: generation-safe ID, type, pending bytes, output high-water mark (HWM), UART bytes considered for that client, queued bytes, read bytes and dropped bytes. `broker clients` helps map the IDs to active transports; `web performance show` supplies each web slot's broker ID.
- HWM is peak output occupancy, bounded by 4,096 bytes. `broker clear-counters` clears totals but seeds each active HWM with current pending occupancy, not zero. It does not drain queues or change ownership.
- `read` means handed to a transport, not received or rendered by a peer. Queued counts successful copies; UART counts bytes considered while the client was connected.
- Disconnected per-client rows disappear and slot/generation reuse resets them. Global counters retain disconnected traffic until cleared, including accepted-but-unread output discarded on disconnect. Thus global drops need not mean only overflow, and queued plus dropped need not equal UART fan-out after disconnect discards.
- Capture before disconnect. **Do not use `broker read` for observation:** it consumes data intended for that transport. Snapshot/counter commands do not consume serial data.
## Independent web performance capture
Use physical UART0 for the least intrusive capture. Commands are also available through the authenticated admin SSH registry. `web performance enable|disable|show|clear` is separate from admission tracing (`web diagnostics enable|disable|show|clear`) and from ordinary `web counters|clear-counters`.
Performance capture defaults disabled. Enable resumes aggregates; disable freezes aggregates; clear resets aggregates while preserving enable state. Each control operation advances a nonwrapping epoch and fences in-flight samples. Exhaustion fails closed. Slot generation and broker identity also fence reuse; this is not a disconnected-session archive.
Each active one of two fixed slots shows fd, generation, broker ID, pending/measured-pending/executing state and current-epoch `pending_age_us`. Age starts at reservation-path entry and includes an executing send. If `measured_pending=0`, zero age is unavailable, not evidence of immediate service. Disabling invalidates current-epoch pending age; capture a live `show` only if investigating a stuck queue, accepting its perturbation.
Only serial **binary TX** is measured. Text/control frames are not samples, though they can delay subsequent binary work. No payloads, passwords, keys, cookies, tickets, verifier material or other authentication secrets are retained or printed.
| Printed timing | Exact interpretation |
|---|---|
| `queue->callback-entry` | Reservation-path entry to HTTPD callback entry, timestamped before the callback takes the transport lock. Includes submission-path work, not just HTTPD queue residence; owned callbacks retired without sending can count. |
| `send-call` | Time around the actual synchronous HTTPD-owner send API call. Return is not peer acknowledgement, browser receipt or rendering. |
| `completion->first-drain-attempt-return` | Successful send-call completion to return of the first subsequent broker read attempt, including empty/error attempts. |
| `completion->next-nonempty-drain-return (includes idle)` | Completion to the next successful nonempty broker read return, possibly after empty attempts and source idle time. |
| `completion->first-attempt-nonempty-return` | Subset where the first subsequent read returns data. Excludes observed empty attempts, but does not prove backlog existed at send completion. |
Each timing reports count, sum in microseconds, integer estimated average (`sum/count`, zero for no samples) and maximum. Completion intervals are not scheduler-only: they include transport work, broker read/mutex time and intervening control-frame work, and may include idle time even in the first-attempt-nonempty subset. They do not isolate a particular task, TLS operation or network cause.
`queued_frames/bytes` counts transport reservations, including reported queue failures; `queue_errors` records those failures. `sent_frames/bytes` counts successful send-call returns. `send_errors` includes owner-context rejection, whereas send-call timings require an actual API call. `retired` records measured callbacks retired without sending. Saturating aggregates set `saturated=1`; totals and averages then cease to be reliable. Epoch fencing intentionally prevents old work contaminating a new capture, so do not demand equality across toggles or in-flight clears.
## Reproducible UART0 capture
1. Record firmware/build identity, baud/framing/flow control, source burst size, browser versions, client IDs/roles and full-mix topology. Establish all serial clients and both admin routes first. Quiet the UART source and allow queues and connection activity to settle. Do not reconnect/login during the trace.
2. Optionally stop independent admission tracing before the timed capture. Then reset in this order, while quiet:
```text
web diagnostics disable
web performance clear
web performance enable
broker clear-counters
serial clear-counters
web clear-counters
```
The first command is optional; record whether admission tracing was enabled. Resets are sequential, not an atomic cross-service snapshot.
3. Send the known burst once, stop the source and wait for transport queues to drain. Avoid repeated console/status printing during the burst. A quiet final broker pending count of zero alone does not prove browser receipt; retain peer byte counts too.
4. Stop performance capture first, then capture all counters **before disconnecting any client**:
```text
web performance disable
web performance show
broker counters
serial counters
web counters
broker clients
```
Disable already prints the performance snapshot; `show` is an explicit repeatable frozen-aggregate record. Keep connection counts and source/peer totals with the sample.
5. Only after saving evidence, change to one browser, keeping the other full-mix participants and serial settings equivalent. Quiet/settle and repeat the complete reset/burst/drain/stop/capture sequence. Do not compare a fresh capture to lifetime counters.
6. Separately compare equivalent performance-enabled and performance-disabled bursts with fresh ordinary counter resets. Keep performance disabled for the latter; do not treat retained performance aggregates as that run's measurements. Keep admission tracing state equal and record it.
At 230400 baud, 8N1, 4,096 bytes represents approximately **177.8 ms** of continuous input retention from an empty broker queue, ignoring flow-control pauses. Compare per-client drops/HWM with latency maxima against this scale, not as a hard deadline or proof of cause; occupancy, idle gaps and timings cover different boundaries. For future regressions, gather per-client attribution and controlled comparisons before changing buffers, priorities, scheduling or CPU.
## Resources and regression references
Two fixed diagnostic records plus nonwrapping epoch/timestamp/slot fences and broker HWM fields add bounded storage, no new instrumentation allocations/tasks/queues/payloads. Disabled is not zero overhead: gate branches remain and broker occupancy/HWM is always active. Enabled timing and short aggregate locks have runtime costs not established by host tests. The combined-send local scratch adds HTTPD stack use; actual reserve remains a target measurement.
`tests/session_broker_diagnostics/run.py` covers counters and generation-safe management; `tests/web_serial_performance/run.py` covers production send/drain paths, epochs, retirement, SDK wire compatibility and failed-send/control-reply isolation. `tests/admin_ssh_policy/run.py` covers SSH-allowed/browser-denied tracing commands. Prior focused/broad host validation and firmware builds passed; no tests were run during documentation consolidation. These harnesses do not prove real TLS/scheduler overhead, peer-byte integrity or long-soak behavior. Use the capture procedure above for future attribution, retaining explicit workload/duration/counter provenance.