Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26ed569fcb | ||
|
|
c5bf3e32f5 | ||
|
|
f925293d3f | ||
|
|
5255d6ff5e | ||
|
|
c257158ada | ||
|
|
901afbe754 | ||
|
|
ab6a8010ba | ||
|
|
655f26a611 |
@@ -41,4 +41,4 @@ pio device monitor -b 115200
|
||||
|
||||
The first migration from the former default/factory layout to the custom partition table requires `pio run --target erase`. Erasing destroys persisted configuration and credentials; never run it without explicit user approval.
|
||||
|
||||
No automated host test command is defined in the repository. Hardware validation procedures live in `docs/electrical_tests.md` and `docs/user_administration_tests.md`; do not claim they passed unless actually performed.
|
||||
No automated host test command is defined in the repository. Hardware validation procedures live in `docs/electrical_tests.md` and `docs/devel/user_administration_tests.md`; do not claim they passed unless actually performed.
|
||||
|
||||
@@ -4,105 +4,66 @@
|
||||
|
||||
ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one MAX3243-backed UART1 serial port and safely shares it between native USB CDC-ACM, an HTTPS/WebSocket browser terminal, and SSH: one connected client can write while others observe. The firmware also provides persistent serial and Wi-Fi configuration, a UART0 recovery console, and hardware diagnostics; it is not a general-purpose router, captive portal, or unauthenticated TCP serial server.
|
||||
|
||||
## Initial hardware target
|
||||
## Supported hardware
|
||||
|
||||
- ESP32-S3-DevKitC-1-compatible development board
|
||||
- ESP32-S3-WROOM-1-N16R8 module
|
||||
- 16 MB flash
|
||||
- 8 MB octal PSRAM
|
||||
- Adafruit MAX3243 full-pinout RS-232 breakout; the male connector version is preferred (see [Hardware wiring](docs/wiring.md) for supported connector variants)
|
||||
|
||||
## Development status
|
||||
|
||||
Hardware characterization, serial/USB/Wi-Fi/HTTPS/SSH and local display/control are implemented and hardware-validated. **Phase 8 role-based users and administration is complete:** 8A–8C were target-hardware validated and the user explicitly signed off tested firmware at **8D.22 (2026-09-13)**. See the [roadmap](docs/roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and its [consolidated acceptance evidence](docs/roadmap.md#phase8-acceptance-evidence). Very low internal/DMA lifetime minima remain a nonblocking headroom follow-up, not an approved reserve. The reduced Phase 9 focuses on intermittent trusted-network use, standard no-dump/silent-panic defaults and a short [operational checklist](docs/security_operations.md), without dependency patches or upgrades. **The reduced Phase 9 is complete by user sign-off on 2026-09-18.** The user waived a new device check based on prior Phase 8 validation; application code is unchanged, but crash-diagnostic defaults changed. No new hardware or panic-test pass is claimed.
|
||||
|
||||
**Phase 10 application firmware upload is complete by explicit user acceptance on 2026-09-18:** upload works and normal operation is verified. This does not claim specific fault-injection, NVS before/after comparison, power-loss or wired-recovery checks. Administrators can upload a locally built application from **Settings → HTTPS / Reboot → Firmware update**. Install this OTA-enabled firmware by wire first; future uploads use `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. See [firmware update and recovery](docs/roadmap.md#phase10-update-and-recovery) for compatibility, interruption and validation limits.
|
||||
|
||||
### Browser administration
|
||||
|
||||
Cookie login/logout supports both roles. Administrators also have an explicitly opened Admin shell sharing the canonical dispatcher, and typed **Serial, Accounts/password/SSH authorized keys, Network, Display, Broker, SSH, and HTTPS/Reboot** settings. Serial/Wi-Fi/client/writer quick controls reuse the same settings controllers. Navigation preserves terminal sessions and serial writer ownership; explicit transfer is confirmed and generation-safe. Browser-shell permissions remain narrower than typed Settings or UART0/admin SSH.
|
||||
|
||||
Network uses UTF-8 text/reversible hex SSIDs, explicit password Keep/Replace/disabled-STA Clear (never AP clear), RAM edits and explicit Save. Saved secrets are never prefilled/exported. Next profile follows enabled priority order, not the editor's selected index. Network `accepted` means owner admission, not online/DNS completion; disruption may precede acknowledgement.
|
||||
|
||||
HTTPS and SSH identity settings display public fingerprints and confirm both service and identity generations before rotation. HTTPS rotation commits before stop/restart and invalidates web logins; SSH stops before commit/restart and can disconnect clients even when persistence fails. A committed identity is never rolled back on restart failure. A lost response or timeout is not cancellation: inspect state rather than automatically replaying. Verify changed trust on UART0 with `web certificate info` or `ssh host-key info` before renewing browser trust/known_hosts; HTTPS restart requires fresh login. Browser identity reset/recovery/export is excluded; canonical CLI recovery remains available.
|
||||
|
||||
Keep UART0 ready for administrative recovery and native USB for network-independent UART1 access. USB is not an admin console, and whole-device reboot interrupts every transport. [Web administration contracts](docs/web_administration.md) cover API bounds, ownership, permissions, uncertainty and recovery. Dedicated typed network diagnostics and the unimplemented browser-session/USB control expansion were removed from scope; existing shell diagnostics and SSH settings remain.
|
||||
- Optional 128×64 I²C OLED and three buttons, as described in the wiring guide
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
|
||||
- [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures.
|
||||
- [Role-based user database and UART0 administration](docs/user_administration_tests.md): user provisioning and administration, HTTPS/SSH authentication, session revocation, and reusable integrated web-administration regression procedures (not execution evidence).
|
||||
- [Web administration](docs/web_administration.md): current bounded API/owner and recovery contracts; [roadmap acceptance evidence](docs/roadmap.md#phase8-acceptance-evidence) records sign-off and telemetry limits.
|
||||
- [Firmware update](docs/roadmap.md#phase-10--simple-admin-web-firmware-upload): admin-only application upload, NVS preservation, uncertain outcomes, wired recovery, acceptance evidence and reusable regression checks.
|
||||
- [Security operations](docs/security_operations.md): trusted-network use, credentials, identity verification, shutdown and recovery; physical-extraction limits and lightweight upstream maintenance.
|
||||
- [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands.
|
||||
- [Dual-stack networking and discovery](docs/roadmap.md#phase-12--advanced-network-integration): DHCPv4 hostname publication, STA SLAAC/link-local IPv6, HTTPS/SSH DNS-SD and dual-stack access; complete by explicit user validation, including fresh boot and the full client mix at 230400 baud. Includes recorded heap/stack measurements, evidence limits, saved-config requirements and the narrowly guarded mDNS membership fix.
|
||||
Start with [Getting started](docs/getting_started.md) for installation, first-time configuration, and USB or network access.
|
||||
|
||||
## 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 for future LittleFS web assets, logs, and files |
|
||||
|
||||
Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
|
||||
|
||||
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.
|
||||
|
||||
### One-time migration from the default partition table
|
||||
|
||||
The previous 1 MiB factory application began at `0x10000`, which now lies inside the enlarged NVS range. A normal upload does not erase stale data in that range. When first switching to this layout, erase the flash completely:
|
||||
|
||||
```sh
|
||||
pio run --target erase
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
This removes saved serial configuration and all other flash contents. The firmware recreates NVS with safe defaults. **Do not erase for subsequent ordinary wired or web application updates.** This destructive migration is only for the former default/factory layout, not for installing the updater on an existing custom-layout device.
|
||||
- [Hardware wiring](docs/wiring.md): GPIO assignments, power safety, cables, USB connectors, OLED, and buttons.
|
||||
- [Electrical tests](docs/electrical_tests.md): safe hardware bring-up, loopback, flow control, and shared-serial checks.
|
||||
- [Command reference](docs/command_reference.md): administration, serial, USB, Wi-Fi, discovery, SSH, and diagnostics.
|
||||
- [Web administration](docs/web_administration.md): browser Serial, Admin, Settings, writer control, and reconnecting after changes.
|
||||
- [User administration](docs/user_administration.md): accounts, passwords, SSH client keys, and local recovery.
|
||||
- [Firmware update](docs/firmware_update.md): application upload through the browser, update precautions, and wired recovery.
|
||||
- [Security operations](docs/security_operations.md): trusted-network use, identity verification, credentials, shutdown, and recovery.
|
||||
|
||||
## Build
|
||||
|
||||
Install PlatformIO Core and run from the project root:
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
The small Phase 9 sets `CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y` and `CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y` in `sdkconfig.defaults`. An existing generated `sdkconfig.esp32-s3-devkitc-1-n16r8` can retain earlier selections: check its resolved values after configuration, or change them using `pio run --target menuconfig`. Defaults alone do not enforce existing configurations. Silent panic reboot removes panic register/backtrace output; ordinary logs remain. No existing flash contents are erased, and stored secrets remain unencrypted.
|
||||
The [getting-started guide](docs/getting_started.md#2-install-firmware-by-wire) covers prerequisites and first installation. The configured target is `esp32-s3-devkitc-1-n16r8`.
|
||||
|
||||
## Upload and monitor
|
||||
|
||||
Connect the board's **USB-to-UART** port for firmware upload and the UART0 development console, then run:
|
||||
Connect the board's **USB-to-UART** port for firmware upload and the UART0 recovery console:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
The firmware provides an interactive UART0 console at `serial-tool>`. Run `help` for available commands. The USB-to-UART bridge normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport, which normally appears as `/dev/ttyACM*`.
|
||||
If necessary, add `--upload-port` with your actual bridge device path. The bridge normally appears as `/dev/ttyUSB*`; it is separate from native USB CDC (`/dev/ttyACM*`), which carries UART1 data. At `serial-tool>`, press Enter and run `help`.
|
||||
|
||||
The console supports session history, line editing, cursor movement, and hierarchical Tab completion. After an unattended boot, attach an ANSI-capable terminal and press Enter once to enable enhanced editing; this avoids blocking while no terminal is attached.
|
||||
**Do not erase for routine updates.** Erasing destroys all persisted configuration and credentials. Migration from the former default/factory layout to the custom OTA layout is a separate destructive step; see [partition migration](docs/getting_started.md#partition-migration-and-erasing). Subsequent application-only updates can use the [web updater](docs/firmware_update.md).
|
||||
|
||||
Serial, Wi-Fi, and mDNS hostname edits remain in RAM until explicitly saved with `serial save`, `wifi save`, or `mdns save`. Authenticated admin SSH sessions expose the shared operational administration registry, including interactive secrets, TLS/SSH identity management, network diagnostics, and deferred reboot/SSH lifecycle commands. Create the first administrator on UART0 with `user add <username> admin` (optionally `--generate`). Explicit recovery of an unavailable user database remains UART0-only and rebuilds it empty; it refuses a healthy database. An administrator also cannot generate a replacement password for its own account over SSH, preventing the one-time value from being lost when that mutation revokes the session. Legacy web credential commands and `user bootstrap` are removed.
|
||||
## Before using network access
|
||||
|
||||
## Security notes
|
||||
Create the first administrator on trusted UART0 with `user add <username> admin`; there is no default shared login. Use a separate `user` account for SSH serial access: SSH `admin` opens device administration instead. Verify HTTPS and SSH fingerprints locally before trusting a new or changed identity.
|
||||
|
||||
The HTTPS interface uses a device-specific self-signed certificate and a same-origin login page with bounded server-side cookie sessions; HTTP Basic is no longer accepted. Open `/` or `/login`, sign in with a user-database password, and use **Sign out** before switching accounts. Four sessions have a one-hour absolute lifetime, including active serial connections; logout closes only that session's serial access. Login is globally limited to five credential verifications per 60 seconds, with explicit capacity/backoff errors. Direct-IP and mDNS access use separate host-only Secure/HttpOnly/SameSite=Strict cookies. Non-browser clients also require cookies, strict Origin and CSRF for mutations rather than Basic credentials. There is no plaintext HTTP or TCP serial listener. SSH accepts role-based passwords and authorized Ed25519/ECDSA P-256 public keys. User passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but the HTTPS private key, SSH private key, and Wi-Fi credentials remain recoverable from unencrypted application-owned NVS blobs. Offline password guessing and stale append-oriented flash copies also remain possible. The reserved `nvs_key` partition does not enable encryption. This firmware is not resistant to physical flash or RAM extraction; the completed reduced Phase 9 deliberately does not add that protection.
|
||||
Serial, Wi-Fi, mDNS hostname, and display edits need their explicit Save commands to survive reboot. Account and device-identity changes persist immediately. USB host baud/framing choices do not change physical UART1 settings.
|
||||
|
||||
Use a trusted LAN or the password-protected device AP, not Internet exposure. HTTPS uses a device-specific self-signed certificate and password login; SSH accepts passwords and supported public keys. Browser logins expire after one hour, even during active use. **Disconnect** is not sign-out: use **Sign out** when finished.
|
||||
|
||||
The firmware is **not resistant to physical flash or RAM extraction**. Passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but Wi-Fi secrets and private device keys remain recoverable from unencrypted NVS; offline password guessing and old flash copies also remain possible. Keep local ports physically controlled. Read [Security operations](docs/security_operations.md) before deployment.
|
||||
|
||||
## Development documentation
|
||||
|
||||
[Developer index](docs/devel/README.md) · [Roadmap and acceptance history](docs/devel/roadmap.md) · [Current development state](docs/agent/current-state.md)
|
||||
|
||||
Implementation contracts, regression procedures, PCB research, and the flash layout live under `docs/devel/`. Agent working memory lives under `docs/agent/`. Historical acceptance records are not claims that every fault or hardware test has passed; the carrier PCB remains a draft, not fabrication-ready.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU General Public License version 3 only](LICENSE) (`GPL-3.0-only`). Third-party components remain subject to their respective licenses. The integration baseline uses Espressif registry components `espressif/mdns` `1.12.0`, `wolfssl/wolfssl` `5.8.2~1`, and `wolfssl/wolfssh` `1.4.20`; review upstream security releases before production use.
|
||||
|
||||
### Legacy credential removal
|
||||
|
||||
Missing user storage is persisted as an empty database; no shared credential is imported or synchronized. Existing valid v1 user records retain their accounts, roles, IDs, verifiers and keys without a schema change. HTTPS `web_sec/material` upgrades valid 1,392-byte v1 storage to 1,340-byte TLS-only v2, retaining exact certificate/key DER, fingerprint and generation, and committing before publication. Invalid records or migration failures fail closed rather than triggering fallback replacement. `web certificate rotate --force` remains available; `web reset --force` replaces TLS identity only, not users.
|
||||
|
||||
**Downgrade warning:** older v1-only firmware cannot read v2 HTTPS material. Logical NVS replacement is not a secure flash wipe; historical plaintext credentials can remain in flash. This cleanup requires no factory/partition erase. See [consolidated compatibility and evidence limits](docs/roadmap.md#phase8-legacy-credential-compatibility); Phase 8 acceptance is recorded there.
|
||||
|
||||
@@ -25,7 +25,7 @@ SSH role=admin ------> shared administration dispatcher <------ UART0
|
||||
|
||||
## Typed Network settings
|
||||
|
||||
`web_network_settings` admits bounded current-admin operations to the existing ID-only dispatcher; `wifi_manager` remains radio/event/reannouncement owner and `mdns_service` owns independent hostname persistence. Zero-wait secret-free projections never copy saved PSKs onto HTTPD. Conditional compare/merge preserves omitted secrets, queue admission precedes Wi-Fi publication, and edits require explicit Save. Wi-Fi Load is stored-only; mDNS may load deterministic defaults. SSIDs remain byte-reversible; AP clear is denied. One login-bound slot and one-second timer bound queued secret retention to 30 seconds plus scheduling latency, not cancellation after owner admission. `accepted` is not online/DNS completion. See [Network contracts](../web_administration.md#network).
|
||||
`web_network_settings` admits bounded current-admin operations to the existing ID-only dispatcher; `wifi_manager` remains radio/event/reannouncement owner and `mdns_service` owns independent hostname persistence. Zero-wait secret-free projections never copy saved PSKs onto HTTPD. Conditional compare/merge preserves omitted secrets, queue admission precedes Wi-Fi publication, and edits require explicit Save. Wi-Fi Load is stored-only; mDNS may load deterministic defaults. SSIDs remain byte-reversible; AP clear is denied. One login-bound slot and one-second timer bound queued secret retention to 30 seconds plus scheduling latency, not cancellation after owner admission. `accepted` is not online/DNS completion. See [Network contracts](../devel/web_administration.md#network).
|
||||
|
||||
## Startup and initialization
|
||||
|
||||
@@ -80,7 +80,7 @@ The broker drains serial input even with no clients. A full client output stream
|
||||
|
||||
Binary serial WebSocket output uses the IDF-5.5.0-pinned `web_httpd_ws_send_binary` adapter: one owner-only session-override send of a bounded header+payload copy (516 bytes of local scratch, 512-byte payload). Existing generation validation and one outstanding work item per slot remain required. Non-full sends immediately replace that session's send override with a reject-only guard, mark it closing and shut down the socket before deferred cleanup; this prevents automatic SDK control replies from reentering TLS after incomplete output. HTTPD retains TLS destruction ownership. Text/control and admin output retain the SDK sender. One send call does not imply one TLS record, packet or peer receipt; the user signed off drop-free230400-baud full-client-mix operation at160MHz. Runtime stack-margin validation remains a separate follow-up; evidence and acceptance scope are in `current-state.md`.
|
||||
|
||||
Active-client counter snapshots expose ID/type/pending/HWM/UART/queued/read/dropped. HWM is maintained independently of web tracing and counter clear seeds current occupancy. Read means transport handoff, not peer receipt. Disconnect removes the client row while global totals retain traffic and unread-output discards. Independent default-disabled web performance capture uses two fixed slot records and nonwrapping epoch/generation fences; toggles fence in-flight samples, disable freezes aggregates, clear preserves enable state. Binary-only timestamps bound reservation-entry to callback-entry before locking, synchronous send calls and completion to broker-read return; idle and intervening work prevent scheduler-only/backlog-at-completion claims. No new instrumentation allocations or scheduling/buffer changes. Exact fields, overhead and capture contracts: [throughput diagnostics](../web_throughput_diagnostics.md).
|
||||
Active-client counter snapshots expose ID/type/pending/HWM/UART/queued/read/dropped. HWM is maintained independently of web tracing and counter clear seeds current occupancy. Read means transport handoff, not peer receipt. Disconnect removes the client row while global totals retain traffic and unread-output discards. Independent default-disabled web performance capture uses two fixed slot records and nonwrapping epoch/generation fences; toggles fence in-flight samples, disable freezes aggregates, clear preserves enable state. Binary-only timestamps bound reservation-entry to callback-entry before locking, synchronous send calls and completion to broker-read return; idle and intervening work prevent scheduler-only/backlog-at-completion claims. No new instrumentation allocations or scheduling/buffer changes. Exact fields, overhead and capture contracts: [throughput diagnostics](../devel/web_throughput_diagnostics.md).
|
||||
|
||||
### Clients to RS-232
|
||||
|
||||
@@ -106,31 +106,31 @@ TinyUSB callbacks enqueue/copy data and state; the transport task owns broker li
|
||||
|
||||
`web_server` owns HTTPS on port 443 with a persisted self-signed P-256 identity and IDF's dual-stack listener. Shared Host/Origin parsing accepts canonical bracketed IPv6 literals (no interface zones) while preserving same-origin/session binding; DNS discovery does not confer certificate trust. `web_serial_transport` mediates two fixed WebSocket slots through the broker; HTTPD owns socket sends/close, the transport task owns broker IO. Four outstanding serial tickets, four cookie sessions, one optional admin WebSocket and six total HTTPD sockets are distinct limits; LRU is disabled. Current handler capacity is 40. Base HTTPS can serve authenticated non-WebSocket routes if optional serial/admin transport initialization fails.
|
||||
|
||||
Cookie login/logout replaces Basic/cache. Digest-only records carry copied principals, CSRF state, absolute expiry and nonreused originating-session IDs. Strict same-origin/CSRF mutations and session/principal checks gate admission; logout invalidates its session before transport cleanup, account mutations invalidate only the affected account, and ongoing currentness is authoritative. Authentication initialization failure gates HTTPS; failed start/accepted stop wipes records. RNG/SHA/database calls run outside short spinlocks with post-call epoch/identity revalidation. [Authentication contract](../web_administration.md#authentication-and-admission).
|
||||
Cookie login/logout replaces Basic/cache. Digest-only records carry copied principals, CSRF state, absolute expiry and nonreused originating-session IDs. Strict same-origin/CSRF mutations and session/principal checks gate admission; logout invalidates its session before transport cleanup, account mutations invalidate only the affected account, and ongoing currentness is authoritative. Authentication initialization failure gates HTTPS; failed start/accepted stop wipes records. RNG/SHA/database calls run outside short spinlocks with post-call epoch/identity revalidation. [Authentication contract](../devel/web_administration.md#authentication-and-admission).
|
||||
|
||||
`web_httpd_adapter` is the sole private IDF 5.5.0 boundary for duplicate headers, admission-before-101, consumed-scratch wiping, staged optional URI registration, combined binary sends and owner-only idle sweeps. Re-audit its version guard on SDK upgrades. HTTPD debug logging must not expose headers/tickets. `web_diagnostics` independently observes public post-TLS callbacks using six metadata records and a default-disabled 32-event ring; it cannot see preaccept/in-progress/failed TLS. [Admission diagnostics](../web_admission_diagnostics.md).
|
||||
`web_httpd_adapter` is the sole private IDF 5.5.0 boundary for duplicate headers, admission-before-101, consumed-scratch wiping, staged optional URI registration, combined binary sends and owner-only idle sweeps. Re-audit its version guard on SDK upgrades. HTTPD debug logging must not expose headers/tickets. `web_diagnostics` independently observes public post-TLS callbacks using six metadata records and a default-disabled 32-event ring; it cannot see preaccept/in-progress/failed TLS. [Admission diagnostics](../devel/web_admission_diagnostics.md).
|
||||
|
||||
`web_httpd_idle` uses one one-second timer, six rows and at most one queued owner probe. Current-owner shutdown follows 15 seconds of observed ordinary idle, exempting actual WebSockets/async/pending input. Owner delays prevent hard timeout guarantees; accepted-but-lost work stays reserved until successful destruction, failed stop retains ownership. [Idle lifecycle contract](../https_idle_cleanup.md).
|
||||
`web_httpd_idle` uses one one-second timer, six rows and at most one queued owner probe. Current-owner shutdown follows 15 seconds of observed ordinary idle, exempting actual WebSockets/async/pending input. Owner delays prevent hard timeout guarantees; accepted-but-lost work stays reserved until successful destruction, failed stop retains ownership. [Idle lifecycle contract](../devel/https_idle_cleanup.md).
|
||||
|
||||
`web_ui`/`web_login_ui` own authored documents/scripts and hash-bound CSP loaders; authentication documents/app are no-store. Checked-in generated xterm/logo assets are compiled, not regenerated by ordinary builds. Browser Serial/Admin/Settings navigation changes view/input only, preserving serial client/lease and hidden output draining. Session-identity changes require a clean document; pagehide/restore revalidates before exposing buffers. One shared quick-settings host/controller preserves drafts, stale selections and pending uncertainty. [Terminal and console contract](../web_administration.md#terminal-and-console-ownership).
|
||||
`web_ui`/`web_login_ui` own authored documents/scripts and hash-bound CSP loaders; authentication documents/app are no-store. Checked-in generated xterm/logo assets are compiled, not regenerated by ordinary builds. Browser Serial/Admin/Settings navigation changes view/input only, preserving serial client/lease and hidden output draining. Session-identity changes require a clean document; pagehide/restore revalidates before exposing buffers. One shared quick-settings host/controller preserves drafts, stale selections and pending uncertainty. [Terminal and console contract](../devel/web_administration.md#terminal-and-console-ownership).
|
||||
|
||||
### Browser administration and HTTPS lifecycle
|
||||
|
||||
`web_admin_transport`/`web_admin_tickets` add one optional admin socket, two session-bound tickets and admission to the same two remote-console slots shared with SSH, never a broker client. HTTPD owns a 1,552-byte PSRAM-only payload and IO; a 20 ms timer queues at most one poll. Current-owner shutdown avoids queued reusable socket pointers. Detach fences submissions and only successful HTTPD stop retires old work. Console/owner checks enforce currentness before sensitive work; unsupported parsed shell commands reject before effects. [Browser-shell policy](../web_administration.md#browser-shell-policy).
|
||||
`web_admin_transport`/`web_admin_tickets` add one optional admin socket, two session-bound tickets and admission to the same two remote-console slots shared with SSH, never a broker client. HTTPD owns a 1,552-byte PSRAM-only payload and IO; a 20 ms timer queues at most one poll. Current-owner shutdown avoids queued reusable socket pointers. Detach fences submissions and only successful HTTPD stop retires old work. Console/owner checks enforce currentness before sensitive work; unsupported parsed shell commands reject before effects. [Browser-shell policy](../devel/web_administration.md#browser-shell-policy).
|
||||
|
||||
`web_lifecycle_settings` uses one original-login slot and send-return → nonreused-ID HTTPD callback → existing dispatcher for self-cutting HTTPS/reboot actions. Two-second ACK and 30-second dequeue bounds precede admission, not receipt/completion. Accepted-but-lost callbacks retain one reservation through failed stop; only callback/successful destruction retires it. Conditional stop/restart reserve saturated lifecycle generation; restart retains ownership through stop/start. Conditional reboot invokes canonical `esp_restart()` outside locks, never HTTPD self-stop or console-cleanup waits.
|
||||
|
||||
`web_server_replace_identity` reserves service before identity and retains both through commit → reserved stop/start. Direct security and canonical CLI/browser-shell paths share task-bound nonreused identity reservations. Crypto/NVS run outside short security/service locks; commit precedes publication/wipe. Precommit failure leaves identity/HTTPD/logins unchanged; postcommit lifecycle failure never rolls back identity and can leave served/stored fingerprints different. Failed stop skips start and retains canonical recovery. Public service/security projections are separate observations, not authorization. [HTTPS ownership, generation and recovery contract](../web_administration.md#https-and-reboot).
|
||||
`web_server_replace_identity` reserves service before identity and retains both through commit → reserved stop/start. Direct security and canonical CLI/browser-shell paths share task-bound nonreused identity reservations. Crypto/NVS run outside short security/service locks; commit precedes publication/wipe. Precommit failure leaves identity/HTTPD/logins unchanged; postcommit lifecycle failure never rolls back identity and can leave served/stored fingerprints different. Failed stop skips start and retains canonical recovery. Public service/security projections are separate observations, not authorization. [HTTPS ownership, generation and recovery contract](../devel/web_administration.md#https-and-reboot).
|
||||
|
||||
### Admin firmware upload
|
||||
|
||||
`web_firmware_update` streams a raw admin-only application image through HTTPD into the inactive OTA slot using standard ESP-IDF APIs. Existing Origin/CSRF/session checks precede body IO; raw length is bounded before HTTPD's narrowed length is trusted, SDK image validation and final principal currentness precede boot selection. One 4KiB buffer and a transient reboot task bound application storage; NVS and partition layout are untouched. HTTPD is occupied during upload, so normal web traffic can stall; deadlines bound receive progress, not flash-operation latency.
|
||||
|
||||
The server transition and identity reservations exclude competing lifecycle work. A separate atomic gate excludes ordinary reboot paths without depending on HTTPS initialization, preserving UART0 recovery. Successful response schedules delayed reset while retaining reservations; failed response after selection retains the selected-image latch but releases resources for manual reboot. A subsequent upload is refused until reset. No automatic retry, rollback, signing infrastructure or dependency patching. [Contract and validation limits](../roadmap.md#phase-10--simple-admin-web-firmware-upload).
|
||||
The server transition and identity reservations exclude competing lifecycle work. A separate atomic gate excludes ordinary reboot paths without depending on HTTPS initialization, preserving UART0 recovery. Successful response schedules delayed reset while retaining reservations; failed response after selection retains the selected-image latch but releases resources for manual reboot. A subsequent upload is refused until reset. No automatic retry, rollback, signing infrastructure or dependency patching. [Contract and validation limits](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload).
|
||||
|
||||
### SSH
|
||||
|
||||
Typed SSH settings use the existing ID dispatcher and original-login result slot, never HTTPD wolfSSH calls or owner waits. Conditional lifecycle/session controls compare a saturated service generation and exact nonreused session ID under canonical locks. `ssh_transport_replace_identity` reserves service then identity before stop, retaining the command mutex across stop → commit → conditional restart. Failed stop skips mutation/start; failed persistence may follow disconnection; committed identity is never rolled back after restart failure. Only the SSH owner frees context after all slots retire, and start rejects orphan handles. Direct security/CLI/deferred SSH callers share task-bound identity reservations; crypto/NVS run outside security locks. HTTPS remains available, so no self-cutting HTTP ACK gate is needed. [SSH contracts](../web_administration.md#ssh).
|
||||
Typed SSH settings use the existing ID dispatcher and original-login result slot, never HTTPD wolfSSH calls or owner waits. Conditional lifecycle/session controls compare a saturated service generation and exact nonreused session ID under canonical locks. `ssh_transport_replace_identity` reserves service then identity before stop, retaining the command mutex across stop → commit → conditional restart. Failed stop skips mutation/start; failed persistence may follow disconnection; committed identity is never rolled back after restart failure. Only the SSH owner frees context after all slots retire, and start rejects orphan handles. Direct security/CLI/deferred SSH callers share task-bound identity reservations; crypto/NVS run outside security locks. HTTPS remains available, so no self-cutting HTTP ACK gate is needed. [SSH contracts](../devel/web_administration.md#ssh).
|
||||
|
||||
`ssh_transport` uses wolfSSH on port 22 with one explicitly dual-stack IPv6 wildcard listener and two fixed session/handshake slots. Peer formatting preserves IPv6 interface scope; IPv4 shares the listener, not a second socket. Initialization calls `wolfSSH_Init()` in the caller before task creation; after that, one owner task pinned to core 1 exclusively owns runtime contexts/sessions and wolfSSH calls. It enforces bounded handshakes, authentication attempts, receive work, and session buffers.
|
||||
|
||||
@@ -160,9 +160,9 @@ NVS is not encrypted. Password verifiers improve password storage, but Wi-Fi cre
|
||||
|
||||
## Typed Accounts and Serial settings
|
||||
|
||||
`web_account_settings` owns one login-bound slot for create/password/role/delete/key operations; HTTPD reads compact zero-wait metadata and queues only IDs. The database compares target username/account-ID/auth-generation inside its mutation lock and shares canonical invariant/commit logic. Successful commands target-revoke, including self; result loss is uncertain. A one-second timer wipes queued non-executing credentials after 30 seconds plus scheduling latency; admitted work wipes locals on return. Generated password delivery is a separate no-retained-retrieval POST, not mutation; key listing is fingerprint-only with stable sparse indices. [Accounts contracts](../web_administration.md#accounts-and-authorized-keys).
|
||||
`web_account_settings` owns one login-bound slot for create/password/role/delete/key operations; HTTPD reads compact zero-wait metadata and queues only IDs. The database compares target username/account-ID/auth-generation inside its mutation lock and shares canonical invariant/commit logic. Successful commands target-revoke, including self; result loss is uncertain. A one-second timer wipes queued non-executing credentials after 30 seconds plus scheduling latency; admitted work wipes locals on return. Generated password delivery is a separate no-retained-retrieval POST, not mutation; key listing is fingerprint-only with stable sparse indices. [Accounts contracts](../devel/web_administration.md#accounts-and-authorized-keys).
|
||||
|
||||
`web_serial_settings` queues bounded typed operations to the same dispatcher, retaining one original-login result and a 30-second dequeue check. Apply/Defaults are RAM-only; Save persists device working state. Reconfiguration can discard serial-service pending bytes while broker clients/lease/output remain. Snapshot reads are zero-wait and consistent; `/api/status` reports unavailable running state as null. Settings navigation preserves both terminals; bounded completion checks and manual uncertainty recovery never replay mutations. [Typed API/lifetime](../web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
`web_serial_settings` queues bounded typed operations to the same dispatcher, retaining one original-login result and a 30-second dequeue check. Apply/Defaults are RAM-only; Save persists device working state. Reconfiguration can discard serial-service pending bytes while broker clients/lease/output remain. Snapshot reads are zero-wait and consistent; `/api/status` reports unavailable running state as null. Settings navigation preserves both terminals; bounded completion checks and manual uncertainty recovery never replay mutations. [Typed API/lifetime](../devel/web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
|
||||
## Console architecture
|
||||
|
||||
@@ -185,7 +185,7 @@ For SSH, standard output/error is redirected to the invoking session's bounded o
|
||||
|
||||
Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset use deferred control. The control task waits up to ten seconds for command state plus administration and transport application buffers to clear, then adds a short delay; this is a bounded best-effort heuristic, not peer-delivery confirmation. UART0 invokes these actions synchronously. User mutations and their revocations are not part of this mechanism. UART0 linenoise and the SSH editor consume the same manually maintained completion matcher and candidate formatter, so the two administration routes cannot drift in offered or displayed ambiguous completions; the hints can still drift from command registration and are not an authorization list.
|
||||
|
||||
Browser stop/reboot uses this same owner-adapter control path. Exact forced certificate rotation instead uses the typed queue union and immutable `dispatcher_actions` mask to hand off after drain/200 ms to the existing 12 KiB dispatcher, not the 4 KiB control stack. Pending input is discarded through execution and an executing slot remains reserved across self-detach. Canonical shared service/identity replacement preserves commit/stop/start failure semantics. Parsed other-account interactive add/password and forced role/delete are allowed. Canonical Wi-Fi commands are also allowed, including hidden secret prompts and disruptive changes; these use the ordinary owner queue and may cut off the browser before output arrives, without a deferred acknowledgement guarantee. Browser self/generated/key/recovery, mDNS mutations and restricted SSH shell actions remain denied; typed Settings has separate permissions. Post-prompt currentness is operation admission, not an atomic session-liveness/NVS guarantee. [Browser policy and recovery](../web_administration.md#browser-shell-policy).
|
||||
Browser stop/reboot uses this same owner-adapter control path. Exact forced certificate rotation instead uses the typed queue union and immutable `dispatcher_actions` mask to hand off after drain/200 ms to the existing 12 KiB dispatcher, not the 4 KiB control stack. Pending input is discarded through execution and an executing slot remains reserved across self-detach. Canonical shared service/identity replacement preserves commit/stop/start failure semantics. Parsed other-account interactive add/password and forced role/delete are allowed. Canonical Wi-Fi commands are also allowed, including hidden secret prompts and disruptive changes; these use the ordinary owner queue and may cut off the browser before output arrives, without a deferred acknowledgement guarantee. Browser self/generated/key/recovery, mDNS mutations and restricted SSH shell actions remain denied; typed Settings has separate permissions. Post-prompt currentness is operation admission, not an atomic session-liveness/NVS guarantee. [Browser policy and recovery](../devel/web_administration.md#browser-shell-policy).
|
||||
|
||||
### Shared IPv4/IPv6 diagnostics
|
||||
|
||||
@@ -217,9 +217,9 @@ Configuration modules generally choose RAM defaults without erasing incompatible
|
||||
|
||||
## Typed Display and Broker settings
|
||||
|
||||
`web_display_settings` queues IDs; `local_status_ui` owns a nonwrapping configuration generation and zero-wait writer reservation shared with CLI/legacy Apply. NVS runs outside critical sections. Save stabilizes RAM, Load preserves fallback behavior, Reset commits before RAM publication. Buttons/diagnostic holds change independent activity state, not config generation. Settings need an available UI task, not an attached OLED. [Display contract](../web_administration.md#serial-and-display).
|
||||
`web_display_settings` queues IDs; `local_status_ui` owns a nonwrapping configuration generation and zero-wait writer reservation shared with CLI/legacy Apply. NVS runs outside critical sections. Save stabilizes RAM, Load preserves fallback behavior, Reset commits before RAM publication. Buttons/diagnostic holds change independent activity state, not config generation. Settings need an available UI task, not an attached OLED. [Display contract](../devel/web_administration.md#serial-and-display).
|
||||
|
||||
`web_broker_settings` exposes compact zero-wait rows and confirmed writer assignment through one login-isolated slot. The broker atomically snapshots clients/writer/lease version and compares selected target/version inside the force-writer lock before effects. Three-bit slot/29-bit client generations retire rather than wrap; a separate saturated 32-bit lease generation advances before advisory event delivery and survives counter clear. Saturation blocks conditional assignment, not ordinary request/release/disconnect/recovery force. Contextual refresh never silently rebases explicit selections or clears sticky stale/absence latches; deliberate reselection is required. [Broker/context contracts](../web_administration.md#broker-and-contextual-controls).
|
||||
`web_broker_settings` exposes compact zero-wait rows and confirmed writer assignment through one login-isolated slot. The broker atomically snapshots clients/writer/lease version and compares selected target/version inside the force-writer lock before effects. Three-bit slot/29-bit client generations retire rather than wrap; a separate saturated 32-bit lease generation advances before advisory event delivery and survives counter clear. Saturation blocks conditional assignment, not ordinary request/release/disconnect/recovery force. Contextual refresh never silently rebases explicit selections or clears sticky stale/absence latches; deliberate reselection is required. [Broker/context contracts](../devel/web_administration.md#broker-and-contextual-controls).
|
||||
|
||||
## Local UI and hardware boundaries
|
||||
|
||||
@@ -247,4 +247,4 @@ Hardware diagnostics are synchronous console commands. RS-232 tests own the phys
|
||||
|
||||
`user_database_init(load_result)` has no credential input. Missing storage is persisted empty; `user_database_recover_empty()` is the unavailable-only destructive recovery API. Valid v1 user bytes load without rewriting or account changes. The private `v1_admin_marker` retains its byte position and is derived from administrator count during mutations; it is not a public bootstrap state, new role or schema change. No user migration/bootstrap/synchronization API remains.
|
||||
|
||||
`web_security` owns TLS only. A private reader validates 1,392-byte v1 `web_sec/material`, copies exact key/certificate DER, fingerprint and generation into 1,340-byte v2, commits, then publishes. Temporary v1 credential-bearing input is wiped; no public legacy credential type/getter/rotation remains. Malformed/unknown records and read/validation/commit failures fail closed, with no fallback regeneration or overwrite of rejected records. Missing material may be generated; explicit reset replaces TLS only. Downgrade to v1-only firmware is incompatible. Logical NVS replacement is not secure flash erasure. Contracts/evidence: [legacy compatibility](../roadmap.md#phase8-legacy-credential-compatibility).
|
||||
`web_security` owns TLS only. A private reader validates 1,392-byte v1 `web_sec/material`, copies exact key/certificate DER, fingerprint and generation into 1,340-byte v2, commits, then publishes. Temporary v1 credential-bearing input is wiped; no public legacy credential type/getter/rotation remains. Malformed/unknown records and read/validation/commit failures fail closed, with no fallback regeneration or overwrite of rejected records. Missing material may be generated; explicit reset replaces TLS only. Downgrade to v1-only firmware is incompatible. Logical NVS replacement is not secure flash erasure. Contracts/evidence: [legacy compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
|
||||
@@ -16,7 +16,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
|
||||
- `sdkconfig.defaults` explicitly selects no core dumps and silent panic reboot; existing generated configs can override defaults. README documents verification. No dependency overlays, custom crypto policy or version changes are part of this phase.
|
||||
- User accepted the reduced Phase 9 on 2026-09-18 and waived a new hardware check; this is not a claim of a new hardware-test pass.
|
||||
- [Operational checklist](../security_operations.md) and [scope/acceptance](../roadmap.md#phase-9--security-and-production-hardening): intermittent trusted-network use, unchanged authentication/recovery, no physical-extraction protection. Keep upgrades separate and do not restore the abandoned extensive patchset.
|
||||
- [Operational checklist](../security_operations.md) and [scope/acceptance](../devel/roadmap.md#phase-9--security-and-production-hardening): intermittent trusted-network use, unchanged authentication/recovery, no physical-extraction protection. Keep upgrades separate and do not restore the abandoned extensive patchset.
|
||||
|
||||
## Secure randomness
|
||||
|
||||
@@ -49,7 +49,7 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
- Data path: `transport -> broker -> serial service -> UART1`; reverse data is fanned out per client.
|
||||
- Ownership: client IDs are slot/generation-safe; events are advisory and can drop, so use snapshots as authority.
|
||||
- Lifecycle: one permanent task and eight preallocated client slots; slow output drops only for the affected client.
|
||||
- Diagnostics: `broker counters` adds active-client ID/type/pending/HWM/UART/queued/read/dropped rows; clear seeds HWM from pending, disconnect removes rows but retains global discard accounting. `tests/session_broker_diagnostics/run.py`; capture before disconnect, never use consuming `broker read` as a probe. Semantics/recipe: `docs/web_throughput_diagnostics.md`.
|
||||
- Diagnostics: `broker counters` adds active-client ID/type/pending/HWM/UART/queued/read/dropped rows; clear seeds HWM from pending, disconnect removes rows but retains global discard accounting. `tests/session_broker_diagnostics/run.py`; capture before disconnect, never use consuming `broker read` as a probe. Semantics/recipe: `docs/devel/web_throughput_diagnostics.md`.
|
||||
|
||||
## Native USB CDC
|
||||
|
||||
@@ -68,12 +68,12 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
**Responsibility:** serve authenticated HTTPS UI/API, issue WebSocket tickets, and adapt browser serial sessions to broker clients.
|
||||
|
||||
- Files: `src/web_server.{h,c}`, `src/web_serial_transport.{h,c}`, `src/web_ui.{h,c}`, `src/web_console.{h,c}`
|
||||
- Ordinary HTTPS idle cleanup: `src/web_httpd_idle.{c,h}`, owner sweep in `web_httpd_adapter.{c,h}`, lifecycle/TLS composition in `web_server.c`; `tests/web_httpd_idle/run.py`. Independent of diagnostics/optional transports: 15-second observed idle, one-second timer/one queued probe, six rows, actual WS/async/pending exemptions, safe current-owner shutdown and stop/restart fencing. No LRU/socket/timeout/stack increase. SDK queue/owner-delay and regression contract: `docs/https_idle_cleanup.md`.
|
||||
- Independent throughput diagnostics: `web_serial_transport.{c,h}` owns two fixed per-slot binary-TX aggregates and epoch fences; `web_console.c` exposes default-disabled `web performance enable|disable|show|clear`. Queue-entry/callback-entry, synchronous-send and completion/drain-return estimates, not peer receipt or scheduler-only latency. `tests/web_serial_performance/run.py`; resource/evidence limits and UART0 paired capture: `docs/web_throughput_diagnostics.md`.
|
||||
- Storage compatibility: `user_database` persists missing storage empty and preserves valid v1 user bytes; private derived `v1_admin_marker`, no public bootstrap/migration/sync APIs. `web_security` privately migrates v1 1392-byte material to TLS-only v2 1340-byte material, exact identity/generation retained, commit before publish, fail closed without fallback overwrite. Credential commands removed; user generated passwords and TLS rotation remain. Contracts, downgrade and evidence limits: [legacy compatibility](../roadmap.md#phase8-legacy-credential-compatibility).
|
||||
- Ordinary HTTPS idle cleanup: `src/web_httpd_idle.{c,h}`, owner sweep in `web_httpd_adapter.{c,h}`, lifecycle/TLS composition in `web_server.c`; `tests/web_httpd_idle/run.py`. Independent of diagnostics/optional transports: 15-second observed idle, one-second timer/one queued probe, six rows, actual WS/async/pending exemptions, safe current-owner shutdown and stop/restart fencing. No LRU/socket/timeout/stack increase. SDK queue/owner-delay and regression contract: `docs/devel/https_idle_cleanup.md`.
|
||||
- Independent throughput diagnostics: `web_serial_transport.{c,h}` owns two fixed per-slot binary-TX aggregates and epoch fences; `web_console.c` exposes default-disabled `web performance enable|disable|show|clear`. Queue-entry/callback-entry, synchronous-send and completion/drain-return estimates, not peer receipt or scheduler-only latency. `tests/web_serial_performance/run.py`; resource/evidence limits and UART0 paired capture: `docs/devel/web_throughput_diagnostics.md`.
|
||||
- Storage compatibility: `user_database` persists missing storage empty and preserves valid v1 user bytes; private derived `v1_admin_marker`, no public bootstrap/migration/sync APIs. `web_security` privately migrates v1 1392-byte material to TLS-only v2 1340-byte material, exact identity/generation retained, commit before publish, fail closed without fallback overwrite. Credential commands removed; user generated passwords and TLS rotation remain. Contracts, downgrade and evidence limits: [legacy compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
- Security files: `src/web_security.{h,c}`, `src/web_cookie_auth.{h,c}`, `src/web_session_store.{h,c}`, `src/web_auth_parse.{h,c}`. Private IDF boundary: `src/web_httpd_adapter.{h,c}`.
|
||||
- HTTP policy/UI: `web_cookie_auth` + `web_auth_parse` enforce bounded cookie/Origin/CSRF/admin admission; `web_login_ui.{c,h}` serves login, `web_ui.c` owns session-fenced Serial/Admin/Settings and shared quick controllers. Tests: `tests/web_cookie_auth/run.py` (domain variants), `tests/web_auth_parse/run.py`, `tests/web_login_ui/run.py`, `tests/web_ui_session/run.py`.
|
||||
- Admission diagnostics: `web_diagnostics.{c,h}`, `tests/web_diagnostics/run.py`; six post-TLS records/32-event opt-in ring, no HTTPD off-owner inspection. [Contract](../web_admission_diagnostics.md).
|
||||
- Admission diagnostics: `web_diagnostics.{c,h}`, `tests/web_diagnostics/run.py`; six post-TLS records/32-event opt-in ring, no HTTPD off-owner inspection. [Contract](../devel/web_admission_diagnostics.md).
|
||||
- Identity/lifecycle: `web_server_replace_identity()` + `web_security` reserve service before identity; commit before reserved stop/start, no rollback after commit. `web_lifecycle_settings.{c,h}` owns original-login ID/ACK handoff. Tests: `tests/web_security/run.py`, `tests/web_admin_transport/server_lifecycle.py`.
|
||||
- Asset files: authored/generated boundary in `src/web_assets_data.{h,c}`, `web_assets/SOURCES.md`, `web_assets/generate_embedded_assets.py`
|
||||
- Interfaces: web init/start/stop/snapshots; HTTP handlers; ticket mint/consume; attach/detach; targeted session revocation
|
||||
@@ -89,18 +89,18 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
|
||||
- Files: `src/web_admin_transport.{c,h}`, `src/web_admin_tickets.{c,h}`, protected registration/lifecycle in `web_server.c`, revocation through `web_serial_transport_revoke_*`, diagnostics in `web_console.c`.
|
||||
- Routes: CSRF-protected admin-only `POST /api/admin/ws-ticket`; ordinary `GET /ws/admin` with cookie/Origin/ticket/shared-console admission before explicit 101. Admin UI entry is explicit; no admin broker client. One socket, two tickets, existing two shared console slots; six total HTTPD sockets, LRU disabled; current overall capacity is 40 URI handlers.
|
||||
- Currentness/policy: `admin_ssh_console_open_available()` shares two slots with runtime SSH; transport-qualified tokens and owner adapters revalidate outside console locks before commands/prompts. Parsed browser policy remains narrower than typed Settings; [shell contract](../web_administration.md#browser-shell-policy). Tests: `tests/admin_console_boundary/{run,accounts,lifecycle}.py`, `tests/admin_ssh_policy/run.py`, `tests/web_admin_transport/run.py --tickets`, `tests/web_cookie_auth/run.py --admin`.
|
||||
- Currentness/policy: `admin_ssh_console_open_available()` shares two slots with runtime SSH; transport-qualified tokens and owner adapters revalidate outside console locks before commands/prompts. Parsed browser policy remains narrower than typed Settings; [shell contract](../devel/web_administration.md#browser-shell-policy). Tests: `tests/admin_console_boundary/{run,accounts,lifecycle}.py`, `tests/admin_ssh_policy/run.py`, `tests/web_admin_transport/run.py --tickets`, `tests/web_cookie_auth/run.py --admin`.
|
||||
- Ownership: 20 ms ESP timer queues at most one HTTPD poll, no new task; HTTPD owns 1,552 B PSRAM-only payload and IO. Closure uses HTTPD-owned `shutdown`, not IDF's reusable-pointer queued close. Detach fences submitters; only successful HTTPD stop retires queued state before restart. Session/principal currentness and generation checks protect all sensitive boundaries.
|
||||
|
||||
## Firmware upload
|
||||
|
||||
- `src/web_firmware_update.{c,h}`: admin raw `POST /api/firmware`, cookie/Origin/CSRF admission via `web_cookie_auth`, registration in `web_server`, UI in `web_ui`. Standard SDK OTA into inactive app only; bounded4KiB buffer, validated raw length/header/final image and principal before boot selection. No NVS/layout writes or vendor patches.
|
||||
- HTTPD handles streaming synchronously; upload is deliberately disruptive to web traffic. Server/identity reservations fence lifecycle changes; atomic ordinary-reboot gate also covers console/SSH/browser/button reset paths. Success-response schedules delayed reset; response failure after selection latches uploads until deliberate manual reboot. No blind retries.
|
||||
- Tests: `tests/web_firmware_update/run.py` (active-build SDK5.5.0 headers and actual begin/abort contract), `tests/web_ui_session/run.py`, existing auth/lifecycle tests. [Usage, acceptance limits and reusable regression checks](../roadmap.md#phase-10--simple-admin-web-firmware-upload). Phase 10 complete by explicit user acceptance on 2026-09-18: upload works and normal operation verified; no specific fault/NVS-comparison/power-loss/recovery pass implied. Initial install by wire; subsequent upload is application `firmware.bin`, not full-flash image.
|
||||
- Tests: `tests/web_firmware_update/run.py` (active-build SDK5.5.0 headers and actual begin/abort contract), `tests/web_ui_session/run.py`, existing auth/lifecycle tests. [Usage, acceptance limits and reusable regression checks](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload). Phase 10 complete by explicit user acceptance on 2026-09-18: upload works and normal operation verified; no specific fault/NVS-comparison/power-loss/recovery pass implied. Initial install by wire; subsequent upload is application `firmware.bin`, not full-flash image.
|
||||
|
||||
## Typed settings source and regression map
|
||||
|
||||
HTTPD reads zero-wait projections and queues only IDs to the existing dispatcher. One original-login slot per domain; canonical owners compare/reserve at execution. [API/lifetime and failure contracts](../web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
HTTPD reads zero-wait projections and queues only IDs to the existing dispatcher. One original-login slot per domain; canonical owners compare/reserve at execution. [API/lifetime and failure contracts](../devel/web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
|
||||
| Domain | API / canonical owner | Focused source tests |
|
||||
|---|---|---|
|
||||
|
||||
@@ -2,10 +2,59 @@
|
||||
|
||||
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
|
||||
|
||||
## Documentation reorganization handoff
|
||||
|
||||
- Human-facing guides live in `docs/`; implementation contracts and development evidence live in `docs/devel/`; durable agent memory remains in `docs/agent/`. Agent/test web-contract references target `docs/devel/web_administration.md`, not the top-level operator guide.
|
||||
- README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts.
|
||||
- Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed.
|
||||
|
||||
## Parametric enclosure — initial concept, physical validation pending
|
||||
|
||||
- Label-only follow-up: removed DISPLAY, grouped `USB: UART0 / CDC` (local wiring/measurement evidence does not establish physical left/right roles), and raised `ESP32 / Serial Swiss / Army Knife` on the blank roof left of the display. Shared labels export retained; unlabeled lid CSG exactly matches the pre-change source. Lid STL and three previews refreshed; base/plungers and PCB unchanged. CGAL simple lid/labels exports, source/datum/mesh checks and actual-glyph roof clearance >=1 mm passed; no physical print/test. Lid detail now views readable branding from the USB side.
|
||||
|
||||
- `hardware/case/` corrected per user: 5 mm base insert bosses directly support carrier underside Z7.4/top Z9; two M3 RS-232 and four M2 OLED brass male/female bodies ABOVE carrier span 10 mm to module underside Z19. Studs cross actual 1.6 mm carrier holes into respective inserts; four separate carrier holes take direct M3 screws. ESP32 has no supplied mounting holes and remains header-supported. No under-carrier brass or printable spacers remain. Shell now **106 × 86.8 × 37.4 mm**, roof underside Z35 (10 mm lower); ports/display/DB9 panel/hangers/buttons follow the corrected datums. Red RF overlay/enclosure reserve restriction removed; physical antenna retained, no Wi-Fi guarantee and no PCB/schematic/copper/reserve/layout/export changes.
|
||||
- Superseding DB9 change: separate plate/export/mode and M3 insert hangers removed. Cover now has integral nominal 48 mm-wide × 2.4 mm-thick connector panel at Y−0.47…1.93, Z21.07…35, continuous roof connection and two nominal 6 mm forward ribs outside flange bounds, extended into the wall to close former 1 mm side slits. Bottom-open 18.078 mm shell slot and closed Ø3.6 post holes at real 25 mm spacing permit straight vertical lowering **after removing both screw posts and all cables**; front-wall recess also opens to seam. Panel clears PCB by 0.5 mm and anchors by 3.10 mm in Y. No horizontal-slide claim. Insert count now 10 M3 + 4 M2, all base. Added forward-only base closure Y−3.4…−0.5: shoulders Z20.82, 17.578038-wide tongue Z21.69762024, 0.25 mm seams, 0.6 below STEP shell, 0.5 forward of module PCB. PCB files untouched.
|
||||
- Three default CGAL exports, native registration (including flange plane, hole diameters/no ESP32 NPTH), evaluated stack/connector/stud/web fixtures, edge-manifold meshes, three parameter CGAL smoke tests and continuous 60 mm vertical envelope sweeps passed; restored-post negative control detects blockage. Thicker-panel sweep caught and fixed residual inner-wall strip; default and raised/thicker variant now pass. Closure follow-up regenerated base/lid and all three previews (unchanged plungers revalidated); default plus two variant sweeps include per-step upstand/lid travel, base/lid seating and electronics insertion. Four parameter mesh tests pass; closure guards and STEP shell-minimum registration added. Three regenerated previews visually inspected, including bare-lid detail. This is selected-envelope CAD clearance, not complete hardware or strength qualification.
|
||||
- DB9 hardware remains a gate: no inferred M3/4-40 thread; removability, stud length, engagement and cable reach unmeasured. Panel consumes 2.4 mm of stud reach; verified longer studs/extensions may be needed. Reseated 4.8 mm bodies move mouths to Y−5.27, **1.4 mm ahead of nose**: full mating may require compatible shorter-body hardware and must be physically verified. Nose remains 0.47 mm beyond outer wall / 3.4 mm beyond panel. Do not force an incompatible post/cable combination or claim stock threads sufficient. Actual underside solder/prong clearance within 5 mm, brass lands/studs/inserts, header height, USB/glass/buttons and load strength also remain gates. See `hardware/case/README.md` and `checks.md`; no print, physical assembly or hardware test performed.
|
||||
|
||||
## PCB accepted-fit labels and worksheet — complete
|
||||
|
||||
- Removed visible `USB ACCESS - VERIFY` and title comment `USB / antenna geometry pending; verify module pinout / assembly`; replaced the duplicated pending-clearance caption with `Nominal RS-232 metal verified; RF qualification not established`. Specific RF and connector cautions remain; this is user acceptance, not certification.
|
||||
- Changed only the native worksheet from A4 to A3 landscape (420 × 297 mm), keeping all absolute board coordinates (carrier X=100…182, Y=100…180 mm). No physical translation/reroute/net change. `update_presentation.py` guards locks/concurrent changes, uses `SaveBoard(..., True)` and exact parsed-delta/idempotence checks. Generator and PDF exporter no longer emit the stale pending captions; original historical baselines remain unchanged.
|
||||
- Placement PDF now explicitly includes the border/title block at 1:1; PCB SVG/PNG and carrier-only 3D refreshed. Full-page render is `hardware/PCB/validation/pcb-placement-page.png`. Print the A3 PDF at actual size, not fit-to-A4. Schematic/OLED fit template are unaffected.
|
||||
- PASS: cleanup twice, full group/retained-copper/settings audit, U1/silk/stale-geometry regression, fresh PCB DRC/parity (all zero), independent routing checks, two footprint tests, legacy cleanup compatibility, Python syntax and `git diff --check` (after stripping KiCad SVG trailing whitespace). Rendered full A3 PDF visually checked: PCB and bottom-right title block are well separated; copper preview and 3D also inspected. KiCad emits known enum-property assertions but completes. No firmware/device tests, fabrication outputs or orders.
|
||||
|
||||
## PCB group alignment and user acceptance — complete
|
||||
|
||||
- Current record: `docs/devel/hardware/PCB/user-acceptance.md`. This supersedes older pending-user-fit/USB/male-map/hardware-selection statuses below without upgrading their original evidence. User reports visual component fit and USB clearance correct, male pinout/header/footprint equivalence and pin-by-pin review; purchased Omron B3F-1000 and 2 kΩ 1% metal-film resistors. M3 carrier/daughterboard hardware, 10 mm brass standoffs into heat inserts and M2 display hardware approved. Intended/reported assembled module elevation is 10 mm; 8.5 mm female plus described 2.5 mm male is not an independently verified mating-height calculation. RS-232 prongs clear per user.
|
||||
- Retained U2 body centre X=156.970 mm (local 56.970); OLED +0.970 X, resistor group −0.840 X / −1.3315 Y, button group −1.280 X. Horizontal groups retained. Resistor body Y=131.4185…133.9185 leaves 2.0815 mm nominal clearance to both RS-232 and OLED PCBs; OLED/button body gap 5.25 mm. U1/measured USB, U2, outline, carrier holes and RF area unchanged.
|
||||
- Native board mutated with `SaveBoard(..., True)`, no whole-board regeneration/reset. 476/587 original copper items retained exactly, 111 removed and 503 added during incremental clearance/connectivity repair; current 938 segments / 41 vias. Snapshot `validation/pcb-before-group-alignment.kicad_pcb`, migration manifest and full parsed-content/retained-copper/body-geometry audit added. Project/protected preference hashes unchanged; original A0/U1 evidence preserved. U1 validator verifies original integration against the pre-alignment snapshot, then the exact authorized delta; no blanket exclusions/rebaselining.
|
||||
- R1–R3 now `2k 1%` in PCB/schematic/BOM/generator/validator. Purchased-part and U2 status fields synchronized; previously completed U2 footprint assignment and strict zero-parity validators preserved. User accepts nonisolated USB and reports successful 3.3 V operation, onboard OLED pull-ups and existing RF/power/ground routing. Backfeed safety, regulator margin, pull-up resistance/rail and manufacturer RF/EMC qualification remain unverified; no invented values or new hardware-test claims.
|
||||
- PASS: schematic ERC/exact nets/firmware pin contract, PCB physical DRC and parity (all zero), continuous routing clearance/edge/RF checks, group/preservation audit, U1/silk idempotence and stale-geometry rejection, both footprint tests, DB9 source/CAD validator. Minimum independent copper/NPTH gap 0.252762 mm; edge gap 1.900 mm. Silkscreen (94 items), DB9 projection, 1:1 placement PDF, PCB SVG/PNG/3D and schematic PDF/SVG/PNG refreshed. Missing module courtyards and absence of module assembly 3D models remain explicit limits. No Gerbers/order/firmware changes or hardware tests.
|
||||
|
||||
## U2 schematic footprint — assigned
|
||||
|
||||
- User confirmed the male Adafruit module has the reference pinout/header mapping/footprint. Assigned only schematic U2's `Footprint` field to the existing PCB identifier `Carrier:MAX3243_Reference_Provisional`; PCB, placement, routing, resistors and BOM unchanged. Legacy hidden status metadata remains historical, not current assignment status.
|
||||
- Both validators now require the assignment and clean parity. Fresh PCB validator and schematic ERC pass: zero physical DRC violations, unrouted connections, parity issues and ERC violations. KiCad emitted enum-property assertions but completed successfully. Review exports were not regenerated; no fabrication or hardware qualification is implied. Earlier expected-U2-warning records below are historical.
|
||||
|
||||
## PCB presentation labels — removed and exports refreshed
|
||||
|
||||
- Removed PCB title suffix `draft`, revision suffix `DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and title-comment prefix `NOT FOR FABRICATION -` (also in the PDF exporter). Generator no longer emits them. Specific technical cautions, hidden metadata, footprint identifiers, filenames and schematic remain unchanged; this is not fabrication approval.
|
||||
- `remove_draft_markers.py` guards editor locks/concurrent edits, saves with `SaveBoard(..., True)`, and audits the exact parsed-content delta and unchanged project settings. Two separate cleanup runs passed; same-process repeated board access hit a KiCad SWIG binding failure before any source overwrite.
|
||||
- 1:1 placement PDF and SVG/PNG/3D previews regenerated and visually reviewed. U1 preservation audit permits only the exact label delta in addition to prior measured-U1/device-silk changes; original baseline retained. U1/silk regression, two footprint tests and fresh PCB checks pass: 565 segments, 22 vias, zero physical DRC/unrouted, expected U2 parity warning. Existing mechanical/electrical release gates and historical A0 settings-hash limitation remain; no firmware/device tests or fabrication outputs.
|
||||
|
||||
## ESP32 mechanical measurements — board and review exports integrated
|
||||
|
||||
- User supplied main PCB 56.07 × 28.09 mm excluding 6.89 mm antenna projection; antenna left/right insets 5.41/5.05 mm. Overall length 62.96 mm. Both USB shells 8.87 mm wide, outer-edge offsets 4.16 mm left / 3.66 mm right, mouth faces flush with main PCB bottom.
|
||||
- Preserve 2.54 mm pitch / 25.40 mm row spacing. User accepted approximate 1.365 mm top/bottom pin-centre offset and same left offset; derived right margin 1.325 mm. Library HW678 F.Fab now has main PCB/antenna rectangles and USB mouth spans; no guessed USB depth, RF keepout, courtyard or heights. Footprint regression updated.
|
||||
- Embedded U1 now has the four measured F.Fab shapes and two measurement-limit annotations. `update_u1_graphics.py` adds only owned graphics; pads/nets/existing UUIDs/placement/routing/project settings and existing footprint fields remain unchanged. Old hidden status/description fields are historical, not current measurement evidence. `add_device_silkscreen.py` replaces header guides with carrier/mask-clipped measured outlines and 0.30 mm width-only USB mouth markers. PDF now verifies saved U1 instead of overlaying library geometry; PDF/SVG/PNG/3D exports refreshed.
|
||||
- New `validate_u1_update.py` audits exact pre-update preservation, library geometry, silk idempotence/clipping/mouth positions and stale-geometry rejection; snapshot/report retained in hardware/PCB/validation. Fresh PCB, independent routing, footprint and DB9 CAD checks pass: 565 segments, 22 vias, no physical DRC/unrouted, one expected U2 parity warning. Historical A0 preservation script fails its old `serial-carrier.kicad_prl` hash gate; not weakened or rebaselined. New A1-specific preservation passes.
|
||||
- At unchanged A1 placement USB faces sit 2.295 mm inside carrier bottom edge; plug clearance and physical 1:1 fit remain pending. Antenna geometry is not RF guidance; no keepout, USB depth, courtyard or height was invented. No firmware/assets changes, rerouting, hardware tests or fabrication outputs.
|
||||
|
||||
## Carrier PCB A1 — revised placement and CAD integration
|
||||
|
||||
- `hardware/PCB/serial-carrier.kicad_pcb`: routed 82 × 80 mm two-layer draft; 36 × 19 mm top-left notch, ESP32 shifted down for bottom USB access, RS-232 bare PCB front flush with carrier top. Exact ESP32 body/USB/antenna envelope remains pending user measurements; RF reserve is provisional.
|
||||
- Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `hardware/PCB/db9-mechanical-notes.md`. Flange front sits 1.930 mm inside carrier edge, screw-lock mouths 2.870 mm outside, shell nose 3.870 mm outside. Case wall plane is not yet fixed. Seven board-local Dwgs.User projections added; rerun `add_db9_envelope.py` and `add_device_silkscreen.py` after moving modules.
|
||||
- `hardware/PCB/serial-carrier.kicad_pcb`: routed 82 × 80 mm two-layer draft; 36 × 19 mm top-left notch, ESP32 shifted down for bottom USB access, RS-232 bare PCB front flush with carrier top. Measured ESP32 planar body/antenna and USB mouths are integrated; full connector/assembly envelopes and RF qualification remain pending.
|
||||
- Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `docs/devel/hardware/PCB/db9-mechanical-notes.md`. Flange front sits 1.930 mm inside carrier edge, screw-lock mouths 2.870 mm outside, shell nose 3.870 mm outside. Case wall plane is not yet fixed. Seven board-local Dwgs.User projections added; rerun `add_db9_envelope.py` and `add_device_silkscreen.py` after moving modules.
|
||||
- Placement PDF and SVG/PNG/3D previews refreshed. Fresh PCB, independent routing, preservation and CAD validators pass: 38/38 connections, 565 segments, 22 vias, 0 physical DRC violations/unrouted, one expected provisional U2 footprint parity warning. Not fabrication-ready; no hardware qualification or Gerbers. Existing project/schematic/library work preserved; board saves must use `SaveBoard(..., True)` and require checking editor lock. Prior A0 preserved in validation backup.
|
||||
|
||||
## Dual-stack diagnostics follow-up — implemented, device validation pending
|
||||
@@ -18,7 +67,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
|
||||
|
||||
## Phase 12 COMPLETE — explicit user validation
|
||||
|
||||
- User states: “I just validated Phase 12 successfully.” Record acceptance, not a pending phase. Canonical device evidence and reusable regression guidance: [Phase 12](../roadmap.md#phase-12--advanced-network-integration). No unreported DHCP capture, DNS-zone update, exhaustive address/service transition, fault/soak or byte-integrity pass is implied.
|
||||
- User states: “I just validated Phase 12 successfully.” Record acceptance, not a pending phase. Canonical device evidence and reusable regression guidance: [Phase 12](../devel/roadmap.md#phase-12--advanced-network-integration). No unreported DHCP capture, DNS-zone update, exhaustive address/service transition, fault/soak or byte-integrity pass is implied.
|
||||
- User validated IPv6 ping/HTTPS and Avahi AAAA; Fedora authselect dual-family mDNS resolved the normal hostname lookup issue. Fresh-boot and full-mix captures show healthy reported service lifecycle. At 230400 baud 8N1 RTS/CTS: two SSH sessions with IPv6 ULA peers (user observer/admin console), two serial WebSockets (writer/observer), active browser admin, USB observer; four broker clients, one writer, zero pending/event counts at capture.
|
||||
- Memory table preserved in roadmap. Fresh internal8/DMA/PSRAM free: 65,880 / 58,124 / 8,196,732 B. Full-mix free: 35,408 / 27,652 / 8,111,952 B; lifetime minima: 8,528 / 772 / 8,072,612 B; largest blocks: 22,528 / 22,528 / 7,995,392 B. Capability pools overlap; minima are conservative per-region lifetime sums, not simultaneous reserves. DMA772 remains a watch item, not proof of OOM or an acceptance blocker. SSH stack20,480B minimum-free18,476→16,284B.
|
||||
- SSH two successful handshakes, no auth/handshake/timeouts/I/O failures; RX68 accepted65 rejected3, TX161,209B, broker revocations2. Web serial RX27B accepted, TX318,230B across782 frames; send/queue/protocol failures0. Browser admin RX106/TX3,721B with no reported send/queue failures. Do not reinterpret nonzero rejected bytes/writer denials as proven transport loss or claim zero UART/observer drops without their counters.
|
||||
@@ -45,14 +94,14 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
|
||||
- mDNS action API can silently drop queued work; missing families disable each pass, available families repair/re-probe every 30 seconds. Transient stale/zero A window remains until processed; AAAA follows valid-address semantics including deprecated addresses. Upstream calls can block, so polling is not a hard deadline. The later user acceptance establishes the reported device/AAAA behavior, not exhaustive multicast fault/transition testing.
|
||||
- New **narrow** `cmake/mdns_membership.*` overlay fixes verified mDNS 1.12.0 per-family multicast reference imbalance and failed-PCB join cleanup. Only a build-local source copy is changed; version/source hash guarded, managed source immutable. This is not the abandoned Phase 9 patch set. Membership/CMake tests include negative controls and repeated transitions.
|
||||
- Final `pio run` PASS: **94,396 B RAM / 1,853,025 B flash** (+176 / +5,380 versus recorded Phase 10); not runtime headroom. Local saved sdkconfig mDNS capacity changed to 2; durable defaults pin IPv4/IPv6 and two services. Initial bool-atomic target failure fixed with portMUX; final target build includes overlay. Existing SDK Kconfig notes remain.
|
||||
- Host suites PASS: Wi-Fi, mDNS, membership, SSH dual-stack and existing management/runtime/security, auth parser689 (host + actual lwIP), cookie variants, Network settings (after updated netif fakes), UI171+CSP, HTTPS lifecycle45+status8+identity, firmware88+SDKcontract, broker diagnostics and session-store/serial. No network packet, hardware, fault-injection or new high-speed serial pass is implied. Canonical contracts, limits and device checklist: [Phase 12](../roadmap.md#phase-12--advanced-network-integration).
|
||||
- Host suites PASS: Wi-Fi, mDNS, membership, SSH dual-stack and existing management/runtime/security, auth parser689 (host + actual lwIP), cookie variants, Network settings (after updated netif fakes), UI171+CSP, HTTPS lifecycle45+status8+identity, firmware88+SDKcontract, broker diagnostics and session-store/serial. No network packet, hardware, fault-injection or new high-speed serial pass is implied. Canonical contracts, limits and device checklist: [Phase 12](../devel/roadmap.md#phase-12--advanced-network-integration).
|
||||
|
||||
## Phase 10 COMPLETE — explicit user acceptance, 2026-09-18
|
||||
|
||||
- User confirmed after firmware upload implementation and the concise-UI fix: “That works perfectly. And the usual operation is also verified.” Acceptance establishes that upload works and normal operation is verified. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or recovery passes. The roadmap's compact regression guidance is reusable, not an acceptance blocker.
|
||||
- Latest concise-UI change was copy-only; reported UI regression **169 groups PASS**. No rebuild after that text change. The integration build below is historical, not validation of a newly rebuilt UI or this documentation update.
|
||||
|
||||
- User authorized simple OTA implementation; initial Git clean. New web_firmware_update module with standard SDK APIs, raw POST /api/firmware, admin cookie/Origin/CSRF and final principal currentness. Settings → HTTPS / Reboot has File/XHR upload progress, confirmation, session fencing and no automatic retries. Only inactive app/otadata writes; NVS/layout untouched. No signatures/antirollback/automaticrollback, dependencies or generated assets changed. Consolidated procedure, contract and regression guidance: [Phase 10](../roadmap.md#phase-10--simple-admin-web-firmware-upload); standalone guide removed.
|
||||
- User authorized simple OTA implementation; initial Git clean. New web_firmware_update module with standard SDK APIs, raw POST /api/firmware, admin cookie/Origin/CSRF and final principal currentness. Settings → HTTPS / Reboot has File/XHR upload progress, confirmation, session fencing and no automatic retries. Only inactive app/otadata writes; NVS/layout untouched. No signatures/antirollback/automaticrollback, dependencies or generated assets changed. Consolidated procedure, contract and regression guidance: [Phase 10](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload); standalone guide removed.
|
||||
- 4KiB internal buffer + transient2048B-stack reboot owner allocated before erase; 10s stall/120s receive-loop budget, not totalflashdeadline. HTTPD synchronously blocks other web work during upload; networkserial maystall/drop, reboot disruptsall. No task/request/socket lifetime capture after handler. Service/identity reservation and atomic ordinary-reboot gate cover UI/UART0/SSH/browser/localbutton paths. Failed response after bootselect schedules noautomaticreset; selected latch rejects further uploads409, manual reboot available. Successful response schedules500ms reboot retaining reservations.
|
||||
- Review fixed two actualSDK5.5.0 edge cases: failed esp_ota_begin maypublishlivehandle beforeeraseerror (abortthat handle); rawContentLength64 canwrapHTTPDsize_t32 (overflow-safe actualslotbound/strictdecimal/equality check beforebody/erase). End consumes handle evenerror. SDKvalidation followed by exactparsedimage length includingSHA; basic header requiresS3appdescriptor/hash. Unrelated old/privateSDK code unpatched.
|
||||
- Parent final pio PASS **94,220 B RAM / 1,847,645 B flash**, +24RAM/+19,080flash vsPhase9, not runtimeheadroom. Parent newbackend88cases+actualSDKbeginfailurecontract, UI169groups+CSP, serverlifecycle44, admin25, consolelifecycle, SSHruntime, cookielifecyclePASS. Additionalbase/admin/display/lifecyclecookie, idle18, SSHmanagement/runtime/security agentPASS after adding missing rebootfake to adminfixture (no productionchange). Independent review final noactionablefindings; realbuilt firmware parsed with SDKmetadata bothOTAoffsets (notdeviceflashproof).
|
||||
@@ -67,7 +116,7 @@ Working memory, not an implementation timeline. Source is authoritative; begin w
|
||||
- **Reduced Phase 9 complete by explicit user sign-off.** User waived a new whole-phase device check based on prior Phase 8 validation. Application code was unchanged, but no-core-dump/silent-panic defaults changed. Do not record the waived check as executed or claim a new panic/hardware pass.
|
||||
- Small scope: standard `sdkconfig.defaults` options, source-reviewed [operational checklist](../security_operations.md), README/roadmap guidance. No dependency patches, crypto policy replacement, allocator hooks, SDK migration, encryption, eFuse or partition changes. User abandoned the extensive earlier Phase 9 and restored baseline `f40c09c`; do not resurrect it.
|
||||
- Historical Phase 9 build: normal `pio run` PASS on PlatformIO6.12.0 / IDF5.5.0 / original20241119 toolchains, **94,196 B linked RAM / 1,828,565 B flash**. Generated configuration confirmed no dumps/silent panic; compilation inputs had no abandoned overlays/crypto guard. Existing generated config already selected these options. Defaults do not override saved sdkconfig; README explains verification. No new build is implied by this documentation consolidation.
|
||||
- **Phase 8 complete:** 8A–C target validated; explicit 8D.22 user acceptance on 2026-09-13. Completion/telemetry and legacy compatibility now live in [roadmap acceptance](../roadmap.md#phase8-acceptance-evidence) and [storage compatibility](../roadmap.md#phase8-legacy-credential-compatibility). Separate acceptance/legacy history documents removed; active [web contracts](../web_administration.md) and [regression procedures](../user_administration_tests.md) retained.
|
||||
- **Phase 8 complete:** 8A–C target validated; explicit 8D.22 user acceptance on 2026-09-13. Completion/telemetry and legacy compatibility now live in [roadmap acceptance](../devel/roadmap.md#phase8-acceptance-evidence) and [storage compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility). Separate acceptance/legacy history documents removed; active [web contracts](../devel/web_administration.md) and [regression procedures](../devel/user_administration_tests.md) retained.
|
||||
- Post-acceptance baseline includes PSRAM-only ping payload/user snapshot allocations and refined web quick panels. Prior focused host/geometry/build checks passed; those are not additional hardware claims. Preserve lazy-allocation failure isolation and unchanged serial hot path.
|
||||
|
||||
## Evidence limits and follow-ups
|
||||
|
||||
@@ -6,13 +6,13 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Decision:** `local_status_ui` owns a nonwrapping configuration generation and a zero-wait reservation shared by typed Display and CLI/legacy Apply. NVS runs outside timing critical sections; Save stabilizes selected bytes and Reset commits defaults before RAM publication. Load retains canonical fallback. Buttons/diagnostic holds update activity, not configuration generation.
|
||||
|
||||
**Consequence:** Compare before mutation, never overwrite intervening CLI edits, and do not make physical panel presence a configuration prerequisite. A successful config API is not proof of display IO. [Owner and persistence contract](../web_administration.md#serial-and-display).
|
||||
**Consequence:** Compare before mutation, never overwrite intervening CLI edits, and do not make physical panel presence a configuration prerequisite. A successful config API is not proof of display IO. [Owner and persistence contract](../devel/web_administration.md#serial-and-display).
|
||||
|
||||
## Typed Network edits preserve manager ownership and current secret bytes
|
||||
|
||||
**Decision:** HTTPD uses zero-wait secret-free projections and an ID-only dispatcher. Wi-Fi compare/merge/whole-candidate validation and queue-before-publication preserve omitted secrets and reject stale changes; its manager alone owns radio/reannouncement. mDNS generation/persistence is independent. Save stabilizes selected RAM; Wi-Fi Load is stored-only, not fallback-secret generation.
|
||||
|
||||
**Consequence:** Keep SSIDs reversible bytes, omitted/Replace/disabled-STA Clear distinct, AP clear denied, and Next profile separate from editor selection. mDNS may report RAM applied but reannouncement not queued, without rollback. A one-second queued-secret timer is not hard cancellation; `accepted` is not online/DNS. Recovery and no automatic replay are correctness requirements. [Network contract](../web_administration.md#network).
|
||||
**Consequence:** Keep SSIDs reversible bytes, omitted/Replace/disabled-STA Clear distinct, AP clear denied, and Next profile separate from editor selection. mDNS may report RAM applied but reannouncement not queued, without rollback. A one-second queued-secret timer is not hard cancellation; `accepted` is not online/DNS. Recovery and no automatic replay are correctness requirements. [Network contract](../devel/web_administration.md#network).
|
||||
|
||||
## One broker mediates all production serial transports
|
||||
|
||||
@@ -32,9 +32,9 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence for future changes:** Do not replace fan-out with a blocking shared queue. Any added transport must tolerate partial/no-progress reads and expose drop/backpressure counters.
|
||||
|
||||
**Throughput observation and controlled experiments:** The initial diagnostic baseline used CPU160MHz; a CPU240MHz-only experiment reduced but did not eliminate browser queue overflow. Combining binary WebSocket header/payload into one bounded session-override send eliminated reported drops, and the user signed off230400-baud full-client-mix operation after returning to160MHz. Retain the combined send, not the frequency increase; evidence and limits are in `current-state.md`. Preserve scheduling/priorities and 4096/512-byte broker/web buffers while gathering per-client HWM/drop attribution and independent opt-in web binary-TX timing. Fixed-slot epoch/generation-fenced aggregates avoid stale attribution; no new runtime allocations. Clear preserves queued data and seeds broker HWM; disconnected rows disappear while global discard counts remain. Callback timestamps precede the transport lock; synchronous send return is not peer receipt. Completion-to-read intervals include broker/mutex/control work and possible idle, even when the first read is nonempty; never label them pure scheduling latency or proof of backlog at completion. Compare enabled/disabled target captures before drawing overhead conclusions. Contracts and reproduction: [throughput diagnostics](../web_throughput_diagnostics.md).
|
||||
**Throughput observation and controlled experiments:** The initial diagnostic baseline used CPU160MHz; a CPU240MHz-only experiment reduced but did not eliminate browser queue overflow. Combining binary WebSocket header/payload into one bounded session-override send eliminated reported drops, and the user signed off230400-baud full-client-mix operation after returning to160MHz. Retain the combined send, not the frequency increase; evidence and limits are in `current-state.md`. Preserve scheduling/priorities and 4096/512-byte broker/web buffers while gathering per-client HWM/drop attribution and independent opt-in web binary-TX timing. Fixed-slot epoch/generation-fenced aggregates avoid stale attribution; no new runtime allocations. Clear preserves queued data and seeds broker HWM; disconnected rows disappear while global discard counts remain. Callback timestamps precede the transport lock; synchronous send return is not peer receipt. Completion-to-read intervals include broker/mutex/control work and possible idle, even when the first read is nonempty; never label them pure scheduling latency or proof of backlog at completion. Compare enabled/disabled target captures before drawing overhead conclusions. Contracts and reproduction: [throughput diagnostics](../devel/web_throughput_diagnostics.md).
|
||||
|
||||
**Relevant files:** `src/session_broker.c`, `src/session_broker.h`, `docs/roadmap.md`
|
||||
**Relevant files:** `src/session_broker.c`, `src/session_broker.h`, `docs/devel/roadmap.md`
|
||||
|
||||
## Physical UART ownership and logical writer ownership remain separate
|
||||
|
||||
@@ -50,13 +50,13 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Decision:** Broker/SSH/WebSocket slots, originating web sessions, queued admin operations and account principals carry distinct generation/identity fences. Browser cookie-session identity is not interchangeable with account identity. Invalidate session records before socket cleanup and retain authoritative currentness checks even when notifications fail.
|
||||
|
||||
**Consequence:** Never turn selected transport IDs into arbitrary broker IDs/fds, rebase a stale confirmation or wrap a published token. Exhausted broker/SSH slots retire; operation/reservation IDs do not reuse; saturated service/lease generations fence ABA without disabling canonical recovery. Reboot invalidates old logins. Validate/reserve at the owner immediately before effects, not snapshot-check/unlock/unconditional mutation. [Broker](../web_administration.md#broker-and-contextual-controls) and [service](../web_administration.md#service-lifecycle-and-identity-rotation) contracts.
|
||||
**Consequence:** Never turn selected transport IDs into arbitrary broker IDs/fds, rebase a stale confirmation or wrap a published token. Exhausted broker/SSH slots retire; operation/reservation IDs do not reuse; saturated service/lease generations fence ABA without disabling canonical recovery. Reboot invalidates old logins. Validate/reserve at the owner immediately before effects, not snapshot-check/unlock/unconditional mutation. [Broker](../devel/web_administration.md#broker-and-contextual-controls) and [service](../devel/web_administration.md#service-lifecycle-and-identity-rotation) contracts.
|
||||
|
||||
## Confirmed writer transfer compares a lease version inside the broker lock
|
||||
|
||||
**Decision:** Compare the connected nonreused target and separate lease generation inside the force-writer lock. A client ID alone cannot fence release/reacquire ABA. Lease generation saturates, survives counter clear and advances before advisory event delivery, potentially twice for force transfer.
|
||||
|
||||
**Consequence:** Saturation rejects typed assignment but preserves ordinary request/release/disconnect and recovery force. Accepted serial TX is not recalled. Live contextual refresh must retain the selected versions and sticky stale/absence state until explicit reselection. [Wrap and UI contract](../web_administration.md#broker-and-contextual-controls).
|
||||
**Consequence:** Saturation rejects typed assignment but preserves ordinary request/release/disconnect and recovery force. Accepted serial TX is not recalled. Live contextual refresh must retain the selected versions and sticky stale/absence state until explicit reselection. [Wrap and UI contract](../devel/web_administration.md#broker-and-contextual-controls).
|
||||
|
||||
## UART0 is the physical recovery authority
|
||||
|
||||
@@ -66,7 +66,7 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence for future changes:** Network failures or credential corruption must not remove UART0 recovery. Do not expose unauthenticated first-admin provisioning or recovery through web or admin SSH without an explicit security redesign.
|
||||
|
||||
**Relevant files:** `src/main.c`, `src/admin_ssh_console.c`, `src/user_console.c`, `docs/roadmap.md`
|
||||
**Relevant files:** `src/main.c`, `src/admin_ssh_console.c`, `src/user_console.c`, `docs/devel/roadmap.md`
|
||||
|
||||
## Admin SSH and user SSH are different routes
|
||||
|
||||
@@ -96,31 +96,31 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Identity replacement:** Compare/reserve service then task-bound nonreused identity token, shared by canonical/direct callers. Crypto/NVS run outside security locks. HTTPS commits before stop/start; SSH stops before commit/restart. Failed SSH stop skips mutation/start; persistence failure can follow client disconnection. Committed material is never rolled back on restart failure. SSH owner retains context until all slots retire; start rejects orphan handles. Public fingerprint projections do not authorize mutation, and stored/served HTTPS identity can differ after failed stop.
|
||||
|
||||
**Consequence:** Explicitly communicate partial effects, trusted UART0 fingerprint verification and fresh HTTPS login after restart. Preserve CLI recovery/reset but do not add browser Reset/export as if it were ordinary rotation. Native USB is independent UART1, not administration or uninterrupted reboot. [Lifecycle/identity contract](../web_administration.md#service-lifecycle-and-identity-rotation).
|
||||
**Consequence:** Explicitly communicate partial effects, trusted UART0 fingerprint verification and fresh HTTPS login after restart. Preserve CLI recovery/reset but do not add browser Reset/export as if it were ordinary rotation. Native USB is independent UART1, not administration or uninterrupted reboot. [Lifecycle/identity contract](../devel/web_administration.md#service-lifecycle-and-identity-rotation).
|
||||
|
||||
## Authentication uses copied principals and fail-safe currentness checks
|
||||
|
||||
**Decision:** Network sessions retain secret-free copied principals. Database/account ID/auth generation and originating web-session identity must be current at admission, before sensitive input and during reconciliation. Best-effort target notifications supplement, never replace, these checks and cannot roll back committed mutations.
|
||||
|
||||
**Consequence:** Shared remote-console slots require transport-qualified tokens and immutable owner adapters. Validate owner currentness outside console locks, then recheck identity. Owner-side HTTPD/SSH IO and generation-safe cleanup remain mandatory; session liveness checks do not cancel executing handlers. Browser-shell permissions are parsed and narrower than typed Settings. [Authentication](../web_administration.md#authentication-and-admission), [console policy](../web_administration.md#browser-shell-policy).
|
||||
**Consequence:** Shared remote-console slots require transport-qualified tokens and immutable owner adapters. Validate owner currentness outside console locks, then recheck identity. Owner-side HTTPD/SSH IO and generation-safe cleanup remain mandatory; session liveness checks do not cancel executing handlers. Browser-shell permissions are parsed and narrower than typed Settings. [Authentication](../devel/web_administration.md#authentication-and-admission), [console policy](../devel/web_administration.md#browser-shell-policy).
|
||||
|
||||
## Typed serial mutations share the administration dispatcher
|
||||
|
||||
**Decision:** Typed domains queue IDs to the existing serialized dispatcher, never CLI strings or secrets. One original-login slot per domain and a nonreused ID fence stale work; session/deadline checks precede canonical admission. Results are replaceable observations, not durable history/idempotency.
|
||||
|
||||
**Consequence:** No automatic mutation replay, including after navigation, timeout or logout. Ordinary deadlines limit dequeue admission, not execution. Accounts/Network queued-secret timers wipe only non-executing inputs; locals wipe after admitted work returns. Explicit RAM/NVS/reset semantics and partial-effect uncertainty must match each canonical owner. [API bounds and lifetime](../web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
**Consequence:** No automatic mutation replay, including after navigation, timeout or logout. Ordinary deadlines limit dequeue admission, not execution. Accounts/Network queued-secret timers wipe only non-executing inputs; locals wipe after admitted work returns. Explicit RAM/NVS/reset semantics and partial-effect uncertainty must match each canonical owner. [API bounds and lifetime](../devel/web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
|
||||
## Typed account selection is checked inside the database mutation lock
|
||||
|
||||
**Decision:** Target username/account ID/auth generation compare occurs inside the canonical mutation lock for role/delete/password/key changes. HTTPD uses compact zero-wait secret-free projections, not the blocking CLI snapshot. Successful changes notify only the target's web/SSH sessions, including self.
|
||||
|
||||
**Consequence:** Separate generated-value delivery from mutation and retain no retrieval history; context-bound saved acknowledgement is UX, not receipt proof. Self-revocation can deny results, so 401/disconnect cannot mean success or cancellation. Key slots are stable and sparse, fingerprint-only on output; import shares canonical validation. Final-admin invariants and UART0 provisioning/recovery remain. [Accounts contract](../web_administration.md#accounts-and-authorized-keys).
|
||||
**Consequence:** Separate generated-value delivery from mutation and retain no retrieval history; context-bound saved acknowledgement is UX, not receipt proof. Self-revocation can deny results, so 401/disconnect cannot mean success or cancellation. Key slots are stable and sparse, fingerprint-only on output; import shares canonical validation. Final-admin invariants and UART0 provisioning/recovery remain. [Accounts contract](../devel/web_administration.md#accounts-and-authorized-keys).
|
||||
|
||||
## Browser authentication has a narrow version-pinned HTTPD boundary
|
||||
|
||||
**Decision:** `web_httpd_adapter` alone accesses private IDF 5.5.0 parsed-header/session state. Reject duplicate/ambiguous headers; defer 101 until cookie/Origin/ticket/transport admission; wipe consumed scratch while preserving unread bytes. Stage optional Settings descriptor/name allocations before publication. Compile header/ticket debug logging out.
|
||||
|
||||
**Consequence:** Re-audit SDK assumptions on upgrade; never patch around Origin `null` by weakening same-origin policy. Browser authentication POST uses CORS mode with fixed same-origin URLs/credentials because no-referrer non-CORS POST can serialize Origin as null. Digest-only cookie/challenge sessions replace Basic without fallback or live-record eviction. CSP loader hashes and authored scripts change atomically. Navigation preserves terminals/lease, while session-identity changes require a clean document before showing retained buffers. [Authentication and terminal contracts](../web_administration.md#authentication-and-admission).
|
||||
**Consequence:** Re-audit SDK assumptions on upgrade; never patch around Origin `null` by weakening same-origin policy. Browser authentication POST uses CORS mode with fixed same-origin URLs/credentials because no-referrer non-CORS POST can serialize Origin as null. Digest-only cookie/challenge sessions replace Basic without fallback or live-record eviction. CSP loader hashes and authored scripts change atomically. Navigation preserves terminals/lease, while session-identity changes require a clean document before showing retained buffers. [Authentication and terminal contracts](../devel/web_administration.md#authentication-and-admission).
|
||||
|
||||
## Security material and configuration use bounded, versioned NVS records
|
||||
|
||||
@@ -128,7 +128,7 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Rationale/evidence:** Serial, Wi-Fi, local UI, web security, users, and SSH security each validate schema/size and own their namespace. User/security mutations build and validate candidate state before committing it; security modules avoid silently replacing an established identity. The live user database remains internal while its 5,360-byte candidate is a persistent PSRAM-preferred allocation with internal fallback and is wiped after every transaction.
|
||||
|
||||
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Legacy credential synchronization and reconciliation are removed. Missing user storage commits empty; valid user v1 bytes remain compatible, with private `v1_admin_marker` derived from admin count, not a public bootstrap contract. HTTPS v1 (1,392 bytes) migrates through a private validated reader to TLS-only v2 (1,340 bytes), preserving exact DER/fingerprint/generation and committing before publication. Failures fail closed without fallback regeneration or overwriting rejected records. See [legacy compatibility](../roadmap.md#phase8-legacy-credential-compatibility).
|
||||
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Legacy credential synchronization and reconciliation are removed. Missing user storage commits empty; valid user v1 bytes remain compatible, with private `v1_admin_marker` derived from admin count, not a public bootstrap contract. HTTPS v1 (1,392 bytes) migrates through a private validated reader to TLS-only v2 (1,340 bytes), preserving exact DER/fingerprint/generation and committing before publication. Failures fail closed without fallback regeneration or overwriting rejected records. See [legacy compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
|
||||
**Relevant files:** `src/serial_config.c`, `src/wifi_config.c`, `src/mdns_config.c`, `src/mdns_service.c`, `src/local_ui_config.c`, `src/web_security.c`, `src/user_database.c`, `src/ssh_security.c`
|
||||
|
||||
@@ -180,7 +180,7 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence for future changes:** Do not remove these definitions as a performance cleanup. Any re-enablement needs target-hardware concurrency testing with simultaneous USB, WebSocket, SSH, and serial traffic plus watchdog/stack telemetry.
|
||||
|
||||
**Relevant files:** `CMakeLists.txt`, `src/CMakeLists.txt`, `docs/roadmap.md`, relevant `sdkconfig.defaults` crypto settings
|
||||
**Relevant files:** `CMakeLists.txt`, `src/CMakeLists.txt`, `docs/devel/roadmap.md`, relevant `sdkconfig.defaults` crypto settings
|
||||
|
||||
## Embedded web assets are checked-in generated artifacts
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Command reference
|
||||
|
||||
UART0 and authenticated `admin` SSH sessions use the same registered command implementations through one serialized dispatcher. Admin SSH exposes the full operational registry, including interactive prompts, network diagnostics, reboot, and HTTPS/SSH material mutation. Create the first administrator through normal `user add <username> admin` on physical UART0; explicit recovery of an unavailable database is UART0-only. Admin SSH also rejects generating a replacement password for its own account so the one-time value cannot be lost when the session is revoked. Run `help` for root commands and `<group> help` for a group summary. Configuration changes are RAM-only unless explicitly saved.
|
||||
UART0 and authenticated `admin` SSH sessions use the same registered command implementations through one serialized dispatcher. Admin SSH exposes the full operational registry, including interactive prompts, network diagnostics, reboot, and HTTPS/SSH material mutation. Create the first administrator through normal `user add <username> admin` on physical UART0; explicit recovery of an unavailable database is UART0-only. Admin SSH also rejects generating a replacement password for its own account so the one-time value cannot be lost when the session is revoked. Run `help` for root commands and `<group> help` for a group summary. For serial, Wi-Fi, mDNS, and display settings, use the documented save/reset commands to persist working changes. Do not assume account or certificate/key changes are RAM-only.
|
||||
|
||||
Browser admin uses the same dispatcher with a [narrower parsed frontend policy](web_administration.md#browser-shell-policy), independent of typed Settings permissions. It supports bounded deferred `reboot`, `web stop`, exact `web certificate rotate --force` and owner-relative `exit`. Drain (up to ten seconds plus 200 ms) is best-effort application-buffer acknowledgement, not peer receipt or an execution deadline; pending input is discarded. Certificate work runs on the existing dispatcher through the shared service-before-identity reservation, commits before stop/restart and never rolls back a committed identity after lifecycle failure. Failed stop retains ownership and skips start. Verify changed trust through UART0 `web certificate info`, recover with UART0/admin SSH `web stop` / `web start`, then sign in freshly. HTTPS-only actions leave SSH/native USB/UART0 independent; reboot affects every transport and loses unsaved RAM.
|
||||
## Choosing an administration connection
|
||||
|
||||
Browser `web` allows only status/stop/exact forced certificate rotation; `mdns` allows status only. Browser `wifi` supports the canonical commands, including bare `wifi` status, settings, hidden secret prompts and explicit `wifi ap show-secret`. Secret input is not echoed, retained in history or completed. Wi-Fi changes can disconnect the browser before a result arrives, without deferred drain or cancellation; reconnect and inspect before retrying, or recover through UART0. Browser `user` allows status/list/show and interactive add/password plus forced role/delete for **other accounts**, not self/generated/key/recovery commands. Restricted SSH stop/disconnect/reset/host-key mutation remains unavailable in the browser shell. Typed Accounts/Network/SSH settings separately provide their documented bounded workflows; this is not shell parity. See [web administration](web_administration.md) for lifecycle/API ownership and uncertainty.
|
||||
Keep physical UART0 available for recovery when changing network access, accounts, or service identities. Browser Admin uses a restricted command set, separate from the permissions of the Settings dialogs. It supports `reboot`, `web stop`, exact `web certificate rotate --force`, and `exit`. These actions briefly wait for queued output before disconnecting; this is not proof that your browser received the result, and pending input is discarded.
|
||||
|
||||
Certificate replacement is saved before HTTPS restarts. A restart failure does not restore the old identity. Verify the fingerprint through UART0 `web certificate info`; recover with UART0/admin SSH `web stop` / `web start`, then trust the new certificate and sign in again. HTTPS-only actions leave SSH, native USB, and UART0 independent. Reboot affects every transport and loses unsaved RAM settings.
|
||||
|
||||
Browser `web` allows only status/stop/exact forced certificate rotation; `mdns` allows status only. Browser `wifi` supports the canonical commands, including bare `wifi` status, settings, hidden secret prompts and explicit `wifi ap show-secret`. Secret input is not echoed, retained in history or completed. Wi-Fi changes can disconnect the browser before a result arrives, without deferred drain or cancellation; reconnect and inspect before retrying, or recover through UART0. Browser `user` allows status/list/show and interactive add/password plus forced role/delete for **other accounts**, not self/generated/key/recovery commands. Restricted SSH stop/disconnect/reset/host-key mutation remains unavailable in the browser shell. The Accounts, Network, and SSH Settings dialogs offer additional actions; an action available in Settings is not necessarily available in the browser shell. See the [browser user guide](web_administration.md).
|
||||
|
||||
## System
|
||||
|
||||
@@ -14,7 +18,7 @@ Browser `web` allows only status/stop/exact forced certificate rotation; `mdns`
|
||||
| `reboot` | Drain console output briefly and restart the ESP32; refused while a firmware upload or another reboot owns exclusion. |
|
||||
| `exit` | Close the current administrative SSH or browser session after its acknowledgement drains; unavailable on UART0. Browser `exit` leaves serial connected. Ctrl+D on an empty administrative command line does the same. |
|
||||
|
||||
Firmware upload is an admin **Settings → HTTPS / Reboot** action, not a shell command. Ordinary UART0/admin-SSH/browser-shell/local-display and typed reboot paths are gated against an active upload. If upload boot selection succeeded but its response failed, no automatic restart is scheduled: the selected image remains, another admissible upload returns 409 until reset, and a deliberate manual `reboot` is permitted after reservations release. Inspect first; a lost response is not cancellation. See [firmware update and wired recovery](roadmap.md#phase10-update-and-recovery). Do not erase for an ordinary update; native USB is UART1 access, not recovery administration.
|
||||
Firmware upload is an admin **Settings → HTTPS / Reboot** action, not a shell command. Reboot requests are refused while an upload is active. If the new image was selected for boot but the upload response failed, no automatic restart is scheduled. The selected image remains, and further upload attempts return 409 until reset; a deliberate manual `reboot` is permitted once the upload has released the service. Inspect first; a lost response is not cancellation. See the [browser user guide](web_administration.md) for firmware upload and recovery. Do not erase for an ordinary update; native USB is UART1 access, not recovery administration.
|
||||
|
||||
## Role-based users
|
||||
|
||||
@@ -34,9 +38,9 @@ Firmware upload is an admin **Settings → HTTPS / Reboot** action, not a shell
|
||||
| `user key clear <username> --force` | Delete all public keys for an account. |
|
||||
| `user recover --force` | When normal user-database initialization failed, explicitly replace only its blob with an empty database; UART0-only, refuses a healthy database. |
|
||||
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; initial key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; supported key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
|
||||
Missing `user_db/database` storage is committed empty. On UART0 run `user add <username> admin`, optionally with `--generate`, to create the first administrator. There is no bootstrap command, imported shared credential, or synchronization with HTTPS material. Existing valid v1 user databases load unchanged, including previously migrated role-`user` accounts; no account is silently promoted.
|
||||
A device with no stored user database starts with an empty database. On UART0 run `user add <username> admin`, optionally with `--generate`, to create the first administrator. There is no default shared login. Existing valid user databases retain their accounts and roles; no account is silently promoted.
|
||||
|
||||
`user recover --force` is destructive and succeeds only while the database is unavailable. It rebuilds only the user blob empty, importing no credentials; then create an administrator with normal `user add` on UART0. It refuses a healthy initialized database, including a healthy empty one, and does not erase unrelated NVS data. Successful password, role, key and delete operations invalidate only that username's outstanding tickets and active web/SSH sessions; unrelated users remain connected.
|
||||
|
||||
@@ -50,9 +54,9 @@ Missing `user_db/database` storage is committed empty. On UART0 run `user add <u
|
||||
| `display save` / `display load` | Save the working aging settings to NVS or load them. |
|
||||
| `display defaults` / `display reset` | Apply 300/600-second defaults in RAM, or save defaults first and then apply them. |
|
||||
|
||||
When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. At normal boot, an initialized OLED shows a bounded five-second identity animation before the status UI begins; it scrolls the device name in yellow and draws the compact upright-terminal logo in blue. A missing OLED remains nonfatal; after reconnecting it safely, one new button press requests a bounded reprobe and is consumed without navigating.
|
||||
When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. At normal boot, an initialized OLED shows a bounded five-second identity animation before the status UI begins; it scrolls the device name in yellow and draws the compact upright-terminal logo in blue. A missing OLED remains nonfatal. With the local UI task running and the I²C bus initialized, one new button press requests a bounded reprobe and is consumed without navigating. If `debug display status` reports `bus=unavailable`, correct the wiring with power off and reboot instead.
|
||||
|
||||
Display settings require an available local UI task, not an attached panel. CLI and browser **Settings → Display** share the public configuration owner; concurrent mutations can report busy, and browser operations reject an intervening configuration edit rather than overwrite it. Save persists working RAM, not browser drafts. Load selects defaults when saved storage is absent/incompatible without rewriting NVS. Reset storage failure leaves RAM unchanged (commit-before-publication, no RAM rollback). Browser timeout/navigation does not cancel already-admitted work; Check Result and Refresh before retrying. [Display settings contract](web_administration.md#serial-and-display).
|
||||
Display settings require a running local UI task, not an attached panel. Concurrent console/browser changes can report busy or reject an outdated browser edit. Save persists the device's working settings, not unsent browser drafts. Load selects defaults when saved storage is absent or incompatible without rewriting it. If Reset cannot save defaults, the working settings remain unchanged. A browser timeout or navigation does not cancel an accepted action; use **Check Result** and **Refresh** before retrying.
|
||||
|
||||
## Serial service
|
||||
|
||||
@@ -80,7 +84,7 @@ Defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control,
|
||||
| `broker read <client-id> [maximum-bytes]` | Read queued serial output for a client. |
|
||||
| `broker events <client-id>` | Show ownership and connection events for a client. |
|
||||
|
||||
Each client has a generation-safe ID. There can be one writer and multiple observers; a slow observer loses only its own queued output. HWM is bounded by 4,096 bytes; read counts transport handoff, not peer receipt. Disconnected client rows disappear, while global totals retain traffic and unread-output discards until cleared. Capture counters before disconnect. `broker read` consumes queued data and must not be used as a throughput diagnostic probe. See [Web throughput diagnostics](web_throughput_diagnostics.md).
|
||||
Each client has a generation-safe ID. There can be one writer and multiple observers; a slow observer loses only its own queued output. HWM is bounded by 4,096 bytes; read counts transport handoff, not peer receipt. Disconnected client rows disappear, while global totals retain traffic and unread-output discards until cleared. Capture counters before disconnect. `broker read` consumes queued data and must not be used as a throughput diagnostic probe. See [Web throughput diagnostics](devel/web_throughput_diagnostics.md).
|
||||
|
||||
## Native USB CDC-ACM
|
||||
|
||||
@@ -134,11 +138,9 @@ Link-local **probe destinations** require a scope on the ESP32: `%sta`, `%ap`, a
|
||||
|
||||
`nslookup` without a selector performs separate A and AAAA queries. The installed resolver returns one selected address per family (`CONFIG_LWIP_DNS_MAX_HOST_IP=1`), not a complete DNS RRset. Missing records, DNS timeouts and server failures can share the same SDK error; partial success is displayed rather than hidden. Unicast names require a usable configured DNS server; SLAAC alone does not provision one in this firmware. `.local` uses the existing lwIP mDNS query support. No DNS server or RA/DHCP policy is changed by these commands.
|
||||
|
||||
Traceroute sends one eight-byte echo probe per hop with a one-second absolute receive deadline; it can stop up to 999 microseconds early to avoid the SDK's zero-millisecond/infinite-wait conversion. Hop addresses are numeric. ICMPv6 replies are checked against the actual IDF raw framing/checksum, destination and probe identity; IPv6 extension headers/fragments in replies or quotes are unsupported and ignored. Filtering/rate limiting can produce `*` even when ordinary connectivity works. DNS, scheduler and send latency are outside the receive deadline.
|
||||
Traceroute sends one eight-byte echo probe per hop and waits up to approximately one second for a reply. Hop addresses are numeric. IPv6 extension headers/fragments in replies or quoted packets are unsupported and ignored. Filtering/rate limiting can produce `*` even when ordinary connectivity works. DNS, scheduler and send latency are outside the receive deadline.
|
||||
|
||||
Ping retains the SDK's bounded-count transient task and existing PSRAM event queue. Its console wait is bounded to `count * 2 + 2` seconds after start, but unrelated ICMP can extend the SDK receive loop: after a console timeout, further pings are refused until the old END event arrives. END finishes callback production; SDK resource teardown may briefly overlap a subsequent session. No IPv6 TTL is printed because the installed SDK does not provide a valid received IPv6 hop-limit profile. These commands serialize the administration dispatcher while executing; they do not run on HTTPD or change the broker/serial path.
|
||||
|
||||
Focused host/SDK and surface tests: `tests/network_diagnostics/` and `tests/network_diagnostics_surfaces/`. Real IPv4/IPv6/scoped ping, DNS and multi-hop traceroute remain device-validation tasks for this follow-up.
|
||||
Ping waits up to `count * 2 + 2` seconds in the console after starting. After a console timeout, further pings may be refused until the previous probe session finishes. IPv6 replies do not include a received TTL/hop-limit value in the output. Network diagnostics occupy the shared administration dispatcher while executing, so other administrative commands wait; serial traffic uses a separate path.
|
||||
|
||||
## mDNS
|
||||
|
||||
@@ -151,7 +153,7 @@ Focused host/SDK and surface tests: `tests/network_diagnostics/` and `tests/netw
|
||||
|
||||
When the Wi-Fi station has IPv4 or a preferred IPv6 address, the Wi-Fi manager announces `sak-<suffix>.local` and advertises available HTTPS/SSH services through DNS-SD. This is STA-only, local-link discovery, not certificate or host-key trust. The default suffix is the lower-case hexadecimal STA MAC address. Suffixes may contain lowercase ASCII letters, digits, and internal hyphens only. Changing a suffix queues a best-effort reannouncement and updates the DHCPv4 hostname (`sak-<suffix>`, without `.local`) for subsequent DHCP exchanges; it does not force a lease restart. A configured DHCP/DNS server may publish that name in its own zone.
|
||||
|
||||
`wifi status` and browser Network status distinguish IPv4 absence and IPv6 link-local/ULA/GUA availability. `wifi` / `wifi status` and the web Network settings dialog additionally list up to three actual preferred IPv6 addresses, labelled link-local, ULA or GUA; these are copied with the flags, not inferred from enabled IPv6 support. A link-local destination needs the client's interface as its zone. To separate client address preference from IPv6 reachability, run `ping -6 -c 3 sak-1024.local` on an IPv6-capable client; choosing IPv4 with plain `ping` does not mean the device lacks IPv6. If lookup fails, inspect AAAA resolution (for example `avahi-resolve-host-name -6 sak-1024.local` where Avahi is installed), then test the numeric address from `wifi status` directly. Link-local-only connectivity counts as `ONLINE` and can retire the fallback AP after the existing stability interval; it does not establish Internet access. HTTPS/WebSocket and SSH support both families; browser IPv6 literals require brackets and cannot contain interface zones. Use the `.local` hostname for link-local browser access where supported by the client. On-device diagnostics support explicit family selection and scoped IPv6 as described above; Linux client syntax and interface scope are separate. mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access. See [Phase 12](roadmap.md#phase-12--advanced-network-integration) for discovery convergence limits and recorded user acceptance.
|
||||
`wifi status` and browser Network status distinguish IPv4 absence and IPv6 link-local/ULA/GUA availability. `wifi` / `wifi status` and the web Network settings dialog additionally list up to three actual preferred IPv6 addresses, labelled link-local, ULA or GUA; these are copied with the flags, not inferred from enabled IPv6 support. A link-local destination needs the client's interface as its zone. To separate client address preference from IPv6 reachability, run `ping -6 -c 3 sak-1024.local` on an IPv6-capable client; choosing IPv4 with plain `ping` does not mean the device lacks IPv6. If lookup fails, inspect AAAA resolution (for example `avahi-resolve-host-name -6 sak-1024.local` where Avahi is installed), then test the numeric address from `wifi status` directly. Link-local-only connectivity counts as `ONLINE` and can retire the fallback AP after the existing stability interval; it does not establish Internet access. HTTPS/WebSocket and SSH support both families; browser IPv6 literals require brackets and cannot contain interface zones. Use the `.local` hostname for link-local browser access where supported by the client. On-device diagnostics support explicit family selection and scoped IPv6 as described above; Linux client syntax and interface scope are separate. mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access. Discovery is best-effort and may not update immediately after network or service changes.
|
||||
|
||||
## HTTPS web terminal
|
||||
|
||||
@@ -167,10 +169,14 @@ When the Wi-Fi station has IPv4 or a preferred IPv6 address, the Wi-Fi manager a
|
||||
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. Browser admin defers commit and HTTPS restart; both browser routes close and new certificate trust/relogin is required. UART0/admin SSH behavior is unchanged. |
|
||||
| `web reset --force` | Explicitly replace HTTPS certificate/private key only, including missing, incompatible or damaged material; never changes users. |
|
||||
|
||||
Use UART0 for quiet throughput captures (authenticated admin SSH also exposes these commands). Performance output includes current-epoch pending age and count/sum/estimated-average/max timings for queue entry to callback entry, synchronous send, and completion to drain return. These are not peer acknowledgements or scheduler-only measurements; nonempty intervals may include idle. No secrets or serial payloads are printed. See [Web throughput diagnostics](web_throughput_diagnostics.md) for exact semantics, saturation limits and the reset/burst/drain/disable/capture-before-disconnect recipe.
|
||||
Use UART0 for quiet throughput captures (authenticated admin SSH also exposes these commands). Performance output includes current-epoch pending age and count/sum/estimated-average/max timings for queue entry to callback entry, synchronous send, and completion to drain return. These are not peer acknowledgements or scheduler-only measurements; nonempty intervals may include idle. No secrets or serial payloads are printed. See [Web throughput diagnostics](devel/web_throughput_diagnostics.md) for exact semantics, saturation limits and the reset/burst/drain/disable/capture-before-disconnect recipe.
|
||||
|
||||
HTTPS listens on port 443 only. Authenticate with any current user-database username/password; both `user` and `admin` roles receive the existing status and browser-terminal interface. The device serves vendored xterm.js without Internet access. Browser sessions use one-time account-bound tickets, binary WebSocket frames, and the broker's one-writer rule. The combined **Connect**/**Disconnect** control closes the current WebSocket and pauses automatic reconnect when active; after a user-paused disconnect, it changes to **Connect** to resume connection attempts. Account mutations revoke only that account's tickets and sessions.
|
||||
|
||||
### Older firmware compatibility
|
||||
|
||||
HTTPS storage upgrades preserve the certificate identity, but older firmware that understands only the v1 storage format cannot read the newer v2 format. Logical replacement of stored credentials or keys is not secure flash erasure. A factory erase is not required for the storage upgrade. See [storage compatibility notes](devel/roadmap.md#phase8-legacy-credential-compatibility) before downgrading firmware.
|
||||
|
||||
## SSH serial transport
|
||||
|
||||
| Command | Description |
|
||||
@@ -185,13 +191,13 @@ HTTPS listens on port 443 only. Authenticate with any current user-database user
|
||||
| `ssh host-key rotate --force` | Replace the persistent SSH host key. |
|
||||
| `ssh reset --force` | Explicitly replace invalid or missing SSH material. |
|
||||
|
||||
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
|
||||
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream and automatically requests writer ownership on connection. If denied, the session remains an observer and its input is discarded. Releasing another writer does not automatically promote it: deliberately assign control through administration or reconnect when the lease is free. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
|
||||
|
||||
UART0 and admin SSH submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, SSH commands execute the canonical UART0 handlers and produce the same status and mutation behavior rather than using a second command implementation. Remote output is routed into the authenticated session's bounded output ring; only the SSH transport task accesses wolfSSH.
|
||||
UART0, admin SSH, and browser Admin share one command dispatcher. A long-running command or interactive prompt can therefore delay commands from another administrator.
|
||||
|
||||
UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix expands inline; a Tab that cannot extend an ambiguous prefix prints the matching candidates and redraws the unchanged input line instead of cycling candidates. Admin SSH additionally supports four-entry per-session command history with Up/Down, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. Its history is RAM-only, private to the session, and wiped on disconnect. Ping callbacks enqueue bounded typed results so all formatting remains on the dispatcher task.
|
||||
UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix expands inline; a Tab that cannot extend an ambiguous prefix prints the matching candidates and redraws the unchanged input line instead of cycling candidates. Admin SSH additionally supports four-entry per-session command history with Up/Down, inline cursor editing with Left/Right, Home/End (including Pos1/Ende terminal sequences), Backspace/Delete, Ctrl-C, and visible or no-echo interactive prompts. Its history is RAM-only, private to the session, and wiped on disconnect.
|
||||
|
||||
`exit`, `reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation use bounded deferred control. The firmware waits on a best-effort basis for the administration output ring and transport TX buffer to drain before acting; this is not confirmation that the peer received the acknowledgement. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. TLS certificate management, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user recover --force` remains UART0-only; `user bootstrap` and all `web credentials` commands are removed. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||
`exit`, `reboot`, `ssh stop`, session disconnect, and SSH host-key reset/rotation use bounded deferred control. The firmware waits on a best-effort basis for the administration output ring and transport TX buffer to drain before acting; this is not confirmation that the peer received the acknowledgement. The shell stops accepting another command while such an action is pending. SSH host-key replacement or service stop closes all SSH sessions; reconnect and verify the new fingerprint where applicable. TLS certificate management, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user recover --force` is UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||
|
||||
## Hardware diagnostics
|
||||
|
||||
@@ -217,6 +223,4 @@ UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix
|
||||
| `debug buttons status` | Show the current active-low state of previous/back GPIO10, select/confirm GPIO13, and next GPIO14. |
|
||||
| `debug buttons test [seconds]` | Run the bounded button event test for 1–30 seconds; the default is 10 seconds. |
|
||||
|
||||
Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. The OLED must be powered from 3.3 V because module I²C pull-ups may connect to `VCC`; verify that all external pull-ups also terminate at 3.3 V. Display diagnostics probe the standard SSD1315-compatible 7-bit `0x3c`/`0x3d` addresses. The currently tested module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78`/`0x79`; an explicit `scan --force` is available only for the dedicated local-UI bus. Diagnostics initially run at 100 kHz and treat an absent display as nonfatal. RS-232 diagnostics that require UART1 refuse to use it until `serial stop` releases it. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed.
|
||||
|
||||
HTTPS storage migration preserves the exact TLS identity and commits TLS-only v2 before publication. Older v1-only firmware cannot read v2. Logical NVS replacement is not secure flash erasure; no factory erase is required. See [legacy removal](roadmap.md#phase8-legacy-credential-compatibility).
|
||||
Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. The OLED must be powered from 3.3 V because module I²C pull-ups may connect to `VCC`; verify that all external pull-ups also terminate at 3.3 V. Display diagnostics probe the standard SSD1315-compatible 7-bit `0x3c`/`0x3d` addresses. The currently tested module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78`/`0x79`; an explicit `scan --force` is available only for the dedicated local-UI bus. Diagnostics initially run at 100 kHz and treat an absent display as nonfatal. All RS-232 `debug` commands, including `debug status`, require `serial stop` to release the port. If diagnostic cleanup reports a port fault, reboot before continuing. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
@@ -17,13 +20,13 @@ Authoritative chain:
|
||||
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 `reference/`, unchanged upstream contents:
|
||||
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` |
|
||||
| `README.md` | `2825ba741a8ce83abdaf2c7cb9170641473357630ccbee40a0016dcdb5cd80bb` |
|
||||
| `../../../../hardware/PCB/reference/README.md` | `2825ba741a8ce83abdaf2c7cb9170641473357630ccbee40a0016dcdb5cd80bb` |
|
||||
| `license.txt` | `075dad5e5fc96c27014fabc269f4f5732909cffd178a486f546d982b6cf86b74` |
|
||||
|
||||
Raw URL prefix for each filename (URL-encode spaces):
|
||||
@@ -1,8 +1,13 @@
|
||||
# Button selection: Omron B3F-1000
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
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.
|
||||
**Integration decision:** the A0 schematic now implements B3F-1000 switches with one **2 kΩ, 1%, metal-film external pull-up (design rating at least 0.125 W)** per input. The lower-current/internal-pull-up-only discussion below records the selection rationale, not an unselected schematic option. Firmware is unchanged. User reports B3F-1000 and resistors purchased. Exact resistor MPN/wattage and measured supply margin remain unspecified.
|
||||
|
||||
## Schematic-ready recommendation
|
||||
|
||||
@@ -89,7 +94,7 @@ Inspected `/usr/share/kicad/footprints/Button_Switch_THT.pretty/SW_TH_Tactile_Om
|
||||
|
||||
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.
|
||||
The original A0 recommendation was 2.2 kΩ (about 1.5 mA); the purchased-part update uses **one external 2 kΩ 1% pull-up to 3.3 V per button** (nominal 1.65 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.
|
||||
|
||||
@@ -100,8 +105,8 @@ Retain firmware debouncing; a mechanical switch is not bounce-free. The device i
|
||||
## 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](reference/button-omron-b3f.pdf).
|
||||
- [Extracted text](reference/button-omron-b3f.txt) for searching; use the PDF for drawing interpretation.
|
||||
- [Rendered page 4](reference/button-omron-b3f-page4.png), visually checked for the internal connections and dimensions.
|
||||
- [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.
|
||||
@@ -1,5 +1,10 @@
|
||||
# DE-9 mechanical reference — pinned Adafruit 5988 STEP
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
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.
|
||||
@@ -9,25 +14,25 @@ Research: 2026-09-21. **Nominal CAD geometry, not a toleranced connector drawing
|
||||
- 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 `reference/db9-cad/`: STEP, repository-root `README.md`, repository-root `LICENSE`. `sources.json` records **full immutable raw URLs and SHA-256 hashes**.
|
||||
- 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 |
|
||||
|---|---|
|
||||
| `5988 RS-232 Level Shifter.step` | `8eab9dc591a985fc987ceec2d7534e5e259263c73bebc64c0c3cd847e135906f` |
|
||||
| `LICENSE` | `f60436644e066301d4c2e8f1f0674a157b1f60e4fd5586717b99dde255cc18cd` |
|
||||
| `README.md` | `9ed9beac7600623b7b9bd0d02e1fdcd86610ce22966723179e6c6b618e399dce` |
|
||||
| `../../../../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 `reference/README.md` and `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.
|
||||
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.
|
||||
|
||||
- `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.
|
||||
- `geometry.json`: all 15 solids and their faces, bounds, validity, analytic cylinder radii/axes, plane normals. All 15 pass `BRepCheck_Analyzer`.
|
||||
- `validate.py`: checks original hashes, all 12 header centres against actual Eagle XML, board bounds, mounting holes and selected front-face geometry; writes `measurements.json` containing explicit face selections and their bounds.
|
||||
- `../../../../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.
|
||||
@@ -1,6 +1,11 @@
|
||||
# Provisional carrier module footprints
|
||||
|
||||
These project-local footprints are in `Carrier.pretty`. They are mechanical
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
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
|
||||
@@ -37,7 +42,7 @@ silently reverse the numbering.
|
||||
|
||||
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 `README.md`:
|
||||
mechanical-evidence section of `../../../../hardware/PCB/README.md`:
|
||||
|
||||
| Pad | Signal | X | Y |
|
||||
|---|---|---:|---:|
|
||||
@@ -75,10 +80,38 @@ centre (pad 1), **not a PCB corner**.
|
||||
- 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.
|
||||
- User-measured planar geometry is now drawn on `F.Fab`, relative to pad 1:
|
||||
|
||||
| Feature | Start (X, Y), mm | End (X, Y), mm |
|
||||
|---|---|---|
|
||||
| Main PCB rectangle | (-1.365, -1.365) | (26.725, 54.705) |
|
||||
| Projecting antenna rectangle | (4.045, -8.255) | (21.675, -1.365) |
|
||||
| Left USB-C shell mouth span | (2.795, 54.705) | (11.665, 54.705) |
|
||||
| Right USB-C shell mouth span | (14.195, 54.705) | (23.065, 54.705) |
|
||||
|
||||
Main PCB is 28.09 mm wide × 56.07 mm long; antenna projection adds 6.89 mm,
|
||||
giving 62.96 mm overall. Antenna left/right insets are 5.41/5.05 mm, giving
|
||||
17.63 mm width and a centre 0.18 mm right of the PCB centre. Both shell widths
|
||||
are 8.87 mm, with outer-edge offsets 4.16 mm left / 3.66 mm right; mouths are
|
||||
flush with the bottom PCB edge. Lines indicate mouth spans, not shell depth.
|
||||
- User accepted approximate 1.365 mm top/bottom pin-centre offsets and supplied
|
||||
approximately the same left offset. Nominal pin pitch/row spacing are retained;
|
||||
resulting right margin is 1.325 mm. This 0.04 mm asymmetry follows the selected
|
||||
modelling datum, not a precision measurement of header asymmetry.
|
||||
- **No RF keepout, courtyard, full USB envelope, mounting holes or 3D model is
|
||||
inferred.** Shell depth/height, component/underside heights and plug clearances
|
||||
remain unknown. The antenna rectangle is physical geometry, not RF guidance.
|
||||
- Routed embedded U1 now contains the four measured F.Fab shapes and the two
|
||||
antenna/USB measurement-limit annotations. Generic library captions are omitted
|
||||
to avoid overlapping existing board labels. Existing footprint fields (including
|
||||
the historical hidden status/description), reference/value positions, pads,
|
||||
nets and UUIDs are deliberately retained; this is not a full footprint replacement.
|
||||
The library is authoritative for measured geometry, not those old status strings.
|
||||
- Measured board-local silkscreen and PDF/SVG/PNG/3D previews are refreshed.
|
||||
`update_u1_graphics.py` synchronizes only owned graphics, with deterministic new
|
||||
UUIDs and lock/concurrent-change guards; `validate_u1_update.py` checks exact
|
||||
preservation against the pre-update A1 snapshot. A physical 1:1 fit check is
|
||||
still pending; USB mouths sit 2.295 mm inside the carrier bottom edge.
|
||||
|
||||
## Validation and release gates
|
||||
|
||||
@@ -86,7 +119,9 @@ 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
|
||||
The HW678 regression now checks the two measured rectangles and two USB mouth
|
||||
spans, while retaining exact pad geometry checks and prohibiting guessed
|
||||
courtyards, zones and 3D models. 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.
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
# Compact two-layer PCB — A1 draft
|
||||
|
||||
Open **`serial-carrier.kicad_pcb`** in KiCad 10. The routed board belongs to the
|
||||
existing `serial-carrier.kicad_pro` / `.kicad_sch` project. **Not for fabrication.**
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
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.
|
||||
|
||||
## Measurement update — embedded U1, silkscreen and exports
|
||||
|
||||
The user has now supplied ESP32 main-PCB, antenna and USB mouth geometry; see
|
||||
`footprint-notes.md` and the hardware README. The library HW678 footprint contains
|
||||
these measured F.Fab bounds. Embedded U1 now has the same four shapes and two
|
||||
measurement-limit annotations; generic library captions are omitted to avoid
|
||||
colliding with existing board labels. Silkscreen, PDF and SVG/PNG/3D previews are
|
||||
refreshed. Existing footprint fields, all existing non-generated UUIDs, pads/nets,
|
||||
placement, routing, edges, RF reserve and project settings are unchanged. Old
|
||||
hidden U1 status/description fields are retained historical metadata, not evidence
|
||||
that planar measurements are missing. RF requirements, full USB envelopes and
|
||||
assembly heights remain pending; the antenna rectangle is not an RF keepout.
|
||||
|
||||
At unchanged A1 U1 placement, the measured main PCB occupies carrier-local
|
||||
X=6.635…34.725, Y=21.635…77.705 mm; the projecting antenna occupies
|
||||
X=12.045…29.675, Y=14.745…21.635 mm. USB mouths are at Y=77.705 mm,
|
||||
**2.295 mm inside the carrier bottom edge**, so flush-with-module USB does not
|
||||
mean flush-with-carrier access. Review mating-plug clearance before accepting
|
||||
placement; no relocation, rerouting or RF sign-off is implied by this update.
|
||||
|
||||
## Size and placement
|
||||
|
||||
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
|
||||
@@ -11,8 +36,8 @@ No Gerbers, production drills or manufacturing archive were generated.
|
||||
- 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.
|
||||
the bottom and RS-232 toward the top. A1 moved U1 down by 19 mm; this measurement
|
||||
integration retains that placement and uses the user-accepted approximate header datum.
|
||||
- Module and mating-plug overhangs are not included in the carrier dimensions.
|
||||
The final complete assembly/enclosure envelope still needs measurement.
|
||||
|
||||
@@ -26,24 +51,24 @@ component body corners.
|
||||
|---|---|---|
|
||||
| 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 |
|
||||
| DS1 | (43.970, 36) | OLED module upper-left PCB corner; display faces up |
|
||||
| R1 / R2 / R3 | (42.160 / 53.160 / 64.160, 32.6685) | Axial pull-ups in the gap between RS-232 and OLED |
|
||||
| SW1 / SW2 / SW3 | (41.720 / 53.720 / 65.720, 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 `validation/pcb-a0-before-reposition.kicad_pcb`.
|
||||
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
|
||||
H1–H4 are user-approved **Ø3.2 mm NPTH M3 clearance holes**. The user selected
|
||||
10 mm brass standoffs into heat inserts; see the current acceptance record. 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.
|
||||
must be checked against the final case and assembly heights. Exact insert dimensions remain unspecified. The user intends/reports 10 mm
|
||||
assembled module elevation, not independently measured; see user-acceptance.md.
|
||||
|
||||
## Routing and electrical status
|
||||
|
||||
@@ -51,8 +76,8 @@ 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**.
|
||||
- **938 segments** after incremental group-alignment repair, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
|
||||
- **41 through vias** after repair, **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
|
||||
@@ -77,15 +102,14 @@ 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
|
||||
U1's planar PCB/antenna/USB-mouth offsets are now recorded. Antenna clearance
|
||||
and USB access must still 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
|
||||
U2 uses **`Carrier:MAX3243_Reference_Provisional`** in the schematic and PCB. 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
|
||||
@@ -108,7 +132,7 @@ holes. The source models Ø3.000 mm bores but **no threads**; this does not esta
|
||||
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.
|
||||
|
||||
`add_db9_envelope.py` places the measured metal bounding projections and screw-lock
|
||||
`../../../../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.
|
||||
@@ -120,12 +144,9 @@ 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.
|
||||
The user has now confirmed male pinout/header/footprint equivalence and reviewed
|
||||
connections pin by pin. U2 is assigned in both schematic and PCB; validators
|
||||
require **zero schematic parity warnings**, without exclusions.
|
||||
|
||||
### OLED and controls
|
||||
|
||||
@@ -144,11 +165,13 @@ 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.
|
||||
U1 now has **measured body/antenna outlines**, replacing the header-row guides
|
||||
and `HEADER GUIDES` label. Silkscreen is clipped inside the stepped carrier with
|
||||
0.22 mm additional edge/mask clearance; the off-carrier antenna projection remains
|
||||
visible only on F.Fab. Two **0.30 mm strokes** mark the 8.87 mm USB mouth spans,
|
||||
replacing coincident 0.12 mm body-edge strokes. Wider strokes receive correspondingly
|
||||
wider clearance. No USB depth or RF keepout is inferred. Existing button/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:
|
||||
@@ -157,7 +180,8 @@ module placement or routing changes, close PCB Editor and refresh them with:
|
||||
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
|
||||
```
|
||||
|
||||
The script requires pcbnew and Shapely, replaces only its own generated graphics,
|
||||
The script first verifies the saved U1 measured graphics against the library.
|
||||
It 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.
|
||||
@@ -171,14 +195,57 @@ warning. PDF and both previews have been refreshed.
|
||||
|
||||
## Review outputs
|
||||
|
||||
- `validation/pcb-draft.svg` / `pcb-draft.png`: combined copper/outline/fab review.
|
||||
- `validation/pcb-draft-3d.png`: KiCad render of the carrier and available standard
|
||||
PCB presentation labels have been removed: title suffix `draft`, revision suffix
|
||||
`DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and the title-comment prefix
|
||||
`NOT FOR FABRICATION -` (including the PDF exporter). The generator no longer
|
||||
emits these labels. Specific technical cautions, dimensions, geometry, hidden
|
||||
metadata and footprint identifiers remain; artifact filenames are unchanged.
|
||||
This is a presentation change, **not fabrication approval**. The schematic and
|
||||
its exports are outside this PCB-only cleanup.
|
||||
|
||||
`remove_draft_markers.py` performs the guarded, idempotent cleanup and audits
|
||||
that no other parsed board content or project settings change. The U1 regression
|
||||
now permits exactly these label edits against its unchanged historical baseline,
|
||||
without relaxing geometry/pad/net/routing checks. Fresh PCB DRC, U1/silkscreen
|
||||
regressions and both footprint tests pass; the expected U2 parity warning remains.
|
||||
PDF/SVG/PNG/3D outputs were regenerated and visually reviewed after cleanup.
|
||||
|
||||
- `../../../../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.
|
||||
- `pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at
|
||||
- `../../../../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.
|
||||
bounding projections and measured U1 planar geometry; no guessed USB depth,
|
||||
RF keepout or case-wall geometry.
|
||||
|
||||
Refresh the embedded measurement graphics and PDF without replacing the footprint:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/update_u1_graphics.py
|
||||
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
|
||||
/usr/bin/python3 -B hardware/PCB/export_placement_pdf.py
|
||||
```
|
||||
|
||||
Both board-writing helpers refuse PCB Editor locks and concurrent board changes,
|
||||
use `SaveBoard(..., True)`, and check unchanged project settings. The PDF exporter
|
||||
verifies embedded geometry instead of overlaying the library; only review captions
|
||||
change on its temporary plotting board. Print at 100%, not fit-to-page.
|
||||
|
||||
Preview refresh commands (repository root, KiCad and Inkscape installed):
|
||||
|
||||
```sh
|
||||
kicad-cli pcb export svg --mode-single --layers F.Cu,B.Cu,F.SilkS,F.Fab,Dwgs.User,Edge.Cuts --sketch-pads-on-fab-layers --page-size-mode 2 --output hardware/PCB/validation/pcb-draft.svg hardware/PCB/serial-carrier.kicad_pcb
|
||||
inkscape hardware/PCB/validation/pcb-draft.svg --export-type=png --export-width=1800 --export-filename=hardware/PCB/validation/pcb-draft.png
|
||||
kicad-cli pcb render --width 1600 --height 1200 --side top --quality high --output hardware/PCB/validation/pcb-draft-3d.png hardware/PCB/serial-carrier.kicad_pcb
|
||||
```
|
||||
|
||||
KiCad's SVG trailing whitespace was normalized after export (`git diff --check`
|
||||
passes); the rendered geometry is unchanged. PDF and previews were visually reviewed.
|
||||
|
||||
The board-area SVG crop omits outside-board review captions/parts of the DE-9
|
||||
projection; use the full-page PDF for the complete placement drawing.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
@@ -187,21 +254,43 @@ 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
|
||||
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_u1_update.py
|
||||
/usr/bin/python3 -B hardware/PCB/reference/db9-cad/validate.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
|
||||
`validation/pcb-parity-drc.json` and `pcb-validation-summary.json`.
|
||||
It requires zero parity issues, DRC violations and unconnected items, including
|
||||
the retained user-confirmed U2 assignment. 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**:
|
||||
Measurement-integration validation with KiCad **10.0.6**:
|
||||
|
||||
- All five commands above pass. New `validate_u1_update.py` compares the complete
|
||||
parsed board against `validation/pcb-before-u1-measured.kicad_pcb`, excluding
|
||||
only owned silk and six new U1 graphics. Snapshot and project hashes are pinned;
|
||||
full pad/net/UUID/placement/routing/edge/zone/settings preservation passes.
|
||||
- Measured embedded/library geometry, silk regeneration idempotence, carrier/notch
|
||||
clipping, two exact USB mouth markers and a stale-geometry negative control pass;
|
||||
**91 generated silk items**. U1 refresh byte-idempotence also passed; an already
|
||||
current U1 now returns without writing. Evidence: `hardware/PCB/validation/pcb-u1-update.json`.
|
||||
- The historical A0 `pcb-routing-preservation.py` does **not** pass: it stops on
|
||||
its historical `serial-carrier.kicad_prl` protected hash, already stale for this
|
||||
working state. Its older exact-footprint contract also predates measured U1
|
||||
additions. Historical script/reports were not weakened or rebaselined; the new
|
||||
A1 pre-update comparison is the preservation evidence for this change.
|
||||
- PCB, project and schematic retain their existing content except the authorized
|
||||
board graphics. No firmware/assets, reroute, upload, erase or hardware test.
|
||||
|
||||
Historical measurement-integration results (current alignment results are in
|
||||
[user-acceptance.md](user-acceptance.md)):
|
||||
|
||||
- **0 physical DRC errors/warnings; 0 unconnected items.**
|
||||
- All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**.
|
||||
@@ -222,7 +311,7 @@ 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
|
||||
`generate_pcb_draft.py` recreates the *unrouted* A1 placement and discards manual
|
||||
`../../../../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
|
||||
@@ -230,7 +319,7 @@ populated the minimal project with rules; this revision leaves those settings an
|
||||
the schematic unchanged. Refresh device silkscreen and CAD envelope helpers after
|
||||
any intentional regeneration or placement/routing changes.
|
||||
|
||||
`route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
|
||||
`../../../../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`
|
||||
@@ -1,7 +1,12 @@
|
||||
# MAX3243 carrier — PROVISIONAL female CAD reference / VERIFY MALE
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
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
|
||||
`Carrier.pretty/MAX3243_Reference_Provisional.kicad_mod`.
|
||||
`../../../../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.
|
||||
@@ -16,7 +21,7 @@ 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: `reference/Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd`;
|
||||
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.
|
||||
@@ -25,7 +30,7 @@ CAD-derived geometry adapted from the design by **Limor Fried/Ladyada for Adafru
|
||||
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
|
||||
`reference/README.md` and `reference/license.txt` when redistributing this adaptation.
|
||||
`../../../../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.
|
||||
|
||||
@@ -114,6 +119,6 @@ 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 `validate_footprints.py` was not changed and does not
|
||||
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,129 @@
|
||||
# Carrier group alignment and user acceptance — 2026-09-22
|
||||
|
||||
This is the current acceptance record. It supersedes pending-user-confirmation
|
||||
items in the earlier research and A0/A1 notes; it does not replace their original
|
||||
manufacturer evidence or claim new hardware tests, fabrication approval or certification.
|
||||
|
||||
## Saved placement
|
||||
|
||||
Coordinates below are **carrier-local mm**, X right/Y down. Add (100,100) for
|
||||
KiCad coordinates. All footprint rotations remain zero. Body centres, not
|
||||
header/pad origins, define the common vertical axis **X=56.970 mm**.
|
||||
|
||||
| Item | Old footprint origin | New footprint origin | Movement |
|
||||
|---|---|---|---|
|
||||
| U2 RS-232 | (43,26.670) | unchanged | none |
|
||||
| DS1 OLED | (43,36) | (43.970,36) | +0.970 X |
|
||||
| R1 | (43,34) | (42.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| R2 | (54,34) | (53.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| R3 | (65,34) | (64.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| SW1 | (43,68) | (41.720,68) | −1.280 X |
|
||||
| SW2 | (55,68) | (53.720,68) | −1.280 X |
|
||||
| SW3 | (67,68) | (65.720,68) | −1.280 X |
|
||||
|
||||
U1 remains (8,23); carrier outline, four carrier mounting holes, RF reserve and
|
||||
confirmed USB geometry are unchanged. Resistors remain a horizontal group with
|
||||
11 mm pitch; buttons remain a horizontal group with 12 mm pitch. Neither group
|
||||
was rearranged into a vertical stack.
|
||||
|
||||
The published RS-232 bare PCB spans X=41.095…72.845, Y=0…29.337, including
|
||||
its rounded rear corners beyond the header row. Its centre X=56.970 is retained
|
||||
to minimize module movement and preserve connector/case registration. OLED PCB
|
||||
bounds are X=43.970…69.970, Y=36…62. The OLED's offset display window is not
|
||||
used as the module centre.
|
||||
|
||||
The nominal resistor bodies are 6.3 × 2.5 mm, centred 3.81 mm right of pad 1.
|
||||
Their group spans X=42.820…71.120, Y=31.4185…33.9185. The 6.663 mm gap between
|
||||
module boards is therefore split into **2.0815 mm clearance / 2.5 mm body /
|
||||
2.0815 mm clearance**. Adjacent resistor bodies have 4.7 mm horizontal clearance.
|
||||
These are nominal planar CAD clearances, not new tolerance or height measurements.
|
||||
|
||||
Omron B3F-1000 nominal bodies are 6 × 6 mm with centres 3.25 mm right and
|
||||
2.25 mm below their pad-1 origins. Group body bounds X=41.970…71.970,
|
||||
Y=67.250…73.250; OLED-to-button body gap **5.250 mm**, adjacent button body
|
||||
gaps **6 mm**. Footprint courtyard and lead envelopes are wider than the bodies;
|
||||
the alignment audit uses the actual F.Fab bodies, not a pad-origin average.
|
||||
|
||||
## Closed by explicit user acceptance/report
|
||||
|
||||
- All component dimensions and physical fit match visually; USB placement and
|
||||
clearance are correct. This is user acceptance, not an independently performed
|
||||
new print-fit or hardware test by the agent.
|
||||
- Female headers are described as 8.5 mm and standard male headers as adding
|
||||
2.5 mm. User intends/reports **10 mm assembled elevation**. The arithmetic sum
|
||||
is 11 mm, but mating height cannot be inferred by simply adding those descriptions.
|
||||
**10 mm is not independently measured or precision-verified.** User reports
|
||||
RS-232 mounting prongs clear the carrier at the intended assembly height.
|
||||
- Four Ø3.2 mm M3 carrier holes approved; **10 mm brass standoffs into heat
|
||||
inserts**, M3 daughterboard screws, and **M2 display hardware** selected.
|
||||
This does not establish DE-9 cable-retention thread type, exact screw lengths,
|
||||
insert dimensions or the final printed case wall/cutout.
|
||||
- **Omron B3F-1000 switches purchased.** Existing standard KiCad footprint is
|
||||
retained; documented duplicate-pad mapping is unchanged.
|
||||
- **2 kΩ, 1% metal-film resistors purchased.** R1–R3 values updated in schematic,
|
||||
PCB and BOM; nominal DIN0207 6.3 × 2.5 mm body / 7.62 mm lead pitch retained
|
||||
under the visual-fit confirmation. Exact resistor MPN and purchased wattage
|
||||
were not supplied. Design requirement remains at least 0.125 W. At nominal
|
||||
3.3 V, each pressed external pull-up draws 1.65 mA and dissipates 5.445 mW;
|
||||
these calculations do not quantify regulator margin or GPIO internal pull-ups.
|
||||
- Male Adafruit module has identical reference pinout/header mapping/footprint
|
||||
per user, who reviewed module connections pin by pin. Existing schematic U2
|
||||
assignment `Carrier:MAX3243_Reference_Provisional` is preserved; clean parity
|
||||
is required, not waived. Original female CAD provenance is unchanged.
|
||||
- Nonisolated USB power arrangement explicitly accepted and reported already
|
||||
tested by user. **Successful operation does not establish backfeed safety**, in
|
||||
particular with simultaneous powered hosts. No isolation/backfeed protection
|
||||
was added or claimed.
|
||||
- Existing 3.3 V supply successfully powers the modules per user. This is operating
|
||||
evidence, not measured regulator current/thermal/transient headroom.
|
||||
- User confirms backside OLED I²C pull-ups and operation without extras. **Their
|
||||
resistance and termination rail remain unmeasured.** No values/rail are invented
|
||||
and no external I²C pull-ups added.
|
||||
- Current RF reserve and power/ground routing accepted based on visual and
|
||||
operational experience. This is not manufacturer RF qualification, measured
|
||||
signal/power integrity, regulator-margin proof, EMC testing or certification.
|
||||
|
||||
## Verification and remaining limits
|
||||
|
||||
Incremental routing starts from `hardware/PCB/validation/pcb-before-group-alignment.kicad_pcb`.
|
||||
It removes copper obstructed by moved pads, reconnects retained routes and trims
|
||||
KiCad-reported dangling stubs. **476/587 original copper items remain exactly
|
||||
unchanged**; 111 original items removed. No whole-board regeneration or route
|
||||
reset was used. `pcb-group-migration.json` records the removed UUIDs and repairs;
|
||||
`validate_group_alignment.py` checks the full parsed non-copper delta, retained
|
||||
copper UUID/content, body centrelines/gaps, and unchanged project settings.
|
||||
Original A0 and measured-U1 snapshots/reports are retained, not rebaselined.
|
||||
|
||||
Run from repository root:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/validate_schematic.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_group_alignment.py
|
||||
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_u1_update.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
|
||||
/usr/bin/python3 -B hardware/PCB/reference/db9-cad/validate.py
|
||||
```
|
||||
|
||||
Validation completed with KiCad 10.0.6: schematic ERC **0 errors/warnings**;
|
||||
PCB DRC **0 physical violations, 0 unconnected items, 0 parity issues**. All
|
||||
17 connected nets, 9 electrical components, 44 U1 pins and 15 firmware GPIO
|
||||
contracts checked. Current routing: **938 segments, 41 vias**; unchanged width
|
||||
rules (0.25 mm signal / 0.50 mm power and ground), 0.70/0.30 mm vias. Independent
|
||||
minimum different-net/copper-to-NPTH gap **0.252762 mm**, minimum board-edge gap
|
||||
**1.900 mm**; RF reserve remains clear. Exact group/preservation and U1/silk
|
||||
regressions, two footprint tests and DB9 CAD/source checks passed. KiCad bindings
|
||||
emit known startup enum-property assertions but all checks complete successfully.
|
||||
|
||||
DB9 projections and 94 device-silkscreen items refreshed. Placement PDF (1:1),
|
||||
PCB SVG/PNG/3D and schematic PDF/SVG/PNG regenerated; PNG previews visually
|
||||
reviewed. The 3D view still lacks complete module models and is not assembly
|
||||
qualification. No removed PCB presentation/draft markers were reintroduced.
|
||||
|
||||
Current acceptance closes the user-review gates above. Still not independently
|
||||
qualified: regulator margin, USB backfeed safety, pull-up values/rail, RF/EMC,
|
||||
complete tolerance/height/case/actuator envelopes, finished-hole fabrication
|
||||
process and certification. Missing module courtyards remain a known model limit;
|
||||
DRC does not validate complete assembled-module or case fit. No new hardware
|
||||
tests, firmware changes, Gerbers, fabrication order or manufacturing approval.
|
||||
@@ -1,6 +1,9 @@
|
||||
# A1 top-left-notch / bottom-USB routing — current result
|
||||
|
||||
Date: 2026-09-21. Actual revised board: `../serial-carrier.kicad_pcb`.
|
||||
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.**
|
||||
@@ -27,9 +30,9 @@ Coordinates below are local to absolute origin (100,100) mm; all dimensions mm.
|
||||
both layers; no tracks, vias or fills inside it. Boundary remains provisional.
|
||||
|
||||
The existing A0 board was mutated, **not regenerated**. The one-time
|
||||
`pcb-a0-before-reposition.kicad_pcb` backup remains byte-for-byte intact.
|
||||
`pcb-routing-input.kicad_pcb` is now the A1 unrouted audit input, not A0.
|
||||
`pcb-routing-preservation.py` verified all 13 complete serialized footprints
|
||||
`../../../../../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
|
||||
@@ -56,7 +59,7 @@ 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 `pcb-routing-a1-pass1.kicad_pcb` for evidence.
|
||||
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.
|
||||
@@ -92,12 +95,12 @@ Rerouting remains explicitly destructive to existing/manual copper and requires:
|
||||
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: `pcb-routing-a1-pass2.log`, `pcb-routing-run.json`,
|
||||
`pcb-routing-drc.json`, `pcb-routing-all-drc.json`, `pcb-parity-drc.json`,
|
||||
`pcb-validation-summary.json`, `pcb-routing-check.json`,
|
||||
`pcb-routing-reposition.json`, and `pcb-routing-preservation.json`.
|
||||
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
|
||||
`pcb-routing-check.json`; the CAD projection additions do not change copper.
|
||||
`../../../../../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
|
||||
@@ -118,7 +121,7 @@ Date: 2026-09-20. KiCad 10.0.6; system `/usr/bin/python3`.
|
||||
|
||||
## Saved result
|
||||
|
||||
Actual copper routing is saved in `../serial-carrier.kicad_pcb`, not just a preview
|
||||
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
|
||||
@@ -155,7 +158,7 @@ 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 (`pcb-routing-check.json`):
|
||||
Independent continuous-geometry audit (`../../../../../hardware/PCB/validation/pcb-routing-check.json`):
|
||||
|
||||
| Check | Measured minimum | Requirement |
|
||||
|---|---:|---:|
|
||||
@@ -179,11 +182,11 @@ 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 `pcb-parity-drc.json` and `pcb-validation-summary.json`.
|
||||
See `../../../../../hardware/PCB/validation/pcb-parity-drc.json` and `../../../../../hardware/PCB/validation/pcb-validation-summary.json`.
|
||||
|
||||
## Method and bounded passes
|
||||
|
||||
`../route_pcb_draft.py` uses a two-layer 8-direction raster A* search, an explicit
|
||||
`../../../../../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,
|
||||
@@ -232,18 +235,18 @@ the script exit status.
|
||||
|
||||
## Evidence and remaining limitations
|
||||
|
||||
- `pcb-routing-input.kicad_pcb`: unrouted input snapshot, after baseline DRC save.
|
||||
- `pcb-routing-baseline.json`: 38 unconnected / no violations before routing.
|
||||
- `pcb-routing-pass1-drc.json`, `pcb-routing-pass2-drc.json`: intermediate DRC.
|
||||
- `pcb-routing-pass2.log`, `pcb-routing-pass3.log`: bounded routing progress.
|
||||
- `pcb-routing-run.json`: final saved-route metrics and connection list.
|
||||
- `pcb-routing-drc.json`, `pcb-routing-all-drc.json`: final KiCad DRC evidence.
|
||||
- `pcb-routing-check.py`, `pcb-routing-check.json`: repeatable independent audit.
|
||||
- `pcb-routing-dry-run.log`: successful final no-save reproduction.
|
||||
- `../../../../../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 `pcb-routing-check.json`;
|
||||
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,
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
@@ -58,7 +61,7 @@ Implemented and hardware-validated:
|
||||
- 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).
|
||||
Detailed procedures are in [Electrical tests](../electrical_tests.md) and [Hardware wiring](../wiring.md).
|
||||
|
||||
### Phase 1 — Serial-service foundation
|
||||
|
||||
@@ -166,7 +169,7 @@ Implemented and target-hardware validated:
|
||||
- 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).
|
||||
- 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
|
||||
|
||||
@@ -241,7 +244,7 @@ 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.
|
||||
- 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.
|
||||
@@ -272,8 +275,8 @@ Implemented scope:
|
||||
|
||||
#### 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](../README.md#one-time-migration-from-the-default-partition-table) 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](../README.md#legacy-credential-removal).
|
||||
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.
|
||||
@@ -314,7 +317,7 @@ The device is not intended to become a general-purpose router. Captive-portal in
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -375,7 +378,7 @@ Every phase should satisfy the following before being marked complete, with any
|
||||
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.
|
||||
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
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
@@ -26,7 +29,7 @@ The [Network contract](web_administration.md#network) defines the current byte c
|
||||
|
||||
## 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.
|
||||
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.
|
||||
@@ -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.
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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
|
||||
@@ -1,5 +1,8 @@
|
||||
# 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.
|
||||
@@ -4,7 +4,7 @@ These manual procedures verify the OLED and buttons, MAX3243 breakout, UART1 dat
|
||||
|
||||
> **Safety:** With power removed, install only the wiring required by the selected test. DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are driven outputs. Never connect one of these outputs to another driven output. Keep temporary Dupont wiring short and secure. Power the OLED only from 3.3 V because module-mounted I²C pull-ups may connect SDA and SCL to the OLED `VCC` rail.
|
||||
|
||||
## Phase 7 OLED and button bring-up
|
||||
## OLED and button checks
|
||||
|
||||
Use the exact OLED and button connections in [Hardware wiring](wiring.md). Display diagnostics initially operate I²C at 100 kHz and probe the standard 7-bit `0x3c` and `0x3d` addresses. The connected test module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78` and `0x79`. A missing or unresponsive display is nonfatal: diagnostics should report it without disrupting UART0 or the serial services.
|
||||
|
||||
@@ -31,7 +31,7 @@ If the expected address does not respond, treat the result as a nonfatal hardwar
|
||||
|
||||
### 3. Initialization and display patterns
|
||||
|
||||
Initialize the address observed during the scan. The tested module uses 7-bit `0x3c`, equivalently 8-bit `0x78` (write) and `0x79` (read):
|
||||
Initialize the supported address found by the probe: `0x3c` or `0x3d`. An unexpected scan address is a finding to investigate, not an address accepted by `debug display init`. The tested module uses 7-bit `0x3c`, equivalently 8-bit `0x78` (write) and `0x79` (read):
|
||||
|
||||
```text
|
||||
debug display init 0x3c
|
||||
@@ -51,7 +51,7 @@ Confirm that clear and fill affect the full 128×64 area, checker and grid have
|
||||
|
||||
### 4. Rendered status/content layout
|
||||
|
||||
Run the Phase 7B panel-layout pattern:
|
||||
Run the panel-layout pattern:
|
||||
|
||||
```text
|
||||
debug display pattern layout
|
||||
@@ -75,7 +75,7 @@ debug display pattern clear
|
||||
debug display row 16
|
||||
```
|
||||
|
||||
**Verified result:** row 15 is the last yellow addressable row and row 16 is the first blue addressable row. The two colored areas are separated by a narrow physical black divider, so later UI rendering must treat the 128×16 yellow and 128×48 blue regions as separate panels rather than one visually continuous canvas. Also test another endpoint row if needed with `debug display row <0..63>` to confirm row addressing and orientation.
|
||||
On the tested two-color module, row 15 is the last yellow addressable row and row 16 is the first blue addressable row. The two colored areas are separated by a narrow physical black divider. Confirm that your module matches this layout: the UI uses separate 128×16 yellow and 128×48 blue panels. Also test another endpoint row if needed with `debug display row <0..63>` to confirm row addressing and orientation.
|
||||
|
||||
### 6. Contrast, inversion, and display-off checks
|
||||
|
||||
@@ -133,7 +133,7 @@ Confirm that each page remains entirely within the blue content panel. The yello
|
||||
|
||||
The UI refreshes at 4 Hz maximum. Status pages are observational and must not expose passwords, credentials, tickets, or key material. A short Select press opens the separate Controls menu described below; merely navigating status pages must not change a service, alter writer ownership, or inject serial data. With UART0, USB CDC, WebSocket, and SSH active, navigate pages and confirm that the display continues to update without disrupting serial traffic or UART0 recovery.
|
||||
|
||||
For the OLED-aging policy, leave all three buttons untouched and verify:
|
||||
With the default dim/off settings (check `display status`), leave all three buttons untouched and verify:
|
||||
|
||||
1. At five minutes, display contrast drops to `1` while the contents remain present.
|
||||
2. At ten minutes, the OLED switches off.
|
||||
@@ -142,7 +142,7 @@ For the OLED-aging policy, leave all three buttons untouched and verify:
|
||||
|
||||
`debug display status` may verify an individual transition in a separate timing run. Every display diagnostic counts as activity and holds normal UI rendering for 30 seconds, so do not invoke it between the five- and ten-minute observations of one continuous run. Record any missing, stale, clipped, or implausible status value or incorrect dim/off/wake transition.
|
||||
|
||||
### 9. Local controls (Phase 7D)
|
||||
### 9. Local controls
|
||||
|
||||
From a status page, use a short Select press to open **Controls**. Previous/next selects an item; Select activates it. The menu includes serial start/stop, Wi-Fi start/stop/reconnect/next-profile, HTTPS start/stop, SSH start/stop, writer revocation, display off, and reboot. `WiFi:next profile` requests the enabled station profile after the active one in priority order and wraps safely; it must never show or change credentials, Wi-Fi profile configuration, serial framing, TLS/SSH keys, or I²C scan controls.
|
||||
|
||||
@@ -159,11 +159,13 @@ For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then u
|
||||
|
||||
Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data.
|
||||
|
||||
### 10. Phase 7E boot animation, persistence, and fault recovery
|
||||
### 10. Boot animation, persistence, and fault recovery
|
||||
|
||||
On each normal boot with an initialized OLED, first verify the bounded five-second identity animation: the full `ESP32 SERIAL SWISS ARMY KNIFE` name scrolls across the yellow panel; the blue panel shows the compact right-oriented monochrome logo with an upright `>_` prompt and serial/USB cable ends entering from the left. Wi-Fi activity and the cursor blink at the 4 Hz animation cadence. A missing OLED must skip the animation without preventing normal UART0 recovery.
|
||||
|
||||
Then shorten the delays for a bounded aging-policy test:
|
||||
Record your current `display status` settings before this test: it changes saved dim/off delays and finishes by saving defaults. Restore and save your preferred values afterward if needed.
|
||||
|
||||
Shorten the delays for a bounded dim/off test:
|
||||
|
||||
```text
|
||||
display status
|
||||
@@ -175,7 +177,7 @@ reboot
|
||||
|
||||
After reboot, `display status` must report the stored 5/10-second values. Verify dim at five seconds, off at ten seconds, and one consumed wake press. Run `display load`, then test `display defaults` without saving and confirm 300/600 seconds are restored only in RAM. Run `display reset`, reboot, and confirm the defaults persisted. Verify invalid combinations such as dim `10` with off `5` are rejected without changing the working values. Set either timeout to `0`, save/reboot, and confirm that transition is disabled; finish with `display reset`.
|
||||
|
||||
Exercise the following fault matrix while keeping UART0 available:
|
||||
Exercise the following fault checks while keeping UART0 available. Button-triggered display recovery requires a running local UI task and an initialized I²C bus. If `debug display status` reports `bus=unavailable`, correct the wiring with power off and reboot; a button press cannot recreate the bus.
|
||||
|
||||
| Fault/stress | Procedure | Required result |
|
||||
|---|---|---|
|
||||
@@ -186,7 +188,11 @@ Exercise the following fault matrix while keeping UART0 available:
|
||||
| Real chord | Press two healthy buttons together before either is quarantined. | The chord is ignored and cannot confirm an action. |
|
||||
| Repeated actions | Re-enter Controls and repeat start/stop, reconnect, next-profile, writer-revoke, and display-off requests during transitions. | Each confirmation hold executes once; stale state produces an error rather than the opposite action; queue saturation, if reached, reports an error and leaves the Wi-Fi alert/counter visible. |
|
||||
|
||||
For the final concurrency regression, connect USB CDC, one WebSocket terminal, and one SSH terminal while UART1 carries sustained bidirectional traffic. Navigate the UI, run the persistence checks, exercise service stop/start and writer revocation, and inject the display/stuck-button faults above. Confirm UART0 remains responsive, the UI never appears as a broker client or injects serial bytes, writer ownership is always the expected client or none, and serial/broker drop counters do not increase unexpectedly. Record `memory`, transport counters, `wifi counters`, and `debug display status` before and after the run.
|
||||
For a concurrent-transport check, connect USB CDC, one WebSocket terminal, and one SSH terminal while UART1 carries sustained bidirectional traffic. Navigate the UI, run the persistence checks, exercise service stop/start and writer revocation, and inject the display/stuck-button faults above. Confirm UART0 remains responsive, the UI never appears as a broker client or injects serial bytes, writer ownership is always the expected client or none, and serial/broker drop counters do not increase unexpectedly. Record `memory`, transport counters, `wifi counters`, and `debug display status` before and after the run.
|
||||
|
||||
## Before RS-232 diagnostics
|
||||
|
||||
Disconnect external DE-9 equipment and use UART0 for these checks. After each power-up, run `serial stop` before any RS-232 `debug` command, including `debug status`. The diagnostics refuse access while the serial service owns the port. If cleanup reports a port fault or a UART driver that could not be removed, reboot before continuing.
|
||||
|
||||
## Configuration A: data and handshake pairs
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# Firmware update
|
||||
|
||||
The administrator can upload a locally built **application binary** through HTTPS. There is no release server, automatic download, or automatic update service.
|
||||
|
||||
Related guides: [Web administration](web_administration.md), [account provisioning](user_administration.md), [command reference](command_reference.md), and [security operations](security_operations.md). Implementation details are in the [web development contract](devel/web_administration.md#application-firmware-upload); historical acceptance and regression guidance remain in [roadmap Phase 10](devel/roadmap.md#phase-10--simple-admin-web-firmware-upload).
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Keep **USB-to-UART** available for UART0 administration and wired flashing. Native USB CDC is UART1 serial access, not the administration/flashing recovery path described here.
|
||||
- Install OTA-enabled firmware **by wire first**. Firmware without the upload route cannot install its own first web updater. The custom OTA partition layout is required. [One-time migration from the old default layout](getting_started.md#partition-migration-and-erasing) is a separate, destructive procedure—not an ordinary update step.
|
||||
- Select trusted firmware for **`esp32-s3-devkitc-1-n16r8`**, compatible with this board, partition layout, and existing stored configuration schemas. Check the [legacy downgrade warning](devel/roadmap.md#phase8-legacy-credential-compatibility): older v1-only firmware cannot read the newer TLS-only HTTPS material. An older build is not automatically a safe recovery choice.
|
||||
- Save desired RAM-only serial, Wi-Fi, hostname, and display settings. Record nonsecret configuration and verified public HTTPS/SSH fingerprints. Keep passwords separately in a password manager; there is no supported NVS backup/restore facility.
|
||||
- Arrange a maintenance window, stable power, and stable network access. Stop work that depends on uninterrupted serial traffic. Use trusted HTTPS and an `admin` account, verifying device identity as described in [security operations](security_operations.md).
|
||||
|
||||
## Build and upload
|
||||
|
||||
1. From the trusted project checkout, build:
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
2. Select exactly the application output:
|
||||
|
||||
```text
|
||||
.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin
|
||||
```
|
||||
|
||||
Do not select an ELF, ZIP, merged full-flash binary, bootloader, or partition-table image. Renaming another file to `.bin` does not make it suitable. The browser requires a nonempty `.bin` no larger than **4 MiB**; the device also checks the actual destination capacity and image structure/integrity.
|
||||
3. Sign in as `admin` and open **Settings → HTTPS / Reboot → Firmware update**.
|
||||
4. Choose the file in **Application .bin**, select **Upload and reboot…**, and confirm. Keep power connected.
|
||||
5. Wait for validation and reboot—not just **100%** transmitted bytes. The UI may first report **Upload complete. Validating firmware…**, then **Firmware accepted; rebooting. Reconnect and sign in shortly.**
|
||||
6. Restore network reachability, use **Reconnect after reboot** or reload, and explicitly sign in again. Verify the running application, saved configuration, account access, and HTTPS/SSH identities before another upload. Check UART0 if the result is unclear.
|
||||
|
||||
Upload handling is synchronous and can stall or drop browser serial/admin connections and other web work. UART0 and native USB are independent recovery/data paths, not guarantees of uninterrupted timing during flash operations. Reboot interrupts **all** transports.
|
||||
|
||||
## What is preserved—and what is not guaranteed
|
||||
|
||||
The updater writes only the inactive application slot and OTA selection metadata. It does not rewrite the partition table/bootloader or erase NVS, PHY, storage, or other data partitions. Saved users, credentials, settings, and device identities remain stored. This preserves stored bytes, not compatibility with arbitrary firmware: the uploaded application must understand existing schemas and must not itself erase or incompatibly migrate them.
|
||||
|
||||
The server checks target/header, completeness, exact image length, and SDK image integrity, including the appended SHA-256 digest, before selecting the image. **A digest is not publisher authentication.** Neither validation nor a `.bin` filename proves that the firmware is trusted, fully board-compatible, or able to boot and operate correctly.
|
||||
|
||||
There is **no image-signature/signing-key policy, secure-boot guarantee, anti-rollback/version-downgrade enforcement, automatic rollback, or post-boot health-confirmation mechanism** in this update workflow. There is no partition migration or NVS backup/restore. A valid but broken application can require wired recovery.
|
||||
|
||||
## Lost response, timeout, or failed upload
|
||||
|
||||
**Do not blindly retry or power-cycle an uncertain update.** A lost response, page close, sign-out, timeout, or browser abort proves neither cancellation nor failure. The image may already be selected, and any later reset can boot it.
|
||||
|
||||
- Before boot selection, rejected/incomplete images and receive/write/validation failures abort the upload without selecting the candidate. The inactive slot may already be erased or partially written.
|
||||
- `firmware_commit_failed` means boot-metadata selection failed with SDK transactional uncertainty. Inspect the device before any reset or retry; do not infer that selection definitely stayed unchanged.
|
||||
- A successful response means validation and boot selection succeeded. Successful server send schedules reboot after about 500 ms, but does not prove browser receipt or successful boot.
|
||||
- If sending the response fails **after selection**, the device schedules no automatic reboot and rejects further admissible uploads with **409 `firmware_selected_reboot_required`** until device reset. HTTPS stop/start does not clear this latch. Inspect via UART0 and deliberately reboot when appropriate.
|
||||
- Reloading clears browser-side uncertainty locks only; it does not undo committed work or clear the device's selected-image latch.
|
||||
|
||||
The device's ten-second receive-stall and 120-second receive-loop checks are not hard flash-operation deadlines. Synchronous erase/write/validation can extend elapsed time. The browser's 180-second timeout cannot cancel committed work. Software reboot/lifecycle exclusions cannot prevent physical reset, power loss, or panic.
|
||||
|
||||
### Interpreting errors
|
||||
|
||||
| Response | Meaning / next step |
|
||||
|---|---|
|
||||
| 400 | Malformed, incompatible, or incomplete image/request. Check the build artifact before a deliberate new attempt. |
|
||||
| 401 / 403 | Authentication, Origin/CSRF, or administrator access failure. Restore a valid admin login; if upload had begun, inspect state first. |
|
||||
| 408 `firmware_timeout` | Device receive deadline exceeded. Inspect state and network conditions. |
|
||||
| 409 `firmware_selected_reboot_required` | Image already selected; inspect and deliberately reboot, rather than upload again. |
|
||||
| 413 / 415 | Image exceeds destination capacity or wrong content type. Use the supported application binary/browser workflow. |
|
||||
| 500 `firmware_write_failed` / `firmware_commit_failed` | Flash/selection failure. Use UART0 to inspect; commit failure carries selection uncertainty. |
|
||||
| 503 | Busy, unavailable partition/service, or insufficient resources. Resolve the condition before a deliberate retry. |
|
||||
|
||||
A disconnected browser may never receive the error. Competing web requests may wait during upload rather than promptly report busy.
|
||||
|
||||
## Wired recovery
|
||||
|
||||
If the new application does not boot or no longer serves HTTPS:
|
||||
|
||||
1. Connect the USB-to-UART path and select a known-good **schema- and layout-compatible** project checkout.
|
||||
2. Run the documented wired workflow:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
3. Inspect startup, restore connectivity, verify saved settings and identities, and test normal operation before returning to web updates.
|
||||
|
||||
**Do not erase for ordinary updates or recovery.** Erasing destroys persisted configuration and credentials. There is no automatic recovery flashing or rollback. If the partition layout itself needs migration, use the separate migration procedure knowingly rather than treating erase as generic troubleshooting.
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
# Getting started
|
||||
|
||||
This guide takes a supported, wired board from its first firmware installation to USB or network serial access. If firmware is already installed, start at [Open the recovery console](#3-open-the-recovery-console). Review [security operations](security_operations.md) before enabling network access.
|
||||
|
||||
## 1. Check the hardware
|
||||
|
||||
Use the ESP32-S3-DevKitC-1-compatible **N16R8** target (16 MB flash, 8 MB octal PSRAM) and the Adafruit MAX3243 breakout described in [Hardware wiring](wiring.md). Follow that GPIO map, not a generic ESP32 pinout.
|
||||
|
||||
- Wire with power disconnected. Power the breakout and optional OLED from **3.3 V**, not 5 V.
|
||||
- Never connect RS-232 voltage directly to ESP32 GPIOs. The MAX3243 is a level shifter, not a galvanic isolator.
|
||||
- Select a straight-through cable for a DCE peer or a suitable null-modem cable for another DTE peer. A gender changer alone does not cross signals.
|
||||
- Use [Electrical tests](electrical_tests.md) to verify a new assembly before attaching valuable equipment. Active RS-232 diagnostics require the serial service to be stopped.
|
||||
|
||||
There are two distinct USB paths:
|
||||
|
||||
| Board connector | Typical Linux device | Use |
|
||||
|---|---|---|
|
||||
| USB-to-UART bridge | `/dev/ttyUSB*` | Firmware installation, UART0 logs, and the `serial-tool>` administration/recovery console |
|
||||
| Native USB | `/dev/ttyACM*` | UART1 serial data shared with browser and SSH clients |
|
||||
|
||||
Names and connector labels vary between boards. Identify the actual port rather than assuming the example device name. Consult the board schematic before powering through both connectors.
|
||||
|
||||
## 2. Install firmware by wire
|
||||
|
||||
Install [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html), obtain a trusted project checkout, and run these commands from its root. PlatformIO downloads the configured toolchain and dependencies on the first build, so initial setup needs network access.
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
Connect the **USB-to-UART** port, then upload:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
If automatic port selection is ambiguous, specify your actual bridge port; for example:
|
||||
|
||||
```sh
|
||||
pio run --target upload --upload-port /dev/ttyUSB0
|
||||
```
|
||||
|
||||
Do not erase for ordinary installation over a compatible layout or for routine updates. If migrating from the former default/factory layout or unrelated firmware, read [Partition migration and erasing](#partition-migration-and-erasing) first. Subsequent application-only updates can use the [browser firmware updater](firmware_update.md).
|
||||
|
||||
## 3. Open the recovery console
|
||||
|
||||
Use the bridge port at **115200 baud**:
|
||||
|
||||
```sh
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
Alternatively, on Linux with picocom installed and the actual bridge at `/dev/ttyUSB0`:
|
||||
|
||||
```sh
|
||||
picocom /dev/ttyUSB0 -b 115200
|
||||
```
|
||||
|
||||
After an unattended boot, press Enter once in an ANSI-capable terminal to enable line editing, history, cursor movement, and hierarchical Tab completion. At `serial-tool>`, run:
|
||||
|
||||
```text
|
||||
help
|
||||
serial status
|
||||
wifi status
|
||||
```
|
||||
|
||||
This console controls the ESP32, not the equipment attached to RS-232. Keep it available while changing network settings or credentials. Physical UART0 access is not protected by network account passwords.
|
||||
|
||||
## 4. Set the equipment's serial parameters
|
||||
|
||||
Match UART1 to the attached equipment. For example, for **115200 baud, 8N1, no hardware flow control**:
|
||||
|
||||
```text
|
||||
serial set baud 115200
|
||||
serial set data-bits 8
|
||||
serial set parity none
|
||||
serial set stop-bits 1
|
||||
serial set flow none
|
||||
serial status
|
||||
serial save
|
||||
```
|
||||
|
||||
Use `serial set flow rts-cts` only when the peer and cable support it. DTR is configured separately with `serial set dtr inactive`, `active`, or `on-connect`; choose the behavior your equipment requires. See the [serial command reference](command_reference.md#serial-service) for supported values.
|
||||
|
||||
Working serial edits are RAM-only until `serial save`. Reconfiguration or stopping the service discards pending serial-service data, so do it while traffic is idle. USB terminal baud/framing choices **do not configure physical UART1**.
|
||||
|
||||
## 5. Use native USB without networking
|
||||
|
||||
Connect the native USB port and open its CDC device with a serial terminal that asserts DTR. This creates the `usb-cdc` client, starts UART1 if needed, and requests writer ownership. If another client already owns it, USB joins as an observer.
|
||||
|
||||
On UART0, inspect:
|
||||
|
||||
```text
|
||||
usb status
|
||||
broker clients
|
||||
```
|
||||
|
||||
Exactly **one client can write** to UART1; others receive the incoming serial stream. Coordinate with the current writer before requesting control with `usb request-writer`; use `usb release-writer` to give it up. A slow observer may lose its own buffered output without blocking other clients.
|
||||
|
||||
Native USB remains usable when network services fail. It is not the account-recovery console and does not provide uninterrupted operation through reboot.
|
||||
|
||||
## 6. Provision network access
|
||||
|
||||
On trusted UART0, create an administrator and, if desired, a separate SSH serial account:
|
||||
|
||||
```text
|
||||
user add maint admin
|
||||
user add operator user
|
||||
```
|
||||
|
||||
Enter unique passwords at the hidden prompts. There is **no default shared login**. Account changes persist immediately. See [User administration](user_administration.md) for password rules, SSH keys, and recovery.
|
||||
|
||||
Inspect `wifi profiles` first to avoid replacing an existing profile. For a new device, this example configures slot 0; replace the example SSID with your trusted network's name:
|
||||
|
||||
```text
|
||||
wifi profile set 0 0 mixed "Your network name"
|
||||
wifi profile secret 0
|
||||
wifi profile enable 0
|
||||
wifi start
|
||||
wifi status
|
||||
```
|
||||
|
||||
Enter the Wi-Fi password at the secret prompt, not in the command line. Connection changes are asynchronous: check `wifi status` again for the resulting connection and addresses. Once correct, run:
|
||||
|
||||
```text
|
||||
wifi save
|
||||
mdns status
|
||||
web certificate info
|
||||
ssh host-key info
|
||||
```
|
||||
|
||||
Record the public identity fingerprints through this trusted local connection. Wi-Fi and mDNS hostname settings have separate Save commands. For device-AP policy and password setup, see the [Wi-Fi reference](command_reference.md#wi-fi); revealing an AP secret with `wifi ap show-secret` should only be done in a private, unrecorded console.
|
||||
|
||||
### Browser
|
||||
|
||||
Open `https://` followed by the hostname reported by `mdns status`, or a reachable IP address. Compare the browser certificate fingerprint with the local `web certificate info` output **before entering credentials**. Sign in and follow the [browser guide](web_administration.md) to connect Serial and manage control. Connecting or reconnecting automatically requests the writer lease; check **Role** before typing.
|
||||
|
||||
### SSH
|
||||
|
||||
Compare the client's host-key fingerprint with `ssh host-key info` before trusting it. Use an interactive SSH connection to the device address with the desired account:
|
||||
|
||||
- `user` accounts open the UART1 stream.
|
||||
- `admin` accounts open device administration, not UART1.
|
||||
|
||||
For example, replace `DEVICE_ADDRESS` with the reported hostname or IP:
|
||||
|
||||
```sh
|
||||
ssh operator@DEVICE_ADDRESS
|
||||
```
|
||||
|
||||
A serial SSH connection starts UART1 if needed and automatically requests writer ownership. If USB or another client already owns it, SSH remains an observer and typed input is discarded, not queued for later. Check ownership before typing. Releasing the other writer does not automatically promote this session; deliberately assign it control from administration, or reconnect after the lease is free.
|
||||
|
||||
SSH supports interactive shells, not exec commands, file transfer, or forwarding. Use the [SSH reference](command_reference.md#ssh-serial-transport) for connection details. Inspect the writer with `broker clients` on an administration connection; coordinate before deliberately transferring control through [Broker settings](web_administration.md#serial-observer-and-writer) or `broker force-writer <client-id>`.
|
||||
|
||||
## If something does not work
|
||||
|
||||
| Symptom | First checks |
|
||||
|---|---|
|
||||
| No `serial-tool>` prompt | Check that you opened the bridge, not native USB; use 115200 baud and press Enter. |
|
||||
| USB opens but cannot transmit | Check host DTR, `usb status`, and the current writer in `broker clients`. |
|
||||
| Garbled or missing serial data | Check UART1 settings, cable crossing, ground, and RTS/CTS support. Use the safe loopback procedures before changing wiring. |
|
||||
| Browser or SSH is unreachable | Inspect `wifi status`, `web status`, and `ssh status` on UART0; test a reachable numeric address to separate discovery from connectivity. |
|
||||
| Hostname works on one computer only | Check that the other client supports local mDNS resolution for the required IPv4/IPv6 family. See [network discovery](command_reference.md#mdns). |
|
||||
| Forgotten account password | Use normal `user password` on UART0. Do not erase or invoke unavailable-database recovery for a healthy database. |
|
||||
| Lost response after a settings change | Reconnect and inspect actual state before retrying; disconnection does not cancel accepted work. |
|
||||
|
||||
## Partition migration and erasing
|
||||
|
||||
**Erasing the whole flash destroys all persisted configuration, accounts, Wi-Fi credentials, and device identities.** Record what you need to reprovision first. Do not use it as routine troubleshooting or an ordinary update step.
|
||||
|
||||
The first migration from the former default/factory partition layout to this project's custom OTA layout requires an erase. Migration from unrelated firmware or another layout may also require it. Only when deliberately performing such a migration or a complete destructive reset:
|
||||
|
||||
```sh
|
||||
pio run --target erase
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Then provision the device again and verify its new HTTPS/SSH fingerprints locally. See [flash layout](devel/flash_layout.md) for the partition map and [firmware recovery](firmware_update.md#wired-recovery) for recovery that does not routinely erase NVS.
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
This checklist is for a wireless serial cable used occasionally on a trusted network,
|
||||
not an Internet-facing appliance or a production-security certification.
|
||||
It covers existing firmware operations only: no dependency patches, crypto upgrades,
|
||||
secure-boot, encrypted-NVS, or eFuse changes are part of this scope.
|
||||
It covers provisioning, daily use, credential changes, and recovery with the existing
|
||||
firmware. It does not provide protection against physical extraction or firmware replacement.
|
||||
|
||||
## Provision once, locally
|
||||
|
||||
@@ -87,14 +87,15 @@ inspect current state on UART0 rather than blindly repeating the operation.
|
||||
Treat a lost device or a raw flash copy as potentially exposing stored Wi-Fi secrets,
|
||||
private identities, and password verifiers. Rotation is not a secure flash-erasure claim.
|
||||
|
||||
## Lightweight maintenance
|
||||
## Maintenance precautions
|
||||
|
||||
Occasionally, and before a planned firmware update, review relevant ESP-IDF,
|
||||
wolfSSL/wolfSSH, and bundled web-library upstream release/security notes.
|
||||
If a relevant issue appears, stop network use until a separate upgrade decision is made;
|
||||
USB serial remains an option. Dependency upgrades and their validation are separate work,
|
||||
not an endless CVE audit or an implied promise that this firmware is vulnerability-free.
|
||||
Before installing firmware, review its documented changes and security notices.
|
||||
If a relevant security issue is reported, stop network use until an appropriate
|
||||
update or mitigation is available; native USB serial remains an option.
|
||||
Keep wired UART0 recovery available when updating, and do not assume an update
|
||||
preserves configuration unless its migration guidance says so.
|
||||
|
||||
Commands and limits were checked against application sources and the [command reference](command_reference.md) at `f40c09c`.
|
||||
This is source-reviewed operating guidance, not a build, hardware test, security audit,
|
||||
or verification of any individual device's flash/eFuse configuration.
|
||||
See the [command reference](command_reference.md) for command details.
|
||||
This checklist is operating guidance, not a security audit or verification of any
|
||||
individual device's flash/eFuse configuration. Contributor maintenance and evidence
|
||||
notes are in the [developer index](devel/README.md#security-maintenance-and-evidence).
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# User administration
|
||||
|
||||
Provision accounts locally first, then use the browser or authenticated admin SSH for routine administration.
|
||||
|
||||
Related guides: [Web administration](web_administration.md), [firmware updates](firmware_update.md), [command reference](command_reference.md#role-based-users), [security operations](security_operations.md), and the [web development contract](devel/web_administration.md).
|
||||
|
||||
## Choose roles and keep local recovery
|
||||
|
||||
| Account role | HTTPS | SSH |
|
||||
|---|---|---|
|
||||
| `user` | Serial terminal and status | UART1 serial stream through the broker |
|
||||
| `admin` | Serial, Admin, and Settings | Device administration shell, not UART1 |
|
||||
|
||||
Use a separate `user` account for ordinary SSH serial work. Account roles do not override the broker's one-writer/multiple-observer rule. Admin SSH is not a serial broker client.
|
||||
|
||||
Keep physical **UART0 administration** available on a trusted computer. Native USB CDC is a different, network-independent UART1 data path, not an account-recovery console. Physical local access is not protected by network account authentication.
|
||||
|
||||
## Create the first administrator
|
||||
|
||||
A new device with missing user-database storage starts with a persisted empty database. There is no default shared login, credential import, or `user bootstrap` command. Existing valid user databases retain their accounts unchanged; an older role-`user` account is not silently promoted.
|
||||
|
||||
On UART0:
|
||||
|
||||
```text
|
||||
user status
|
||||
user list
|
||||
user add maint admin
|
||||
user show maint
|
||||
```
|
||||
|
||||
Enter and confirm a unique password at the no-echo prompts. Passwords must contain **12–64 printable ASCII characters**. `Ctrl-C` cancels a prompt. Usernames must match `[a-z][a-z0-9_-]{0,15}`: 1–16 characters, starting with a lowercase letter, followed by lowercase letters, digits, underscore, or hyphen.
|
||||
|
||||
To generate a password instead:
|
||||
|
||||
```text
|
||||
user add maint admin --generate
|
||||
```
|
||||
|
||||
Use one creation form, not both for the same name. Generated passwords are displayed once: save the value in a password manager, avoid terminal recording, and do not share the output. Successful account changes persist immediately; no separate Save command is needed.
|
||||
|
||||
Before testing network login, compare `web certificate info` and `ssh host-key info` from trusted UART0 with the browser/SSH client's fingerprints. Follow [security operations](security_operations.md), then [sign in through HTTPS](web_administration.md#connect-and-sign-in). For SSH, connect using an interactive shell with the chosen account; exec/subsystem and file-transfer workflows are not supported.
|
||||
|
||||
## Add and maintain accounts
|
||||
|
||||
Examples for UART0 or an authenticated admin SSH shell:
|
||||
|
||||
```text
|
||||
user add operator user
|
||||
user list
|
||||
user show operator
|
||||
user password operator
|
||||
```
|
||||
|
||||
The database holds at most **eight accounts**. Account summaries expose roles and public-key fingerprints, not passwords, salts, or verifiers. Passwords cannot be recovered from status output; set a replacement when necessary.
|
||||
|
||||
For an intentional role change or removal:
|
||||
|
||||
```text
|
||||
user role operator admin --force
|
||||
user role operator user --force
|
||||
user delete operator --force
|
||||
```
|
||||
|
||||
These are separate examples, not a sequence to run routinely. Check the target and intended access before using `--force`. The final administrator cannot be deleted or demoted. Password, role, key, and deletion changes invalidate the affected username's outstanding tickets and active web/SSH sessions; unrelated accounts are not intentionally revoked. Your own change may disconnect you before you see its result. Inspect state and test the intended login before repeating an uncertain operation.
|
||||
|
||||
`user password <username> --generate` displays a replacement once. Admin SSH rejects generation for its own account to avoid losing the value during revocation; use trusted UART0 or the supported browser workflow instead.
|
||||
|
||||
### Browser account management
|
||||
|
||||
Sign in as `admin`, open **Settings → Accounts**, and select the intended account for password, role, deletion, or authorized-key operations. Changes commit on successful submission; they are not RAM-only drafts awaiting a global Save.
|
||||
|
||||
The generated-password workflow first displays a new value without changing the account. Store it securely and acknowledge that it is saved, then explicitly submit the create/password change. Generation alone does not set a password; the saved acknowledgement is not proof that the subsequent operation succeeded. Self changes may end your login immediately.
|
||||
|
||||
The browser **Admin shell** has narrower account permissions than Accounts settings: status/list/show and selected interactive changes to other accounts are supported, but self changes, generated passwords, key commands, and database recovery are denied there. Use typed Accounts controls or UART0 rather than trying to bypass these restrictions.
|
||||
|
||||
## Authorize an SSH client key
|
||||
|
||||
1. On a trusted client, generate or choose an OpenSSH **Ed25519** or **ECDSA P-256** key. Keep the private key on that client; import only the single-line `.pub` public key.
|
||||
2. On UART0 or authenticated admin SSH, run:
|
||||
|
||||
```text
|
||||
user key add operator
|
||||
```
|
||||
|
||||
3. Paste the public-key line at the prompt. `Ctrl-C` cancels. Alternatively use the Accounts key-import controls in the browser.
|
||||
4. Run `user show operator` and compare the reported key type and `SHA256:` fingerprint with the client's public key. Test a new SSH login with the intended username before closing local recovery access.
|
||||
|
||||
Each account supports **three keys**. Supported types are `ssh-ed25519` and `ecdsa-sha2-nistp256`; RSA and OpenSSH certificate keys are not supported. Duplicate keys within an account are rejected. The same key can be assigned to multiple accounts, but access then follows the username selected at login.
|
||||
|
||||
Delete by the stable index shown in `user show`, not by its position in a displayed list:
|
||||
|
||||
```text
|
||||
user key delete operator 0 --force
|
||||
```
|
||||
|
||||
Use `user key clear operator --force` only when intentionally removing all that account's keys. Key changes persist immediately and revoke that account's sessions. Changing a password does **not** remove authorized keys; review both after compromise. Authorized client keys are separate from the device's SSH host identity.
|
||||
|
||||
## Forgotten passwords and database failure
|
||||
|
||||
For a healthy database, use UART0 and normal commands such as `user password maint` to restore access. First inspect `user status` and `user list`; do not erase flash or reset identities to repair a forgotten account password.
|
||||
|
||||
Only if database initialization has failed and the database is **unavailable**, deliberate UART0 recovery is:
|
||||
|
||||
```text
|
||||
user recover --force
|
||||
user status
|
||||
user add maint admin
|
||||
```
|
||||
|
||||
**Recovery destroys the user database's contents and rebuilds it empty.** It refuses a healthy database, including a healthy empty database, and is unavailable over SSH/browser. It does not erase unrelated Wi-Fi/serial configuration or HTTPS/SSH identities. Recreate required accounts and authorized keys afterward. Check each command result; storage failures are not evidence of successful recovery.
|
||||
|
||||
`web reset --force` and `ssh reset --force` recover their respective device identities, not user accounts. Neither is a whole-device factory reset.
|
||||
|
||||
## Persistence and security limits
|
||||
|
||||
Keep a private reprovisioning record of usernames/roles, public keys, settings, and verified device fingerprints; store passwords separately. There is no supported configuration/credential/identity backup-export command. Application-only [firmware updates](firmware_update.md) preserve stored bytes, but compatibility with the new firmware still matters.
|
||||
|
||||
NVS credentials are not protected against physical extraction by this workflow. Password verifiers can be attacked offline, and logical replacement does not securely erase historical flash contents. Keep the device and local ports controlled, avoid Internet exposure, and follow the [security checklist](security_operations.md).
|
||||
@@ -1,138 +1,78 @@
|
||||
# Web administration contracts
|
||||
# Web administration
|
||||
|
||||
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).
|
||||
Use the browser to operate the UART1 serial terminal and, with an administrator account, manage the device. Use a trusted LAN or the device's password-protected AP—not an Internet-exposed connection.
|
||||
|
||||
## Authentication and admission
|
||||
Related guides: [Account provisioning](user_administration.md), [firmware updates](firmware_update.md), [command reference](command_reference.md), and [security operations](security_operations.md). Protocol and implementation details are in the preserved [development contract](devel/web_administration.md).
|
||||
|
||||
- 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.
|
||||
## Connect and sign in
|
||||
|
||||
`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.
|
||||
1. Provision an account through physical **UART0** first; see [user administration](user_administration.md). Native USB CDC carries UART1 data and is not the administration console.
|
||||
2. Connect to the device's network and open its HTTPS address (port 443), using its configured hostname or reachable IP address. The terminal assets are served by the device; Internet access is not required.
|
||||
3. Before entering credentials, compare the browser's SHA-256 certificate fingerprint with `web certificate info` on trusted UART0. Do not blindly accept an unexpected certificate warning.
|
||||
4. Sign in with your account username and password. Both `user` and `admin` can use Serial; only `admin` can use Admin and Settings. SSH client keys do not replace the browser password.
|
||||
|
||||
## Terminal and console ownership
|
||||
A login expires **one hour after sign-in**, even while active. There are four login-session slots, two browser serial connections, and one browser admin connection; these are separate limits. Login attempts are globally limited to five per 60 seconds. If admission fails, close unused connections or sign out unused sessions and wait rather than repeatedly submitting credentials.
|
||||
|
||||
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.
|
||||
**Disconnect** closes the serial connection and pauses its automatic reconnect; **Connect** resumes it. Neither signs you out. Use **Sign out** when finished. If sign-out is not confirmed because the network failed, do not assume server-side logout succeeded; retry when reachable.
|
||||
|
||||
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.
|
||||
## Serial: observer and writer
|
||||
|
||||
`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.
|
||||
All transports share one physical RS-232 port. Exactly one broker client can write; other clients observe the incoming UART1 stream.
|
||||
|
||||
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.
|
||||
Connecting or reconnecting Serial automatically requests control. If no other client owns the writer lease, your connection becomes writer. Check **Role** after every reconnect; **Release control** applies to the current connection, not future reconnects.
|
||||
|
||||
### Browser-shell policy
|
||||
- Check **Role**, **Active writer**, and the connection status before typing. The dashboard's Writer/Observer role is not your account's `admin`/`user` role.
|
||||
- Choose **Request control** to request the writer lease. An administrator account does not automatically make this serial connection the writer. If another client owns control, coordinate with that operator.
|
||||
- Type only when writer status is confirmed. Observer input is disabled.
|
||||
- Choose **Release control** when finished transmitting so another client can write.
|
||||
- Administrators can inspect **Broker** settings or the writer/client quick views and explicitly assign control to a selected current client. Refresh and deliberately reselect after a stale-target warning. Transfer cannot recall bytes already accepted for transmission.
|
||||
|
||||
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
|
||||
Slow observers can lose their own output without blocking other clients. The browser has bounded output buffering and 5,000 lines of scrollback per terminal; it is not a durable recording. Observe reported drop counts when completeness matters. Serial transport remains binary-transparent: there are no in-band administration escape sequences.
|
||||
|
||||
- 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.
|
||||
## Serial, Admin, and Settings
|
||||
|
||||
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.
|
||||
Switching views does **not** reconnect Serial or request/release its writer lease. Hidden connected terminals continue receiving output; keyboard input goes only to the selected terminal.
|
||||
|
||||
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.
|
||||
- **Serial** is the connected equipment's UART1 stream.
|
||||
- **Admin** is the ESP32 command console, not the connected equipment. Choose **Open admin** explicitly. Closing Admin leaves Serial connected. One browser admin connection and two remote-console slots shared with admin SSH bound availability.
|
||||
- **Settings** provides typed controls for Serial settings, Accounts, Network, Display, Broker, SSH, and HTTPS / Reboot. Opening a page, selecting a client, or opening a quick view does not apply changes.
|
||||
|
||||
## Typed settings API and operation lifetime
|
||||
Use the [command reference](command_reference.md) for commands, but do not assume full UART0/SSH parity in the browser shell. Browser account commands are limited to selected operations on other accounts; use **Accounts** for supported self/password-generation/key workflows. Browser SSH identity/service mutations belong in typed **SSH** settings. Browser `web` commands are restricted to status, stop, and certificate rotation; recovery remains on UART0 or reachable admin SSH. Wi-Fi commands include hidden secret prompts and can immediately disconnect you. Avoid explicit secret-display commands in recorded/shared terminals.
|
||||
|
||||
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.
|
||||
## Apply, Save, and persistent state
|
||||
|
||||
| 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 |
|
||||
| Area | Operator rule |
|
||||
|---|---|
|
||||
| Serial settings | Apply and Defaults change working RAM. Save persists the device's working values, not unsent browser drafts. Reconfiguration or stop discards pending serial-service data, though broker clients and writer ownership remain. |
|
||||
| Network | Wi-Fi edits require explicit Save; hostname/mDNS settings have separate persistence. Start/Stop also change the RAM boot-enabled policy. Save only after checking the desired working state. |
|
||||
| Display | Apply changes working configuration; Save persists it. Load reads saved state; Reset persists defaults. |
|
||||
| Accounts and authorized keys | Successful mutations are committed immediately; there is no separate account Save. They revoke the affected account's network sessions. |
|
||||
| HTTPS/SSH identities | Successful replacement persists immediately. Rotation is not an account-password change. |
|
||||
| Broker/service controls | These operate on live connections/services; they are not saved configuration profiles. |
|
||||
|
||||
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.
|
||||
Load, Defaults, and Reset are not synonyms. Read each confirmation and the [command reference](command_reference.md) before discarding configuration. Network quick views intentionally omit password controls; use the full Network page. USB host baud/framing selections do not configure UART1; use Serial settings.
|
||||
|
||||
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.
|
||||
Save desired working settings before reboot or [firmware upload](firmware_update.md). Saved configuration survives an ordinary reboot; unsaved RAM settings and browser drafts may not.
|
||||
|
||||
### Serial and Display
|
||||
## Disruptive changes and uncertain results
|
||||
|
||||
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).
|
||||
Plan a maintenance window and keep UART0 available before changing networking, identities, services, or firmware.
|
||||
|
||||
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.
|
||||
- **Network:** profile/AP changes, stop, or reconnect may cut off the result. An accepted operation is not proof of Wi-Fi association, address acquisition, or name resolution. Restore connectivity and inspect actual state before retrying.
|
||||
- **Accounts:** changing your own password, role, or keys may revoke your login before the result appears. See [account management](user_administration.md).
|
||||
- **HTTPS stop/restart:** closes all web logins and both browser terminal routes. A stopped web service cannot be restarted through its own page; use `web start` on UART0 or still-reachable admin SSH.
|
||||
- **HTTPS certificate rotation:** verify the new fingerprint through UART0 before renewing browser trust. A failure after storage commit can leave a new stored identity even if restart fails; a retained server may still serve the old certificate. Do not repeat rotation blindly.
|
||||
- **SSH controls/host-key rotation:** can close SSH sessions. Verify a changed host-key fingerprint with `ssh host-key info` on UART0 before updating client trust. HTTPS is not inherently restarted by SSH rotation.
|
||||
- **Reboot:** interrupts every transport, including native USB and UART operation during restart. Reconnect and sign in explicitly afterward.
|
||||
- **Firmware upload:** can stall or drop browser sessions before reboot. Follow the separate [firmware procedure](firmware_update.md).
|
||||
|
||||
### Accounts and authorized keys
|
||||
A timeout, lost response, navigation, logout, or revoked login does **not** cancel already admitted work. Settings results are temporary observations, not a durable operation history. Use **Check Result**/**Refresh** where offered (SSH and HTTPS / Reboot require manual checking), then inspect actual device state. Never replay a destructive action merely because its acknowledgement disappeared.
|
||||
|
||||
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.
|
||||
## Recovery
|
||||
|
||||
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.
|
||||
1. If the page or session fails, restore network access, reload, and sign in again. Check identity warnings locally before entering credentials.
|
||||
2. If networking or HTTPS is unavailable, use physical UART0 for status and repair. Reachable admin SSH can also recover HTTPS with `web stop` / `web start` when appropriate.
|
||||
3. Repair forgotten passwords with normal UART0 account commands. `user recover --force` is destructive unavailable-database recovery, not a password-reset shortcut; see [user administration](user_administration.md).
|
||||
4. If a firmware update prevents boot or web access, use [wired recovery](firmware_update.md#wired-recovery) as described in the firmware guide.
|
||||
|
||||
### 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.
|
||||
Native USB remains network-independent UART1 access when network services fail, but cannot repair accounts or identities and does not promise uninterrupted data through flash operations or reboot. Keep local ports physically controlled. See [security operations](security_operations.md) for backup limits, identity recovery, and physical-extraction risks.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hardware wiring
|
||||
|
||||
This document records the GPIO assignments for the initial hardware profile. Additional profiles should use separate sections rather than treating this GPIO map as universal.
|
||||
Use these connections for the hardware profile below. Do not apply this GPIO map to another board without checking its schematic and pin assignments.
|
||||
|
||||
## Profile: ESP32-S3-DevKitC-1 N16R8 with Adafruit MAX3243
|
||||
|
||||
@@ -87,7 +87,7 @@ The breakout's `OFF` pin drives the MAX3243 active-low `!FORCEOFF` input. High o
|
||||
|
||||
GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels rather than counting header positions.
|
||||
|
||||
### Development and native USB connectors
|
||||
### Administration and native USB connectors
|
||||
|
||||
| DevKit connector | ESP32-S3 path | Typical Linux device | Purpose |
|
||||
|---|---|---|---|
|
||||
@@ -100,9 +100,9 @@ The USB-to-UART bridge's DTR/RTS controls serve automatic boot/reset and do not
|
||||
|
||||
Native USB CDC DTR controls the lifetime of the `usb-cdc` broker client but is not forwarded to physical DE-9 DTR. Physical DTR follows the `serial` configuration. CDC RTS is status information only; GPIO15/DE-9 RTS remains UART1 receive flow control when `flow=rts-cts` is enabled.
|
||||
|
||||
## Phase 7A OLED and button wiring
|
||||
## OLED and button wiring
|
||||
|
||||
Phase 7A hardware validation used the following connections for the 128×64 I²C OLED and three local buttons. The selected module acknowledges at 7-bit I²C address `0x3c` (8-bit `0x78` write / `0x79` read) and has separate yellow rows 0–15 and blue rows 16–63, divided by a narrow physical black separator:
|
||||
Use the following connections for the 128×64 I²C OLED and three local buttons. The selected module acknowledges at 7-bit I²C address `0x3c` (8-bit `0x78` write / `0x79` read) and has separate yellow rows 0–15 and blue rows 16–63, divided by a narrow physical black separator:
|
||||
|
||||
| Device connection | ESP32-S3 connection | Electrical behavior | Purpose |
|
||||
|---|---:|---|---|
|
||||
@@ -133,8 +133,8 @@ Before applying power, verify whether the module already includes SDA and SCL pu
|
||||
|
||||
## Electrical verification
|
||||
|
||||
See [Electrical tests](electrical_tests.md) for Phase 7 OLED/button bring-up, persistent aging settings, optional-display and stuck-button fault recovery, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker/concurrent-transport testing.
|
||||
See [Electrical tests](electrical_tests.md) for OLED/button checks, persistent dim/off settings, optional-display and stuck-button fault recovery, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker/concurrent-transport testing.
|
||||
|
||||
## Future hardware profiles
|
||||
## Using a different board or transceiver
|
||||
|
||||
Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers require separate profiles. Review display, buttons, USB, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals before assigning GPIOs.
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
*-backups/
|
||||
*.bak
|
||||
__pycache__/
|
||||
.history/
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
(version 20260206)
|
||||
(generator "pcbnew")
|
||||
(layer "F.Cu")
|
||||
(descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; only header centres verified; no GPIO mapping, outline, courtyard or antenna keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
|
||||
(descr "HW-678, component/top view antenna up USB down; origin pad 1; left top-bottom 1..22, right top-bottom 23..44; nominal header centres with user-measured main PCB/antenna bounds and USB shell mouth spans on F.Fab; approximate 1.365 mm top/left registration; no GPIO mapping, complete USB envelope, courtyard or RF keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
|
||||
(tags "module provisional THT")
|
||||
(attr through_hole)
|
||||
(property "Reference" "REF**" (at 12.7 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(property "Value" "HW678_2x22_Provisional" (at 12.7 57) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "PROVISIONAL CONNECTOR FIT" (at 12.7 25) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "ANTENNA UP / USB DOWN" (at 12.7 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_rect (start -1.365 -1.365) (end 26.725 54.705) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_rect (start 4.045 -8.255) (end 21.675 -1.365) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_line (start 2.795 54.705) (end 11.665 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_line (start 14.195 54.705) (end 23.065 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT" (at 12.7 -9.5) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(fp_text user "USB SHELL MOUTHS ONLY" (at 12.7 59) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(pad "1" thru_hole rect (at 0.00 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "2" thru_hole circle (at 0.00 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "3" thru_hole circle (at 0.00 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
|
||||
@@ -6,15 +6,30 @@ This is a native, editable schematic with actual connected nets, not an image
|
||||
or a SKiDL-only netlist. **`serial-carrier.kicad_pcb` now contains a routed,
|
||||
82 × 80 mm two-copper-layer A1 carrier draft with a top-left antenna notch,
|
||||
USB toward the bottom and RS-232 toward the top.** It is **not fabrication-ready**.
|
||||
See **`pcb-draft-notes.md`** for placement, chosen dimensions/rules, provisional
|
||||
See **[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md)** for placement, chosen dimensions/rules, provisional
|
||||
mechanical assumptions and validation. A review drawing is `pcb-placement-draft.pdf`.
|
||||
|
||||
The schematic contains the complete ESP32 development-board interface, MAX3243
|
||||
breakout logic interface, I²C OLED, three Omron B3F-1000 buttons and three 2.2 kΩ
|
||||
breakout logic interface, I²C OLED, three Omron B3F-1000 buttons and three 2 kΩ
|
||||
button pull-ups. USB connectors, regulator, reset/boot circuit, DE-9 and MAX3243
|
||||
charge pump are already on their respective modules and are not duplicated on
|
||||
the carrier. All 3.3 V and ground connections are explicit named nets.
|
||||
|
||||
## Current user acceptance and placement
|
||||
|
||||
See [user-acceptance.md](../../docs/devel/hardware/PCB/user-acceptance.md) for the exact group placements, nominal body clearances and evidence limits. Common body centreline is carrier-local X=56.970 mm; resistor bodies are centred in the RS-232/OLED gap with 2.0815 mm nominal clearance to each PCB. U1, U2, outline, carrier holes and USB placement are unchanged. R1–R3 are purchased 2 kΩ 1% metal-film parts; Omron B3F-1000 switches are also purchased.
|
||||
|
||||
The user accepts visual fit/USB clearance, male module mapping, M3 carrier/daughterboard and M2 display hardware, current RF reserve and power/ground routing. Intended/reported assembled module elevation is 10 mm, not an independently verified sum of the 8.5 mm female and described 2.5 mm male contributions. USB nonisolation is accepted; backfeed safety, regulator margin and RF qualification are not established. OLED backside pull-ups are confirmed by user, but their values/rail are unmeasured. Earlier pending-status language below is historical where superseded by this record.
|
||||
|
||||
PCB presentation uses **A3 landscape (420 × 297 mm)** instead of A4; the
|
||||
carrier stays at absolute X=100…182, Y=100…180 mm. No origin, footprint,
|
||||
routing or zone was moved. The 1:1 placement PDF includes the worksheet/title
|
||||
block, clear of the PCB; `validation/pcb-placement-page.png` previews the full
|
||||
page. Print at actual size on A3 for the 1:1 fit check, not “fit to A4”.
|
||||
`update_presentation.py` removes the accepted USB/geometry/pinout/assembly
|
||||
pending labels with lock/concurrent-edit guards and an exact-content audit.
|
||||
RF qualification and connector/case-cutout cautions remain.
|
||||
|
||||
## Files and use
|
||||
|
||||
| File | Purpose |
|
||||
@@ -22,24 +37,30 @@ the carrier. All 3.3 V and ground connections are explicit named nets.
|
||||
| `serial-carrier.kicad_pro`, `serial-carrier.kicad_sch` | Native KiCad project and A3 single-sheet schematic |
|
||||
| `serial-carrier.pdf` | Schematic review/print export |
|
||||
| `serial-carrier.kicad_pcb` | Routed 82 × 80 mm stepped, two-copper-layer PCB draft |
|
||||
| `pcb-draft-notes.md`, `pcb-placement-draft.pdf` | PCB design decisions/limits and 1:1 placement review |
|
||||
| [pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md), `pcb-placement-draft.pdf` | PCB design decisions/limits and 1:1 placement review |
|
||||
| `validation/pcb-draft.png`, `validation/pcb-draft-3d.png` | Copper/fab preview and carrier-only 3D render |
|
||||
| `validate_pcb_draft.py` | Fresh schematic-to-PCB net checks, bounds/layers and DRC/parity report |
|
||||
| `generate_pcb_draft.py`, `route_pcb_draft.py` | Optional destructive-regeneration tools with explicit overwrite guards |
|
||||
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 outlines and ESP32 header guides after placement/routing; preserves copper and project settings |
|
||||
| `db9-mechanical-notes.md`, `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
|
||||
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 and measured ESP32 outlines/USB mouth markers after placement/routing; preserves copper and project settings |
|
||||
| [db9-mechanical-notes.md](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
|
||||
| `update_u1_graphics.py`, `validate_u1_update.py` | Guarded measured-U1 graphics integration and exact pre-update preservation / geometry / silkscreen regressions |
|
||||
| `export_placement_pdf.py` | 1:1 PDF from saved board geometry; refuses stale embedded U1 graphics |
|
||||
| `remove_draft_markers.py` | Guarded presentation-label cleanup with exact-content preservation audit; does not qualify fabrication |
|
||||
| `add_db9_envelope.py` | Refresh nominal connector metal projections on Dwgs.User; not a case cutout or assembly clearance |
|
||||
| `Carrier.kicad_sym`, `sym-lib-table` | Project-local module, switch and resistor symbols |
|
||||
| `Carrier.pretty/`, `fp-lib-table` | OLED/ESP32/RS-232-reference provisional footprints; standard KiCad 10 switch/resistor/mounting-hole library references |
|
||||
| `oled-fit-check.svg` | User-fit-verified OLED mechanical template |
|
||||
| `footprint-notes.md`, `rs232-footprint-notes.md` | Footprint origins, provisional pad/drill choices, validation and RS-232 adaptation attribution |
|
||||
| `adafruit-research.md`, `button-notes.md`, `reference/` | Manufacturer evidence, pad maps and source/license records |
|
||||
| [footprint-notes.md](../../docs/devel/hardware/PCB/footprint-notes.md), [rs232-footprint-notes.md](../../docs/devel/hardware/PCB/rs232-footprint-notes.md) | Footprint origins, provisional pad/drill choices, validation and RS-232 adaptation attribution |
|
||||
| [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md), [button-notes.md](../../docs/devel/hardware/PCB/button-notes.md), `reference/` | Manufacturer evidence, pad maps and source/license records |
|
||||
| `bom-draft.csv` | Draft procurement list and unresolved selections |
|
||||
| `validate_schematic.py`, `validation/` | Repeatable netlist/firmware/ERC checks and exported evidence |
|
||||
| `validate_footprints.py` | Native KiCad footprint geometry regression checks via system Python/pcbnew |
|
||||
| `generate_schematic.py` | Optional stdlib-only initial design generator; not needed to open/edit in KiCad |
|
||||
|
||||
Keep these files together so project-relative libraries resolve. Standard KiCad
|
||||
Development notes live in [docs/devel](../../docs/devel/README.md); the
|
||||
[routing evidence summary](../../docs/devel/hardware/PCB/validation/pcb-routing-summary.md)
|
||||
is retained there too. Keep CAD, libraries, assets and scripts together here so
|
||||
project-relative libraries resolve. Standard KiCad
|
||||
10 footprint libraries must be installed for the THT buttons, resistors and mounting holes.
|
||||
SKiDL 2.3.0 is installed, but no SKiDL dependency is required for this native design.
|
||||
|
||||
@@ -70,13 +91,12 @@ modify footprints. The generated KiCad files are the editable design artifacts.
|
||||
matching board markings. This is photo/label evidence, not a continuity test:
|
||||
verify actual pin-to-function mapping before release, especially on clones.
|
||||
- DS1 pins 1–4 are GND, VCC, SCL, SDA, front/display view with header at top.
|
||||
- U2 pin numbering follows published Adafruit **5988 female** CAD as a provisional
|
||||
reference for the selected **6253 male** module. Its footprint is deliberately
|
||||
**unassigned in the schematic** until the actual male header and mechanics are
|
||||
verified. The PCB draft assigns `Carrier:MAX3243_Reference_Provisional` on the
|
||||
PCB only, causing one documented footprint-parity warning. Its reference nets
|
||||
are routed for review, not male-board qualification. See `adafruit-research.md`
|
||||
and `pcb-draft-notes.md`; this is not a generic DE-9 footprint.
|
||||
- U2 pin numbering follows published Adafruit **5988 female** CAD for the selected
|
||||
**6253 male** module. The user confirmed the same pinout, header mapping and
|
||||
footprint as the reference. Both schematic and PCB now assign
|
||||
`Carrier:MAX3243_Reference_Provisional`; clean footprint parity is required.
|
||||
The identifier is unchanged; this assignment is not hardware or fabrication qualification. See [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md)
|
||||
and [pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md); this is not a generic DE-9 footprint.
|
||||
- No-connect crosses mean unused on the carrier, not disconnected internally.
|
||||
Native USB GPIO19/20, UART0 GPIO43/44, memory-conflicting GPIO35–37 and the 5 V
|
||||
header are not connected to carrier circuitry.
|
||||
@@ -86,8 +106,8 @@ modify footprints. The generated KiCad files are the editable design artifacts.
|
||||
bidirectional pad; firmware **must use open-drain mode** for OFF. ERC does not
|
||||
enforce that configuration or model the breakout's internal OFF pull-up.
|
||||
- Selected buttons are Omron B3F-1000 with their verified KiCad THT footprint.
|
||||
Added R1–R3, **2.2 kΩ, 1%, at least 0.125 W**, from each button input to 3.3 V.
|
||||
They provide nominal 1.5 mA contact current / 4.95 mW dissipation while pressed,
|
||||
Added R1–R3, **2 kΩ, 1%, at least 0.125 W**, from each button input to 3.3 V.
|
||||
They provide nominal 1.65 mA contact current / 5.445 mW dissipation while pressed,
|
||||
rather than relying on the internal pull-up for the silver-contact rated load.
|
||||
Firmware's internal pull-ups and debouncing remain enabled; no firmware changes
|
||||
are required. Final supply tolerance/load budget still needs checking.
|
||||
@@ -105,19 +125,19 @@ The PDF was rendered and visually inspected for layout/readability. Both local
|
||||
footprints loaded in KiCad's native footprint loader. Two retained regression tests
|
||||
passed with `/usr/bin/python3 -B hardware/PCB/validate_footprints.py`; the binding
|
||||
emits startup PROPERTY_ENUM assertions but completes the checks. See
|
||||
`footprint-notes.md` for scope and limitations.
|
||||
[footprint-notes.md](../../docs/devel/hardware/PCB/footprint-notes.md) for scope and limitations.
|
||||
|
||||
OLED PCB/header/display/mounting-hole geometry was physically fit-verified by
|
||||
the user. This does **not** validate connector drills, male RS-232 geometry,
|
||||
electrical operation, power budget, procurement or fabrication.
|
||||
|
||||
The routed PCB draft has **0 physical DRC violations and 0 unconnected items**;
|
||||
a fresh netlist-to-PCB check passes. One deliberate U2 footprint-parity warning
|
||||
remains (PCB reference footprint versus blank schematic assignment). No added
|
||||
DRC exclusions hide this warning. Missing courtyards are ignored by default DRC
|
||||
a fresh netlist-to-PCB check passes with **zero schematic-parity issues**,
|
||||
including the user-confirmed U2 footprint assignment. No DRC exclusions were
|
||||
added. Missing courtyards are ignored by default DRC
|
||||
and remain a manual release check. Run `/usr/bin/python3 -B
|
||||
hardware/PCB/validate_pcb_draft.py` from the repository root; see
|
||||
`pcb-draft-notes.md` for the complete verification scope and remaining gates.
|
||||
[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md) for the complete verification scope and remaining gates.
|
||||
No firmware build or device tests were run: this PCB work did not modify firmware.
|
||||
|
||||
## Evidence and components
|
||||
@@ -127,22 +147,23 @@ Repository sources: `../../docs/wiring.md`, `../../src/board_pins.h`,
|
||||
|
||||
| Assembly | Repository identification | Footprint status |
|
||||
|---|---|---|
|
||||
| ESP32 development board | User confirmed HW-678 V0.0.0 markings and S3-N16R8 shield, two USB-C ports | Two 22-pin rows; nominal 2.54 mm pitch and 25.40 mm row spacing supported by simultaneous breadboard fit. Mechanical outline registration and socket/drill selection remain pending. |
|
||||
| RS-232 module | Adafruit MAX3243 full-pinout breakout; male is product 6253, female is 5988 | Eagle pin map/PCB geometry and STEP metal dimensions extracted. User confirms identical male/female external dimensions; actual fit and male electrical map remain release checks. Reference footprint used on PCB only. |
|
||||
| ESP32 development board | User confirmed HW-678 V0.0.0 markings and S3-N16R8 shield, two USB-C ports | Two 22-pin rows; nominal 2.54 mm pitch and 25.40 mm row spacing supported by simultaneous breadboard fit. Measured planar outline and visual fit accepted by user; finished-hole process remains unqualified. |
|
||||
| RS-232 module | Adafruit MAX3243 full-pinout breakout; male is product 6253, female is 5988 | Eagle pin map/PCB geometry and STEP metal dimensions extracted. User confirms identical male pinout/header/footprint, visual fit and pin-by-pin review. Reference footprint assigned in schematic and PCB. |
|
||||
| Display | 128×64 yellow/blue I²C OLED, address 0x3c; Amazon ASIN B0F13SSSFB | 26 mm square board, 22.50 mm hole spacing, Ø2 mm holes and centred header 1.50 mm from top edge are user-fit-verified. Local footprint assigned; header pad/drill selection remains provisional. |
|
||||
| Three buttons | Omron B3F-1000; previous, select, next; normally open to ground, active low | Datasheet and `Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x` pad mapping verified. External 2.2 kΩ pull-ups included. Live distributor stock remains unverified. |
|
||||
| Three buttons | Omron B3F-1000; previous, select, next; normally open to ground, active low | Datasheet and `Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x` pad mapping verified. Purchased per user; external purchased 2 kΩ 1% metal-film pull-ups included. |
|
||||
|
||||
Adafruit manufacturer CAD starting point:
|
||||
https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads
|
||||
|
||||
Published female-module Eagle CAD and its header map were retrieved and verified;
|
||||
see `adafruit-research.md` for pinned revision, hashes, attribution and geometry.
|
||||
see [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md) for pinned revision, hashes, attribution and geometry.
|
||||
The original files and CC BY-SA 3.0 notices are retained in `reference/`; they
|
||||
remain under their upstream license. Omron reference documents retain their
|
||||
manufacturer notices. The CAD-parts repository's STEP and MIT license are separately
|
||||
retained under `reference/db9-cad/`. External metal geometry is used for the male
|
||||
module on the user's explicit equivalence confirmation; this does not validate
|
||||
its electrical map, connector threads, tolerances, assembly fit or case cutout.
|
||||
module on the user's explicit equivalence confirmation. The user separately
|
||||
confirmed electrical mapping and visual fit; connector threads, tolerances and
|
||||
final case cutout are not independently qualified.
|
||||
|
||||
## Verified logical connections for the existing firmware profile
|
||||
|
||||
@@ -175,11 +196,11 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
- Keep UART0 / GPIO43 and GPIO44 available through the onboard USB-UART bridge.
|
||||
- Reserve GPIO19 and GPIO20 for onboard native USB.
|
||||
- Existing firmware profile powers RS-232 Vin and OLED VCC from 3.3 V, not 5 V.
|
||||
- Confirm the exact development board's regulator capacity and USB power-path
|
||||
behavior before finalizing the power design, especially with both USB ports attached.
|
||||
- Display pull-up presence, resistance and termination rail must be checked on
|
||||
the actual module. Do not select external pull-up values without this information.
|
||||
- Firmware enables internal button pull-ups; this carrier additionally uses 2.2 kΩ
|
||||
- User reports successful 3.3 V module operation and accepts nonisolated USB power.
|
||||
Regulator margin and USB backfeed safety remain unverified, especially with two powered hosts.
|
||||
- User confirms backside OLED I²C pull-ups and operation without extras.
|
||||
Resistance and termination rail are unmeasured; no external values are invented.
|
||||
- Firmware enables internal button pull-ups; this carrier additionally uses 2 kΩ
|
||||
external pull-ups for the selected buttons' contact-current requirement.
|
||||
- RS-232 OFF uses the breakout's onboard pull-up; GPIO9 releases it to enable.
|
||||
- The breakout is not galvanically isolated; signal grounds are common.
|
||||
@@ -192,17 +213,18 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
- OLED purchase: https://www.amazon.de/dp/B0F13SSSFB — I²C, not SPI.
|
||||
- Both Amazon URLs returned an access-check page on inspection; the ASINs alone
|
||||
do not establish a verified manufacturer drawing or footprint.
|
||||
- Use the male Adafruit breakout. User reports male/female board dimensions match;
|
||||
verify the selected male board against its manufacturer CAD.
|
||||
- Use the male Adafruit breakout. User confirms dimensions, pinout/header/footprint
|
||||
equivalence and completed a pin-by-pin module connection review.
|
||||
- Carrier for complete modules: soldered module headers for final assembly,
|
||||
sockets for beta testing without a case. Header pitch, row spacing, drill size
|
||||
and socket clearance must be validated against selected hardware.
|
||||
- Power only through the ESP32 development board's USB connectors, typically a
|
||||
powerbank or the serial peer's USB port, with common ground. Do not introduce
|
||||
another carrier power input or assume simultaneous USB supplies are isolated.
|
||||
- Select readily available THT buttons; the existing random switches need not be reused.
|
||||
- Case will be 3D-printed after PCB finalization. Include mounting provisions in
|
||||
the layout, but screw size, hole diameter and insert bosses are not yet selected.
|
||||
- User purchased Omron B3F-1000 THT buttons and 2 kΩ 1% metal-film resistors.
|
||||
- Case will be 3D-printed after PCB finalization. User approves M3 carrier holes,
|
||||
10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display
|
||||
hardware. Exact insert/boss dimensions and screw lengths are not supplied.
|
||||
|
||||
## User-supplied mechanical evidence
|
||||
|
||||
@@ -214,9 +236,24 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
rows fit a standard breadboard simultaneously without forcing. Use nominal
|
||||
2.54 mm pitch (53.34 mm over 21 intervals) and 25.40 mm row spacing based on
|
||||
that fit check, not the raw caliper measurements. Verify with a 1:1 print before fabrication.
|
||||
- ESP32 seller drawing states 28 mm width and annotates 63 mm length; its 20 mm
|
||||
arrow is not header row spacing. Antenna extent and header-to-outline offsets
|
||||
are not fully dimensioned.
|
||||
- ESP32 actual user measurements supersede the seller's 28 × 63 mm drawing:
|
||||
main PCB **56.07 × 28.09 mm**, excluding the antenna projection. Top/component
|
||||
view, antenna up and USB down: antenna projects **6.89 mm** above the main PCB,
|
||||
inset **5.41 mm left / 5.05 mm right**. Derived antenna width **17.63 mm**,
|
||||
overall length **62.96 mm**, antenna centre **0.18 mm right** of PCB centre.
|
||||
- Header end offsets were initially approximately 1.5 mm; user accepted **1.365 mm**
|
||||
top/bottom to preserve the breadboard-confirmed 53.34 mm span. Left-edge-to-left-row
|
||||
centre distance is also approximately **1.365 mm**. Preserve 25.40 mm row spacing:
|
||||
the resulting right margin is **1.325 mm**, not an independently measured offset.
|
||||
These modelling offsets are not a claim of micron-level measurement accuracy.
|
||||
- Both USB-C metal shells are **8.87 mm wide**, with opening faces flush with the
|
||||
main PCB bottom edge. Left shell's outside left edge is **4.16 mm** from the PCB
|
||||
left edge; right shell's outside right edge is **3.66 mm** from the PCB right edge.
|
||||
User accepts USB placement and mating-plug clearance visually; shell depth/height remain unmeasured.
|
||||
- The library and routed board's embedded U1 now record these bounds on F.Fab;
|
||||
USB mouths are width spans only, not complete connector outlines. Measured
|
||||
board-local silkscreen, 1:1 placement PDF and SVG/PNG/3D previews are refreshed. See
|
||||
`../../docs/devel/hardware/PCB/footprint-notes.md` for coordinates and limits.
|
||||
- OLED actual PCB outline measured by user: 26 × 26 mm, superseding the seller's
|
||||
28.7 × 27.8 mm dimensions. Seller's 4.1 mm height remains unverified.
|
||||
- OLED front view, connector at top: GND, VCC, SCL, SDA from left to right,
|
||||
@@ -250,19 +287,19 @@ The wiring guide warns that compatible ESP32 boards may have different layouts.
|
||||
Crosshairs denote header centres, not specified pad or drill diameters. This
|
||||
template is not a KiCad footprint or a fabrication file.
|
||||
|
||||
## Remaining mechanical/electrical checks
|
||||
## Current acceptance checklist and evidence limits
|
||||
|
||||
1. OLED outline, mounting-hole placement, revised header position and display
|
||||
outline are user fit-verified. Select carrier header/socket pads and drills
|
||||
from the actual connector drawings.
|
||||
2. Before final module outlines/courtyards, establish their registration to header
|
||||
centres, connector overhang and display position from drawings or measurements.
|
||||
3. Verify power-path schematic/regulator capacity and OLED pull-up implementation.
|
||||
Photos alone do not establish those electrical properties.
|
||||
- [x] Component dimensions and visual physical fit accepted by user, including USB placement/clearance and RS-232 prong clearance.
|
||||
- [x] Male Adafruit pinout/header/footprint equivalence and pin-by-pin module connection review confirmed by user; existing U2 schematic assignment retained with clean parity.
|
||||
- [x] M3 carrier holes, 10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display hardware approved.
|
||||
- [x] Omron B3F-1000 and 2 kΩ 1% metal-film resistors purchased per user.
|
||||
- [x] Existing 3.3 V supply operation and OLED onboard pull-ups reported; no extra I²C pull-ups needed in reported operation.
|
||||
- [x] Nonisolated USB power, existing RF reserve and power/ground routing accepted by user.
|
||||
- [ ] Independently verified regulator margin, USB backfeed safety, OLED pull-up resistance/rail and manufacturer RF/EMC qualification are not available.
|
||||
- [ ] Detailed fastener lengths/insert dimensions, final case cutout, tolerance/height envelopes and fabrication-process qualification remain outside this acceptance.
|
||||
|
||||
The native schematic, local libraries and routed two-layer PCB draft are present.
|
||||
Before fabrication, finish the above checks, qualify the male RS-232 module map
|
||||
and footprint, and confirm procurement. The carrier now proposes an 82 × 80 mm
|
||||
stepped outline and four Ø3.2 mm M3 mounting holes; case hardware is not yet fixed.
|
||||
The PCB's RF reserve is a draft design allocation, not a verified module antenna
|
||||
keepout. Module/USB/DE-9 clearances must still be established from actual hardware.
|
||||
The intended/reported 10 mm assembled elevation is not independently measured;
|
||||
8.5 + 2.5 = 11 mm is not evidence of actual mated height. User acceptance closes
|
||||
the corresponding review gates, not these factual evidence limits. Missing
|
||||
module courtyards still limit automated assembly checks. No new hardware tests,
|
||||
firmware changes, Gerbers, order or certification are implied.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add nominal STEP-derived metal bounding projections on Dwgs.User only.
|
||||
|
||||
Not silkscreen, a courtyard, or a case cutout. See db9-mechanical-notes.md for
|
||||
Not silkscreen, a courtyard, or a case cutout. See ../../docs/devel/hardware/PCB/db9-mechanical-notes.md for
|
||||
source attribution, selected face subsets and the user-confirmed male/female
|
||||
external-geometry equivalence. No module height or case-wall thickness assumed.
|
||||
"""
|
||||
|
||||
@@ -10,6 +10,7 @@ import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import LineString, Point, box
|
||||
from shapely.ops import unary_union, polygonize
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
@@ -38,6 +39,7 @@ def outline_lines(shape):
|
||||
|
||||
|
||||
def add_outlines(board):
|
||||
verify(board)
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)}
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if drawing.m_Uuid.AsString() in owned:
|
||||
@@ -82,8 +84,11 @@ def add_outlines(board):
|
||||
board.Add(shape)
|
||||
count+=1
|
||||
|
||||
def line(a,b):
|
||||
clipped=LineString([a,b]).intersection(silk_area).difference(blocked)
|
||||
def line(a,b,width=WIDTH):
|
||||
extra = max(0, (width-WIDTH)/2)
|
||||
area = silk_area.buffer(-extra) if extra else silk_area
|
||||
obstacles = blocked.buffer(extra) if extra else blocked
|
||||
clipped=LineString([a,b]).intersection(area).difference(obstacles)
|
||||
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
|
||||
for piece in pieces:
|
||||
if piece.geom_type!="LineString" or piece.length < 0.25:
|
||||
@@ -91,7 +96,7 @@ def add_outlines(board):
|
||||
shape=p.PCB_SHAPE(board)
|
||||
shape.SetShape(p.SHAPE_T_SEGMENT)
|
||||
shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1]))
|
||||
shape.SetWidth(p.FromMM(WIDTH))
|
||||
shape.SetWidth(p.FromMM(width))
|
||||
add(shape)
|
||||
|
||||
for ref in ("DS1","U2"):
|
||||
@@ -137,22 +142,19 @@ def add_outlines(board):
|
||||
distance+=1.3
|
||||
else:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
# These are pad-row guides, not inferred socket/body or ESP32 PCB dimensions.
|
||||
fp=footprints["U1"]
|
||||
assert fp.GetOrientationDegrees()==0, "Review guide construction for rotated U1"
|
||||
pads={pad.GetNumber():xy(pad.GetPosition()) for pad in fp.Pads()}
|
||||
for first,last in (("1","22"),("23","44")):
|
||||
x,ytop=pads[first]; _,ybottom=pads[last]
|
||||
corners=[(x-1.2,ytop-1.2),(x+1.2,ytop-1.2),(x+1.2,ybottom+1.2),(x-1.2,ybottom+1.2)]
|
||||
for a,b in zip(corners,corners[1:]+corners[:1]):
|
||||
line(a,b)
|
||||
label=p.PCB_TEXT(board)
|
||||
label.SetText("HEADER GUIDES")
|
||||
a,b=pads["1"],pads["23"]
|
||||
label.SetPosition(vec(((a[0]+b[0])/2,a[1]+28)))
|
||||
label.SetTextSize(p.VECTOR2I(p.FromMM(0.85),p.FromMM(0.85)))
|
||||
label.SetTextThickness(p.FromMM(0.12))
|
||||
add(label)
|
||||
# Union removes the shared body/antenna edge duplication. Mouth spans replace
|
||||
# the coincident bottom-edge strokes, with their measured library line weight;
|
||||
# this marks width only, without inventing connector depth or an RF keepout.
|
||||
sources=[g for g in footprints["U1"].GraphicalItems()
|
||||
if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab]
|
||||
mouths=[g for g in sources if g.GetShape()==p.SHAPE_T_SEGMENT]
|
||||
mouth_lines=unary_union([edge for g in mouths for edge in outline_lines(g)])
|
||||
perimeter=unary_union([edge for g in sources if g.GetShape()==p.SHAPE_T_RECT
|
||||
for edge in outline_lines(g)]).difference(mouth_lines)
|
||||
for edge in perimeter.geoms:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
for mouth in mouths:
|
||||
line(xy(mouth.GetStart()),xy(mouth.GetEnd()),p.ToMM(mouth.GetWidth()))
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
References,Quantity,Description,Manufacturer,MPN or identification,Footprint,Status
|
||||
U1,1,ESP32-S3 N16R8 development board,User supplied,HW-678 V0.0.0 / ASIN B0F3XMYYQY,Carrier:HW678_2x22_Provisional,Header pitch/spacing confirmed; connector drill fit and module envelope pending
|
||||
U2,1,MAX3243 full-pinout RS-232 breakout male,Adafruit,6253,Carrier:MAX3243_Reference_Provisional,PCB-only reference footprint; schematic assignment blank; male header/geometry verification required
|
||||
DS1,1,128x64 yellow-blue I2C OLED 26mm PCB,User supplied,ASIN B0F13SSSFB,Carrier:OLED_26mm_I2C_Provisional,Module geometry user-fit-verified; carrier header drill fit pending
|
||||
SW1 SW2 SW3,3,Normally-open 6mm THT tactile switch,Omron,B3F-1000,Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x,Datasheet/footprint checked; stock unverified
|
||||
R1 R2 R3,3,2.2k ohm 1 percent axial resistor at least 0.125W,TBD,TBD,Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,Design value selected; exact MPN and body/lead fit pending
|
||||
H1 H2 H3 H4,4,Proposed M3 case mounting locations with 3.2mm NPTH,TBD,TBD,MountingHole:MountingHole_3.2mm_M3,Board-only hole locations; screws and insert/standoff dimensions not selected
|
||||
Assembly hardware,TBD,Module headers and optional beta-test sockets plus mounting hardware,TBD,TBD,,Not yet selected; not included in electrical component count
|
||||
U1,1,ESP32-S3 N16R8 development board,User supplied,HW-678 V0.0.0 / ASIN B0F3XMYYQY,Carrier:HW678_2x22_Provisional,Dimensions and visual fit including USB placement/clearance user accepted
|
||||
U2,1,MAX3243 full-pinout RS-232 breakout male,Adafruit,6253,Carrier:MAX3243_Reference_Provisional,Assigned in schematic and PCB; male pinout/header/footprint equivalence and pin-by-pin review user confirmed
|
||||
DS1,1,128x64 yellow-blue I2C OLED 26mm PCB,User supplied,ASIN B0F13SSSFB,Carrier:OLED_26mm_I2C_Provisional,Module geometry and visual fit user confirmed; onboard I2C pull-ups present but values/rail unmeasured
|
||||
SW1 SW2 SW3,3,Normally-open 6mm THT tactile switch,Omron,B3F-1000,Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x,User reports purchased; datasheet/footprint mapping checked
|
||||
R1 R2 R3,3,2k ohm 1 percent metal-film axial resistor,TBD,User purchased; exact MPN and wattage unspecified,Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,Visual dimensions accepted; retained nominal 6.3x2.5mm body and 7.62mm pitch; design rating at least 0.125W
|
||||
H1 H2 H3 H4,4,M3 carrier mounting locations with 3.2mm NPTH,User selected,M3 screws and 10mm brass standoffs into heat inserts,MountingHole:MountingHole_3.2mm_M3,User approved; board-only holes excluded from electrical component count
|
||||
Module mounting hardware,TBD,M3 daughterboard screws and M2 OLED hardware,User selected,Exact insert and screw lengths unspecified,,User approved; case wall and detailed fastener specifications not defined
|
||||
Module headers,TBD,8.5mm female headers and standard male headers,User selected,Male described as adding 2.5mm,,User intended/reported assembled elevation 10mm; not independently measured; arithmetic sum 11mm is not verified mating height; RS232 prongs clear per user
|
||||
|
||||
|
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export a 1:1 placement PDF with measured HW678 geometry, without editing CAD.
|
||||
|
||||
Export the saved board's verified measured U1 graphics and A3 worksheet.
|
||||
No library overlay or temporary caption hides stale CAD.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
PDF = ROOT / "pcb-placement-draft.pdf"
|
||||
|
||||
|
||||
def export():
|
||||
original = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
from update_presentation import expected_tree
|
||||
from validate_u1_update import parse, canonical
|
||||
assert canonical(expected_tree(parse(PCB))) == canonical(parse(PCB)), "Update PCB presentation first"
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-placement-") as temporary:
|
||||
draft = Path(temporary) / PCB.name
|
||||
result = Path(temporary) / PDF.name
|
||||
if not p.SaveBoard(str(draft), board, True):
|
||||
raise RuntimeError("Cannot save temporary plotting board")
|
||||
subprocess.run([
|
||||
"kicad-cli", "pcb", "export", "pdf", "--mode-single", "--include-border-title",
|
||||
"--layers", "F.Fab,F.SilkS,Dwgs.User,Edge.Cuts",
|
||||
"--sketch-pads-on-fab-layers", "--black-and-white", "--scale", "1",
|
||||
"--output", str(result), str(draft),
|
||||
], check=True)
|
||||
if PCB.read_bytes() != original:
|
||||
raise RuntimeError("Source PCB changed during export; PDF not replaced")
|
||||
PDF.write_bytes(result.read_bytes())
|
||||
print(f"Exported {PDF}; scale 1:1; source PCB unchanged")
|
||||
print(f"Source PCB SHA256: {hashlib.sha256(original).hexdigest()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export()
|
||||
@@ -0,0 +1 @@
|
||||
{"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false, "OPEN BROWSER": true, "BACKUP_OPT": true}
|
||||
@@ -22,13 +22,13 @@ OUTLINE = [(36,0), (82,0), (82,80), (0,80), (0,19), (36,19)]
|
||||
PLACEMENT = {
|
||||
"U1": ("Carrier:HW678_2x22_Provisional", 8, 23),
|
||||
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 26.670),
|
||||
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43, 36),
|
||||
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 43, 34),
|
||||
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 54, 34),
|
||||
"R3": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 65, 34),
|
||||
"SW1": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 43, 68),
|
||||
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 55, 68),
|
||||
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 67, 68),
|
||||
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43.97, 36),
|
||||
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 42.16, 32.6685),
|
||||
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 53.16, 32.6685),
|
||||
"R3": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 64.16, 32.6685),
|
||||
"SW1": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 41.72, 68),
|
||||
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 53.72, 68),
|
||||
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 65.72, 68),
|
||||
}
|
||||
MOUNTS = [(3.5,22.5), (3.5,75), (78,4), (78,76)]
|
||||
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
|
||||
@@ -114,9 +114,9 @@ def main():
|
||||
settings.m_ViasMinSize=mm(0.7)
|
||||
# Proposed in-memory draft rules only; never overwrite the user's project settings.
|
||||
title=board.GetTitleBlock()
|
||||
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier draft")
|
||||
title.SetRevision("A1 DRAFT")
|
||||
title.SetComment(0,"NOT FOR FABRICATION - USB / antenna geometry pending; verify module pinout / assembly")
|
||||
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier")
|
||||
title.SetRevision("A1")
|
||||
title.SetComment(0,"")
|
||||
pad_nets={}
|
||||
for source_net in source.findall("nets/net"):
|
||||
net=p.NETINFO_ITEM(board,source_net.get("name"))
|
||||
@@ -125,8 +125,7 @@ def main():
|
||||
pad_nets[node.get("ref"),node.get("pin")]=(net,node)
|
||||
for ref,(lib_id,x,y) in PLACEMENT.items():
|
||||
comp=components[ref]
|
||||
if ref != "U2":
|
||||
assert comp.findtext("footprint")==lib_id,(ref,"Schematic footprint changed")
|
||||
assert comp.findtext("footprint")==lib_id,(ref,"Schematic footprint changed")
|
||||
fp=load(lib_id)
|
||||
fp.SetReference(ref)
|
||||
fp.SetValue(comp.findtext("value"))
|
||||
@@ -145,7 +144,7 @@ def main():
|
||||
elif ref=="U2":
|
||||
fp.Reference().SetPosition(xy(56.97,14.670))
|
||||
elif ref=="DS1":
|
||||
fp.Reference().SetPosition(xy(56,49))
|
||||
fp.Reference().SetPosition(xy(56.97,49))
|
||||
elif ref.startswith("SW"):
|
||||
fp.Reference().SetPosition(xy(x+3.25,y-2))
|
||||
else:
|
||||
@@ -188,19 +187,23 @@ def main():
|
||||
text(board,"RF RESERVE",20.5,26,p.Dwgs_User,1.2)
|
||||
text(board,"VERIFY ANTENNA",20.5,29,p.Dwgs_User,1)
|
||||
text(board,"ESP32 HW678",20.7,48)
|
||||
text(board,"USB ACCESS - VERIFY",18,83,p.Dwgs_User,1)
|
||||
|
||||
text(board,"DE-9 OUT / NOMINAL MALE",56.97,-1.830,p.Dwgs_User,1)
|
||||
text(board,"MAX3243 REF",56.97,17.670)
|
||||
text(board,"26mm OLED",56,52)
|
||||
for label,x in zip(("BACK","SELECT","NEXT"),(46.25,58.25,70.25)):
|
||||
text(board,label,x,75, size=0.9)
|
||||
text(board,"DRAFT - NOT FOR FAB",56,78,size=0.9)
|
||||
|
||||
text(board,"82 x 80 mm / 2 copper layers",41,-5,p.Dwgs_User,1.5)
|
||||
text(board,"Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending",41,-2.5,p.Dwgs_User,0.9)
|
||||
from update_presentation import CAPTION
|
||||
text(board,CAPTION,41,-2.5,p.Dwgs_User,0.9)
|
||||
board.BuildConnectivity()
|
||||
assert p.SaveBoard(str(PCB),board,True)
|
||||
# Separate process avoids repeated-load SWIG lifetime issues in pcbnew.
|
||||
import sys
|
||||
subprocess.run([sys.executable, "-B", str(ROOT/"update_presentation.py")], check=True)
|
||||
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
|
||||
print("Placement draft only. U2 nominal male external metal dimensions user-confirmed; pin map / assembly still require qualification.")
|
||||
print("Placement draft only. User-confirmed fit and pin map; RF/electrical/fabrication qualification not established.")
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
|
||||
@@ -88,7 +88,7 @@ def symbol_definition(name, embedded=False):
|
||||
|
||||
ROOT.joinpath("Carrier.kicad_sym").write_text('(kicad_symbol_lib (version 20231120) (generator "kicad_symbol_editor")\n'+'\n'.join(symbol_definition(name) for name in SPECS)+'\n)\n')
|
||||
ROOT.joinpath("sym-lib-table").write_text('(sym_lib_table (version 7)\n (lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.kicad_sym")(options "")(descr "Project-local module interfaces"))\n)\n')
|
||||
ROOT.joinpath("fp-lib-table").write_text('(fp_lib_table (version 7)\n (lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.pretty")(options "")(descr "Provisional module carrier footprints; see footprint-notes.md"))\n (lib (name "Button_Switch_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Button_Switch_THT.pretty")(options "")(descr "KiCad standard THT switches"))\n (lib (name "Resistor_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Resistor_THT.pretty")(options "")(descr "KiCad standard axial resistors"))\n (lib (name "MountingHole")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr "KiCad standard mounting holes"))\n)\n')
|
||||
ROOT.joinpath("fp-lib-table").write_text('(fp_lib_table (version 7)\n (lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.pretty")(options "")(descr "Provisional module carrier footprints; see ../../docs/devel/hardware/PCB/footprint-notes.md"))\n (lib (name "Button_Switch_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Button_Switch_THT.pretty")(options "")(descr "KiCad standard THT switches"))\n (lib (name "Resistor_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Resistor_THT.pretty")(options "")(descr "KiCad standard axial resistors"))\n (lib (name "MountingHole")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr "KiCad standard mounting holes"))\n)\n')
|
||||
# Preserve project settings on regeneration.
|
||||
project_file = ROOT / (PROJECT + ".kicad_pro")
|
||||
if not project_file.exists():
|
||||
@@ -111,6 +111,8 @@ def note(text,x,y,size=1.27):
|
||||
|
||||
def place(name,ref,value,x,y,nets,extra=None):
|
||||
pins,hw,hh,_,footprint,description = SPECS[name]
|
||||
if ref == "U2":
|
||||
footprint = "Carrier:MAX3243_Reference_Provisional"
|
||||
instance = [f'(symbol (lib_id {q("Carrier:"+name)}) (at {n(x)} {n(y)} 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid(ref))})',
|
||||
prop("Reference",ref,x,y-hh-5.08),prop("Value",value,x,y-hh-2.54),prop("Footprint",footprint,x,y,True),prop("Datasheet","",x,y,True)]
|
||||
for k,v in (extra or {}).items():
|
||||
@@ -140,12 +142,12 @@ for number,name,*_ in MCU:
|
||||
mcu_nets[number]=GPIO_NET[int(name[4:])]
|
||||
place("HW678_N16R8","U1","HW678 / S3-N16R8",95.25,109.22,mcu_nets,{"Status":"Header geometry nominal; outline/antenna clearance pending","Pin_numbering":"Left 1-22; right 23-44; both top-to-bottom, antenna up"})
|
||||
rs_nets=dict(zip(map(str,range(1,13)),["+3V3","GND","RS_DCD","RS_RX","RS_TX","RS_DTR","RS_DSR","RS_RTS","RS_CTS","RS_RI","RS_VALID","RS_OFF_N"]))
|
||||
place("MAX3243_Module_Reference","U2","Adafruit MAX3243 / male 6253",228.6,96.52,rs_nets,{"Status":"VERIFY male header numbering; footprint intentionally unassigned","Datasheet_source":"adafruit-research.md; reference female 5988 CAD"})
|
||||
place("MAX3243_Module_Reference","U2","Adafruit MAX3243 / male 6253",228.6,96.52,rs_nets,{"Status":"User confirmed male pinout/header/footprint and pin-by-pin connections","Datasheet_source":"../../docs/devel/hardware/PCB/adafruit-research.md; reference female 5988 CAD"})
|
||||
place("OLED_I2C_26mm","DS1","128x64 I2C OLED / 0x3C",228.6,152.4,{"1":"GND","2":"+3V3","3":"OLED_SCL","4":"OLED_SDA"},{"Status":"Module outline/header/display user fit-verified; pad/drill fit provisional"})
|
||||
for index,(net,function) in enumerate((("BTN_PREVIOUS","Previous / back"),("BTN_SELECT","Select / confirm"),("BTN_NEXT","Next")),1):
|
||||
y=88.9+(index-1)*35.56
|
||||
place("R",f"R{index}","2.2k 1%",327.66,y,{"1":"+3V3","2":net},{"Rating":"At least 0.125 W; exact resistor MPN pending"})
|
||||
place("SW_NO",f"SW{index}","B3F-1000",327.66,y+15.24,{"1":net,"2":"GND"},{"Manufacturer":"Omron","MPN":"B3F-1000","Function":function,"Procurement":"Live stock not verified"})
|
||||
place("R",f"R{index}","2k 1%",327.66,y,{"1":"+3V3","2":net},{"Rating":"Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"})
|
||||
place("SW_NO",f"SW{index}","B3F-1000",327.66,y+15.24,{"1":net,"2":"GND"},{"Manufacturer":"Omron","MPN":"B3F-1000","Function":function,"Procurement":"User reports purchased"})
|
||||
note(function,302.26,y+21.59,1.016)
|
||||
|
||||
note("MODULE CARRIER - INITIAL SCHEMATIC",15.24,15.24,2.54)
|
||||
@@ -153,14 +155,14 @@ note("Native KiCad 10 schematic. Named wire labels join matching nets on this sh
|
||||
note("U1: complete development board",53.34,57.15,1.778)
|
||||
note("Antenna up / USB down: left row 1-22, right row 23-44.\nNC marks mean unused on carrier, not unused inside module.",53.34,64.77,1.016)
|
||||
note("U2: complete RS-232 breakout",177.8,57.15,1.778)
|
||||
note("Male 6253 selected. Numbers from published 5988 female CAD.\nVERIFY male header before routing; no footprint assigned.\nDE-9 and charge pump are already on the module.",177.8,64.77,1.016)
|
||||
note("Male 6253 selected. Numbers from published 5988 female CAD.\nMale mapping user-reviewed; reference footprint assigned.\nDE-9 and charge pump are already on the module.",177.8,64.77,1.016)
|
||||
note("Controls: active-low, firmware-debounced",292.1,57.15,1.778)
|
||||
note("External 2.2k pull-ups: ~1.5 mA pressed at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending.",292.1,64.77,1.016)
|
||||
note("External 2k 1% pull-ups: ~1.65 mA at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending.",292.1,64.77,1.016)
|
||||
note("DS1: front view GND / VCC / SCL / SDA",177.8,128.27,1.524)
|
||||
note("Power at 3.3 V only. Verify on-module I2C pull-ups;\nexternal values intentionally not guessed.",177.8,165.1,1.016)
|
||||
note("POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nVerify USB power isolation before attaching two powered hosts.\nKeep both USB connectors and reset/boot buttons accessible.",15.24,185.42)
|
||||
note("Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups.",177.8,165.1,1.016)
|
||||
note("POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nUSB nonisolated accepted; backfeed safety not verified.\nKeep both USB connectors and reset/boot buttons accessible.",15.24,185.42)
|
||||
note("RS-232 / ELECTRICAL LIMITS\nTX/RX and modem labels are at the ESP32 logic side.\nOFF_N is active low; GPIO9 must remain open-drain.\nBreakout provides its OFF pull-up. No galvanic isolation.\nNo carrier connection to raw RS-232 voltages.\nCarrier GND, USB GND and RS-232 signal ground are common.\nNever power OLED or MAX3243 logic from 5 V.",152.4,185.42)
|
||||
note("BEFORE PCB RELEASE\nVerify male breakout pin order/footprint.\nConfirm header/socket finished-hole requirements.\nComplete module courtyards and antenna clearance.\nVerify 3.3 V budget, OLED pull-ups and USB power path.\nChoose mounting hardware and confirm button stock.\nERC is not hardware or fabrication approval.",292.1,185.42)
|
||||
note("USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\nERC is not hardware or fabrication approval.",292.1,185.42)
|
||||
items.append(')')
|
||||
ROOT.joinpath(PROJECT+".kicad_sch").write_text('\n'.join(items)+'\n')
|
||||
ROOT.joinpath("validation/expected-nets.json").write_text(json.dumps(expected,indent=2,sort_keys=True)+'\n')
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Designator,Footprint,Quantity,Value,LCSC Part #
|
||||
DS1,OLED_26mm_I2C_Provisional,1,128x64 I2C OLED / 0x3C,
|
||||
"R1, R2, R3",R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,3,2k 1%,
|
||||
"SW1, SW2, SW3",SW_TH_Tactile_Omron_B3F-100x,3,B3F-1000,B3F-1000
|
||||
U1,HW678_2x22_Provisional,1,HW678 / S3-N16R8,
|
||||
U2,MAX3243_Reference_Provisional,1,Adafruit MAX3243 / male 6253,
|
||||
|
@@ -0,0 +1,13 @@
|
||||
DS1:1
|
||||
H1:1
|
||||
H2:1
|
||||
H3:1
|
||||
H4:1
|
||||
R1:1
|
||||
R2:1
|
||||
R3:1
|
||||
SW1:1
|
||||
SW2:1
|
||||
SW3:1
|
||||
U1:1
|
||||
U2:1
|
||||
|
@@ -0,0 +1,133 @@
|
||||
P CODE 00
|
||||
P UNITS CUST 0
|
||||
P arrayDim N
|
||||
317/BTN_SELECT VIA MD0118PA00X+063602Y-066122X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+059528Y-052362X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+058563Y-054055X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+057047Y-053681X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+059154Y-053465X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+057874Y-054626X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+056811Y-051909X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+061535Y-053681X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+060650Y-052776X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+057677Y-051909X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+058839Y-050886X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+056260Y-052638X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+059921Y-052047X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+053091Y-051575X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+056713Y-050453X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+051791Y-052421X0276Y0000R000S3
|
||||
317/RS_RI VIA MD0118PA00X+059154Y-054567X0276Y0000R000S3
|
||||
317/RS_RI VIA MD0118PA00X+053091Y-055571X0276Y0000R000S3
|
||||
317/RS_DTR VIA MD0118PA00X+051870Y-053917X0276Y0000R000S3
|
||||
317/RS_DTR VIA MD0118PA00X+058189Y-052402X0276Y0000R000S3
|
||||
317/RS_RTS VIA MD0118PA00X+050866Y-054921X0276Y0000R000S3
|
||||
317/RS_RTS VIA MD0118PA00X+053878Y-054921X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+059567Y-053169X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+060079Y-053524X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+057697Y-055866X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+058189Y-053976X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+051240Y-056417X0276Y0000R000S3
|
||||
317/RS_TX VIA MD0118PA00X+054705Y-053228X0276Y0000R000S3
|
||||
317/RS_TX VIA MD0118PA00X+057343Y-052402X0276Y0000R000S3
|
||||
317/RS_RX VIA MD0118PA00X+056004Y-052933X0276Y0000R000S3
|
||||
317/RS_RX VIA MD0118PA00X+056850Y-052303X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+063858Y-052362X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+062500Y-053209X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+060689Y-054921X0276Y0000R000S3
|
||||
317/RS_OFF_N VIA MD0118PA00X+058563Y-058917X0276Y0000R000S3
|
||||
317/RS_OFF_N VIA MD0118PA00X+056398Y-059213X0276Y0000R000S3
|
||||
317/OLED_SDA VIA MD0118PA00X+051949Y-064272X0276Y0000R000S3
|
||||
317/OLED_SDA VIA MD0118PA00X+053071Y-063602X0276Y0000R000S3
|
||||
317/OLED_SCL VIA MD0118PA00X+044823Y-063130X0276Y0000R000S3
|
||||
317/BTN_NEXT VIA MD0118PA00X+051969Y-067244X0276Y0000R000S3
|
||||
317/BTN_NEXT VIA MD0118PA00X+053169Y-066929X0276Y0000R000S3
|
||||
317/BTN_SELECT SW2 -1 D0394PA00X+060520Y-066142X0669Y0000R000S0
|
||||
317/BTN_SELECT SW2 -1 D0394PA00X+063079Y-066142X0669Y0000R000S0
|
||||
317/GND SW2 -2 D0394PA00X+060520Y-067913X0669Y0000R000S0
|
||||
317/GND SW2 -2 D0394PA00X+063079Y-067913X0669Y0000R000S0
|
||||
367N/C H1 D1260UA00X+040748Y-048228X1260Y0000R000S0
|
||||
317/+3V3 U1 -1 D0394PA00X+042520Y-048425X0669Y0669R000S0
|
||||
317/+3V3 U1 -2 D0394PA00X+042520Y-049425X0669Y0000R000S0
|
||||
317D-(U1-EN-PAD3) U1 -3 D0394PA00X+042520Y-050425X0669Y0000R000S0
|
||||
317/RS_DCD U1 -4 D0394PA00X+042520Y-051425X0669Y0000R000S0
|
||||
317/RS_DSR U1 -5 D0394PA00X+042520Y-052425X0669Y0000R000S0
|
||||
317/RS_RI U1 -6 D0394PA00X+042520Y-053425X0669Y0000R000S0
|
||||
317/RS_DTR U1 -7 D0394PA00X+042520Y-054425X0669Y0000R000S0
|
||||
317/RS_RTS U1 -8 D0394PA00X+042520Y-055425X0669Y0000R000S0
|
||||
317/RS_CTS U1 -9 D0394PA00X+042520Y-056425X0669Y0000R000S0
|
||||
317/RS_TX U1 -10 D0394PA00X+042520Y-057425X0669Y0000R000S0
|
||||
317/RS_RX U1 -11 D0394PA00X+042520Y-058425X0669Y0000R000S0
|
||||
317/RS_VALID U1 -12 D0394PA00X+042520Y-059425X0669Y0000R000S0
|
||||
3171-GPIO3-PAD13) U1 -13 D0394PA00X+042520Y-060425X0669Y0000R000S0
|
||||
317-GPIO46-PAD14) U1 -14 D0394PA00X+042520Y-061425X0669Y0000R000S0
|
||||
317/RS_OFF_N U1 -15 D0394PA00X+042520Y-062425X0669Y0000R000S0
|
||||
317/BTN_PREVIOUS U1 -16 D0394PA00X+042520Y-063425X0669Y0000R000S0
|
||||
317/OLED_SDA U1 -17 D0394PA00X+042520Y-064425X0669Y0000R000S0
|
||||
317/OLED_SCL U1 -18 D0394PA00X+042520Y-065425X0669Y0000R000S0
|
||||
317/BTN_SELECT U1 -19 D0394PA00X+042520Y-066425X0669Y0000R000S0
|
||||
317/BTN_NEXT U1 -20 D0394PA00X+042520Y-067425X0669Y0000R000S0
|
||||
317-(U1-5V-PAD21) U1 -21 D0394PA00X+042520Y-068425X0669Y0000R000S0
|
||||
317/GND U1 -22 D0394PA00X+042520Y-069425X0669Y0000R000S0
|
||||
317/GND U1 -23 D0394PA00X+052520Y-048425X0669Y0000R000S0
|
||||
317-GPIO43-PAD24) U1 -24 D0394PA00X+052520Y-049425X0669Y0000R000S0
|
||||
317-GPIO44-PAD25) U1 -25 D0394PA00X+052520Y-050425X0669Y0000R000S0
|
||||
3171-GPIO1-PAD26) U1 -26 D0394PA00X+052520Y-051425X0669Y0000R000S0
|
||||
3171-GPIO2-PAD27) U1 -27 D0394PA00X+052520Y-052425X0669Y0000R000S0
|
||||
317-GPIO42-PAD28) U1 -28 D0394PA00X+052520Y-053425X0669Y0000R000S0
|
||||
317-GPIO41-PAD29) U1 -29 D0394PA00X+052520Y-054425X0669Y0000R000S0
|
||||
317-GPIO40-PAD30) U1 -30 D0394PA00X+052520Y-055425X0669Y0000R000S0
|
||||
317-GPIO39-PAD31) U1 -31 D0394PA00X+052520Y-056425X0669Y0000R000S0
|
||||
317-GPIO38-PAD32) U1 -32 D0394PA00X+052520Y-057425X0669Y0000R000S0
|
||||
317-GPIO37-PAD33) U1 -33 D0394PA00X+052520Y-058425X0669Y0000R000S0
|
||||
317-GPIO36-PAD34) U1 -34 D0394PA00X+052520Y-059425X0669Y0000R000S0
|
||||
317-GPIO35-PAD35) U1 -35 D0394PA00X+052520Y-060425X0669Y0000R000S0
|
||||
3171-GPIO0-PAD36) U1 -36 D0394PA00X+052520Y-061425X0669Y0000R000S0
|
||||
317-GPIO45-PAD37) U1 -37 D0394PA00X+052520Y-062425X0669Y0000R000S0
|
||||
317-GPIO48-PAD38) U1 -38 D0394PA00X+052520Y-063425X0669Y0000R000S0
|
||||
317-GPIO47-PAD39) U1 -39 D0394PA00X+052520Y-064425X0669Y0000R000S0
|
||||
317-GPIO21-PAD40) U1 -40 D0394PA00X+052520Y-065425X0669Y0000R000S0
|
||||
317-GPIO20-PAD41) U1 -41 D0394PA00X+052520Y-066425X0669Y0000R000S0
|
||||
317-GPIO19-PAD42) U1 -42 D0394PA00X+052520Y-067425X0669Y0000R000S0
|
||||
317/GND U1 -43 D0394PA00X+052520Y-068425X0669Y0000R000S0
|
||||
317/GND U1 -44 D0394PA00X+052520Y-069425X0669Y0000R000S0
|
||||
367N/C H3 D1260UA00X+070079Y-040945X1260Y0000R000S0
|
||||
367N/C U2 D1181UA00X+057049Y-046420X1181Y0000R000S0
|
||||
367N/C U2 D1181UA00X+066549Y-046420X1181Y0000R000S0
|
||||
317/+3V3 U2 -1 D0394PA00X+056299Y-049870X0669Y0669R000S0
|
||||
317/GND U2 -2 D0394PA00X+057299Y-049870X0669Y0000R000S0
|
||||
317/RS_DCD U2 -3 D0394PA00X+058299Y-049870X0669Y0000R000S0
|
||||
317/RS_RX U2 -4 D0394PA00X+059299Y-049870X0669Y0000R000S0
|
||||
317/RS_TX U2 -5 D0394PA00X+060299Y-049870X0669Y0000R000S0
|
||||
317/RS_DTR U2 -6 D0394PA00X+061299Y-049870X0669Y0000R000S0
|
||||
317/RS_DSR U2 -7 D0394PA00X+062299Y-049870X0669Y0000R000S0
|
||||
317/RS_RTS U2 -8 D0394PA00X+063299Y-049870X0669Y0000R000S0
|
||||
317/RS_CTS U2 -9 D0394PA00X+064299Y-049870X0669Y0000R000S0
|
||||
317/RS_RI U2 -10 D0394PA00X+065299Y-049870X0669Y0000R000S0
|
||||
317/RS_VALID U2 -11 D0394PA00X+066299Y-049870X0669Y0000R000S0
|
||||
317/RS_OFF_N U2 -12 D0394PA00X+067299Y-049870X0669Y0000R000S0
|
||||
367N/C H4 D1260UA00X+070079Y-069291X1260Y0000R000S0
|
||||
317/+3V3 R2 -1 D0315PA00X+060299Y-052232X0630Y0000R000S0
|
||||
317/BTN_SELECT R2 -2 D0315PA00X+063299Y-052232X0630Y0000R000S0
|
||||
367N/C H2 D1260UA00X+040748Y-068898X1260Y0000R000S0
|
||||
317/+3V3 R3 -1 D0315PA00X+064630Y-052232X0630Y0000R000S0
|
||||
317/BTN_NEXT R3 -2 D0315PA00X+067630Y-052232X0630Y0000R000S0
|
||||
317/BTN_NEXT SW3 -1 D0394PA00X+065244Y-066142X0669Y0000R000S0
|
||||
317/BTN_NEXT SW3 -1 D0394PA00X+067803Y-066142X0669Y0000R000S0
|
||||
317/GND SW3 -2 D0394PA00X+065244Y-067913X0669Y0000R000S0
|
||||
317/GND SW3 -2 D0394PA00X+067803Y-067913X0669Y0000R000S0
|
||||
317/+3V3 R1 -1 D0315PA00X+055969Y-052232X0630Y0000R000S0
|
||||
317/BTN_PREVIOUS R1 -2 D0315PA00X+058969Y-052232X0630Y0000R000S0
|
||||
317/BTN_PREVIOUS SW1 -1 D0394PA00X+055795Y-066142X0669Y0000R000S0
|
||||
317/BTN_PREVIOUS SW1 -1 D0394PA00X+058354Y-066142X0669Y0000R000S0
|
||||
317/GND SW1 -2 D0394PA00X+055795Y-067913X0669Y0000R000S0
|
||||
317/GND SW1 -2 D0394PA00X+058354Y-067913X0669Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+057370Y-054232X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+057370Y-063091X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+066228Y-054232X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+066228Y-063091X0787Y0000R000S0
|
||||
317/GND DS1 -1 D0394PA00X+060299Y-054134X0669Y0669R000S0
|
||||
317/+3V3 DS1 -2 D0394PA00X+061299Y-054134X0669Y0000R000S0
|
||||
317/OLED_SCL DS1 -3 D0394PA00X+062299Y-054134X0669Y0000R000S0
|
||||
317/OLED_SDA DS1 -4 D0394PA00X+063299Y-054134X0669Y0000R000S0
|
||||
999
|
||||
@@ -0,0 +1,10 @@
|
||||
Designator,Mid X,Mid Y,Rotation,Layer
|
||||
DS1,156.97,-148.95,0.0,top
|
||||
R1,145.97,-132.6685,0.0,top
|
||||
R2,156.97,-132.6685,0.0,top
|
||||
R3,167.97,-132.6685,0.0,top
|
||||
SW1,144.97,-170.25,0.0,top
|
||||
SW2,156.97,-170.25,0.0,top
|
||||
SW3,168.97,-170.25,0.0,top
|
||||
U1,120.7,-149.67,0.0,top
|
||||
U2,156.97,-121.9635,0.0,top
|
||||
|
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/python3
|
||||
"""One-shot body-centred placement with incremental copper repair, not regeneration.
|
||||
|
||||
Writes a candidate only. Review its DRC before promoting it to the native board.
|
||||
Requires the retained pre-alignment snapshot and no editor locks.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Point, LineString, box
|
||||
import route_pcb_draft as r
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BASE = ROOT/'validation/pcb-before-group-alignment.kicad_pcb'
|
||||
MOVES = {'DS1': ((143,136),(143.97,136)),
|
||||
**{f'R{i+1}': ((143+11*i,134),(142.16+11*i,132.6685)) for i in range(3)},
|
||||
**{f'SW{i+1}': ((143+12*i,168),(141.72+12*i,168)) for i in range(3)}}
|
||||
|
||||
|
||||
def unlocked():
|
||||
assert not list(ROOT.glob('*.lck')), 'KiCad editor lock exists'
|
||||
assert not list(ROOT.glob('.~lock.*')), 'Document editor lock exists'
|
||||
|
||||
|
||||
def geom(item):
|
||||
if isinstance(item,p.PAD):
|
||||
if item.GetShape()==p.PAD_SHAPE_CIRCLE:
|
||||
return Point(r.pos(item)).buffer(p.ToMM(item.GetSize().x)/2,quad_segs=32)
|
||||
bb=item.GetBoundingBox()
|
||||
return box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
|
||||
if isinstance(item,p.PCB_VIA):
|
||||
return Point(r.pos(item)).buffer(.35,quad_segs=32)
|
||||
return LineString([xy(item.GetStart()),xy(item.GetEnd())]).buffer(p.ToMM(item.GetWidth())/2,quad_segs=32)
|
||||
|
||||
|
||||
def xy(v):
|
||||
return (p.ToMM(v.x),p.ToMM(v.y))
|
||||
|
||||
|
||||
def layers(item):
|
||||
return set(r.LAYERS) if isinstance(item,(p.PAD,p.PCB_VIA)) else {item.GetLayer()}
|
||||
|
||||
|
||||
def groups(board, net):
|
||||
items=[a for f in board.GetFootprints() for a in f.Pads() if a.GetNetCode()==net]
|
||||
items += [t for t in board.GetTracks() if t.GetNetCode()==net]
|
||||
shapes=[geom(i) for i in items]
|
||||
parents=list(range(len(items)))
|
||||
def root(i):
|
||||
while parents[i]!=i:
|
||||
i=parents[i]
|
||||
return i
|
||||
for i,a in enumerate(items):
|
||||
for j in range(i):
|
||||
if layers(a)&layers(items[j]) and shapes[i].distance(shapes[j])<1e-6:
|
||||
parents[root(i)]=root(j)
|
||||
result={}
|
||||
for i,a in enumerate(items):
|
||||
result.setdefault(root(i),[]).append(a)
|
||||
return list(result.values())
|
||||
|
||||
|
||||
def anchors(group):
|
||||
result=[]
|
||||
for a in group:
|
||||
points=[r.pos(a)] if isinstance(a,(p.PAD,p.PCB_VIA)) else [xy(a.GetStart()),xy(a.GetEnd())]
|
||||
result.extend((point,layers(a)) for point in points)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
unlocked()
|
||||
before=r.PCB.read_bytes()
|
||||
assert before==BASE.read_bytes(), 'Input differs from preserved baseline'
|
||||
protected={name:(ROOT/name).read_bytes() for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl','serial-carrier.kicad_sch')}
|
||||
board=p.LoadBoard(str(r.PCB))
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
for ref,(old,new) in MOVES.items():
|
||||
assert r.pos(fps[ref])==old
|
||||
fps[ref].SetPosition(r.point(new))
|
||||
if ref.startswith('R'):
|
||||
fps[ref].SetValue('2k 1%')
|
||||
pads=[a for f in board.GetFootprints() for a in f.Pads()]
|
||||
removed=[]
|
||||
# Only remove copper made illegal by a moved pad/hole. Everything else is
|
||||
# initially retained, including all unrelated nets and manual routing.
|
||||
moved_pads=[a for ref in MOVES for a in fps[ref].Pads()]
|
||||
for t in list(board.GetTracks()):
|
||||
if any((not a.GetNetCode() or a.GetNetCode()!=t.GetNetCode()) and
|
||||
geom(t).distance(geom(a))<.25-1e-6 for a in moved_pads):
|
||||
removed.append(t.m_Uuid.AsString())
|
||||
board.Delete(t)
|
||||
|
||||
repaired=[]
|
||||
deadline=time.monotonic()+600
|
||||
for ref,(old,new) in MOVES.items():
|
||||
for pad in fps[ref].Pads():
|
||||
if not pad.GetNumber(): continue
|
||||
target=r.pos(pad)
|
||||
start=(round(target[0]+old[0]-new[0],6),round(target[1]+old[1]-new[1],6))
|
||||
if geom(pad).covers(Point(start)): continue
|
||||
attached=[t for t in board.GetTracks() if t.GetNetCode()==pad.GetNetCode() and start in (xy(t.GetStart()),xy(t.GetEnd()))]
|
||||
if not attached: continue
|
||||
width=.5 if pad.GetNetname() in ('/+3V3','/GND') else .25
|
||||
blocked,via=r.masks(board,pads,pad.GetNetCode(),width)
|
||||
allowed=set.union(*(layers(t) for t in attached))
|
||||
for z,layer in enumerate(r.LAYERS):
|
||||
if layer not in allowed: blocked[r.node(start,z)]=True
|
||||
path,_=r.astar(blocked,via,start,target,deadline,500000)
|
||||
assert path, ('Cannot extend moved pad',ref,pad.GetNumber())
|
||||
r.add_path(board,path,start,target,pad.GetNetCode(),width)
|
||||
repaired.append(dict(net=pad.GetNetname(),start=start,end=target))
|
||||
nets=sorted({a.GetNetCode() for a in pads if a.GetNumber() and a.GetNetCode()}, key=lambda n: (board.FindNet(n).GetNetname() in ('/+3V3','/GND'), not board.FindNet(n).GetNetname().startswith('/OLED'), n))
|
||||
for net in nets:
|
||||
while len(parts:=groups(board,net))>1:
|
||||
options=[]
|
||||
for i,aa in enumerate(parts):
|
||||
for bb in parts[i+1:]:
|
||||
for a,la in anchors(aa):
|
||||
for b,lb in anchors(bb):
|
||||
options.append(((a[0]-b[0])**2+(a[1]-b[1])**2,a,la,b,lb))
|
||||
options.sort(key=lambda v:v[0])
|
||||
name=parts[0][0].GetNetname()
|
||||
width=.5 if name in ('/+3V3','/GND') else .25
|
||||
blocked,via=r.masks(board,pads,net,width)
|
||||
success=False
|
||||
for _,a,la,b,lb in options[:400]:
|
||||
mask=blocked.copy()
|
||||
for z,layer in enumerate(r.LAYERS):
|
||||
if layer not in la: mask[r.node(a,z)]=True
|
||||
if layer not in lb: mask[r.node(b,z)]=True
|
||||
path,_=r.astar(mask,via,a,b,deadline,250000)
|
||||
if path:
|
||||
r.add_path(board,path,a,b,net,width)
|
||||
repaired.append(dict(net=name,start=a,end=b))
|
||||
print(repaired[-1],flush=True)
|
||||
success=True
|
||||
break
|
||||
assert time.monotonic()<deadline, 'Bounded routing deadline reached'
|
||||
assert success, ('Cannot reconnect retained copper',name)
|
||||
unlocked()
|
||||
assert r.PCB.read_bytes()==before
|
||||
assert all((ROOT/n).read_bytes()==data for n,data in protected.items())
|
||||
out=ROOT/'validation/pcb-group-candidate.kicad_pcb'
|
||||
# Let KiCad identify true dangling stubs after repair. Do not prune valid
|
||||
# retained routes before reconnecting the moved pads.
|
||||
from add_device_silkscreen import add_outlines
|
||||
drc=ROOT/'validation/pcb-group-candidate-drc.json'
|
||||
for attempt in range(20):
|
||||
add_outlines(board)
|
||||
assert p.SaveBoard(str(out),board,True)
|
||||
subprocess.run(['kicad-cli','pcb','drc','--format','json','--all-track-errors','--severity-all','-o',str(drc),str(out)],check=True,timeout=60)
|
||||
results=json.loads(drc.read_text())
|
||||
ids={i['uuid'] for v in results['violations'] if v['type']=='track_dangling' for i in v['items']}
|
||||
if not ids:
|
||||
assert not results['unconnected_items'], results['unconnected_items']
|
||||
assert all(v['type']=='lib_footprint_issues' for v in results['violations']), results['violations']
|
||||
break
|
||||
for t in list(board.GetTracks()):
|
||||
if t.m_Uuid.AsString() in ids:
|
||||
removed.append(t.m_Uuid.AsString())
|
||||
board.Delete(t)
|
||||
else:
|
||||
raise AssertionError('Dangling-stub cleanup did not converge')
|
||||
report=dict(baseline_sha256=hashlib.sha256(before).hexdigest(),moves_absolute_mm=MOVES,
|
||||
removed_track_uuids=removed,repaired_connections=repaired,
|
||||
retained_tracks=sum(t.m_Uuid.AsString() not in removed for t in p.LoadBoard(str(BASE)).GetTracks()),
|
||||
protected_sha256={n:hashlib.sha256(data).hexdigest() for n,data in protected.items()})
|
||||
(ROOT/'validation/pcb-group-migration.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove presentation-only PCB draft labels; retain all technical cautions."""
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
TITLE = "Serial Swiss Army Knife - 82x80 mm carrier"
|
||||
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
|
||||
SILK = "DRAFT - NOT FOR FAB"
|
||||
|
||||
|
||||
def remove_labels(board):
|
||||
title = board.GetTitleBlock()
|
||||
assert title.GetTitle() in (TITLE, TITLE + " draft")
|
||||
assert title.GetRevision() in ("A1", "A1 DRAFT")
|
||||
assert title.GetComment(0) in ("", COMMENT, "NOT FOR FABRICATION - " + COMMENT)
|
||||
title.SetTitle(TITLE)
|
||||
title.SetRevision("A1")
|
||||
if title.GetComment(0):
|
||||
title.SetComment(0, COMMENT)
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if isinstance(drawing, p.PCB_TEXT) and drawing.GetText() == SILK:
|
||||
assert drawing.GetLayer() == p.F_SilkS
|
||||
board.Remove(drawing)
|
||||
|
||||
|
||||
def expected_tree(tree):
|
||||
"""Exact permitted delta for preservation audits, independent of pcbnew edits."""
|
||||
import copy
|
||||
result = copy.deepcopy(tree)
|
||||
for item in list(result):
|
||||
if not isinstance(item, list):
|
||||
continue
|
||||
if item[0] == "title_block":
|
||||
for field in item[1:]:
|
||||
replacements = {
|
||||
('title', '"' + TITLE + ' draft"'): '"' + TITLE + '"',
|
||||
('rev', '"A1 DRAFT"'): '"A1"',
|
||||
('comment', '"NOT FOR FABRICATION - ' + COMMENT + '"'): '"' + COMMENT + '"',
|
||||
}
|
||||
replacement = replacements.get((field[0], field[-1]))
|
||||
if replacement is not None:
|
||||
field[-1] = replacement
|
||||
elif item[0] == "gr_text" and item[1] == '"' + SILK + '"':
|
||||
assert ['layer', '"F.SilkS"'] in item
|
||||
result.remove(item)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
from validate_u1_update import parse, canonical
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise RuntimeError("Close PCB Editor before changing labels")
|
||||
original = PCB.read_bytes()
|
||||
settings = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
|
||||
expected = expected_tree(parse(PCB))
|
||||
board = p.LoadBoard(str(PCB))
|
||||
remove_labels(board)
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-labels-", dir=ROOT) as temporary:
|
||||
result = Path(temporary) / PCB.name
|
||||
assert p.SaveBoard(str(result), board, True)
|
||||
assert canonical(parse(result)) == canonical(expected), "Change beyond exact label edits"
|
||||
assert canonical(expected_tree(parse(result))) == canonical(expected), "Label cleanup not idempotent"
|
||||
if lock.exists() or PCB.read_bytes() != original:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert all(path.read_bytes() == data for path, data in settings.items()), "Project settings changed"
|
||||
result.replace(PCB)
|
||||
print("Removed draft presentation labels; exact preservation and idempotence passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -156,7 +156,7 @@
|
||||
"min_track_width": 0.25,
|
||||
"min_via_annular_width": 0.1,
|
||||
"min_via_diameter": 0.7,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"solder_mask_to_copper_clearance": 0.005,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
|
||||
@@ -234,9 +234,9 @@
|
||||
(symbol (lib_id "Carrier:MAX3243_Module_Reference") (at 228.6 96.52 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "1127195e-e393-5d1d-9119-b25d10556144")
|
||||
(property "Reference" "U2" (at 228.6 73.66 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "Adafruit MAX3243 / male 6253" (at 228.6 76.2 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Footprint" "" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Footprint" "Carrier:MAX3243_Reference_Provisional" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Status" "VERIFY male header numbering; footprint intentionally unassigned" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Status" "User confirmed male pinout/header/footprint and pin-by-pin connections" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet_source" "adafruit-research.md; reference female 5988 CAD" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "14d1ea7b-f53a-592f-9305-545890b94099"))
|
||||
(pin "2" (uuid "b2027da1-66f1-5c08-b29b-b63e30a160ff"))
|
||||
@@ -296,10 +296,10 @@
|
||||
(label "OLED_SDA" (at 200.66 156.21 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "dc86b9ed-3904-50d0-8423-c02004518d88"))
|
||||
(symbol (lib_id "Carrier:R") (at 327.66 88.9 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "defb78eb-632f-5f9d-a59d-dffbb1d31dec")
|
||||
(property "Reference" "R1" (at 327.66 82.804 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2.2k 1%" (at 327.66 85.344 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2k 1%" (at 327.66 85.344 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "79a88116-1a52-5fcb-855e-da54defa9400"))
|
||||
(pin "2" (uuid "b56a5773-68f5-5c6d-936a-8289c11714de"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R1") (unit 1)))))
|
||||
@@ -315,7 +315,7 @@
|
||||
(property "Manufacturer" "Omron" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "MPN" "B3F-1000" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Function" "Previous / back" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "Live stock not verified" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "User reports purchased" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "0e490c78-3450-5a5b-b7d3-d55c32de988b"))
|
||||
(pin "2" (uuid "6bbc82d1-8cfb-5fd5-832c-8df2775a89c9"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW1") (unit 1)))))
|
||||
@@ -326,10 +326,10 @@
|
||||
(text "Previous / back" (at 302.26 110.49 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "d84df288-be52-5de4-899d-bf29ba78f4f2"))
|
||||
(symbol (lib_id "Carrier:R") (at 327.66 124.46 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "405055c0-35ab-5334-ad4d-30d45c5ef6f2")
|
||||
(property "Reference" "R2" (at 327.66 118.364 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2.2k 1%" (at 327.66 120.904 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2k 1%" (at 327.66 120.904 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "c23dbef2-d61d-56fa-a158-bb9912ef48ac"))
|
||||
(pin "2" (uuid "f60eb205-60d1-5d8f-ac84-ca04f66e7106"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R2") (unit 1)))))
|
||||
@@ -345,7 +345,7 @@
|
||||
(property "Manufacturer" "Omron" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "MPN" "B3F-1000" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Function" "Select / confirm" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "Live stock not verified" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "User reports purchased" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "19bafbc6-666f-5e53-a4ef-265057c6dc8a"))
|
||||
(pin "2" (uuid "9e3f47ba-92ac-5503-9d87-6b5590a70ccb"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW2") (unit 1)))))
|
||||
@@ -356,10 +356,10 @@
|
||||
(text "Select / confirm" (at 302.26 146.05 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "c28f2639-f392-552b-858e-b031aabc8c0a"))
|
||||
(symbol (lib_id "Carrier:R") (at 327.66 160.02 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "78f9a74f-5d91-5687-b50e-68176333df23")
|
||||
(property "Reference" "R3" (at 327.66 153.924 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2.2k 1%" (at 327.66 156.464 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Value" "2k 1%" (at 327.66 156.464 0) (effects (font (size 1.27 1.27)) ))
|
||||
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "At least 0.125 W; exact resistor MPN pending" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Rating" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "d871ee2e-bc82-5f23-a436-7b7ab74b73a9"))
|
||||
(pin "2" (uuid "96528b84-ba49-5f38-8445-b6ca2cadc93b"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R3") (unit 1)))))
|
||||
@@ -375,7 +375,7 @@
|
||||
(property "Manufacturer" "Omron" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "MPN" "B3F-1000" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Function" "Next" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "Live stock not verified" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Procurement" "User reports purchased" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "85beb3b7-e16f-5450-ba94-5711245a7b25"))
|
||||
(pin "2" (uuid "300c4196-eb91-5e58-a9aa-c84b8f7c4219"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW3") (unit 1)))))
|
||||
@@ -389,12 +389,12 @@
|
||||
(text "U1: complete development board" (at 53.34 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "320a8f4e-ff8b-5100-845b-dfe249b39a2e"))
|
||||
(text "Antenna up / USB down: left row 1-22, right row 23-44.\nNC marks mean unused on carrier, not unused inside module." (at 53.34 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "649df1fa-48c0-580d-aeb8-d019ffd0b61d"))
|
||||
(text "U2: complete RS-232 breakout" (at 177.8 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "5226007c-1904-547d-8ddb-008d14b83d51"))
|
||||
(text "Male 6253 selected. Numbers from published 5988 female CAD.\nVERIFY male header before routing; no footprint assigned.\nDE-9 and charge pump are already on the module." (at 177.8 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "9b62e7ca-695a-51af-bbbe-e7ee4ee45c08"))
|
||||
(text "Male 6253 selected. Numbers from published 5988 female CAD.\nMale mapping user-reviewed; reference footprint assigned.\nDE-9 and charge pump are already on the module." (at 177.8 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "9b62e7ca-695a-51af-bbbe-e7ee4ee45c08"))
|
||||
(text "Controls: active-low, firmware-debounced" (at 292.1 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "9d91985c-7375-5177-9a5e-8280e3bcaff6"))
|
||||
(text "External 2.2k pull-ups: ~1.5 mA pressed at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending." (at 292.1 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "b2dd72f5-86fc-5ce1-b09a-67a6fe9404d4"))
|
||||
(text "External 2k 1% pull-ups: ~1.65 mA at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending." (at 292.1 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "b2dd72f5-86fc-5ce1-b09a-67a6fe9404d4"))
|
||||
(text "DS1: front view GND / VCC / SCL / SDA" (at 177.8 128.27 0) (effects (font (size 1.524 1.524)) (justify left top)) (uuid "7b57748d-38ca-5712-97f9-bf6dab6dafaf"))
|
||||
(text "Power at 3.3 V only. Verify on-module I2C pull-ups;\nexternal values intentionally not guessed." (at 177.8 165.1 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "fb23ec11-40f8-56c7-b78b-0da2d40e4b72"))
|
||||
(text "POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nVerify USB power isolation before attaching two powered hosts.\nKeep both USB connectors and reset/boot buttons accessible." (at 15.24 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "cadf6b77-e888-5f04-b4f6-3873114844ca"))
|
||||
(text "Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups." (at 177.8 165.1 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "fb23ec11-40f8-56c7-b78b-0da2d40e4b72"))
|
||||
(text "POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nUSB nonisolated accepted; backfeed safety not verified.\nKeep both USB connectors and reset/boot buttons accessible." (at 15.24 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "cadf6b77-e888-5f04-b4f6-3873114844ca"))
|
||||
(text "RS-232 / ELECTRICAL LIMITS\nTX/RX and modem labels are at the ESP32 logic side.\nOFF_N is active low; GPIO9 must remain open-drain.\nBreakout provides its OFF pull-up. No galvanic isolation.\nNo carrier connection to raw RS-232 voltages.\nCarrier GND, USB GND and RS-232 signal ground are common.\nNever power OLED or MAX3243 logic from 5 V." (at 152.4 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "0f7da388-3edb-5cc3-9937-13492397a221"))
|
||||
(text "BEFORE PCB RELEASE\nVerify male breakout pin order/footprint.\nConfirm header/socket finished-hole requirements.\nComplete module courtyards and antenna clearance.\nVerify 3.3 V budget, OLED pull-ups and USB power path.\nChoose mounting hardware and confirm button stock.\nERC is not hardware or fabrication approval." (at 292.1 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "1df2bd9e-fbff-5047-92a9-09f33d7cd7ac"))
|
||||
(text "USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\nERC is not hardware or fabrication approval." (at 292.1 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "1df2bd9e-fbff-5047-92a9-09f33d7cd7ac"))
|
||||
)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guarded accepted-fit label cleanup and A3 worksheet; never move board items."""
|
||||
import copy
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
|
||||
USB = "USB ACCESS - VERIFY"
|
||||
OLD_CAPTION = "Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending"
|
||||
CAPTION = "Nominal RS-232 metal verified; RF qualification not established"
|
||||
|
||||
|
||||
def expected_tree(tree):
|
||||
result = copy.deepcopy(tree)
|
||||
for item in list(result):
|
||||
if not isinstance(item, list):
|
||||
continue
|
||||
if item[0] == "paper":
|
||||
assert item in (["paper", '"A4"'], ["paper", '"A3"'])
|
||||
item[1] = '"A3"'
|
||||
elif item[0] == "title_block":
|
||||
for field in list(item[1:]):
|
||||
if field == ["comment", "1", '"' + COMMENT + '"']:
|
||||
item.remove(field)
|
||||
elif item[0] == "gr_text":
|
||||
if item[1] == '"' + USB + '"':
|
||||
assert ['layer', '"Dwgs.User"'] in item
|
||||
result.remove(item)
|
||||
elif item[1] == '"' + OLD_CAPTION + '"':
|
||||
assert ['layer', '"Dwgs.User"'] in item
|
||||
item[1] = '"' + CAPTION + '"'
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
from validate_u1_update import parse, canonical
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise RuntimeError("Close PCB Editor before changing presentation")
|
||||
original = PCB.read_bytes()
|
||||
protected = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
|
||||
expected = expected_tree(parse(PCB))
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-presentation-", dir=ROOT) as temporary:
|
||||
candidate = Path(temporary) / PCB.name
|
||||
# PAGE_INFO is opaque in this pcbnew binding. Load the exact paper-only
|
||||
# substitution, then use the native saver for all label changes.
|
||||
text = original.decode()
|
||||
assert text.count('(paper "A4")') + text.count('(paper "A3")') == 1
|
||||
candidate.write_text(text.replace('(paper "A4")', '(paper "A3")'))
|
||||
board = p.LoadBoard(str(candidate))
|
||||
title = board.GetTitleBlock()
|
||||
assert title.GetComment(0) in (COMMENT, "")
|
||||
title.SetComment(0, "")
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if isinstance(drawing, p.PCB_TEXT):
|
||||
if drawing.GetText() == USB:
|
||||
assert drawing.GetLayer() == p.Dwgs_User
|
||||
board.Remove(drawing)
|
||||
elif drawing.GetText() == OLD_CAPTION:
|
||||
drawing.SetText(CAPTION)
|
||||
assert p.SaveBoard(str(candidate), board, True)
|
||||
assert canonical(parse(candidate)) == canonical(expected), "Unexpected physical/content change"
|
||||
assert canonical(expected_tree(parse(candidate))) == canonical(expected), "Not idempotent"
|
||||
if lock.exists() or PCB.read_bytes() != original:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert all(path.read_bytes() == data for path, data in protected.items()), "Settings changed"
|
||||
candidate.replace(PCB)
|
||||
print("A3 worksheet / accepted-fit labels updated; exact preservation and idempotence PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize measured U1 F.Fab graphics only; never replace the footprint."""
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
NS = uuid.UUID("36693251-e1f8-44e9-a8a9-e201bd658ceb")
|
||||
|
||||
|
||||
def library_graphics():
|
||||
fp = p.FootprintLoad(str(ROOT / "Carrier.pretty"), "HW678_2x22_Provisional")
|
||||
if fp is None:
|
||||
raise RuntimeError("Cannot load measured HW678 footprint")
|
||||
return fp
|
||||
|
||||
|
||||
def signature(g, origin):
|
||||
def point(v):
|
||||
return v.x-origin.x, v.y-origin.y
|
||||
if isinstance(g, p.PCB_SHAPE):
|
||||
return ("shape", g.GetShape(), point(g.GetStart()), point(g.GetEnd()), g.GetWidth())
|
||||
return ("text", g.GetText(), point(g.GetPosition()),
|
||||
(g.GetTextSize().x, g.GetTextSize().y), g.GetTextThickness())
|
||||
|
||||
|
||||
def measured_items(fp):
|
||||
# Generic library captions would overlap existing board labels.
|
||||
return sorted((g for g in fp.GraphicalItems() if g.GetLayer() == p.F_Fab
|
||||
and not (isinstance(g, p.PCB_TEXT) and g.GetText() in (
|
||||
"PROVISIONAL CONNECTOR FIT", "ANTENNA UP / USB DOWN"))),
|
||||
key=lambda g: signature(g, fp.GetPosition()))
|
||||
|
||||
|
||||
def verify(board):
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
assert len([g for g in u1.GraphicalItems() if g.GetLayer() == p.F_Fab]) == 6
|
||||
library = library_graphics()
|
||||
expected = [signature(g, library.GetPosition()) for g in measured_items(library)]
|
||||
actual = [signature(g, u1.GetPosition()) for g in measured_items(u1)]
|
||||
assert actual == expected, "Embedded U1 F.Fab differs from measured library; run update_u1_graphics.py"
|
||||
|
||||
|
||||
def main():
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before updating U1 graphics")
|
||||
before = PCB.read_bytes()
|
||||
project = ROOT / "serial-carrier.kicad_pro"
|
||||
project_before = project.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
library = library_graphics()
|
||||
sources = measured_items(library)
|
||||
assert len(sources) == 6, "Review changed measured library before integration"
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(len(sources))}
|
||||
existing = list(u1.GraphicalItems())
|
||||
assert all(g.m_Uuid.AsString() in owned for g in existing if g.GetLayer() == p.F_Fab), "Unowned U1 fabrication graphics; review manually"
|
||||
if [signature(g, u1.GetPosition()) for g in measured_items(u1)] == [
|
||||
signature(g, library.GetPosition()) for g in sources]:
|
||||
verify(board)
|
||||
print("Embedded measured U1 graphics already current; no board write")
|
||||
return
|
||||
for g in existing:
|
||||
if g.m_Uuid.AsString() in owned:
|
||||
u1.Remove(g)
|
||||
for i, source in enumerate(sources):
|
||||
graphic = source.Duplicate()
|
||||
graphic.SetParent(u1)
|
||||
graphic.SetUuid(p.KIID(str(uuid.uuid5(NS, str(i)))))
|
||||
graphic.Move(u1.GetPosition() - library.GetPosition())
|
||||
u1.Add(graphic)
|
||||
verify(board)
|
||||
temp = ROOT / "validation/pcb-u1-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp), board, True)
|
||||
verify(p.LoadBoard(str(temp)))
|
||||
if lock.exists() or PCB.read_bytes() != before:
|
||||
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
|
||||
assert project.read_bytes() == project_before, "Project settings changed"
|
||||
temp.replace(PCB)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
print("Embedded measured U1 graphics; existing footprint fields, pads and routing retained")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -117,9 +117,23 @@ class FootprintRegression(unittest.TestCase):
|
||||
for row, x in enumerate((0, 25.4)) for index in range(22)
|
||||
}
|
||||
self.check_header(footprint, positions)
|
||||
self.assertFalse(any(isinstance(g, k.PCB_SHAPE)
|
||||
for g in footprint.GraphicalItems()),
|
||||
"HW678 must not acquire guessed outlines or envelopes")
|
||||
shapes = [g for g in footprint.GraphicalItems() if isinstance(g, k.PCB_SHAPE)]
|
||||
expected = [
|
||||
(k.SHAPE_T_RECT, mm(-1.365, -1.365), mm(26.725, 54.705), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_RECT, mm(4.045, -8.255), mm(21.675, -1.365), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_SEGMENT, mm(2.795, 54.705), mm(11.665, 54.705), k.FromMM(0.3)),
|
||||
(k.SHAPE_T_SEGMENT, mm(14.195, 54.705), mm(23.065, 54.705), k.FromMM(0.3)),
|
||||
]
|
||||
self.assertEqual(Counter((g.GetShape(), xy(g.GetStart()), xy(g.GetEnd()),
|
||||
g.GetWidth()) for g in shapes), Counter(expected))
|
||||
for shape in shapes:
|
||||
self.assertEqual(shape.GetLayer(), k.F_Fab)
|
||||
self.assertFalse(shape.IsAnyFill())
|
||||
# Only measured planar bounds/mouth spans: no invented shell depth or RF area.
|
||||
self.assertAlmostEqual(26.725 - (-1.365), 28.09)
|
||||
self.assertAlmostEqual(54.705 - (-1.365), 56.07)
|
||||
self.assertAlmostEqual(54.705 - (-8.255), 62.96)
|
||||
self.assertAlmostEqual(26.725 - 25.4, 1.325)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/python3
|
||||
"""Exact authorized placement/value delta, retained copper and body geometry audit."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from validate_u1_update import parse, canonical, ident
|
||||
from add_device_silkscreen import NS
|
||||
from refine_group_placement import ROOT, BASE, MOVES
|
||||
|
||||
METADATA = {
|
||||
**{f'R{i}': ('Rating', 'Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified') for i in range(1,4)},
|
||||
**{f'SW{i}': ('Procurement', 'User reports purchased') for i in range(1,4)},
|
||||
'U2': ('Status', 'User confirmed male pinout/header/footprint and pin-by-pin connections'),
|
||||
}
|
||||
|
||||
|
||||
def expected_identity(board):
|
||||
"""Apply only the seven authorized placements and three resistor values."""
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
for ref,(old,new) in MOVES.items():
|
||||
fps[ref].SetPosition(p.VECTOR2I(p.FromMM(new[0]),p.FromMM(new[1])))
|
||||
if ref.startswith('R'): fps[ref].SetValue('2k 1%')
|
||||
return board
|
||||
|
||||
|
||||
def audit():
|
||||
assert hashlib.sha256(BASE.read_bytes()).hexdigest()=='b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192'
|
||||
old=parse(BASE)
|
||||
new=parse(ROOT/'serial-carrier.kicad_pcb')
|
||||
from update_presentation import expected_tree
|
||||
wanted=expected_tree(old)
|
||||
for f in wanted:
|
||||
if not isinstance(f,list) or f[0]!='footprint': continue
|
||||
ref=next(v[2].strip('"') for v in f if isinstance(v,list) and v[:2]==['property','"Reference"'])
|
||||
if ref in METADATA:
|
||||
key,value=METADATA[ref]
|
||||
next(v for v in f if isinstance(v,list) and v[:2]==['property',json.dumps(key)])[2]=json.dumps(value)
|
||||
if ref in MOVES:
|
||||
at=next(v for v in f if isinstance(v,list) and v[0]=='at')
|
||||
at[1:3]=[format(x,'.10g') for x in MOVES[ref][1]]
|
||||
if ref.startswith('R'):
|
||||
next(v for v in f if isinstance(v,list) and v[:2]==['property','"Value"'])[2]='"2k 1%"'
|
||||
silk={str(uuid.uuid5(NS,str(i))) for i in range(4096)}
|
||||
def without_copper(tree):
|
||||
return [v for v in tree if not isinstance(v,list) or v[0] not in ('segment','via')]
|
||||
assert canonical(without_copper(wanted),silk)==canonical(without_copper(new),silk), 'Unexpected non-copper edit'
|
||||
before={ident(v):v for v in old if isinstance(v,list) and v[0] in ('segment','via')}
|
||||
after={ident(v):v for v in new if isinstance(v,list) and v[0] in ('segment','via')}
|
||||
migration=json.loads((ROOT/'validation/pcb-group-migration.json').read_text())
|
||||
removed=set(before)-set(after)
|
||||
assert removed==set(before)&set(migration['removed_track_uuids'])
|
||||
assert all(before[k]==after[k] for k in before.keys()&after.keys()), 'Retained copper changed'
|
||||
assert len(before.keys()&after.keys())==476
|
||||
for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl'):
|
||||
assert hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==migration['protected_sha256'][name]
|
||||
board=p.LoadBoard(str(ROOT/'serial-carrier.kicad_pcb'))
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
def xy(v): return p.ToMM(v.x),p.ToMM(v.y)
|
||||
# Published U2 F.Fab outline includes the bare PCB beyond the header row.
|
||||
u2pts=[xy(v) for g in fps['U2'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
|
||||
axis=(min(x for x,y in u2pts)+max(x for x,y in u2pts))/2
|
||||
bottom=max(y for x,y in u2pts)
|
||||
oled=next(g for g in fps['DS1'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetStart()==fps['DS1'].GetPosition())
|
||||
assert abs((xy(oled.GetStart())[0]+xy(oled.GetEnd())[0])/2-axis)<1e-6
|
||||
resistor_boxes=[g for i in range(1,4) for g in fps[f'R{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetShape()==p.SHAPE_T_RECT]
|
||||
assert len(resistor_boxes)==3
|
||||
left=min(xy(g.GetStart())[0] for g in resistor_boxes)
|
||||
right=max(xy(g.GetEnd())[0] for g in resistor_boxes)
|
||||
assert abs((left+right)/2-axis)<1e-6
|
||||
topgap=min(xy(g.GetStart())[1] for g in resistor_boxes)-bottom
|
||||
bottomgap=xy(oled.GetStart())[1]-max(xy(g.GetEnd())[1] for g in resistor_boxes)
|
||||
assert abs(topgap-2.0815)<1e-6 and abs(bottomgap-topgap)<1e-6
|
||||
switchpts=[xy(v) for i in range(1,4) for g in fps[f'SW{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
|
||||
assert abs((min(x for x,y in switchpts)+max(x for x,y in switchpts))/2-axis)<1e-6
|
||||
switchgap=min(y for x,y in switchpts)-xy(oled.GetEnd())[1]
|
||||
assert abs(switchgap-5.25)<1e-6
|
||||
result=dict(common_axis_absolute_x_mm=axis,resistor_to_rs232_body_gap_mm=round(topgap,6),resistor_to_oled_body_gap_mm=round(bottomgap,6),oled_to_button_body_gap_mm=switchgap,
|
||||
original_copper_items=len(before),retained_copper_items=476,removed_original_copper_items=len(removed),new_copper_items=len(after.keys()-before.keys()),
|
||||
exact_unrelated_content_preserved=True,project_settings_preserved=True,baseline_sha256=migration['baseline_sha256'],board_sha256=hashlib.sha256((ROOT/'serial-carrier.kicad_pcb').read_bytes()).hexdigest())
|
||||
(ROOT/'validation/pcb-group-alignment.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
return result
|
||||
|
||||
if __name__=='__main__': print(json.dumps(audit(),indent=2))
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only PCB checks and fresh KiCad reports; does not regenerate or save PCB.
|
||||
|
||||
Requires KiCad's pcbnew binding in system Python. The known U2 footprint parity
|
||||
warning is reported explicitly, not hidden or added to KiCad's exclusions.
|
||||
Requires KiCad's pcbnew binding in system Python. Schematic footprint parity
|
||||
must be clean, including the user-confirmed U2 reference footprint assignment.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
PCB=ROOT/"serial-carrier.kicad_pcb"
|
||||
@@ -21,6 +22,14 @@ def run(*args):
|
||||
run("kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(OUT/"pcb-source-netlist.xml"),str(ROOT/"serial-carrier.kicad_sch"))
|
||||
run("kicad-cli","pcb","drc","--format","json","--all-track-errors","--severity-all","--schematic-parity","-o",str(OUT/"pcb-parity-drc.json"),str(PCB))
|
||||
board=p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
from update_presentation import expected_tree, CAPTION
|
||||
from validate_u1_update import parse, canonical
|
||||
tree=parse(PCB)
|
||||
assert ['paper', '"A3"'] in tree, "A3 worksheet required to clear the title block"
|
||||
assert canonical(expected_tree(tree)) == canonical(tree), "Stale presentation labels"
|
||||
assert board.GetTitleBlock().GetComment(0) == ""
|
||||
assert any(isinstance(g,p.PCB_TEXT) and g.GetText()==CAPTION for g in board.GetDrawings())
|
||||
source=ET.parse(OUT/"pcb-source-netlist.xml").getroot()
|
||||
comps={c.get("ref"):c for c in source.findall("components/comp")}
|
||||
footprints={f.GetReference():f for f in board.GetFootprints()}
|
||||
@@ -36,9 +45,9 @@ expected_edges={frozenset((a,b)) for a,b in zip(expected_outline,expected_outlin
|
||||
actual_edges={frozenset((round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5))
|
||||
for v in (d.GetStart(),d.GetEnd())) for d in outline}
|
||||
assert actual_edges==expected_edges
|
||||
placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43,36),
|
||||
"R1":(43,34),"R2":(54,34),"R3":(65,34),
|
||||
"SW1":(43,68),"SW2":(55,68),"SW3":(67,68),
|
||||
placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43.97,36),
|
||||
"R1":(42.16,32.6685),"R2":(53.16,32.6685),"R3":(64.16,32.6685),
|
||||
"SW1":(41.72,68),"SW2":(53.72,68),"SW3":(65.72,68),
|
||||
"H1":(3.5,22.5),"H2":(3.5,75),"H3":(78,4),"H4":(78,76)}
|
||||
for ref,(x,y) in placements.items():
|
||||
position=footprints[ref].GetPosition()
|
||||
@@ -70,7 +79,7 @@ for ref,comp in comps.items():
|
||||
assert fp.GetPath().AsString().endswith("/"+comp.findtext("tstamps")),ref
|
||||
wanted=comp.findtext("footprint")
|
||||
if ref=="U2":
|
||||
assert not wanted
|
||||
assert wanted=="Carrier:MAX3243_Reference_Provisional"
|
||||
assert fp.GetFPIDAsString()=="Carrier:MAX3243_Reference_Provisional"
|
||||
else:
|
||||
assert fp.GetFPIDAsString()==wanted,ref
|
||||
@@ -100,8 +109,7 @@ report=json.loads((OUT/"pcb-parity-drc.json").read_text())
|
||||
assert not report["violations"],report["violations"]
|
||||
assert not report["unconnected_items"],report["unconnected_items"]
|
||||
parity=report["schematic_parity"]
|
||||
assert len(parity)==1 and parity[0]["type"]=="footprint_symbol_mismatch",parity
|
||||
assert len(parity[0]["items"])==1 and parity[0]["items"][0]["uuid"]==footprints["U2"].m_Uuid.AsString()
|
||||
assert not parity,parity
|
||||
summary={
|
||||
"carrier_bounds_mm":[82,80],
|
||||
"outline_local_mm":expected_outline,
|
||||
@@ -118,12 +126,14 @@ summary={
|
||||
"physical_DRC_violations":0,
|
||||
"unconnected_items":0,
|
||||
"schematic_pad_nets_match":True,
|
||||
"known_parity_warning":"U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
|
||||
"schematic_parity_violations":0,
|
||||
"project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(),
|
||||
"ignored_DRC_checks":report["ignored_checks"],
|
||||
"not_validated":["male module pin map/header fit", "ESP32 geometry and complete assembly envelope/heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"],
|
||||
"user_accepted":["module dimensions/visual fit and USB clearance", "male pinout/header/footprint and pin-by-pin review", "M3 carrier/module and M2 display hardware", "RF keepout and power/ground routing"],
|
||||
"not_independently_verified":["reported 10 mm assembled module elevation", "regulator margin", "USB backfeed safety", "OLED onboard pull-up resistance/rail", "manufacturer RF qualification", "fabrication process/certification"],
|
||||
"new_hardware_tests":False,
|
||||
}
|
||||
(OUT/"pcb-validation-summary.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print("PCB draft verified: 82 x 80 mm; 2 copper layers; 0 physical DRC violations; 0 unrouted.")
|
||||
print("All schematic pad nets match. One expected U2 footprint parity warning remains visible.")
|
||||
print("Not fabrication-ready; see pcb-draft-notes.md and ignored checks in the DRC report.")
|
||||
print("All schematic pad nets and footprint assignments match. Zero schematic parity violations.")
|
||||
print("Not fabrication-ready; see ../../docs/devel/hardware/PCB/pcb-draft-notes.md and ignored checks in the DRC report.")
|
||||
|
||||
@@ -57,7 +57,7 @@ for i, net in enumerate(("BTN_PREVIOUS", "BTN_SELECT", "BTN_NEXT"), 1):
|
||||
assert len(nets[net]) == 3
|
||||
assert (f"SW{i}", "1") in members[net] and (f"R{i}", "2") in members[net]
|
||||
assert components[f"SW{i}"].findtext("value") == "B3F-1000"
|
||||
assert components[f"R{i}"].findtext("value") == "2.2k 1%"
|
||||
assert components[f"R{i}"].findtext("value") == "2k 1%"
|
||||
assert members["+3V3"] == sorted([("U1","1"),("U1","2"),("U2","1"),("DS1","2"),("R1","1"),("R2","1"),("R3","1")])
|
||||
assert members["GND"] == sorted([("U1","22"),("U1","23"),("U1","43"),("U1","44"),("U2","2"),("DS1","1"),("SW1","2"),("SW2","2"),("SW3","2")])
|
||||
active = {name for name in nets if not name.startswith("unconnected-")}
|
||||
@@ -70,8 +70,8 @@ assert sorted(int(node.get("pin")) for nodes in nets.values() for node in nodes
|
||||
# USB/UART0, 5 V and PSRAM-conflicting pins must never be carrier nets.
|
||||
for name in active:
|
||||
assert not any(node.get("ref") == "U1" and node.get("pin") in ("21","24","25","33","34","35","41","42") for node in nets[name])
|
||||
# U2's physical male footprint must stay explicitly unresolved in this draft.
|
||||
assert not components["U2"].findtext("footprint")
|
||||
# User confirmed the male module shares the reference pinout/header/footprint.
|
||||
assert components["U2"].findtext("footprint") == "Carrier:MAX3243_Reference_Provisional"
|
||||
expected = json.loads((OUT / "expected-nets.json").read_text())
|
||||
assert set(expected) == active
|
||||
for net, nodes in expected.items():
|
||||
@@ -87,8 +87,9 @@ summary = "\n".join([
|
||||
"15 firmware GPIO definitions independently checked against exported pin functions.",
|
||||
"All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.",
|
||||
"PDF and SVG exported successfully.",
|
||||
"Not validated: male-module physical mapping, power budget/path, OLED pull-ups,",
|
||||
"selected connector fit, PCB DRC/layout, procurement, or assembled hardware.",
|
||||
"User accepted module mapping/fit and reports purchased switches/resistors.",
|
||||
"No new hardware tests; regulator margin, USB backfeed safety, RF performance",
|
||||
"and fabrication qualification are not established by this schematic check.",
|
||||
]) + "\n"
|
||||
(OUT / "summary.txt").write_text(summary)
|
||||
print(summary)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measured U1 integration regression and exact pre-update preservation audit."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Polygon
|
||||
from add_device_silkscreen import add_outlines, NS as SILK_NS, xy, outline_lines
|
||||
from update_u1_graphics import verify, NS as U1_NS
|
||||
from remove_draft_markers import expected_tree
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
BASE = ROOT / "validation/pcb-before-u1-measured.kicad_pcb"
|
||||
|
||||
|
||||
def parse(path):
|
||||
tokens = iter(re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+', path.read_text()))
|
||||
def group():
|
||||
result = []
|
||||
for token in tokens:
|
||||
if token == ')':
|
||||
return result
|
||||
result.append(group() if token == '(' else token)
|
||||
raise ValueError("Unterminated S-expression")
|
||||
assert next(tokens) == '('
|
||||
return group()
|
||||
|
||||
|
||||
def ident(item):
|
||||
return next((v[1].strip('"') for v in item if isinstance(v,list) and v[0]=='uuid'), None)
|
||||
|
||||
|
||||
def canonical(item, excluded=frozenset()):
|
||||
# KiCad can reorder independent board items when saving; compare their full
|
||||
# parsed contents, not byte order. Pad coordinates/netnames/UUIDs remain exact.
|
||||
if not isinstance(item, list):
|
||||
return item
|
||||
values = [canonical(v, excluded) for v in item
|
||||
if not isinstance(v,list) or ident(v) not in excluded]
|
||||
if item and item[0] in ("kicad_pcb", "footprint"):
|
||||
return ([v for v in values if not isinstance(v,list)] +
|
||||
sorted((v for v in values if isinstance(v,list)), key=repr))
|
||||
# Coordinates, polygon vertex order, text and other ordered values must
|
||||
# remain ordered: sorting these could conceal an X/Y swap or zone edit.
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
assert hashlib.sha256(BASE.read_bytes()).hexdigest() == "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "Pre-update snapshot changed"
|
||||
assert hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest() == "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516", "Project settings changed"
|
||||
before = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
silk_ids = {str(uuid.uuid5(SILK_NS,str(i))) for i in range(4096)}
|
||||
u1_ids = {str(uuid.uuid5(U1_NS,str(i))) for i in range(6)}
|
||||
# The original baseline retains historical draft labels. Permit only the
|
||||
# exact requested title/revision/comment edits and removal of the silk label.
|
||||
# Keep proving the original integration against its original evidence, then
|
||||
# audit the explicitly authorized subsequent placement/routing delta.
|
||||
from validate_group_alignment import audit, BASE as GROUP_BASE
|
||||
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(GROUP_BASE), silk_ids | u1_ids), "Historical U1 integration changed"
|
||||
audit()
|
||||
generated = [g for g in board.GetDrawings() if g.m_Uuid.AsString() in silk_ids]
|
||||
assert not any(isinstance(g,p.PCB_TEXT) and g.GetText()=="HEADER GUIDES" for g in board.GetDrawings())
|
||||
carrier = Polygon([(136,100),(182,100),(182,180),(100,180),(100,119),(136,119)])
|
||||
mouths = []
|
||||
for g in generated:
|
||||
assert g.GetLayer() == p.F_SilkS
|
||||
for edge in outline_lines(g):
|
||||
assert carrier.covers(edge.buffer(p.ToMM(g.GetWidth())/2)), "Silk outside carrier/notch"
|
||||
if g.GetWidth() == p.FromMM(0.3):
|
||||
mouths.append(g)
|
||||
assert len(mouths) == 2
|
||||
assert sorted((xy(g.GetStart()),xy(g.GetEnd())) for g in mouths) == [
|
||||
((110.795,177.705),(119.665,177.705)),
|
||||
((122.195,177.705),(131.065,177.705))]
|
||||
# A second refresh must reproduce all drawing geometry and UUIDs.
|
||||
add_outlines(board)
|
||||
with tempfile.TemporaryDirectory(prefix="u1-regression-") as temporary:
|
||||
result = Path(temporary)/PCB.name
|
||||
assert p.SaveBoard(str(result),board,True)
|
||||
assert canonical(parse(result)) == canonical(parse(PCB)), "Silkscreen refresh not idempotent"
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference()=="U1")
|
||||
shape = next(g for g in u1.GraphicalItems() if isinstance(g,p.PCB_SHAPE))
|
||||
shape.SetWidth(shape.GetWidth()+1)
|
||||
try:
|
||||
verify(board)
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Stale embedded geometry was not rejected")
|
||||
assert PCB.read_bytes() == before
|
||||
assert not (ROOT/"~serial-carrier.kicad_pcb.lck").exists()
|
||||
summary = dict(measured_library_matches_embedded_u1=True,
|
||||
historical_u1_integration_and_subsequent_authorized_group_delta_verified=True,
|
||||
presentation_label_cleanup=True,
|
||||
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
|
||||
silk_clipped_to_carrier=True, usb_mouth_markers=2,
|
||||
generated_silk_items=len(generated),
|
||||
baseline_sha256=hashlib.sha256(BASE.read_bytes()).hexdigest(),
|
||||
board_sha256=hashlib.sha256(before).hexdigest(),
|
||||
project_sha256=hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest())
|
||||
(ROOT/"validation/pcb-u1-group-regression.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print(json.dumps(summary,indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/erc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-20T20:14:47",
|
||||
"date": "2026-09-22T09:55:18",
|
||||
"ignored_checks": [
|
||||
{
|
||||
"description": "Global label only appears once in the schematic",
|
||||
|
||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 347 KiB |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"common_axis_absolute_x_mm": 156.97,
|
||||
"resistor_to_rs232_body_gap_mm": 2.0815,
|
||||
"resistor_to_oled_body_gap_mm": 2.0815,
|
||||
"oled_to_button_body_gap_mm": 5.25,
|
||||
"original_copper_items": 587,
|
||||
"retained_copper_items": 476,
|
||||
"removed_original_copper_items": 111,
|
||||
"new_copper_items": 503,
|
||||
"exact_unrelated_content_preserved": true,
|
||||
"project_settings_preserved": true,
|
||||
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5"
|
||||
}
|
||||
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"moves_absolute_mm": {
|
||||
"DS1": [
|
||||
[
|
||||
143,
|
||||
136
|
||||
],
|
||||
[
|
||||
143.97,
|
||||
136
|
||||
]
|
||||
],
|
||||
"R1": [
|
||||
[
|
||||
143,
|
||||
134
|
||||
],
|
||||
[
|
||||
142.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"R2": [
|
||||
[
|
||||
154,
|
||||
134
|
||||
],
|
||||
[
|
||||
153.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"R3": [
|
||||
[
|
||||
165,
|
||||
134
|
||||
],
|
||||
[
|
||||
164.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"SW1": [
|
||||
[
|
||||
143,
|
||||
168
|
||||
],
|
||||
[
|
||||
141.72,
|
||||
168
|
||||
]
|
||||
],
|
||||
"SW2": [
|
||||
[
|
||||
155,
|
||||
168
|
||||
],
|
||||
[
|
||||
153.72,
|
||||
168
|
||||
]
|
||||
],
|
||||
"SW3": [
|
||||
[
|
||||
167,
|
||||
168
|
||||
],
|
||||
[
|
||||
165.72,
|
||||
168
|
||||
]
|
||||
]
|
||||
},
|
||||
"removed_track_uuids": [
|
||||
"38b14c4a-1117-48f1-84c2-69df406efd45",
|
||||
"0436d2d5-7e3c-4343-a104-b27263f50378",
|
||||
"0cd2f1ba-dd20-43d0-88bf-b78d25caa7f2",
|
||||
"0df43fc8-4848-4075-9ee2-84440d3d4c52",
|
||||
"114df41e-6104-445f-b52b-b623673294c3",
|
||||
"2f38905d-78e9-4853-b3d0-50b257179e7b",
|
||||
"5b48705f-d547-4a77-84fa-6e4149256b50",
|
||||
"68657908-8a9e-41e0-832d-7e05a448acea",
|
||||
"6b91cd8a-246d-4cfc-9398-242d2912fc37",
|
||||
"88c50c34-fa4a-4fb9-89c7-fb8e40fcaad2",
|
||||
"9e80e1c0-4aaa-4ebc-9689-497f9e2d3e92",
|
||||
"b88df4ea-9af3-402e-b7ca-59ce0b2065bb",
|
||||
"e1a92e9c-97e5-4400-b977-42536a8d2f2d",
|
||||
"073a79bf-6f12-4c76-ba49-fbbfb977d470",
|
||||
"18bac46c-c87a-4575-b8af-1934ea6110c4",
|
||||
"20378a0c-b433-42ba-a643-c5af22ce09ed",
|
||||
"28595a1b-947e-4fdd-be3e-d725cdabbcd4",
|
||||
"49bb5dc7-5ca7-4d9c-8bb0-79ddb15d928f",
|
||||
"79379f9c-e912-4b19-af4c-1638bb948703",
|
||||
"97355a54-7801-4a02-8b6f-e1d31d364035",
|
||||
"a2b2baa5-e50d-449a-ab05-53aaaf15c06c",
|
||||
"da453fcc-84cc-470b-bc5a-49aa69700fa2",
|
||||
"eab2ad52-ceaa-4fcb-ba9d-336bbc8745de",
|
||||
"27bfb939-d170-4d17-9aab-8d68ea6995ca",
|
||||
"6bb98a8c-ace7-48cb-8836-47b54273ebc3",
|
||||
"8f0d756a-6380-4e82-b65c-17d2f12cf462",
|
||||
"eb39c8eb-56cd-4331-b798-e769bfd19c05",
|
||||
"16c19532-1b7d-4251-a3f2-07d08811abe4",
|
||||
"45f61cb8-ad1d-4ca5-bb0d-3d26df7a61eb",
|
||||
"4dc2388f-7f09-4efc-ae8d-5593cfd0efc5",
|
||||
"527da12c-1892-4783-8e1e-bbb23491cf13",
|
||||
"643622f8-985f-41ef-9f60-97b85df04a4b",
|
||||
"7201c8b8-a924-4a78-8718-8639e1b84375",
|
||||
"82d24494-1863-4798-8dab-d6acaa6af1ef",
|
||||
"9d886175-0a22-4800-bacf-d027a2594149",
|
||||
"b87e55c6-667a-4320-b42e-5311956f3869",
|
||||
"ca3aebf5-89f6-4d95-920c-7f751b4e7227",
|
||||
"d43c4432-114e-4488-b457-1015c677dd58",
|
||||
"d5c6f4dc-918d-453a-a950-6fa7ff814a53",
|
||||
"db004298-a5ea-4295-a340-99861f12affa",
|
||||
"e058474e-eea8-44c5-9566-227ab00f9713",
|
||||
"ebf07a5b-1b77-4da4-9009-b5456894c1a6",
|
||||
"0348920f-ac8a-4881-925c-2379c1c042a3",
|
||||
"1376e497-a31b-44ba-8756-a0c3909ae497",
|
||||
"1ae9db6c-00ce-498f-a69a-4917829179a3",
|
||||
"3385b064-faba-489a-ab8f-e2efd7bc6078",
|
||||
"41b7e5dc-e873-4e78-add1-4a8aef2cf0f2",
|
||||
"4a9ea5ca-9548-464c-90aa-d5a11171700b",
|
||||
"53084fac-b12d-4262-8e8a-326bb1598e1d",
|
||||
"6278694c-ab84-4d18-966d-cdd87569b736",
|
||||
"70ecf107-6be8-4c81-944d-b9195d0e907b",
|
||||
"7d370000-a728-42ad-8fb3-07bd56f55834",
|
||||
"8a26d3a6-0e4e-47ac-914a-4e2ac03e80b8",
|
||||
"a4794925-fe72-46c4-bea5-8cdf2ead8bbe",
|
||||
"c8f89618-ed14-47ff-8e60-c489062cce99",
|
||||
"d42d944c-5031-40e7-bab4-578d2a05e07c",
|
||||
"fc51bdbd-326c-4620-b799-b1fcbfd0abfa",
|
||||
"08eda9a1-b0e4-4ed1-8d7d-cec706f0162c",
|
||||
"56f6e44c-d9f4-4144-8950-869e365e09d5",
|
||||
"575729f7-8e91-43fc-ba36-47bd1927aebb",
|
||||
"58b8f7f8-efff-41d9-a224-6c87461ab440",
|
||||
"847e753c-6cc0-4300-8d7c-a3aba8914103",
|
||||
"8b3de05f-9c1d-4e9f-82ab-64f1d17763e7",
|
||||
"c0c4c166-7646-41f9-b005-9fe5c7ded558",
|
||||
"c972b3de-08dc-4ad9-a684-3ff40523f29a",
|
||||
"e0902a82-2700-41ac-b7e3-328984d82703",
|
||||
"efb34c99-e6f3-4563-b01e-7d740f4bb0d1",
|
||||
"5a6d5bb5-ab3e-4f95-9afc-8e9b5e1651b3",
|
||||
"679beec8-cb5f-4c3c-9fab-d7d370b03f50",
|
||||
"70fb66f6-deb5-4699-bb7d-effa571a4758",
|
||||
"736e4133-a876-4d18-83ad-2a0649b0a9bf",
|
||||
"785f6aab-ebe2-4476-b49f-a75d98b64c44",
|
||||
"7a2820fc-35e9-408e-8e6d-1fa62e0f3416",
|
||||
"870bf561-8eb8-4dbe-a63b-64a573c40a28",
|
||||
"b519d784-ca35-488d-b0d2-5f4c344e7519",
|
||||
"0225087c-1bcb-4bdc-ac0b-11fe1bff8a27",
|
||||
"0237718c-7284-4cf8-8958-2e7e2e829e16",
|
||||
"18a95172-f9ce-43f8-b9dd-ab56b48eaaa8",
|
||||
"1ca667c6-9ebc-4ed5-9aeb-90c176a57a9f",
|
||||
"241e7690-286a-467b-b825-70d989d4cb66",
|
||||
"2a05aea8-421b-4c13-a82e-d49ae2a0c82e",
|
||||
"2d7c81b1-9664-4486-bb9e-46db89cc5b02",
|
||||
"310d85fb-84d9-440e-80a3-b2824fcd4d15",
|
||||
"3fbbbb13-12d9-42e8-9b0a-7c40cbeef8a5",
|
||||
"4260b587-cd60-4ca0-8a41-71e2e49f46a4",
|
||||
"52415179-03b2-49b7-9e57-1fd6965ef4f4",
|
||||
"7239518a-93d0-46dc-9114-703f779efb33",
|
||||
"7bbda78d-630f-429b-816c-5b3db4ef39d0",
|
||||
"7df7c029-9651-4323-bd67-afbf86fe336b",
|
||||
"7e68296a-607b-446e-91e0-4fd4e30f18c9",
|
||||
"a88c6fe8-c782-4db0-9c1c-afc4a1bc3f4b",
|
||||
"c058ecbb-3e79-457e-a16c-77c319e0107c",
|
||||
"ce63f9cd-80a2-422e-9d7b-251b8f54bbf7",
|
||||
"d1d00ff3-b284-45e3-836a-0f5cfb666142",
|
||||
"da267940-8d82-4b44-9d44-b28614c250cf",
|
||||
"dbfbcc12-4305-4cd7-ba73-6f6dade960e4",
|
||||
"e125d718-5926-473f-b25b-b9419465b0b1",
|
||||
"ec4f2ef8-ca69-4520-aee7-6b12e42625b1",
|
||||
"eca47b24-175d-4f12-9300-8770ec274a4d",
|
||||
"08f34dfb-69ce-4ce7-aeb3-9b04878939bd",
|
||||
"75e60ab2-4a98-4ae8-8155-068279b73dd6",
|
||||
"98485ed4-b71e-46b1-aebe-781d40674d9f",
|
||||
"9cbac9b7-9f29-4742-9e3b-8817a22ad4af",
|
||||
"ab28a160-1194-460a-bcd9-cb165a51a47e",
|
||||
"bac74aef-8326-4288-ac02-51ae39830b3d",
|
||||
"d8112662-ca8b-4828-8ad4-34d71ca66db7",
|
||||
"55f54e2b-ba8d-4c59-97e8-2e3c26ec1d98",
|
||||
"a9579dde-c548-4e39-a3a9-6de3be621548",
|
||||
"30c97d70-ce85-48b4-b576-22926efcb4ae",
|
||||
"2ca96e04-1f1e-4466-b4ed-78484f82acd2",
|
||||
"f120e834-5db9-4ce4-9e4b-3b2b0757fa3f",
|
||||
"15c5caea-afd8-4f13-ad41-65aa162be77b",
|
||||
"64cefc0c-db46-4fe9-bba7-c43c8901e416",
|
||||
"0806ce46-3896-4c68-ab6b-e07f137928b3"
|
||||
],
|
||||
"repaired_connections": [
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
152.19,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
153.16,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.73,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
155.7,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SCL",
|
||||
"start": [
|
||||
157.27,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
158.24,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SDA",
|
||||
"start": [
|
||||
159.81,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
160.78,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
143.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
142.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
150.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
149.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
153.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
160.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
165.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
164.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
172.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
171.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
143.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
141.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
149.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
148.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
143.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
141.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
149.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
148.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
155.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
153.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
160.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
155.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
153.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
161.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
160.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
167.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
165.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
173.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
172.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
167.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
165.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
173.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
172.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SDA",
|
||||
"start": [
|
||||
135.75,
|
||||
161.55
|
||||
],
|
||||
"end": [
|
||||
159.8,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SCL",
|
||||
"start": [
|
||||
134.9,
|
||||
159.85
|
||||
],
|
||||
"end": [
|
||||
157.25,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
161.5,
|
||||
134.1
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_DCD",
|
||||
"start": [
|
||||
133.1,
|
||||
131.9
|
||||
],
|
||||
"end": [
|
||||
148.08,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_RI",
|
||||
"start": [
|
||||
155.25,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
165.86,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_DTR",
|
||||
"start": [
|
||||
148.6,
|
||||
133.1
|
||||
],
|
||||
"end": [
|
||||
155.7,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_CTS",
|
||||
"start": [
|
||||
153.45,
|
||||
138.75
|
||||
],
|
||||
"end": [
|
||||
154.75,
|
||||
135.65
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_RX",
|
||||
"start": [
|
||||
135.75,
|
||||
139.5
|
||||
],
|
||||
"end": [
|
||||
144.45,
|
||||
132.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_VALID",
|
||||
"start": [
|
||||
157.75,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
165.9,
|
||||
127.95
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_OFF_N",
|
||||
"start": [
|
||||
160.3,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
162.4,
|
||||
136.75
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
148.22,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
149.5,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
172.22,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
173.5,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
148.22,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
149.5,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
145.54,
|
||||
126.67
|
||||
],
|
||||
"end": [
|
||||
150.25,
|
||||
135.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
149.5,
|
||||
170.0
|
||||
],
|
||||
"end": [
|
||||
148.1,
|
||||
156.55
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
143.0,
|
||||
133.5
|
||||
],
|
||||
"end": [
|
||||
143.0,
|
||||
134.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
156.35,
|
||||
136.4
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
148.7,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
152.65,
|
||||
132.65
|
||||
]
|
||||
}
|
||||
],
|
||||
"retained_tracks": 476,
|
||||
"protected_sha256": {
|
||||
"serial-carrier.kicad_pro": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516",
|
||||
"serial-carrier.kicad_prl": "f7c452b3be0ad43b4557f4d9c24a38124cd88268edfc2c9bbd59e3f2429e3772",
|
||||
"serial-carrier.kicad_sch": "621acfd9ed8e025a90442e5f82f73d7d5e3228d7a48459a0fa4b0c46e763912a"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/drc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-21T11:39:10",
|
||||
"date": "2026-09-22T10:24:36",
|
||||
"ignored_checks": [
|
||||
{
|
||||
"description": "Footprint has no courtyard defined",
|
||||
@@ -30,23 +30,7 @@
|
||||
"exclusion"
|
||||
],
|
||||
"kicad_version": "10.0.6",
|
||||
"schematic_parity": [
|
||||
{
|
||||
"description": "Carrier:MAX3243_Reference_Provisional stimmt nicht mit dem Footprint des Symbols () überein.",
|
||||
"items": [
|
||||
{
|
||||
"description": "Footprint U2",
|
||||
"pos": {
|
||||
"x": 143.0,
|
||||
"y": 126.67
|
||||
},
|
||||
"uuid": "812387f6-6524-459e-8102-fe9c55122668"
|
||||
}
|
||||
],
|
||||
"severity": "warning",
|
||||
"type": "footprint_symbol_mismatch"
|
||||
}
|
||||
],
|
||||
"schematic_parity": [],
|
||||
"source": "serial-carrier.kicad_pcb",
|
||||
"unconnected_items": [],
|
||||
"violations": []
|
||||
|
||||
|
After Width: | Height: | Size: 49 KiB |
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"immutable_geometry_nets_pads_linkage_preserved": true,
|
||||
"board_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
|
||||
"independent_min_different_net_or_npth_clearance_mm": 0.2705441169852849,
|
||||
"geometry_nets_pads_linkage_match_authorized_group_moves_and_values": true,
|
||||
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5",
|
||||
"independent_min_different_net_or_npth_clearance_mm": 0.2527621919573916,
|
||||
"closest_items": [
|
||||
"U1.38:4b8eabef-8493-437d-bd02-cb9ff4afd7cd",
|
||||
"d95f45cb-7fa4-4203-a786-0a069425e2b8"
|
||||
"DS1.4:91a4dcef-1b36-4e3d-a15c-c4578077c27b",
|
||||
"265214f3-776f-4707-be6e-993749f3c8a7"
|
||||
],
|
||||
"min_copper_or_npth_edge_gap_mm": 1.9,
|
||||
"min_rf_gap_mm": 0.025000000000005684,
|
||||
"min_via_to_other_drilled_hole_gap_mm": 0.8205441169852848,
|
||||
"min_via_to_other_drilled_hole_gap_mm": 0.47246725698856695,
|
||||
"track_width_counts": {
|
||||
"0.25": 451,
|
||||
"0.5": 114
|
||||
"0.25": 642,
|
||||
"0.5": 296
|
||||
},
|
||||
"routed_length_mm_by_net": {
|
||||
"/BTN_SELECT": 88.94832552628598,
|
||||
"/GND": 141.68412246223386,
|
||||
"/+3V3": 90.29604731966161,
|
||||
"/RS_DCD": 43.068490178879834,
|
||||
"/BTN_SELECT": 93.73257585313998,
|
||||
"/GND": 167.13877028476702,
|
||||
"/+3V3": 110.38340361450781,
|
||||
"/RS_DCD": 43.09677445013352,
|
||||
"/RS_DSR": 55.09398706495997,
|
||||
"/RS_RI": 67.91373841554615,
|
||||
"/RS_DTR": 52.53487761643567,
|
||||
"/RS_RI": 75.93696129246699,
|
||||
"/RS_DTR": 53.18208599609399,
|
||||
"/RS_RTS": 60.056714985368814,
|
||||
"/RS_CTS": 63.65082753507114,
|
||||
"/RS_CTS": 81.83108365614024,
|
||||
"/RS_TX": 53.135260491559045,
|
||||
"/RS_RX": 51.637429252872906,
|
||||
"/RS_VALID": 72.10630197626116,
|
||||
"/RS_OFF_N": 78.04100118378895,
|
||||
"/BTN_PREVIOUS": 78.83370879187974,
|
||||
"/OLED_SDA": 62.65168524184884,
|
||||
"/OLED_SCL": 61.1779295330028,
|
||||
"/BTN_NEXT": 101.24898628384834
|
||||
"/RS_RX": 52.38890111544552,
|
||||
"/RS_VALID": 73.53056575207658,
|
||||
"/RS_OFF_N": 78.70963033389047,
|
||||
"/BTN_PREVIOUS": 84.48232420012336,
|
||||
"/OLED_SDA": 63.88671049501825,
|
||||
"/OLED_SCL": 62.3836654642909,
|
||||
"/BTN_NEXT": 106.89760169209195
|
||||
},
|
||||
"tracks": 565,
|
||||
"vias": 22,
|
||||
"tracks": 938,
|
||||
"vias": 41,
|
||||
"copper_zones": 0,
|
||||
"drc_freshly_run": true,
|
||||
"drc_violations": 0,
|
||||
|
||||
@@ -21,7 +21,11 @@ r = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(r)
|
||||
board = p.LoadBoard(str(r.PCB))
|
||||
original = p.LoadBoard(str(ROOT / 'validation/pcb-routing-input.kicad_pcb'))
|
||||
assert r.identity(board) == r.identity(original), 'immutable source geometry/linkage changed'
|
||||
import sys
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from validate_group_alignment import expected_identity, audit
|
||||
audit()
|
||||
assert r.identity(board) == r.identity(expected_identity(original)), 'source geometry/linkage differs beyond authorized group moves/values'
|
||||
items, holes = [], []
|
||||
for f in board.GetFootprints():
|
||||
for pad in f.Pads():
|
||||
@@ -106,7 +110,7 @@ subprocess.run(['kicad-cli', 'pcb', 'drc', '--format', 'json', '--all-track-erro
|
||||
assert r.PCB.read_bytes() == before, 'board changed during audit'
|
||||
drc = json.loads(ROOT.joinpath('validation/pcb-routing-drc.json').read_text())
|
||||
assert not drc['violations'] and not drc['unconnected_items']
|
||||
report = dict(immutable_geometry_nets_pads_linkage_preserved=True,
|
||||
report = dict(geometry_nets_pads_linkage_match_authorized_group_moves_and_values=True,
|
||||
board_sha256=hashlib.sha256(before).hexdigest(),
|
||||
independent_min_different_net_or_npth_clearance_mm=min_clearance,
|
||||
closest_items=closest, min_copper_or_npth_edge_gap_mm=min_edge,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/drc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-21T11:39:13",
|
||||
"date": "2026-09-22T10:21:48",
|
||||
"ignored_checks": [
|
||||
{
|
||||
"description": "Footprint has no courtyard defined",
|
||||
|
||||
@@ -40,9 +40,13 @@ a0=OUT/'pcb-a0-before-reposition.kicad_pcb'
|
||||
a1=ROOT/'serial-carrier.kicad_pcb'
|
||||
assert sha(a0)==report['a0_snapshot_sha256'], 'A0 snapshot changed'
|
||||
assert sha(OUT/'pcb-routing-input.kicad_pcb')==report['a1_unrouted_sha256'], 'A1 routing input changed'
|
||||
# These two handoff documents are intentionally updated during A1 integration.
|
||||
# Historical reports exempt these handoff documents, updated during A1 integration.
|
||||
# Keep the legacy key exemption; new reports use ../../docs/devel/hardware/PCB/
|
||||
# pcb-draft-notes.md and that relocated note must pass the normal hash check.
|
||||
intentional_updates=[name for name in ('README.md','pcb-draft-notes.md')
|
||||
if name in report['protected_file_sha256']]
|
||||
protected={name:digest for name,digest in report['protected_file_sha256'].items()
|
||||
if name not in ('README.md','pcb-draft-notes.md')}
|
||||
if name not in intentional_updates}
|
||||
for name,digest in protected.items():
|
||||
assert sha(ROOT/name)==digest, 'protected file changed: '+name
|
||||
old,new=parse(a0),parse(a1)
|
||||
@@ -88,7 +92,7 @@ assert [x for x in old if isinstance(x,list) and x[0]=='net']==[x for x in new i
|
||||
assert not (ROOT/'~serial-carrier.kicad_pcb.lck').exists(), 'PCB reopened before handoff'
|
||||
result=dict(a0_snapshot_unchanged=True,a1_unrouted_snapshot_unchanged=True,
|
||||
protected_files_unchanged=list(protected),
|
||||
intentional_documentation_updates=['README.md','pcb-draft-notes.md'],
|
||||
intentional_documentation_updates=intentional_updates,
|
||||
cad_projection_graphics=len(cad_owned),
|
||||
footprints_identical_except_authorized_placement=True,
|
||||
footprint_count=len(old_fp),unrelated_board_graphics_preserved=len(unchanged_old),
|
||||
|
||||
@@ -69,10 +69,14 @@ def main():
|
||||
before=PCB.read_bytes()
|
||||
assert before==BACKUP.read_bytes(), 'current board differs from A0 snapshot; refusing migration'
|
||||
protected=[ROOT/name for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_sch',
|
||||
'serial-carrier.kicad_prl','README.md','pcb-draft-notes.md','fp-lib-table',
|
||||
'serial-carrier.kicad_prl','README.md','fp-lib-table',
|
||||
'sym-lib-table','Carrier.kicad_sym')]
|
||||
protected+=list((ROOT/'Carrier.pretty').glob('*'))
|
||||
hashes={str(path.relative_to(ROOT)):sha(path) for path in protected if path.is_file()}
|
||||
# Keep report keys relative to ROOT, including the relocated, required note.
|
||||
# Do not silently omit it through the optional project-file filter above.
|
||||
notes='../../docs/devel/hardware/PCB/pcb-draft-notes.md'
|
||||
hashes[notes]=sha(ROOT/notes)
|
||||
board=p.LoadBoard(str(PCB))
|
||||
immutable=normalized_footprints(board)
|
||||
net_table=identity(board)[-1]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<export version="E">
|
||||
<design>
|
||||
<source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source>
|
||||
<date>2026-09-21T11:39:08</date>
|
||||
<date>2026-09-22T10:24:34</date>
|
||||
<tool>Eeschema 10.0.6-1.fc44</tool>
|
||||
<sheet number="1" name="/" tstamps="/">
|
||||
<title_block>
|
||||
@@ -51,16 +51,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R1">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -75,16 +75,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R2">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -99,16 +99,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R3">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -129,7 +129,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Previous / back</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -138,7 +138,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Previous / back"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -159,7 +159,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Select / confirm</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -168,7 +168,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Select / confirm"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -189,7 +189,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Next</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -198,7 +198,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Next"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -282,15 +282,16 @@
|
||||
</comp>
|
||||
<comp ref="U2">
|
||||
<value>Adafruit MAX3243 / male 6253</value>
|
||||
<footprint>Carrier:MAX3243_Reference_Provisional</footprint>
|
||||
<fields>
|
||||
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
|
||||
<field name="Status">User confirmed male pinout/header/footprint and pin-by-pin connections</field>
|
||||
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
|
||||
<field name="Footprint"/>
|
||||
<field name="Footprint">Carrier:MAX3243_Reference_Provisional</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="MAX3243_Module_Reference" description="Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"/>
|
||||
<property name="Status" value="VERIFY male header numbering; footprint intentionally unassigned"/>
|
||||
<property name="Status" value="User confirmed male pinout/header/footprint and pin-by-pin connections"/>
|
||||
<property name="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"measured_library_matches_embedded_u1": true,
|
||||
"historical_u1_integration_and_subsequent_authorized_group_delta_verified": true,
|
||||
"presentation_label_cleanup": true,
|
||||
"silk_refresh_idempotent": true,
|
||||
"stale_geometry_negative_control": true,
|
||||
"silk_clipped_to_carrier": true,
|
||||
"usb_mouth_markers": 2,
|
||||
"generated_silk_items": 94,
|
||||
"baseline_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
|
||||
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5",
|
||||
"project_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"measured_library_matches_embedded_u1": true,
|
||||
"existing_uuids_pads_nets_placement_routing_settings_and_unrelated_graphics_preserved": true,
|
||||
"presentation_label_cleanup": true,
|
||||
"silk_refresh_idempotent": true,
|
||||
"stale_geometry_negative_control": true,
|
||||
"silk_clipped_to_carrier": true,
|
||||
"usb_mouth_markers": 2,
|
||||
"generated_silk_items": 91,
|
||||
"baseline_sha256": "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045",
|
||||
"board_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"project_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516"
|
||||
}
|
||||
@@ -39,31 +39,31 @@
|
||||
26.67
|
||||
],
|
||||
"DS1": [
|
||||
43,
|
||||
43.97,
|
||||
36
|
||||
],
|
||||
"R1": [
|
||||
43,
|
||||
34
|
||||
42.16,
|
||||
32.6685
|
||||
],
|
||||
"R2": [
|
||||
54,
|
||||
34
|
||||
53.16,
|
||||
32.6685
|
||||
],
|
||||
"R3": [
|
||||
65,
|
||||
34
|
||||
64.16,
|
||||
32.6685
|
||||
],
|
||||
"SW1": [
|
||||
43,
|
||||
41.72,
|
||||
68
|
||||
],
|
||||
"SW2": [
|
||||
55,
|
||||
53.72,
|
||||
68
|
||||
],
|
||||
"SW3": [
|
||||
67,
|
||||
65.72,
|
||||
68
|
||||
],
|
||||
"H1": [
|
||||
@@ -95,8 +95,8 @@
|
||||
"hex_mouth_overhang": 2.87,
|
||||
"flange_front_inset": 1.93
|
||||
},
|
||||
"tracks": 565,
|
||||
"vias": 22,
|
||||
"tracks": 938,
|
||||
"vias": 41,
|
||||
"copper_layers": 2,
|
||||
"draft_thickness_mm": 1.6,
|
||||
"electrical_footprints": 9,
|
||||
@@ -104,7 +104,7 @@
|
||||
"physical_DRC_violations": 0,
|
||||
"unconnected_items": 0,
|
||||
"schematic_pad_nets_match": true,
|
||||
"known_parity_warning": "U2 reference footprint assigned on PCB only; schematic footprint intentionally blank; nominal male external metal dimensions user-confirmed",
|
||||
"schematic_parity_violations": 0,
|
||||
"project_settings_sha256": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516",
|
||||
"ignored_DRC_checks": [
|
||||
{
|
||||
@@ -128,15 +128,19 @@
|
||||
"key": "footprint_type_mismatch"
|
||||
}
|
||||
],
|
||||
"not_validated": [
|
||||
"male module pin map/header fit",
|
||||
"ESP32 geometry and complete assembly envelope/heights",
|
||||
"header/socket fit",
|
||||
"RF performance and antenna clearance",
|
||||
"power/return-path integrity",
|
||||
"USB power path",
|
||||
"OLED pull-ups",
|
||||
"fabrication process",
|
||||
"hardware operation"
|
||||
]
|
||||
"user_accepted": [
|
||||
"module dimensions/visual fit and USB clearance",
|
||||
"male pinout/header/footprint and pin-by-pin review",
|
||||
"M3 carrier/module and M2 display hardware",
|
||||
"RF keepout and power/ground routing"
|
||||
],
|
||||
"not_independently_verified": [
|
||||
"reported 10 mm assembled module elevation",
|
||||
"regulator margin",
|
||||
"USB backfeed safety",
|
||||
"OLED onboard pull-up resistance/rail",
|
||||
"manufacturer RF qualification",
|
||||
"fabrication process/certification"
|
||||
],
|
||||
"new_hardware_tests": false
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@@ -2,7 +2,7 @@
|
||||
<export version="E">
|
||||
<design>
|
||||
<source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source>
|
||||
<date>2026-09-20T20:14:45</date>
|
||||
<date>2026-09-22T09:55:17</date>
|
||||
<tool>Eeschema 10.0.6-1.fc44</tool>
|
||||
<sheet number="1" name="/" tstamps="/">
|
||||
<title_block>
|
||||
@@ -51,16 +51,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R1">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -75,16 +75,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R2">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -99,16 +99,16 @@
|
||||
</units>
|
||||
</comp>
|
||||
<comp ref="R3">
|
||||
<value>2.2k 1%</value>
|
||||
<value>2k 1%</value>
|
||||
<footprint>Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</footprint>
|
||||
<fields>
|
||||
<field name="Rating">At least 0.125 W; exact resistor MPN pending</field>
|
||||
<field name="Rating">Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified</field>
|
||||
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="R" description="Resistor, axial DIN0207 footprint; exact MPN pending"/>
|
||||
<property name="Rating" value="At least 0.125 W; exact resistor MPN pending"/>
|
||||
<property name="Rating" value="Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -129,7 +129,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Previous / back</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -138,7 +138,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Previous / back"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -159,7 +159,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Select / confirm</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -168,7 +168,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Select / confirm"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -189,7 +189,7 @@
|
||||
<field name="Manufacturer">Omron</field>
|
||||
<field name="MPN">B3F-1000</field>
|
||||
<field name="Function">Next</field>
|
||||
<field name="Procurement">Live stock not verified</field>
|
||||
<field name="Procurement">User reports purchased</field>
|
||||
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
@@ -198,7 +198,7 @@
|
||||
<property name="Manufacturer" value="Omron"/>
|
||||
<property name="MPN" value="B3F-1000"/>
|
||||
<property name="Function" value="Next"/>
|
||||
<property name="Procurement" value="Live stock not verified"/>
|
||||
<property name="Procurement" value="User reports purchased"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
<sheetpath names="/" tstamps="/"/>
|
||||
@@ -282,15 +282,16 @@
|
||||
</comp>
|
||||
<comp ref="U2">
|
||||
<value>Adafruit MAX3243 / male 6253</value>
|
||||
<footprint>Carrier:MAX3243_Reference_Provisional</footprint>
|
||||
<fields>
|
||||
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
|
||||
<field name="Status">User confirmed male pinout/header/footprint and pin-by-pin connections</field>
|
||||
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
|
||||
<field name="Footprint"/>
|
||||
<field name="Footprint">Carrier:MAX3243_Reference_Provisional</field>
|
||||
<field name="Datasheet"/>
|
||||
<field name="Description"/>
|
||||
</fields>
|
||||
<libsource lib="Carrier" part="MAX3243_Module_Reference" description="Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"/>
|
||||
<property name="Status" value="VERIFY male header numbering; footprint intentionally unassigned"/>
|
||||
<property name="Status" value="User confirmed male pinout/header/footprint and pin-by-pin connections"/>
|
||||
<property name="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
|
||||
<property name="Sheetname" value="serial-carrier"/>
|
||||
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
|
||||
@@ -448,7 +449,7 @@
|
||||
</library>
|
||||
</libraries>
|
||||
<nets>
|
||||
<net code="1" name="/+3V3" class="Default">
|
||||
<net code="1" name="/+3V3" class="Power">
|
||||
<node ref="DS1" pin="2" pinfunction="VCC_2" pintype="power_in"/>
|
||||
<node ref="R1" pin="1" pintype="passive"/>
|
||||
<node ref="R2" pin="1" pintype="passive"/>
|
||||
@@ -472,7 +473,7 @@
|
||||
<node ref="SW2" pin="1" pintype="passive"/>
|
||||
<node ref="U1" pin="19" pinfunction="GPIO13_19" pintype="input"/>
|
||||
</net>
|
||||
<net code="5" name="/GND" class="Default">
|
||||
<net code="5" name="/GND" class="Power">
|
||||
<node ref="DS1" pin="1" pinfunction="GND_1" pintype="power_in"/>
|
||||
<node ref="SW1" pin="2" pintype="passive"/>
|
||||
<node ref="SW2" pin="2" pintype="passive"/>
|
||||
|
||||
@@ -4,5 +4,6 @@ KiCad ERC: zero reported errors/warnings; no project exclusions added.
|
||||
15 firmware GPIO definitions independently checked against exported pin functions.
|
||||
All 44 MCU header pins accounted for; USB/UART0/5V/PSRAM pins unused on carrier.
|
||||
PDF and SVG exported successfully.
|
||||
Not validated: male-module physical mapping, power budget/path, OLED pull-ups,
|
||||
selected connector fit, PCB DRC/layout, procurement, or assembled hardware.
|
||||
User accepted module mapping/fit and reports purchased switches/resistors.
|
||||
No new hardware tests; regulator margin, USB backfeed safety, RF performance
|
||||
and fabrication qualification are not established by this schematic check.
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
# Parametric prototype enclosure
|
||||
|
||||
**Initial mechanical concept — not print-tested, assembled, load-tested, RF-qualified or a validated fit.** The included `*-default.stl` files are exports of the provisional values in [enclosure.scad](enclosure.scad), not production drawings. No PCB, schematic, routing, footprint or prior hardware export was modified.
|
||||
|
||||

|
||||
|
||||
## Parts and operation
|
||||
|
||||
Open `enclosure.scad` in OpenSCAD (tested here with **2021.01**). Set `part` to:
|
||||
|
||||
| Mode | Result |
|
||||
|---|---|
|
||||
| `assembly` / `exploded` | Assembled / exploded reference preview; **not printable combined parts** |
|
||||
| `base` | Floor, carrier insert bosses, daughterboard insert bosses, side assembly bosses |
|
||||
| `lid` | Roof, skirt, screw head wells, button guides, integral bottom-open DB9 retaining panel and ribs, raised labels |
|
||||
| `plunger` / `plungers` | One / three identical captive button actuators |
|
||||
|
||||
| `labels` | Raised text alone for a second material or separately attached lettering |
|
||||
| `layout` | Spaced parts for inspection; orient/support each part separately in the slicer |
|
||||
|
||||
`show_references` displays simplified hardware only in **F5 preview** (`$preview` and `%` background geometry); it is excluded from STL/F6 geometry. The reference boxes are not complete component models. Orange/brown DB9 post-body envelopes are shown reseated **outside** the integral panel; they illustrate unverified hardware, not guaranteed stock-thread or cable fit. Gold dummies represent **two M3 and four M2 male/female brass standoffs above the carrier**, with lower studs and blind upper bores; their OD, stud length and bore depth are provisional. The red RF overlay and enclosure RF-reserve restriction have been removed at the user's request. Actual antenna geometry remains; no PCB reserve/copper/layout was changed and no Wi-Fi performance guarantee is implied.
|
||||
|
||||
All dimensions are **mm**. Parameter/source coordinates are carrier-local, X right and Y down toward USB; add `(100,100)` for KiCad coordinates. The rendered model maps `Y_world = 80 - Y_carrier`, so the DB9 is above the USB end in top view. Text is separately transformed to remain readable from the USB end.
|
||||
|
||||
### Default envelope and exact chosen stack
|
||||
|
||||
The carrier remains **82 × 80**, with its **36 × 19 top-left notch** represented in the reference. The case is rectangular, covering the notch without adding internal metal there. Its width is **106** (`X=-12…94`), depth **86.8** (`Y=-3.4…83.4`), roof height **37.4**. Raised labels reach **38.0** and released plunger tips **38.9**. This is exactly **10 mm lower** than the mistaken under-carrier-brass stack. Only 1 mm internal clearance and a 2.4 mm wall are added at each cable end; the case assembly hardware is placed laterally, not beyond the top/bottom PCB edges.
|
||||
|
||||
| Datum above exterior bottom | Default Z |
|
||||
|---|---:|
|
||||
| Floor upper surface | 2.4 |
|
||||
| All carrier/daughterboard insert-boss tops / carrier underside | 7.4 |
|
||||
| Carrier top, saved PCB thickness 1.6 / brass body bottoms | 9.0 |
|
||||
| B3F-1000 released actuator top | 13.3 |
|
||||
| Base top / lid skirt bottom | 15.0 / 15.25 |
|
||||
| Button guide bottom / roof underside | 15.6 / 35.0 |
|
||||
| Brass body tops / module underside, **carrier-top-to-underside gap 10** | 19.0 |
|
||||
| RS-232 module top, STEP thickness 1.57 | 20.57 |
|
||||
| ESP32 / OLED module tops, provisional thickness 1.6 | 20.6 |
|
||||
| DB9 shell/lock centre axis | 26.84762024 |
|
||||
| DB9 nominal flange upper edge | 33.12262024 |
|
||||
| Roof underside / exterior | 35.0 / 37.4 |
|
||||
|
||||
The user's intended/reported module elevation is **10 mm**. The descriptions “8.5 mm female + 2.5 mm male” do **not** independently prove a 10 or 11 mm mating height. This design explicitly interprets the requested 10 as the gap between carrier top and module underside, and exposes `module_gap`; verify that datum against the actual mated stack. `module_gap` is the brass body height, **not an additional height below the carrier**. `carrier_bottom = floor_t + carrier_boss_h = 7.4`; the 5 mm bosses give nominal floor-to-carrier-underside clearance for solder/prongs, subject to actual component measurements. There is no under-carrier brass. Unknown module thicknesses, display height and USB height are not promoted to measurements.
|
||||
|
||||
### PCB and daughterboard fixture
|
||||
|
||||
- Carrier M3 axes: **(3.5,22.5), (3.5,75), (78,4), (78,76)**; verified against actual saved Ø3.2 NPTH holes. Base bosses Ø8 end at Z7.4 and directly support the carrier underside. Fasten the carrier with **four M3 screws through these holes into the base inserts**, not with brass underneath. These four positions do not coincide with daughterboard mounting holes.
|
||||
- Case M3 axes: **(-6,12), (-6,68), (88,12), (88,68)**. Ø9 bosses and lateral ribs stay outside the board; top-access Ø6.4 × 2.8 deep head wells, Ø3.4 screw paths, base inserts. Flat butt seam has a 0.25 vertical gap; screws locate it. No snap-fit or sealing claim.
|
||||
- RS-232 module M3 axes: **(44.905,17.907), (69.035,17.907)**, **24.13** spacing, real saved carrier Ø3 holes.
|
||||
- OLED M2 axes: **(45.72,37.75), (68.22,37.75), (45.72,60.25), (68.22,60.25)**, **22.5** grid, real saved carrier Ø2 holes. Do not confuse this with a screw clearance specification.
|
||||
- With `daughter_supports=true`, dedicated insert bosses terminate at **carrier underside Z7.4**. Screw the **two M3 RS-232 and four M2 OLED brass male/female standoffs** down through their respective existing carrier holes into the inserts. Their 10 mm bodies rest on the actual carrier top Z9.0 and support the module undersides at Z19.0. Short screws through each daughterboard engage the upper female threads. M3 boss OD7, brass reference OD6; M2 boss OD6, brass reference OD4. **ESP32 has no supplied mounting holes**: despite the request referring to three components, it remains supported by its two headers (simplified black references), not fictional standoffs or newly drilled PCB holes. No printed column crosses a PCB plane; only the metal studs pass through existing holes.
|
||||
- Check underside solder-joint/prong, trace, insert, washer and pad clearances before tightening; the nominal underside clearance is now **5 mm**, not 15 mm. The user's mostly-copper-free observation is not a measured clearance proof for conductive brass shoulders. The exact underside envelopes and mounting-land allowances are unavailable. Plastic shoulders must not crush components; exposed insert tops must not contact copper. If necessary use insulating washers and account for their thickness, reduce shoulder diameters after checking wall strength, or set `daughter_supports=false` and provide another verified fixture. Do not drill or modify the carrier to force a fit.
|
||||
|
||||
**Insert pockets are examples, not validated fits:** M3 Ø4.2 × 4 deep; M2 Ø3.2 × 3 deep, 0.3 entry flare. These numbers do not specify insert OD/length, thread engagement or thermal process. Choose the actual insert family and set diameter/depth/boss OD using its supplier recommendations and a material-specific test coupon first. Install inserts before electronics. Default carrier pockets leave 3.4 mm solid material to the outside bottom (1 mm boss web plus 2.4 floor).
|
||||
|
||||
Default insert count with all options enabled: **10 M3** (4 carrier, 4 case, 2 RS-232) and **4 M2** (OLED), all in the base. **No DB9 panel insert hangers, panel-to-lid screws or separate printable plate remain.** Connector screw posts use their own verified thread, not an inferred M3 designation.
|
||||
|
||||
Screw-length selection is by measured engagement, not a fixed BOM promise:
|
||||
|
||||
- Carrier screws: **1.6 mm** carrier thickness + washers + engagement into the base insert. Keep the head and shoulder clear of nearby components/copper.
|
||||
- Case screw under-head plane Z34.6 to insert mouth Z15 is **19.6**, plus chosen engagement (e.g. 3 gives 22.6); do not blindly use an overlong 25 mm screw in a 4 mm pocket.
|
||||
- Lower brass studs first traverse **1.6 mm carrier**, then engage the base inserts. `brass_m3_stud_l` / `brass_m2_stud_l` default to **4 mm reference lengths**, not measured hardware: tips at Z5.0 give **2.4 mm engagement**, leaving 1.6 mm to the M3 pocket bottom / 0.6 mm to the M2 pocket bottom. Actual insert usable threads may differ from pocket depth: measure before selecting. Upper daughterboard screws traverse **1.57 RS-232 / 1.6 OLED**, plus washers and chosen engagement into the **top female**, not the base. Preview female depths are 4/3 mm, also unknown. Verify studs/screws pass the nominal 3/2 mm holes without force; these are not generous M3/M2 clearance holes.
|
||||
- Connector posts traverse **2.4 mm** integral panel plus any washers/gap before engaging the connector flange. See the thread-reach and cable-mating gates below; these are not panel-to-lid screws.
|
||||
|
||||
## Cable interfaces
|
||||
|
||||
### USB: retain the accepted inboard mouths
|
||||
|
||||
Measured shell centres **X15.23 and 26.63**, widths **8.87**, mouth plane **Y77.705**, remain unchanged — mouths are **2.295 inside** the carrier bottom. The user's PCB fit/USB acceptance is retained, but it does not validate this new case.
|
||||
|
||||
A single joined bay spans **X9.595…32.265**, **Z19.6…26.6** (22.67 × 7), through the bottom-end wall. It admits both plug overmoulds instead of adding fragile individual webs. There are no internal bosses or tunnels between the wall and mouths. Mouth-to-exterior-wall distance is **5.695**: the plug must reach through that recess. The opening is deliberately generous relative to measured *widths*; shell height/depth and both plug overmoulds still need measurement. `usb_below_module_top`, `usb_above_module_top`, `usb_extra_width` are cutout controls; `usb_reference_depth/h` affect only the approximate preview. Enlarge for the actual plugs rather than moving U1 or growing Y without justification.
|
||||
|
||||
One grouped two-line label, **USB** above **CDC | UART0**, spans the combined bay; it does **not** assign left/right ports. The [wiring guide](../../docs/wiring.md#administration-and-native-usb-connectors) establishes bridge/UART0 and native/CDC roles, but neither it nor the measured HW678 mouth geometry establishes physical connector-to-role order. Seller labels are not used as mapping evidence. Both connections remain physically available independent of networking.
|
||||
|
||||
### DB9: external flange retention, not a guessed thread
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The STEP registers flange front **Y1.930**, original post mouths **Y−2.870**, shell nose **Y−3.870**, with module front Y0. Flange bounds are **X41.57…72.37**, Z20.57262024…33.12262024, thickness 0.4. Shell maximum root/lip envelope is **16.878038 × 9.1**; the design uses a conservative rectangular clearance, not a reconstructed D-profile.
|
||||
|
||||
**Integral cover mounting:** a **48 mm nominal wide × 2.4 mm axial-thickness** panel spans X33…81 (0.02 mm union overlap at each wall), Y−0.470…1.930, Z21.07…35 and joins the roof directly. Two **6 mm nominal wide full-height ribs**, X33…39 and X75…81 (outer union overlap 0.02 mm), run forward to the outer wall plane Y−3.4. They provide broad roof load paths rather than small insert posts, with 2.57/2.63 mm lateral clearance to the flange. The ribs are forward of the connector anchors (nearest anchor Y5.03; panel rear Y1.93 gives 3.10 mm separation). Panel bottom remains **0.5 mm above the module PCB top**; neither wings nor ribs extend through the PCB. Load/print strength still needs testing.
|
||||
|
||||
The shell slot is **18.078038 mm wide**, X47.930981…66.009019, **open through the panel bottom**, with top Z31.99762024 (0.6 above shell top). There is no lower crossbar to trap the connector. The upper rail is **3.00238 mm high**, continuous with the roof. Two **closed Ø3.6 through holes** retain screw posts on **X44.47 / 69.47, Z26.84762024, 25 mm spacing**, parallel to Y. The minimum slot-to-hole ligament is **1.660981 mm**. `external_fastener_clearance` is clearance only, **not a thread designation**. The module mounting holes remain a separate **24.13 mm** Z-axis system; anchor/prong holes are not new mounting points.
|
||||
|
||||
The outer-wall service recess is **X33…81**, open down to the lid seam (**Z15.25**) so no lid crossbar sweeps over the shell. The former 1 mm side slits are closed by extending the integral panel/ribs to join the wall boundaries. A **base-only stepped upstand** fills the lower opening: shoulders span X33.25…80.75 and rise from Z15 to **Z20.82**; the **17.578038 mm-wide central tongue** rises to **Z21.69762024**, leaving **0.6 mm below the STEP shell minimum Z22.29762024**. The steps interfit without an undercut, retaining **0.25 mm** horizontal edge/tongue seams and the vertical shoulder-to-panel seam. The upstand is **2.9 mm deep**, Y−3.4…−0.5: it stays **0.5 mm forward of the PCB edge Y0** and 5.53 mm forward of the nearest anchor Y5.03, rather than filling through the PCB/underside. This closes unwanted openings, not the required manufacturing/cable clearances, and is not a sealed enclosure. Forward ribs narrow the unobstructed central cable approach to **X39…75 (36 mm)**. Shell nose stays **0.47 mm beyond the ordinary outer wall** and **3.4 mm beyond the panel front**. Full cable-hood, tool and mating clearance must be checked against that recess, not assumed from nose protrusion alone.
|
||||
|
||||
**Installation path, not a horizontal-slide claim:** remove both connector screw posts and unplug *all* cables. Hold the lid at its final XY registration and lower it straight down. The shell enters the bottom-open slot; the closed side holes sweep beside the shell, where the removed posts used to obstruct them. The panel remains forward of the flange and above the PCB for the entire descent. The USB shells end at Y77.705, inboard of the rear inner wall Y81 by **3.295 mm**, so a closed USB bay does not require a horizontal slide with cables removed. Downward button guides stay on their final XY axes, stop above switch tops, and case bosses approach their corresponding base bosses without a lateral move. The nominal continuous 60 mm sweep is tested in `validate.py --installation`, including a negative control that detects unremoved posts. This checks selected envelopes, not unmodeled components or manufacturing tolerances.
|
||||
|
||||
**Hardware gate — do not skip:** remove stock posts only if the real connector permits it without damaging captive nuts/anchors. Screw their male studs **from outside through the cover holes into the connector**, leaving post bodies outside and female cable-lock receptacles available. Actual thread/pitch, removability, stud length, usable female depth and clamp torque are **unmeasured**; STEP Ø3 bores/Ø3.4 chamfers prove neither M3 nor 4-40. Ordinary screws inserted into the original front bores do not implement this clamp.
|
||||
|
||||
For under-shoulder stud length `L`, washer thickness `w`, and flange gap `g`, available engagement is **`L − panel_t − w − g`**: default **`L − 2.4 − w`**. To retain the previous engagement, studs must provide **2.4 mm more reach** (plus any washer/gap). Reusing stock posts is conditional; verified longer-stud replacements or compatible extensions may be required, and must neither bottom nor interfere internally. Do not order a guessed thread. Default `panel_flange_gap=0` registers direct flange contact; any positive gap needs a measured, supported clamp stack, not tightening across air and bending the PCB.
|
||||
|
||||
Reseating a same-length **4.8 mm** post body puts its mouth at **Y−5.270**, **1.4 mm forward of the shell nose**, instead of 1 mm behind it. This is a significant **full-mating risk**, not validated by thread engagement. A verified shorter shoulder/body, recessed cable hardware or another compatible panel-mount post geometry may be necessary; retaining the original mouth plane would require a **2.4 mm body length** at the default panel face, whose female thread depth is not established. Keep full shell mating and functional cable locks as acceptance limits. If no compatible hardware exists, revise the cover/hardware design rather than force it. `panel_t`, `panel_flange_gap`, `external_fastener_clearance` and `panel_rib_w` expose the relevant choices; do not thin structural webs just to make stock threads reach.
|
||||
|
||||
`db_panel=false` omits the integral panel/ribs and base upstand for temporary access only, **not equivalent connector retention**. The obsolete `panel` export mode and `panel-default.stl` are removed; there is no duplicate printable plate.
|
||||
|
||||
## Display, buttons and antenna
|
||||
|
||||
- OLED window follows the supplied **display boundary X44.97…69.47, Y40.5…57**, plus 0.6 margin: **25.7 × 17.7**. This is **not a known active-pixel rectangle**. It leaves the M2 fixings outside the aperture. Glass thickness/height and optical viewing angle remain unknown; the provisional 2 mm preview glass top is Z22.6, leaving a fairly deep 12.4 mm viewing recess below the roof. Check readability and consider a later stepped bezel after measuring; no lid load is applied to the glass.
|
||||
- Buttons are Omron **B3F-1000**, body **6 × 6**, actuator **Ø3.5**, released height **4.3 ±0.2**, travel **0.25 +0.2/−0.1**. Actual saved body centres are **(44.97,70.25), (56.97,70.25), (68.97,70.25)**. Labels are BACK / SELECT / NEXT, consistent with existing board/function evidence.
|
||||
- Each **25.6 long Ø5 plunger** contacts the released switch and protrudes **1.5 above the roof**. An Ø8 × 1.2 retaining flange starts 0.5 above its contact tip and stays **below** the Ø9 outer / Ø5.5 bore guide. Guide bottom is 2.3 above the nominal switch top, giving 0.6 upward retention clearance. Insert stems **from under the lid**; they cannot be installed from above. The closed case/switch holds them from below, the flange stops extraction upward. Hold with removable tape during assembly; they can fall out of a detached lid.
|
||||
- These are free sliding, switch-returned actuators, not printed springs. There is **no independent downward overload stop**: use normal tactile-switch force, never drive/hammer the plunger. Verify no preload, reliable return in different orientations, bore friction, tolerances, stroke and switch force before use. Shorten the tip/adjust flange/guide parameters if actual stack requires it. A ruggedized overload stop requires measured switch/body tolerances and is not claimed here.
|
||||
- The case **does not enforce the carrier RF reserve** and has no red RF overlay, per user instruction. The measured projecting antenna **X12.045…29.675/Y14.745…21.635**, at Z19…20.6, remains physically represented and unobstructed by case hardware, with 14.4 mm to the roof underside. The PCB, its reserve and copper are unchanged. The plastic cover is not an RF-performance guarantee; no new RF qualification is claimed.
|
||||
|
||||
## Assembly and printing
|
||||
|
||||
1. Measure insert family, actual assembled stack and connector hardware before committing to a full print. Inspect the rendered assembly against physical parts. Print cutout/insert/guide coupons or slicer-cut samples first.
|
||||
2. Print the base floor down. Indicative starting point only: 0.4 mm nozzle, 0.2 mm layers, 4 perimeters, 4–5 top/bottom layers, 25–40% infill with local solid regions around inserts. PETG is a reasonable prototype option; PLA is for fit checks away from heat. Determine material/thermal suitability yourself. No sealing, flame or temperature rating is established.
|
||||
3. Lid can be printed roof-up with supports beneath the roof, guides, integral panel/ribs and cable openings, avoiding supports in bores where practical. Alternatively print roof-down with `emboss=false`, attach separately printed text, or use a proper multi-material setup. **Integrated raised text prevents a flat roof-down first layer**; do not ignore this 0.6 mm offset. No support-free claim for the supplied embossed lid.
|
||||
4. There is **no separate DB9 plate** to print. **No plastic spacers are printed**; the brass dummies are preview-only purchased hardware. Print plungers with brim and support under their low retaining flange, or choose a slicer orientation that yields smooth, strong shafts. Deburr guides without opening them so far that stems tilt/jam. Test all three independently.
|
||||
5. Heat-set inserts with a controlled tool to the selected supplier process. All inserts are in the base; none are used for the DB9 panel. Remove strings/debris; inspect for splits, exposed insert metal and screw bottoming.
|
||||
6. Seat the carrier directly on all base support bosses and secure its four M3 mounting holes. Install the two M3 and four M2 **10 mm brass standoffs above the carrier**, with lower studs through the corresponding existing carrier holes into inserts. Seat the RS-232/OLED on them and fasten with short screws into the top females; seat the ESP32 on its headers. Check that brass body height matches the header's seated height, neither bending boards nor lifting connectors; check DB9 prongs and all underside solder joints against the 5 mm floor clearance.
|
||||
7. Disconnect all cables and remove the two verified-removable DB9 screw posts **before lowering the lid**. Insert plungers from under the lid and hold with removable tape. Align the cover in its final XY position and lower vertically: shell enters the bottom-open slot, panel wings clear the PCB, guides align over buttons. Do not slide horizontally, tilt around the USB end or force the shell. Verify free seating and button return; start the four case screws loosely.
|
||||
8. Insert verified stock or replacement post studs from outside through the two closed cover holes into the connector. Check clamp contact, engagement and bottoming independently; do not use them to pull a misaligned flange/PCB into position. Tighten case/post hardware cautiously to verified values. Test full USB/DB9 mating and cable locks, particularly the changed post-mouth reach; perform a restrained cable-load test before trusting retention.
|
||||
9. To remove the cover, unplug every cable, unscrew both connector posts completely out of the cover holes and remove the four case screws; lift vertically, retaining the plungers. The electronics remain secured by their independent carrier/daughterboard fasteners.
|
||||
|
||||
The roof prominently carries the README project name on three lines, **ESP32 / Serial Swiss / Army Knife**, left of the display and readable from the USB end. The former DISPLAY caption is removed; RS-232 and button captions are unchanged. All text comes from the shared `labels()` module, including `part="labels"`; `emboss=false` omits it only from the lid.
|
||||
|
||||
Raised labels are positive **0.6 mm emboss**, not engraving. Single-colour is the default; roof-up printing allows a filament change just above the roof for contrasting text (check actual sliced layers). For a separate material export `labels`, and export the lid with `emboss=false`. In a multi-object slicer, preserve XY origin and translate the flat text object up **22.13 mm** relative to the default lid export (its initial 0.02 overlaps the roof). Do not auto-centre individual letters; STL carries neither colour nor assembly transforms. Changes to stack require recalculating that offset: `top_z - eps - split_z - seam_clearance`.
|
||||
|
||||
## Regeneration and checks
|
||||
|
||||
Run from `hardware/case`; no network or added dependencies are needed. Shell commands below terminate; allow roughly a minute per export on slower machines.
|
||||
|
||||
```sh
|
||||
QT_QPA_PLATFORM=offscreen openscad -o base-default.stl -D 'part="base"' enclosure.scad
|
||||
QT_QPA_PLATFORM=offscreen openscad -o lid-default.stl -D 'part="lid"' enclosure.scad
|
||||
|
||||
QT_QPA_PLATFORM=offscreen openscad -o plungers-default.stl -D 'part="plungers"' enclosure.scad
|
||||
|
||||
python3 validate.py
|
||||
/usr/bin/python3 -B validate.py --source --installation --variants
|
||||
QT_QPA_PLATFORM=offscreen openscad -o assembly-preview.png --imgsize=1200,1000 --viewall --autocenter --projection=o --camera=0,0,0,55,0,25,200 -D 'part="exploded"' enclosure.scad
|
||||
QT_QPA_PLATFORM=offscreen openscad -o db9-preview.png --imgsize=1200,900 --viewall --autocenter --projection=o --camera=0,0,0,80,0,180,200 -D 'part="assembly"' enclosure.scad
|
||||
QT_QPA_PLATFORM=offscreen openscad -o lid-detail.png --render --imgsize=1200,900 --viewall --autocenter --projection=o --colorscheme=Metallic --camera=0,0,0,55,0,25,200 -D 'part="lid"' enclosure.scad
|
||||
```
|
||||
|
||||
`validate.py` uses Python's standard library and installed OpenSCAD for evaluated datum, stud-engagement and nozzle-web fixtures, plus ASCII STL checks; `--source` additionally uses the **already installed system KiCad `pcbnew` binding**, read-only. It verifies current outline/holes/placements, measured U1 USB mouth graphics and STEP-derived lock/shell data against case constants. The default checks also project the actual label glyphs against the roof, requiring at least 1 mm clearance from its edges, display/button openings and screw-head recesses. It is a drift tripwire, not a full source parser for every SCAD expression. It checks each STL has two oppositely directed uses of every triangle edge, nonzero triangle area, the expected connected component count (1 base, 1 lid, 3 plungers), positive total signed volume and minimum Z≈0. It does **not** prove vertex-manifoldness, absence of self-intersections, full component collision clearance or physical fit.
|
||||
|
||||
Validation results and bounded parameter/sweep tests are recorded in [checks.md](checks.md). The remaining exports are base, integral lid and three plungers; the corrected 37.4 mm shell / 7.4–9–19 mm stack is unchanged. No slicer, physical print, screw-fit, cable-load, hardware/firmware, thermal or RF acceptance is implied.
|
||||
|
||||
## Evidence and remaining measurements
|
||||
|
||||
Authoritative sources used, all local:
|
||||
|
||||
- Saved [carrier PCB](../PCB/serial-carrier.kicad_pcb): exact stepped outline, 1.6 thickness, all mounting holes, module/button placements and integrated measured U1 graphics.
|
||||
- [User acceptance](../../docs/devel/hardware/PCB/user-acceptance.md): aligned centreline **56.97**, approved M3/M2 hardware and 10 mm brass standoffs, user-accepted USB fit, unresolved mating-height arithmetic.
|
||||
- [Footprint measurements](../../docs/devel/hardware/PCB/footprint-notes.md), [Omron notes](../../docs/devel/hardware/PCB/button-notes.md).
|
||||
- [DB9 mechanical notes](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), [extracted measurements](../PCB/reference/db9-cad/measurements.json), [source STEP](../PCB/reference/db9-cad/5988%20RS-232%20Level%20Shifter.step), [CAD provenance/license](../PCB/reference/db9-cad/README.md). These are extracted **selected face bounds**, not complete connector reconstruction. Original female-module STEP reference is applied to the male module only under the user's reported external-geometry equivalence. No third-party STEP geometry is redistributed into the printed parts.
|
||||
|
||||
**Before calling this a fit:** measure actual insert OD/length/process and all screw heads/studs/engagement; module underside-to-carrier-top gap and board thicknesses; USB shell Z/depth and both real overmoulds; glass top/active area/view angle; component/solder underside heights and support lands; switch height/force/return; actual DB9 thread, removability, panel-jackscrew seating, flange Z, cable hood/lock reach and panel strength. Check assembled height against screw-length choices. Tune parameters and regenerate **all** affected STLs; previews and prior exports do not update automatically. Keep the 10 mm report and header arithmetic ambiguity explicit until measured.
|
||||
|
After Width: | Height: | Size: 91 KiB |
@@ -0,0 +1,79 @@
|
||||
# Integral-cover connector mounting checks — 2026-09-26
|
||||
|
||||
This supersedes the separate DB9 panel/insert-hanger design. **CAD evidence, not physical acceptance.** Installed OpenSCAD 2021.01 and system Python/KiCad were used without downloads. PCB files were read only; changes are confined to the case and agent current-state record.
|
||||
|
||||
## Geometry and outputs
|
||||
|
||||
Shell remains **106 × 86.8 × 37.4 mm**; carrier underside/top **Z7.4/9**, brass bodies 10 mm above carrier, module underside **Z19**, roof underside **Z35**. RF overlay remains removed; PCB layout/copper/reserve unchanged.
|
||||
|
||||
- Integral panel: nominal X33…81 (0.02 mm union overlap into each wall), Y−0.47…1.93, Z21.07…35, **2.4 mm clamp thickness**. Direct roof connection, two **6 mm nominal** forward ribs (inner edges unchanged) outside the flange; no DB9 heat inserts or independent attachment screws.
|
||||
- Closed **Ø3.6** post-clearance holes, **25 mm** spacing, axis Z26.84762024. No M3/4-40 inference.
|
||||
- Bottom-open **18.078038 mm** shell slot; top Z31.99762024. Minimum hole-to-slot web **1.660981 mm**; top rail **3.00238 mm**. Panel bottom clears module PCB by 0.5 mm; anchors begin 3.10 mm behind the panel rear. Flange registration comes from extracted STEP faces, not the module mounting-hole pitch.
|
||||
- Former 1 mm side slits closed by panel/rib extensions into wall boundaries. Base shoulders: X33.25…80.75, top Z20.82; central tongue width 17.578038, top Z21.69762024 (0.6 below STEP shell). Upstand Y−3.4…−0.5, depth 2.9: PCB-edge clearance 0.5, anchor separation 5.53. Shoulder/edge/tongue seams 0.25. No undercut, no change to 36 mm cable approach width.
|
||||
- Front-wall service recess extends down to lid seam. USB opening stays unchanged; installation is vertical with all cables and both connector posts removed, not a horizontal slide under button guides.
|
||||
- Deleted `panel-default.stl`, panel mode and layout object. `db_panel=false` is access-only. Base insert count is **10 M3 + 4 M2**.
|
||||
|
||||
Affected base/lid exports regenerated; unchanged plungers retained and revalidated under bounded 180-second command timeouts; CGAL reported `Simple: yes`:
|
||||
|
||||
| Export | Connected triangle components | Triangles |
|
||||
|---|---:|---:|
|
||||
| `base-default.stl` | 1 | 8404 |
|
||||
| `lid-default.stl` | 1 | 12712 |
|
||||
| `plungers-default.stl` | 3 | 1716 |
|
||||
|
||||
Base and lid geometry changed only at the connector closure; plunger geometry is unchanged. The lid is one connected printable part, not overlapping separate exports. Three PNGs (`assembly-preview.png`, `db9-preview.png`, `lid-detail.png`) regenerated headlessly under 120-second timeouts and visually reviewed. The bare-lid view exposes the closed post holes, open-bottom shell slot, forward ribs and downward guides; the assembled view shows reseated post-body references outside the panel. Simplified background references are not production component CAD.
|
||||
|
||||
## Source, expressions and mesh regression
|
||||
|
||||
Run from this directory:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B validate.py --source --installation --variants
|
||||
```
|
||||
|
||||
PASS:
|
||||
|
||||
- Native saved PCB outline, thickness, placements/orientations, carrier/module NPTH centres **and diameters**, no invented ESP32 mounting holes, button centres and measured USB mouth graphics.
|
||||
- Extracted STEP flange plane/lateral bounds, shell size and true 25 mm lock axes. Read-only KiCad emitted its known enum-property assertions but completed.
|
||||
- Actual SCAD-evaluated stack/connector expressions; independent raised-carrier and taller-module fixtures; stud engagement; obsolete panel/spacer rejection.
|
||||
- Negative assertions for insufficient stud reach, bottoming studs, thin shell/panel, negative flange gap, panel entering the PCB plane, oversized lock holes consuming slot ligaments, ribs entering flange lateral bounds, insufficient sliding seam/shell clearance and inverted closure steps.
|
||||
- Every STL triangle nondegenerate, every edge used twice with opposite winding, expected component counts, positive total volume, print-bed Z and expected bounds. These are **edge-manifold checks**, not exhaustive vertex-manifold or self-intersection proof.
|
||||
|
||||
## Continuous installation sweep
|
||||
|
||||
`--installation` renders CGAL intersections of the **actual lid** with rectangular hardware envelopes swept continuously through **60 mm of relative vertical travel**, from below to the final position. A remote 1 mm³ sentinel makes an empty intersection exportable; a passing result has only that cube, one component and exactly 1 mm³ volume.
|
||||
|
||||
Included envelopes: maximum shell root/lip, flange, STEP anchor-bracket bounds, RS-232/ESP32/OLED boards, antenna, provisional OLED glass and USB shells, full button-body/actuator bounding boxes and conservative square base-boss bounds. Flange contact is inset 0.01 mm solely to avoid a coincident CGAL face; physical tolerances are not inferred. No cable envelopes are included because **cables must be disconnected**. Plungers move with the lid and contact the switches at final seating; their return/retention remains an independent physical gate.
|
||||
|
||||
- **PASS default**: posts removed, zero modeled swept interference.
|
||||
- **PASS variant**: `carrier_boss_h=7`, `module_gap=12`, `lid_above_carrier=28`, `panel_t=3`.
|
||||
- **PASS clearance variant**: `seam_clearance=0.4`, `shell_margin=0.8`, `end_clearance=1.5`.
|
||||
- Each test also intersects the actual base/lid at seating, sweeps both upstand steps separately downward 60 mm against the lid, and sweeps electronics envelopes upward 60 mm against the new base material (equivalent to vertical electronics insertion). Only the sentinel remains. Existing conservative base-boss envelopes remain included.
|
||||
- An initial full-base Minkowski sweep exceeded the outer 240-second command timeout after default passed. Replacing it with a whole-upstand hull introduced false diagonal interference; the final per-step exact sweep avoids that invented material and completes within the bounded runs.
|
||||
- **PASS negative control**: restoring original post-body envelopes causes detected interference, demonstrating that leaving posts installed is not an allowed assembly path.
|
||||
|
||||
The first thicker-panel sweep exposed a thin residual inner-wall strip where the panel front lay forward of the original wall's inner face. The recess now clears at least the full wall thickness, independently of panel depth; the corrected fixture passes. The recess starts at the lid seam; the new forward-only base upstand occupies its lower portion.
|
||||
|
||||
These are selected conservative nominal envelopes, not a full STEP solid-motion/real-component qualification. Unknown socket bodies, component details, wires, tolerances and actual USB/glass heights remain outside the proof.
|
||||
|
||||
## Parameter CGAL smoke tests
|
||||
|
||||
Rendered to temporary files (180-second limit per render), then checked with `validate.mesh_check(path, 1)` for a single closed, consistently wound edge-manifold component:
|
||||
|
||||
```sh
|
||||
QT_QPA_PLATFORM=offscreen openscad -o /tmp/case-integral-variant-lid.stl -D 'part="lid"' -D 'carrier_boss_h=7' -D 'module_gap=12' -D 'lid_above_carrier=28' -D 'panel_t=3' -D 'emboss=false' enclosure.scad
|
||||
QT_QPA_PLATFORM=offscreen openscad -o /tmp/case-integral-access-lid.stl -D 'part="lid"' -D 'db_panel=false' -D 'emboss=false' enclosure.scad
|
||||
QT_QPA_PLATFORM=offscreen openscad -o /tmp/case-integral-variant-base.stl -D 'part="base"' -D 'daughter_supports=false' -D 'side_extra=14' -D 'm3_insert_d=4.4' enclosure.scad
|
||||
```
|
||||
|
||||
The repeatable `--variants` suite now covers these three configurations plus a raised/thicker-panel **base**. All four PASS CGAL and one-component edge-manifold checks. These exercise a thicker integral panel with raised stack, access-only lid and wider base/alternate insert pocket. They are not selected or physically validated configurations. Arbitrary parameter combinations are not guaranteed valid. `git diff --check` also passed.
|
||||
|
||||
## Hardware gates — unresolved, not hidden by CAD success
|
||||
|
||||
1. Verify posts really unscrew and determine **actual thread/pitch**, under-shoulder stud length, female depth, required engagement, clamp torque and compatibility with any captive connector hardware. No thread is established by the STEP bore.
|
||||
2. Default panel consumes **2.4 mm additional reach**: engagement = stud length − 2.4 − washers (− any flange gap). Stock studs may be too short. Verified longer-stud posts/extensions may be needed; do not bottom them or use ordinary screws in the cable-lock mouths.
|
||||
3. A same-length 4.8 mm body reseated outside the panel moves its mouth from Y−2.870 to **Y−5.270**, now **1.4 mm ahead of the shell nose**. Full mating may fail despite adequate thread engagement. A compatible shorter-body/panel-mount solution must be measured; a nominal 2.4 mm body would restore the old mouth plane but is **not a qualified available part or sufficient female-thread-depth claim**.
|
||||
4. Preserve full cable mating/locking and external nose protrusion (0.47 mm beyond ordinary wall, 3.4 mm beyond panel); verify actual hood and wrench access inside the **36 mm** central space between ribs. Revise geometry/hardware if necessary rather than forcing a fit.
|
||||
5. Existing gates remain: insert selection, underside solder/prong and conductive shoulder clearances, header/brass stack agreement, USB envelopes, glass height, button preload/return/overload, material/print orientation and cable-load strength.
|
||||
|
||||
No physical print, slicer analysis, insert installation, connector/thread measurement, full cable mating, load, RF, thermal, firmware or device test was performed. The stronger integral load path is designed and modeled, **not mechanically load-qualified**.
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,447 @@
|
||||
// Provisional enclosure, mm. See README.md before printing or ordering hardware.
|
||||
// Saved carrier coordinates: X right, Y toward USB, Z above base floor.
|
||||
// Display in conventional top view via world(): X right, cable/DB9 end up.
|
||||
part = "assembly"; // assembly, exploded, base, lid, plunger, plungers, labels, layout, check
|
||||
show_references = true; // preview only: never included in STL
|
||||
emboss = true;
|
||||
$fn = 48;
|
||||
eps = 0.02;
|
||||
|
||||
/* [Print and enclosure] */
|
||||
wall = 2.4;
|
||||
floor_t = 2.4;
|
||||
lid_t = 2.4;
|
||||
side_extra = 12; // width added at EACH side of the 82 mm carrier
|
||||
end_clearance = 1; // inner wall to carrier at cable ends
|
||||
corner_r = 3;
|
||||
seam_clearance = 0.25;
|
||||
label_raise = 0.6;
|
||||
|
||||
/* [Carrier and module stack: heights NOT independently measured] */
|
||||
carrier_t = 1.6; // saved PCB setting
|
||||
carrier_boss_h = 5; // directly supports carrier underside; solder/prong clearance above floor
|
||||
module_gap = 10; // brass body height: actual carrier TOP to module UNDERSIDE
|
||||
esp_t = 1.6; // provisional
|
||||
rs232_t = 1.57; // STEP
|
||||
oled_t = 1.6; // provisional
|
||||
lid_above_carrier = 26; // lid underside relative to carrier top
|
||||
split_above_carrier = 6;
|
||||
|
||||
/* [Heat inserts: EXAMPLE pocket sizes, select for actual inserts] */
|
||||
m3_insert_d = 4.2;
|
||||
m3_insert_depth = 4;
|
||||
m2_insert_d = 3.2;
|
||||
m2_insert_depth = 3;
|
||||
insert_lead = 0.3;
|
||||
carrier_boss_d = 8;
|
||||
case_boss_d = 9;
|
||||
m3_clearance = 3.4;
|
||||
m2_clearance = 2.3;
|
||||
m3_head_d = 6.4;
|
||||
m3_head_depth = 2.8;
|
||||
// Insert bosses stop at carrier underside; brass male/female bodies sit ABOVE it.
|
||||
daughter_supports = true;
|
||||
daughter_m3_boss_d = 7;
|
||||
daughter_m2_boss_d = 6;
|
||||
brass_m3_d = 6; // reference OD only; measure purchased hardware
|
||||
brass_m2_d = 4;
|
||||
brass_m3_stud_l = 4; // UNKNOWN actual lengths: reference values, includes carrier thickness
|
||||
brass_m2_stud_l = 4;
|
||||
brass_m3_female_depth = 4; // reference only, not a hardware specification
|
||||
brass_m2_female_depth = 3;
|
||||
nozzle_d = 0.4; // analytical minimum-web checks, not a slicer simulation
|
||||
|
||||
/* [USB: only X widths and mouth Y measured] */
|
||||
usb_extra_width = 2.4; // each shell's total added width; overlap joins into one bay
|
||||
usb_below_module_top = 1.0;
|
||||
usb_above_module_top = 6.0;
|
||||
usb_reference_depth = 7; // preview estimate ONLY
|
||||
usb_reference_h = 3.5; // preview estimate ONLY
|
||||
|
||||
/* [DB9 panel: replacement jackscrews must be verified] */
|
||||
db_panel = true; // false leaves service opening; no case-to-flange retention
|
||||
panel_t = 2.4; // axial clamp thickness; consumes this much additional stud reach
|
||||
panel_flange_gap = 0; // nominal flange contact; tune after physical measurement
|
||||
external_fastener_clearance = 3.6; // NOT a thread designation
|
||||
panel_rib_w = 5; // broad forward ribs outside the flange, not insert hangers
|
||||
shell_margin = 0.6; // per edge on STEP maximum shell envelope
|
||||
panel_bottom_above_module = 0.5;
|
||||
|
||||
/* [Display and controls] */
|
||||
display_margin = 0.6; // around entire measured display, NOT active pixels
|
||||
button_h = 4.3; // Omron B3F-1000, +/-0.2
|
||||
plunger_flange_offset = 0.5; // above switch contact face; no external travel stop
|
||||
plunger_d = 5;
|
||||
plunger_clearance = 0.5; // diametral, tune printer
|
||||
plunger_flange_d = 8;
|
||||
plunger_flange_t = 1.2;
|
||||
plunger_above_lid = 1.5;
|
||||
guide_od = 9;
|
||||
guide_bottom_above_button = 2.3;
|
||||
|
||||
// Source geometry, carrier-local. Do not replace hole centres with body centres.
|
||||
board_outline = [[0,19],[36,19],[36,0],[82,0],[82,80],[0,80]];
|
||||
carrier_holes = [[3.5,22.5],[3.5,75],[78,4],[78,76]];
|
||||
rs_holes = [[44.905,17.907],[69.035,17.907]]; // 24.130 spacing, Z axes
|
||||
oled_holes = [[45.72,37.75],[68.22,37.75],[45.72,60.25],[68.22,60.25]];
|
||||
button_centres = [[44.97,70.25],[56.97,70.25],[68.97,70.25]];
|
||||
usb_centres = [15.23,26.63];
|
||||
usb_mouth_y = 77.705;
|
||||
module_cx = 56.97;
|
||||
lock_x = [44.47,69.47]; // 25.000 spacing, Y axes; NOT rs_holes
|
||||
|
||||
carrier_bottom = floor_t + carrier_boss_h;
|
||||
carrier_top = carrier_bottom + carrier_t;
|
||||
module_bottom = carrier_top + module_gap;
|
||||
rs_top = module_bottom + rs232_t;
|
||||
esp_top = module_bottom + esp_t;
|
||||
oled_top = module_bottom + oled_t;
|
||||
lid_z = carrier_top + lid_above_carrier;
|
||||
top_z = lid_z + lid_t;
|
||||
split_z = carrier_top + split_above_carrier;
|
||||
xmin = -side_extra;
|
||||
xmax = 82 + side_extra;
|
||||
ymin = -end_clearance-wall;
|
||||
ymax = 80+end_clearance+wall;
|
||||
case_holes = [[-side_extra/2,12],[-side_extra/2,68],
|
||||
[82+side_extra/2,12],[82+side_extra/2,68]];
|
||||
db_axis_z = rs_top + 6.27762024;
|
||||
flange_y = 1.930;
|
||||
panel_rear_y = flange_y-panel_flange_gap;
|
||||
panel_front_y = panel_rear_y-panel_t;
|
||||
panel_low_z = rs_top+panel_bottom_above_module;
|
||||
recess_left = 33;
|
||||
recess_right = 81;
|
||||
shell_slot_w = 16.878038+2*shell_margin;
|
||||
upstand_top = db_axis_z-9.1/2-shell_margin;
|
||||
upstand_shoulder = panel_low_z-seam_clearance;
|
||||
upstand_rear = min(-0.5,panel_front_y);
|
||||
button_top = carrier_top+button_h;
|
||||
guide_low_z = button_top+guide_bottom_above_button;
|
||||
guide_high_z = lid_z;
|
||||
flange_bottom_z = button_top+plunger_flange_offset;
|
||||
plunger_length = top_z+plunger_above_lid-button_top;
|
||||
|
||||
assert(side_extra >= case_boss_d+2, "Widen side rails for case bosses");
|
||||
assert(carrier_boss_h > max(m3_insert_depth,m2_insert_depth), "Leave material under inserts");
|
||||
assert(min(wall,floor_t,lid_t) >= 3*nozzle_d, "Shell needs at least three nozzle widths");
|
||||
assert((min(carrier_boss_d,case_boss_d,daughter_m3_boss_d)
|
||||
-m3_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M3 entry web too thin");
|
||||
assert((daughter_m2_boss_d-m2_insert_d-2*insert_lead)/2 >= 2*nozzle_d, "M2 entry web too thin");
|
||||
assert((guide_od-plunger_d-plunger_clearance)/2 >= 2*nozzle_d, "Guide web too thin");
|
||||
assert(brass_m3_stud_l > carrier_t && brass_m3_stud_l-carrier_t < m3_insert_depth,
|
||||
"M3 reference stud must cross carrier and engage without bottoming");
|
||||
assert(brass_m2_stud_l > carrier_t && brass_m2_stud_l-carrier_t < m2_insert_depth,
|
||||
"M2 reference stud must cross carrier and engage without bottoming");
|
||||
assert(module_gap > max(brass_m3_female_depth,brass_m2_female_depth)+1);
|
||||
assert(m3_insert_d+2 < min(carrier_boss_d,case_boss_d,daughter_m3_boss_d));
|
||||
assert(m2_insert_d+2 < daughter_m2_boss_d);
|
||||
assert(lid_z > rs_top+12.55262024+0.8, "Lid intersects DB9 flange");
|
||||
assert(panel_low_z >= rs_top+0.5, "Panel must stay above module PCB during lowering");
|
||||
assert(panel_low_z < db_axis_z-external_fastener_clearance/2-0.8);
|
||||
assert(panel_t >= 3*nozzle_d && panel_rib_w >= 3*nozzle_d);
|
||||
assert(seam_clearance >= 0.2 && seam_clearance <= 0.5, "Keep printable sliding seams");
|
||||
assert(shell_margin >= 0.5, "Retain shell/cable clearance");
|
||||
assert(upstand_rear-ymin >= 3*nozzle_d, "Upstand wall too thin");
|
||||
assert(upstand_shoulder > split_z && upstand_top > upstand_shoulder,
|
||||
"Stepped closure must rise above base and below shell");
|
||||
assert(upstand_rear <= -0.5, "Do not enter projecting module PCB/underside");
|
||||
assert(panel_flange_gap >= 0, "Panel cannot cross flange plane");
|
||||
assert(34+panel_rib_w < 41.57 && 80-panel_rib_w > 72.37, "Ribs must clear flange sides");
|
||||
assert(12.5-16.878038/2-shell_margin-external_fastener_clearance/2 >= 3*nozzle_d,
|
||||
"Closed lock holes need a solid web to the open shell slot");
|
||||
assert(panel_front_y > -2.870, "Panel would obscure stock cable-lock mouth plane");
|
||||
assert(guide_high_z > guide_low_z+4);
|
||||
assert(flange_bottom_z+plunger_flange_t < guide_low_z, "Plunger flange trapped below guide");
|
||||
assert(split_z > carrier_top+2 && split_z < module_bottom-1);
|
||||
assert(usb_above_module_top+esp_top < lid_z, "USB bay cuts roof");
|
||||
|
||||
echo("PROVISIONAL overall W,D,H (without labels/plungers)",xmax-xmin,ymax-ymin,top_z);
|
||||
echo("carrier bottom/top, module underside, RS232 top",carrier_bottom,carrier_top,module_bottom,rs_top);
|
||||
echo("DB9 lock axis Z, split, lid underside",db_axis_z,split_z,lid_z);
|
||||
echo("CHECK stack",[carrier_bottom,carrier_top,module_bottom,rs_top,lid_z,top_z,
|
||||
split_z,button_top,guide_low_z,db_axis_z,panel_low_z,
|
||||
esp_top-usb_below_module_top,esp_top+usb_above_module_top,plunger_length]);
|
||||
echo("CHECK closure",[recess_left,recess_right,upstand_rear,upstand_shoulder,
|
||||
upstand_top,shell_slot_w-2*seam_clearance,seam_clearance]);
|
||||
echo("CHECK engagement",[brass_m3_stud_l-carrier_t,brass_m2_stud_l-carrier_t]);
|
||||
echo("CHECK connector",[panel_front_y,panel_rear_y,panel_t,panel_low_z,
|
||||
db_axis_z+9.1/2+shell_margin,16.878038+2*shell_margin,
|
||||
lock_x[1]-lock_x[0],external_fastener_clearance,panel_rib_w,
|
||||
12.5-16.878038/2-shell_margin-external_fastener_clearance/2]);
|
||||
|
||||
// Reflect carrier Y for a conventional top view; text compensates separately.
|
||||
module world() { translate([0,80,0]) rotate([0,0,0]) mirror([0,1,0]) children(); }
|
||||
module box(x,y,z,w,d,h) { translate([x,y,z]) cube([w,d,h]); }
|
||||
module zcyl(p,z,d,h) { translate([p[0],p[1],z]) cylinder(d=d,h=h); }
|
||||
module ybore(x,y,z,d,len) { translate([x,y,z]) rotate([-90,0,0]) cylinder(d=d,h=len); }
|
||||
module outer_2d(inset=0) {
|
||||
translate([xmin+inset+corner_r,ymin+inset+corner_r])
|
||||
offset(r=corner_r) square([xmax-xmin-2*inset-2*corner_r,
|
||||
ymax-ymin-2*inset-2*corner_r]);
|
||||
}
|
||||
module outline_volume(z,h,inset=0) { translate([0,0,z]) linear_extrude(h) outer_2d(inset); }
|
||||
module walls(z,h) {
|
||||
difference() {
|
||||
outline_volume(z,h);
|
||||
outline_volume(z-eps,h+2*eps,wall);
|
||||
}
|
||||
}
|
||||
module insert_pocket(p,top,d,depth) {
|
||||
zcyl(p,top-depth,d,depth+eps);
|
||||
translate([p[0],p[1],top-insert_lead])
|
||||
cylinder(d1=d,d2=d+2*insert_lead,h=insert_lead+eps);
|
||||
}
|
||||
module daughter_bosses() {
|
||||
for(p=rs_holes) zcyl(p,floor_t-eps,daughter_m3_boss_d,carrier_bottom-floor_t+eps);
|
||||
for(p=oled_holes) zcyl(p,floor_t-eps,daughter_m2_boss_d,carrier_bottom-floor_t+eps);
|
||||
}
|
||||
module cable_cuts() {
|
||||
// Whole DB9 service recess clears PCB overhang, flange and wrench approach.
|
||||
// Panel lives inward of this wall and ABOVE the module PCB, never through it.
|
||||
box(33,ymin-1,split_z+seam_clearance-eps,48,
|
||||
max(panel_front_y,-end_clearance)-ymin+1+eps,
|
||||
lid_z-split_z-seam_clearance+eps);
|
||||
// Wide joined bay admits overmoulds to the inboard USB mouths. No thin web.
|
||||
box(usb_centres[0]-(8.87+usb_extra_width)/2,usb_mouth_y-1,
|
||||
esp_top-usb_below_module_top,
|
||||
usb_centres[1]-usb_centres[0]+8.87+usb_extra_width,
|
||||
ymax-usb_mouth_y+2,usb_below_module_top+usb_above_module_top);
|
||||
}
|
||||
module connector_upstand(travel=0) {
|
||||
// Forward of the PCB edge, including its unknown underside components.
|
||||
// Shoulders meet the ribs/panel with a seam; tongue closes the open shell slot.
|
||||
box(recess_left+seam_clearance,ymin,split_z-eps-travel,
|
||||
recess_right-recess_left-2*seam_clearance,upstand_rear-ymin,
|
||||
upstand_shoulder-split_z+eps+travel);
|
||||
box(module_cx-shell_slot_w/2+seam_clearance,ymin,upstand_shoulder-eps-travel,
|
||||
shell_slot_w-2*seam_clearance,upstand_rear-ymin,
|
||||
upstand_top-upstand_shoulder+eps+travel);
|
||||
}
|
||||
module base() {
|
||||
difference() {
|
||||
union() {
|
||||
outline_volume(0,floor_t);
|
||||
walls(floor_t-eps,split_z-floor_t+eps);
|
||||
for(p=carrier_holes) zcyl(p,floor_t-eps,carrier_boss_d,carrier_boss_h+eps);
|
||||
for(p=case_holes) {
|
||||
zcyl(p,floor_t-eps,case_boss_d,split_z-floor_t+eps);
|
||||
// Lateral ribs connect bosses to walls; not in cable approach paths.
|
||||
box(p[0]<0?xmin:p[0],p[1]-2,floor_t-eps,
|
||||
side_extra/2,4,split_z-floor_t+eps);
|
||||
}
|
||||
if(daughter_supports) daughter_bosses();
|
||||
}
|
||||
for(p=carrier_holes) insert_pocket(p,floor_t+carrier_boss_h,m3_insert_d,m3_insert_depth);
|
||||
for(p=case_holes) insert_pocket(p,split_z,m3_insert_d,m3_insert_depth);
|
||||
if(daughter_supports) {
|
||||
for(p=rs_holes) insert_pocket(p,carrier_bottom,m3_insert_d,m3_insert_depth);
|
||||
for(p=oled_holes) insert_pocket(p,carrier_bottom,m2_insert_d,m2_insert_depth);
|
||||
}
|
||||
cable_cuts();
|
||||
}
|
||||
if(db_panel) connector_upstand();
|
||||
}
|
||||
module connector_panel() {
|
||||
difference() {
|
||||
// Bottom-open slot clears the shell during straight vertical lid installation.
|
||||
// Solid wings and top rail meet the roof; nothing crosses the PCB plane.
|
||||
box(recess_left-eps,panel_front_y,panel_low_z,
|
||||
recess_right-recess_left+2*eps,panel_t,lid_z-panel_low_z+eps);
|
||||
box(module_cx-16.878038/2-shell_margin,panel_front_y-eps,
|
||||
panel_low_z-eps,16.878038+2*shell_margin,
|
||||
panel_t+2*eps,db_axis_z+9.1/2+shell_margin-panel_low_z+eps);
|
||||
for(x=lock_x) ybore(x,panel_front_y-eps,db_axis_z,
|
||||
external_fastener_clearance,panel_t+2*eps);
|
||||
|
||||
}
|
||||
}
|
||||
module connector_ribs() {
|
||||
// Forward of flange/anchors, outside flange X bounds; full-height roof load path.
|
||||
box(recess_left-eps,ymin,panel_low_z,34+panel_rib_w-recess_left+eps,
|
||||
panel_rear_y-ymin,lid_z-panel_low_z+eps);
|
||||
box(80-panel_rib_w,ymin,panel_low_z,recess_right-(80-panel_rib_w)+eps,
|
||||
panel_rear_y-ymin,lid_z-panel_low_z+eps);
|
||||
}
|
||||
module guide(p) {
|
||||
difference() {
|
||||
// Flange stays BELOW this continuous guide: insert stem from underside.
|
||||
zcyl(p,guide_low_z,guide_od,guide_high_z-guide_low_z+eps);
|
||||
zcyl(p,guide_low_z-eps,plunger_d+plunger_clearance,
|
||||
guide_high_z-guide_low_z+2*eps);
|
||||
}
|
||||
}
|
||||
module lid_body() {
|
||||
difference() {
|
||||
union() {
|
||||
outline_volume(lid_z,lid_t);
|
||||
walls(split_z+seam_clearance,lid_z-split_z-seam_clearance+eps);
|
||||
for(p=case_holes) {
|
||||
zcyl(p,split_z+seam_clearance,case_boss_d,lid_z-split_z-seam_clearance+eps);
|
||||
box(p[0]<0?xmin:p[0],p[1]-2,split_z+seam_clearance,
|
||||
side_extra/2,4,lid_z-split_z-seam_clearance+eps);
|
||||
}
|
||||
for(p=button_centres) guide(p);
|
||||
|
||||
}
|
||||
// Measured glass/display boundary plus margin, not a guessed active area.
|
||||
box(44.97-display_margin,40.5-display_margin,lid_z-eps,
|
||||
24.5+2*display_margin,16.5+2*display_margin,lid_t+2*eps);
|
||||
for(p=button_centres) zcyl(p,lid_z-eps,plunger_d+plunger_clearance,lid_t+2*eps);
|
||||
for(p=case_holes) {
|
||||
zcyl(p,split_z-eps,m3_clearance,top_z-split_z+2*eps);
|
||||
// Recess spans thick boss, leaving lid thickness below screw head.
|
||||
zcyl(p,top_z-m3_head_depth,m3_head_d,m3_head_depth+eps);
|
||||
}
|
||||
cable_cuts();
|
||||
}
|
||||
}
|
||||
module label_at(x,y,s,size=2.5) {
|
||||
// Undo world's Y reflection for text only: readable from USB end, DB9 above.
|
||||
translate([x,y,top_z-eps]) mirror([0,1,0]) linear_extrude(label_raise+eps)
|
||||
text(s,size=size,halign="center",valign="center",font="Liberation Sans:style=Bold");
|
||||
}
|
||||
module labels() {
|
||||
label_at(module_cx,7,"RS-232",3);
|
||||
// One grouped bay label: physical UART0/CDC left/right is not established.
|
||||
label_at(20.93,71,"USB",3);
|
||||
label_at(20.93,76,"CDC | UART0",2.4);
|
||||
label_at(20.93,32,"ESP32",5);
|
||||
label_at(20.93,41,"Serial Swiss",4);
|
||||
label_at(20.93,50,"Army Knife",4);
|
||||
for(i=[0:2]) label_at(button_centres[i][0],77,["BACK","SELECT","NEXT"][i],2.1);
|
||||
}
|
||||
module lid() {
|
||||
lid_body();
|
||||
if(db_panel) { connector_panel(); connector_ribs(); }
|
||||
if(emboss) labels();
|
||||
}
|
||||
module plunger() {
|
||||
// Local bottom at zero, prints vertically; brim recommended for slender stem.
|
||||
cylinder(d=plunger_d,h=plunger_length);
|
||||
translate([0,0,flange_bottom_z-button_top]) cylinder(d=plunger_flange_d,h=plunger_flange_t);
|
||||
}
|
||||
module plungers() { for(i=[0:2]) translate([i*12,0,0]) plunger(); }
|
||||
// Preview-only unthreaded hardware dummies, never printable parts.
|
||||
module brass_standoff(p,od,thread_d,stud_l,female_depth) {
|
||||
difference() {
|
||||
zcyl(p,carrier_top,od,module_gap);
|
||||
zcyl(p,module_bottom-female_depth,thread_d,female_depth+eps);
|
||||
}
|
||||
zcyl(p,carrier_top-stud_l,thread_d,stud_l+eps);
|
||||
}
|
||||
module pcb_shape() {
|
||||
difference() {
|
||||
linear_extrude(carrier_t) polygon(board_outline);
|
||||
for(p=carrier_holes) zcyl(p,-eps,3.2,carrier_t+2*eps);
|
||||
for(p=rs_holes) zcyl(p,-eps,3,carrier_t+2*eps);
|
||||
for(p=oled_holes) zcyl(p,-eps,2,carrier_t+2*eps);
|
||||
}
|
||||
}
|
||||
module references() {
|
||||
// All simplified envelopes, not complete component CAD or collision certificates.
|
||||
color("seagreen") translate([0,0,carrier_bottom]) pcb_shape();
|
||||
// Four independent carrier screws, not daughterboard standoff locations.
|
||||
color("silver") for(p=carrier_holes) {
|
||||
zcyl(p,carrier_bottom-2.4,3,carrier_t+2.4);
|
||||
zcyl(p,carrier_top,5.5,2); // head/engagement reference only
|
||||
}
|
||||
// ESP32 is header-supported: no supplied mounting holes, no invented fasteners.
|
||||
color("black") for(x=[8,33.4]) box(x-1.25,21.75,carrier_top,2.5,55.84,module_gap);
|
||||
color("darkgreen") {
|
||||
box(6.635,21.635,module_bottom,28.09,56.07,esp_t);
|
||||
difference() {
|
||||
box(41.095,0,module_bottom,31.75,29.337,rs232_t);
|
||||
for(p=rs_holes) zcyl(p,module_bottom-eps,3,rs232_t+2*eps);
|
||||
}
|
||||
difference() {
|
||||
box(43.97,36,module_bottom,26,26,oled_t);
|
||||
for(p=oled_holes) zcyl(p,module_bottom-eps,2,oled_t+2*eps);
|
||||
}
|
||||
}
|
||||
color("orange") box(12.045,14.745,module_bottom,17.63,6.89,esp_t);
|
||||
|
||||
color("black") box(44.97,40.5,oled_top,24.5,16.5,2); // glass height unknown
|
||||
color("silver") {
|
||||
for(x=usb_centres) box(x-8.87/2,usb_mouth_y-usb_reference_depth,esp_top,
|
||||
8.87,usb_reference_depth,usb_reference_h);
|
||||
box(41.57,1.930,rs_top+0.00262024,30.8,0.4,12.55);
|
||||
box(module_cx-16.878038/2,-3.870,db_axis_z-9.1/2,16.878038,5.8,9.1);
|
||||
}
|
||||
// Reseated 4.8 mm post-body reference ONLY; stud/thread and cable reach unverified.
|
||||
color([0.8,0.5,0.1,0.65]) for(x=lock_x)
|
||||
ybore(x,(db_panel?panel_front_y:flange_y)-4.8,db_axis_z,5.311622,4.8);
|
||||
color("gray") for(p=button_centres) {
|
||||
box(p[0]-3,p[1]-3,carrier_top,6,6,3.4);
|
||||
zcyl(p,carrier_top+3.4,3.5,button_h-3.4);
|
||||
}
|
||||
if(daughter_supports) color("gold") {
|
||||
for(p=rs_holes) brass_standoff(p,brass_m3_d,3,brass_m3_stud_l,brass_m3_female_depth);
|
||||
for(p=oled_holes) brass_standoff(p,brass_m2_d,2,brass_m2_stud_l,brass_m2_female_depth);
|
||||
}
|
||||
}
|
||||
module assembled(explode=0) {
|
||||
color("slategray") render(convexity=10) base();
|
||||
color([0.8,0.85,0.9,0.7]) translate([0,0,explode]) render(convexity=10) lid();
|
||||
|
||||
color("orange") for(p=button_centres) translate([p[0],p[1],button_top+explode]) plunger();
|
||||
if($preview && show_references) %references();
|
||||
}
|
||||
// Conservative rectangular envelopes swept downward in the lid frame: equivalent
|
||||
// to lowering the lid vertically 60 mm onto fixed electronics, posts REMOVED.
|
||||
// Contact faces inset 0.01 only to avoid a coincident flange face in CGAL.
|
||||
module installation_envelopes(stock_posts=false,travel=60,upward=false) {
|
||||
module swept(x,y,z,w,d,h) { box(x,y,upward?z:z-travel,w,d,h+travel); }
|
||||
swept(module_cx-16.878038/2,-3.870,db_axis_z-9.1/2,16.878038,5.8,9.1);
|
||||
swept(41.57,flange_y+0.01,rs_top+0.00262024,30.8,0.39,12.55);
|
||||
// Connector anchor brackets: STEP solids 13/14, not invented mounting holes.
|
||||
for(x=[41.97,66.97]) swept(x,5.03,module_bottom-2.23,5,6.4,12.35262024);
|
||||
swept(41.095,0,module_bottom,31.75,29.337,rs232_t);
|
||||
swept(6.635,21.635,module_bottom,28.09,56.07,esp_t);
|
||||
swept(12.045,14.745,module_bottom,17.63,6.89,esp_t);
|
||||
swept(43.97,36,module_bottom,26,26,oled_t);
|
||||
swept(44.97,40.5,oled_top,24.5,16.5,2);
|
||||
for(x=usb_centres) swept(x-8.87/2,usb_mouth_y-usb_reference_depth,esp_top,
|
||||
8.87,usb_reference_depth,usb_reference_h);
|
||||
for(p=button_centres) swept(p[0]-3,p[1]-3,carrier_top,6,6,button_h);
|
||||
for(p=case_holes) swept(p[0]-case_boss_d/2,p[1]-case_boss_d/2,
|
||||
floor_t,case_boss_d,case_boss_d,split_z-floor_t);
|
||||
if(stock_posts) for(x=lock_x)
|
||||
swept(x-5.311622/2,-2.870,db_axis_z-2.3,5.311622,4.8,4.6);
|
||||
}
|
||||
module installation_check(stock_posts=false) {
|
||||
// Sentinel permits exporting an otherwise empty intersection; expected volume 1.
|
||||
cube(1);
|
||||
translate([0,0,80]) {
|
||||
intersection() { lid(); installation_envelopes(stock_posts); }
|
||||
intersection() { lid(); base(); }
|
||||
// Sweep each step separately: a whole-part hull invents diagonal material.
|
||||
if(db_panel) intersection() { lid(); connector_upstand(60); }
|
||||
// New material must also clear electronics while boards lower into the base.
|
||||
if(db_panel) intersection() {
|
||||
connector_upstand();
|
||||
installation_envelopes(false,60,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Each printable export is on Z=0, without hardware reference geometry.
|
||||
if(part=="base") world() base();
|
||||
else if(part=="lid") translate([0,0,-split_z-seam_clearance]) world() lid();
|
||||
|
||||
else if(part=="plunger") plunger();
|
||||
else if(part=="plungers") plungers();
|
||||
|
||||
else if(part=="labels") translate([0,0,-top_z+eps]) world() labels();
|
||||
else if(part=="layout") {
|
||||
world() base();
|
||||
translate([xmax-xmin+12,0,-split_z-seam_clearance]) world() lid();
|
||||
translate([0,-22,0]) plungers();
|
||||
|
||||
|
||||
}
|
||||
else if(part=="installation-check") installation_check();
|
||||
else if(part=="installation-blocked") installation_check(true);
|
||||
else if(part=="check") {} // datums/assertions only, used by validate.py
|
||||
else if(part=="assembly") world() assembled();
|
||||
else if(part=="exploded") world() assembled(28);
|
||||
else assert(false,"Unknown part");
|
||||
|
After Width: | Height: | Size: 32 KiB |
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only source registration and ASCII STL topology checks; no packages added.
|
||||
|
||||
Use /usr/bin/python3 for optional installed KiCad bindings. This is not a full
|
||||
solid collision, slicer, strength, RF, or physical-fit test.
|
||||
"""
|
||||
import argparse
|
||||
import ast
|
||||
from collections import defaultdict
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT.parent / "PCB"
|
||||
SCAD = (ROOT / "enclosure.scad").read_text()
|
||||
|
||||
|
||||
def literal(name):
|
||||
match = re.search(r"^" + re.escape(name) + r"\s*=\s*([^;]+);", SCAD, re.M)
|
||||
assert match, name
|
||||
return ast.literal_eval(match.group(1))
|
||||
|
||||
|
||||
def near(a, b):
|
||||
assert len(a) == len(b) and all(abs(x-y) < 1e-5 for x, y in zip(a, b)), (a, b)
|
||||
|
||||
|
||||
def source_check():
|
||||
import pcbnew as p # installed KiCad, not required for mesh-only checking
|
||||
board = p.LoadBoard(str(PCB / "serial-carrier.kicad_pcb"))
|
||||
fps = {f.GetReference(): f for f in board.GetFootprints()}
|
||||
|
||||
def local(v):
|
||||
return (p.ToMM(v.x)-100, p.ToMM(v.y)-100)
|
||||
|
||||
def holes(ref):
|
||||
return sorted(local(q.GetPosition()) for q in fps[ref].Pads()
|
||||
if q.GetAttribute() == p.PAD_ATTRIB_NPTH)
|
||||
|
||||
near([p.ToMM(board.GetDesignSettings().GetBoardThickness())], [literal("carrier_t")])
|
||||
for ref, xy in {"U1": [8, 23], "U2": [43, 26.670], "DS1": [43.97, 36]}.items():
|
||||
near(local(fps[ref].GetPosition()), xy)
|
||||
assert fps[ref].GetOrientationDegrees() == 0
|
||||
assert not holes("U1"), "ESP32 has no supplied mounting holes; retain header support"
|
||||
for ref, diameter in [("H1", 3.2), ("H2", 3.2), ("H3", 3.2), ("H4", 3.2), ("U2", 3), ("DS1", 2)]:
|
||||
for pad in fps[ref].Pads():
|
||||
if pad.GetAttribute() == p.PAD_ATTRIB_NPTH:
|
||||
near([p.ToMM(pad.GetDrillSize().x), p.ToMM(pad.GetDrillSize().y)], [diameter]*2)
|
||||
for ref, xy in zip(["H1", "H2", "H3", "H4"], literal("carrier_holes")):
|
||||
near(holes(ref)[0], xy)
|
||||
for ref, key in [("U2", "rs_holes"), ("DS1", "oled_holes")]:
|
||||
for actual, expected in zip(holes(ref), sorted(literal(key))):
|
||||
near(actual, expected)
|
||||
assert len(holes(ref)) == len(literal(key))
|
||||
for ref, xy in zip(["SW1", "SW2", "SW3"], literal("button_centres")):
|
||||
origin = local(fps[ref].GetPosition())
|
||||
near([origin[0]+3.25, origin[1]+2.25], xy)
|
||||
assert fps[ref].GetOrientationDegrees() == 0
|
||||
edges = {tuple(round(v, 6) for v in local(d.GetStart()))
|
||||
for d in board.GetDrawings() if d.GetLayer() == p.Edge_Cuts}
|
||||
assert edges == {tuple(v) for v in literal("board_outline")}
|
||||
# Measured embedded U1 F.Fab lines: mouth widths/position are source authority.
|
||||
mouth_lines = []
|
||||
for item in fps["U1"].GraphicalItems():
|
||||
if isinstance(item, p.PCB_SHAPE) and item.GetLayer() == p.F_Fab:
|
||||
a, b = local(item.GetStart()), local(item.GetEnd())
|
||||
if abs(a[1]-77.705) < 1e-5 and abs(b[1]-77.705) < 1e-5:
|
||||
mouth_lines.append((a, b))
|
||||
measured = sorted((a[0]+b[0])/2 for a, b in mouth_lines
|
||||
if abs(abs(a[0]-b[0])-8.87) < 1e-5)
|
||||
near(measured, literal("usb_centres"))
|
||||
near([literal("usb_mouth_y")], [77.705])
|
||||
cad = json.loads((PCB / "reference/db9-cad/measurements.json").read_text())
|
||||
near([axis["location"][0]+43-1.905 for axis in cad["front_lock_axes"]], literal("lock_x"))
|
||||
near(cad["shell_outer_including_root_and_lip"]["dimensions"], [16.878038, 5.8, 9.1])
|
||||
shell = cad["shell_outer_including_root_and_lip"]["bounds"]
|
||||
near([shell[2], 29.337-shell[4]], [3.29762024, -3.87])
|
||||
flange = cad["front_flange_exposed_faces"]["bounds"]
|
||||
near([29.337-flange[4]], [literal("flange_y")])
|
||||
near([flange[0]+41.095, flange[3]+41.095], [41.57, 72.37])
|
||||
for axis in cad["front_lock_axes"]:
|
||||
near([axis["location"][2]-1.57], [6.27762024])
|
||||
print("PASS native PCB outline, thickness, placements, NPTH centres, buttons, USB mouths, STEP lock/shell data")
|
||||
|
||||
|
||||
def mesh_check(path, expected_components):
|
||||
text = path.read_text() # OpenSCAD 2021 default ASCII STL, fail on other format
|
||||
vertices = [tuple(map(float, xyz)) for xyz in re.findall(
|
||||
r"\bvertex\s+([-+\deE.]+)\s+([-+\deE.]+)\s+([-+\deE.]+)", text)]
|
||||
assert vertices and len(vertices) % 3 == 0, path
|
||||
triangles = [tuple(vertices[i:i+3]) for i in range(0, len(vertices), 3)]
|
||||
edges = defaultdict(list)
|
||||
neighbours = defaultdict(set)
|
||||
volume = 0
|
||||
for idx, (a, b, c) in enumerate(triangles):
|
||||
assert len({a, b, c}) == 3, (path, "degenerate triangle", idx)
|
||||
u, v = [b[i]-a[i] for i in range(3)], [c[i]-a[i] for i in range(3)]
|
||||
cross = (u[1]*v[2]-u[2]*v[1], u[2]*v[0]-u[0]*v[2], u[0]*v[1]-u[1]*v[0])
|
||||
assert sum(q*q for q in cross) > 1e-18, (path, "zero area", idx)
|
||||
volume += sum(a[i]*cross[i] for i in range(3))/6
|
||||
for start, end in [(a, b), (b, c), (c, a)]:
|
||||
edges[tuple(sorted((start, end)))].append((idx, start, end))
|
||||
for edge, uses in edges.items():
|
||||
assert len(uses) == 2, (path, "nonmanifold/open edge", edge, len(uses))
|
||||
assert uses[0][1:] == uses[1][1:][::-1], (path, "inconsistent winding", edge)
|
||||
i, j = uses[0][0], uses[1][0]
|
||||
neighbours[i].add(j)
|
||||
neighbours[j].add(i)
|
||||
remaining, components = set(range(len(triangles))), 0
|
||||
while remaining:
|
||||
stack = [remaining.pop()]
|
||||
components += 1
|
||||
while stack:
|
||||
for n in neighbours[stack.pop()]:
|
||||
if n in remaining:
|
||||
remaining.remove(n)
|
||||
stack.append(n)
|
||||
assert components == expected_components, (path, "components", components)
|
||||
assert volume > 0, (path, "inverted/nonpositive volume", volume)
|
||||
low = [min(v[i] for v in vertices) for i in range(3)]
|
||||
high = [max(v[i] for v in vertices) for i in range(3)]
|
||||
assert abs(low[2]) < 1e-4, (path, "not on print bed", low)
|
||||
return {"file": path.name, "triangles": len(triangles), "components": components,
|
||||
"closed_consistently_wound_edges": True, "volume_mm3": round(volume, 3),
|
||||
"bounds_mm": [low, high], "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
||||
|
||||
|
||||
def geometry_check():
|
||||
"""Evaluate actual SCAD expressions, including negative stud/web fixtures."""
|
||||
assert 'panel_hangers' not in SCAD and 'panel_mount_' not in SCAD
|
||||
assert 'part=="panel"' not in SCAD and not (ROOT/"panel-default.stl").exists()
|
||||
assert 'module spacers(' not in SCAD and 'part=="spacers"' not in SCAD
|
||||
assert not (ROOT/"spacers-default.stl").exists(), "Obsolete printable spacers must not ship"
|
||||
assert 'brass_standoff_h' not in SCAD, "No brass height below carrier"
|
||||
assert 'box(10,19,carrier_top' not in SCAD, "RF overlay must not return"
|
||||
assert 'box(12.045,14.745,module_bottom,17.63,6.89,esp_t)' in SCAD
|
||||
with tempfile.TemporaryDirectory(prefix="case-check-") as temp:
|
||||
def run(defines=(), valid=True):
|
||||
command = ["openscad", "-o", str(Path(temp)/"check.csg"), "-D", 'part="check"']
|
||||
for define in defines:
|
||||
command.extend(["-D", define])
|
||||
result = subprocess.run(command+[str(ROOT/"enclosure.scad")],
|
||||
env={**os.environ, "QT_QPA_PLATFORM": "offscreen"},
|
||||
capture_output=True, text=True, timeout=30)
|
||||
output = result.stdout+result.stderr
|
||||
if not valid:
|
||||
assert "ERROR: Assertion" in output, output
|
||||
return
|
||||
assert result.returncode == 0 and "ERROR:" not in output, output
|
||||
def datum(label):
|
||||
match = re.search(r'ECHO: "CHECK '+label+r'", (\[[^\n]+\])', output)
|
||||
assert match, output
|
||||
return ast.literal_eval(match.group(1))
|
||||
closure = datum("closure")
|
||||
assert closure[2] <= -0.5 and closure[3] < closure[4]
|
||||
if not defines:
|
||||
near(closure, [33,81,-0.5,20.82,21.6976,17.578,0.25])
|
||||
return datum("stack"), datum("engagement"), datum("connector")
|
||||
stack, engagement, connector = run()
|
||||
near(connector, [-0.47, 1.93, 2.4, 21.07, 31.9976, 18.078, 25, 3.6, 5, 1.66098])
|
||||
# Independent expected defaults, not values copied from exported meshes.
|
||||
near(stack, [7.4, 9, 19, 20.57, 35, 37.4, 15, 13.3, 15.6,
|
||||
26.8476, 21.07, 19.6, 26.6, 25.6])
|
||||
near(engagement, [2.4, 2.4])
|
||||
raised, _, _ = run(["carrier_boss_h=7"])
|
||||
near(raised, [value+(0 if i == 13 else 2) for i, value in enumerate(stack)])
|
||||
taller, _, _ = run(["module_gap=12", "lid_above_carrier=28"])
|
||||
near(taller, [value+(2 if i in [2,3,4,5,9,10,11,12,13] else 0)
|
||||
for i, value in enumerate(stack)])
|
||||
run(["brass_m3_stud_l=1.6"], valid=False)
|
||||
run(["brass_m2_stud_l=5"], valid=False)
|
||||
run(["wall=0.8"], valid=False)
|
||||
run(["panel_t=0.8"], valid=False)
|
||||
run(["panel_flange_gap=-0.1"], valid=False)
|
||||
run(["panel_bottom_above_module=-0.1"], valid=False)
|
||||
run(["external_fastener_clearance=5"], valid=False)
|
||||
run(["panel_rib_w=8"], valid=False)
|
||||
run(["seam_clearance=0"], valid=False)
|
||||
run(["shell_margin=0.1"], valid=False)
|
||||
run(["panel_bottom_above_module=2"], valid=False)
|
||||
print("PASS actual SCAD default/raised-carrier/taller-module datums, stud engagement and nozzle-web rejection fixtures")
|
||||
|
||||
|
||||
def installation_check():
|
||||
"""CGAL intersection of actual lid with continuous vertical envelope sweeps."""
|
||||
with tempfile.TemporaryDirectory(prefix="case-install-") as temp:
|
||||
for mode, defines in [
|
||||
("installation-check", []),
|
||||
("installation-check", ["carrier_boss_h=7", "module_gap=12", "lid_above_carrier=28", "panel_t=3"]),
|
||||
("installation-check", ["seam_clearance=0.4", "shell_margin=0.8", "end_clearance=1.5"]),
|
||||
("installation-blocked", []),
|
||||
]:
|
||||
path = Path(temp)/"sweep.stl"
|
||||
command = ["openscad", "-o", str(path), "-D", f'part="{mode}"', "-D", "emboss=false"]
|
||||
for define in defines:
|
||||
command.extend(["-D", define])
|
||||
result = subprocess.run(command+[str(ROOT/"enclosure.scad")],
|
||||
env={**os.environ, "QT_QPA_PLATFORM": "offscreen"},
|
||||
capture_output=True, text=True, timeout=180)
|
||||
assert result.returncode == 0 and "ERROR:" not in result.stderr, result.stderr
|
||||
if mode == "installation-check":
|
||||
report = mesh_check(path, 1)
|
||||
near([report["volume_mm3"]], [1])
|
||||
else:
|
||||
# Negative control: unremoved post envelopes MUST hit the panel.
|
||||
text = path.read_text()
|
||||
zs = [float(z) for z in re.findall(r"\bvertex\s+[-+\deE.]+\s+[-+\deE.]+\s+([-+\deE.]+)", text)]
|
||||
assert max(zs) > 80, "Fixture failed to detect installed posts"
|
||||
print("PASS continuous vertical installation sweep", mode, defines)
|
||||
|
||||
|
||||
def variant_check():
|
||||
fixtures = [
|
||||
('lid', ['carrier_boss_h=7', 'module_gap=12', 'lid_above_carrier=28', 'panel_t=3']),
|
||||
('base', ['carrier_boss_h=7', 'module_gap=12', 'lid_above_carrier=28', 'panel_t=3']),
|
||||
('lid', ['db_panel=false']),
|
||||
('base', ['daughter_supports=false', 'side_extra=14', 'm3_insert_d=4.4']),
|
||||
]
|
||||
with tempfile.TemporaryDirectory(prefix='case-variant-') as temp:
|
||||
for part, defines in fixtures:
|
||||
path = Path(temp)/'variant.stl'
|
||||
command = ['openscad', '-o', str(path), '-D', f'part="{part}"', '-D', 'emboss=false']
|
||||
for define in defines:
|
||||
command.extend(['-D', define])
|
||||
result = subprocess.run(command+[str(ROOT/'enclosure.scad')],
|
||||
env={**os.environ, 'QT_QPA_PLATFORM': 'offscreen'},
|
||||
capture_output=True, text=True, timeout=180)
|
||||
assert result.returncode == 0 and 'ERROR:' not in result.stderr, result.stderr
|
||||
mesh_check(path, 1)
|
||||
print('PASS parameter CGAL mesh', part, defines)
|
||||
|
||||
|
||||
def label_check():
|
||||
"""Actual glyph solids must stay on roof, 1 mm clear of all roof openings."""
|
||||
with tempfile.TemporaryDirectory(prefix="case-labels-") as temp:
|
||||
wrapper = Path(temp)/"labels.scad"
|
||||
wrapper.write_text(
|
||||
f'include <{ROOT / "enclosure.scad"}>\n'
|
||||
'cube(1);\n'
|
||||
'translate([0,0,80]) linear_extrude(1) difference() {\n'
|
||||
' projection() labels();\n'
|
||||
' offset(delta=-1) projection(cut=true)\n'
|
||||
' translate([0,0,-top_z+eps/2]) lid_body();\n'
|
||||
'}\n')
|
||||
path = Path(temp)/"clearance.stl"
|
||||
result = subprocess.run(
|
||||
["openscad", "-o", str(path), "-D", 'part="check"', str(wrapper)],
|
||||
env={**os.environ, "QT_QPA_PLATFORM": "offscreen"},
|
||||
capture_output=True, text=True, timeout=180)
|
||||
assert result.returncode == 0 and "ERROR:" not in result.stderr, result.stderr
|
||||
near([mesh_check(path, 1)["volume_mm3"]], [1])
|
||||
print("PASS actual label glyphs within roof, >=1 mm from edges, display, buttons and screw-head openings")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--source", action="store_true", help="also check saved KiCad/STEP registration")
|
||||
parser.add_argument("--installation", action="store_true", help="CGAL continuous vertical sweep and blocked-post control")
|
||||
parser.add_argument('--variants', action='store_true', help='CGAL parameter mesh smoke tests')
|
||||
args = parser.parse_args()
|
||||
if args.variants:
|
||||
variant_check()
|
||||
if args.installation:
|
||||
installation_check()
|
||||
geometry_check()
|
||||
label_check()
|
||||
if args.source:
|
||||
source_check()
|
||||
reports = [mesh_check(ROOT / name, n) for name, n in [
|
||||
("base-default.stl", 1), ("lid-default.stl", 1),
|
||||
("plungers-default.stl", 3)]]
|
||||
for report, high in zip(reports, [[94,83.4,21.6976], [94,83.4,22.75],
|
||||
[28,4,25.6]]):
|
||||
near(report["bounds_mm"][1], high) # catch stale exports of the old stack
|
||||
print(json.dumps(reports, indent=2))
|
||||
print("PASS edge-manifold topology, winding, component counts, positive total volumes and print-bed Z")
|
||||
print("Does not prove absence of self-intersections, physical fit, strength or printability.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,6 +9,8 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Blocking recursive task-context gate. Pair each successful take with a give
|
||||
* on the same task; this does not replace the canonical command dispatcher. */
|
||||
esp_err_t admin_command_gate_take(void);
|
||||
void admin_command_gate_give(void);
|
||||
|
||||
|
||||
@@ -190,6 +190,8 @@ static bool session_is_current(const admin_ssh_console_token_t *token,
|
||||
if (owner == NULL) {
|
||||
return false;
|
||||
}
|
||||
/* Database and transport-owner checks run outside the console spinlock;
|
||||
* neither an open console slot nor an unchanged account alone is sufficient. */
|
||||
bool account_current = false;
|
||||
bool current = principal->role == USER_ROLE_ADMIN &&
|
||||
user_database_principal_is_current(principal, &account_current) == ESP_OK &&
|
||||
@@ -773,6 +775,8 @@ static void worker_task(void *context)
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Queue admission is not execution authority: revalidate the remote
|
||||
* principal and reserve its exact slot before invoking a handler. */
|
||||
bool current = session_is_current(&request.token, &request.principal);
|
||||
bool active;
|
||||
taskENTER_CRITICAL(&s_lock);
|
||||
@@ -877,6 +881,8 @@ static void control_task(void *context)
|
||||
if (xQueueReceive(s_control_queue, &request, portMAX_DELAY) != pdTRUE) {
|
||||
continue;
|
||||
}
|
||||
/* Drain waiting stays off the dispatcher so it cannot stall other
|
||||
* commands; empty application buffers do not prove peer receipt. */
|
||||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(10000U);
|
||||
bool drained = false;
|
||||
while ((int32_t)(xTaskGetTickCount() - deadline) < 0) {
|
||||
@@ -1127,6 +1133,8 @@ void admin_ssh_console_close(const admin_ssh_console_token_t *token)
|
||||
session->prompt_length = 0U;
|
||||
wake_prompt = true;
|
||||
}
|
||||
/* Invalidate immediately, but retain an executing slot's identity until
|
||||
* the worker returns and wipes it; admission must not reuse it meanwhile. */
|
||||
session->active = false;
|
||||
if (!session->executing) {
|
||||
secure_wipe(session, sizeof(*session));
|
||||
|
||||
@@ -129,6 +129,7 @@ esp_err_t admin_ssh_console_start_uart_frontend(void);
|
||||
/* Valid only while a registered command callback runs on the dispatcher task. */
|
||||
bool admin_ssh_console_dispatch_is_remote(void);
|
||||
bool admin_ssh_console_dispatch_is_web(void);
|
||||
/* Borrowed until this command returns; NULL outside remote dispatch. */
|
||||
const user_principal_t *admin_ssh_console_dispatch_principal(void);
|
||||
/* Revalidate account, originating owner/session and token before side effects.
|
||||
* False outside the dispatcher; UART0 dispatch remains physically trusted. */
|
||||
@@ -146,6 +147,8 @@ void admin_ssh_console_close(const admin_ssh_console_token_t *token);
|
||||
|
||||
/* Called by the session owner. Returns false when input must be backpressured. */
|
||||
bool admin_ssh_console_accepts_input(const admin_ssh_console_token_t *token);
|
||||
/* With valid arguments, consumed is the accepted prefix even on a short feed.
|
||||
* A true result does not imply all bytes were consumed; retain the remainder. */
|
||||
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
|
||||
const uint8_t *data, size_t length,
|
||||
size_t *consumed);
|
||||
|
||||
@@ -15,6 +15,7 @@ extern "C" {
|
||||
/* Install late-terminal upgrade handling and project-specific completion. */
|
||||
void console_completion_install(void);
|
||||
|
||||
/* Called synchronously with a borrowed static string; false stops visitation. */
|
||||
typedef bool (*console_completion_visitor_t)(const char *candidate, void *context);
|
||||
|
||||
/* Visit the same matching hint candidates used by both UART0 and admin SSH. */
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/* Blocking command-handler helpers: remote dispatch uses its session prompt;
|
||||
* otherwise UART0 is read directly, flushing pending input before the prompt.
|
||||
* Capacity includes the trailing NUL; output_length excludes it. Callers own
|
||||
* the output and must wipe secrets after use, including hidden input. */
|
||||
esp_err_t console_input_read_hidden(const char *prompt,
|
||||
uint8_t *output, size_t capacity,
|
||||
size_t minimum_length, size_t maximum_length,
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
/* Plays the OLED-only startup identity animation; a missing display is nonfatal. */
|
||||
/* Synchronous animation with task delays; call after display start and before
|
||||
* the status renderer. Returns display/frame errors, including an absent panel;
|
||||
* the boot caller must treat these as nonfatal to other services. */
|
||||
esp_err_t local_boot_animation_play(void);
|
||||
|
||||