98 Commits
Author SHA1 Message Date
Commander1024 60c1e279d6 Add KiCad module carrier PCB draft
Introduce an editable KiCad 10 schematic and routed two-layer 82 × 80 mm
layout with local provisional footprints, validation tooling, and
component
research. Keep the RS-232 male-module mapping and mechanical clearances
explicitly provisional pending hardware verification.
2026-09-20 23:14:01 +02:00
Commander1024 8902b25d78 Complete Phase 12 dual-stack networking
Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service
reconciliation, and browser Wi-Fi administration.

Include a guarded build-local fix for mDNS 1.12.0 membership handling,
focused regression suites, and Phase 12 acceptance documentation.
2026-09-20 22:35:34 +02:00
Commander1024 ece4ba77e3 Remove BLE and filesystem-backed feature proposals
Update the roadmap and session state to reflect user decisions,
retaining
only the existing transport scope and leaving advanced network work
under
evaluation.
2026-09-18 23:14:21 +02:00
Commander1024 31a22eba06 Add admin firmware upload support
Implement authenticated HTTPS OTA uploads with bounded streaming, image
validation, reboot coordination, and lifecycle exclusion. Add the admin
UI,
regression tests, and Phase 10 acceptance documentation.
2026-09-18 22:22:11 +02:00
Commander1024 4f628a4098 Define Simplified Phase 10 Firmware Upload Plan 2026-09-18 21:16:19 +02:00
Commander1024 b9653426e4 Document the Phase 9 security baseline
Add source-reviewed operational guidance, update acceptance and roadmap
references, consolidate agent state, and disable core dumps with silent
panic reboot defaults.
2026-09-18 20:37:39 +02:00
Commander1024 f40c09c11a Refine web quick panels and Wi-Fi controls 2026-09-14 00:05:27 +02:00
Commander1024 4a53a21f31 Move console payloads to lazy PSRAM storage 2026-09-13 23:06:42 +02:00
Commander1024 91267b371e Consolidate Phase 8 documentation
Mark web administration complete, centralize current contracts and
acceptance evidence, and remove superseded slice records. Update
roadmap,
architecture notes, and test references without changing firmware
sources.
2026-09-13 22:27:10 +02:00
Commander1024 1608641d50 Close scope for remaining Phase 8D.19 work 2026-09-13 20:25:01 +02:00
Commander1024 8df1d2218b Add SSH host identity rotation controls 2026-09-13 19:58:05 +02:00
Commander1024 aa4bbc2c8c Add HTTPS identity rotation support 2026-09-13 18:21:37 +02:00
Commander1024 36e80811e8 Implement HTTPS lifecycle and reboot controls 2026-09-13 17:24:00 +02:00
Commander1024 737bd29f9e Add Typed SSH Service Controls
Provide admin-only SSH status plus generation-safe start, stop, and
single-session disconnect operations through the bounded dispatcher.
Include
Settings UI coverage, lifecycle safeguards, and host-side regression
tests.
2026-09-13 16:07:04 +02:00
Commander1024 7ccc8799e9 Add Broker client and writer quick dialogs 2026-09-13 14:40:51 +02:00
Commander1024 9f6ebf2053 Add Serial and Wi-Fi quick settings
Reuse the existing settings view and typed controllers for accessible
quick editing while keeping network credentials out of quick mode.
Expand browser and layout coverage for focus, dismissal, bounds, and
expiry.
2026-09-13 14:22:31 +02:00
Commander1024 add399908a Remove stray blank line in Phase 8D plan 2026-09-13 14:01:49 +02:00
Commander1024 fa12440606 Add broker management and writer transfer UI 2026-09-13 13:59:28 +02:00
Commander1024 29a4953df0 Remove network diagnostics from phase 8D plan 2026-09-09 10:32:55 +02:00
Commander1024 d9ec3c08de Add Typed Display Settings Administration
Implements admin-only Display settings with generation-checked
Apply, Save, Load, Defaults, and Reset operations across the web UI,
CLI, SSH dispatcher, and local UI owner. Adds bounded HTTP handling,
session-isolated operation results, browser lifecycle support, and
comprehensive host tests and documentation.
2026-09-09 10:15:23 +02:00
Commander1024 60d9c54bb4 Send binary WebSocket frames in one write 2026-09-08 23:52:00 +02:00
Commander1024 042499e4d6 Add broker and web throughput diagnostics 2026-09-08 22:40:58 +02:00
Commander1024 36d41be422 Document 8D.12/8D.13 Functional Sign-Off 2026-09-08 21:40:18 +02:00
Commander1024 4a4d615c59 Unify Settings Layouts and Add Coverage 2026-09-08 21:22:52 +02:00
Commander1024 989821b7c4 Add Typed Admin Network Settings 2026-09-08 20:57:27 +02:00
Commander1024 d9ac1319aa Document legacy credential cleanup sign-off
Record certificate continuity, existing-user validation, and full-mix
target telemetry while preserving the documented evidence limits.
2026-09-08 19:21:09 +02:00
Commander1024 ac80863d80 Remove Legacy Credential Bootstrap Paths
Decouple user provisioning from HTTPS identity storage while retaining
compatible v1 user records and migrating TLS material to the
credential-free
v2 format. Add focused security regression coverage and update operator
documentation.
2026-09-08 19:09:26 +02:00
Commander1024 82f21d6116 Add Bounded Ordinary HTTPS Idle Cleanup 2026-09-08 18:33:33 +02:00
Commander1024 f6263042ff Add Bounded Web Admission Diagnostics 2026-09-08 18:04:46 +02:00
Commander1024 42f6423d4e Implement SSH authorized key management 2026-09-08 16:37:47 +02:00
Commander1024 22a7c7b0a5 Record 8D.8–8D.10 target sign-off
Document user-supplied telemetry, functional testing, and acceptance of
the implemented Serial and account settings scope.
2026-09-08 15:19:49 +02:00
Commander1024 23c190bcf0 Load the PlatformIO extra script 2026-09-08 15:17:00 +02:00
Commander1024 94433ef975 Add typed account and password settings
- Add admin account list, create, role, delete, and password workflows
- Execute identity-checked mutations through the existing dispatcher
- Bound queued credential lifetime and wipe transient secrets
- Add explicit password generation with saved-value acknowledgement
- Handle self-revocation and uncertain outcomes without automatic
  retries
- Register optional account routes without disrupting terminal
  transports
- Expand host regressions and document contracts and pending target
  checks

Validated host suites and pio run; hardware validation remains pending.
2026-09-08 09:27:02 +02:00
Commander1024 42548f6334 Add typed serial settings operations
Route bounded admin mutations through the existing administration
dispatcher,
covering apply, lifecycle, persistence, authorization, and result
tracking.
Add the browser controls, automatic result refresh, regression coverage,
and
phase documentation.
2026-09-08 00:25:31 +02:00
Commander1024 5a2aa0d4d8 Add admin serial settings view 2026-09-07 20:12:33 +02:00
Commander1024 c73674cda2 Document M2 sign-off and update project status 2026-09-07 19:29:56 +02:00
Commander1024 93d8d1e5ca Mark 8D.7 Implemented Scope Validated 2026-09-07 19:04:49 +02:00
Commander1024 fe1e2d98b4 Enable bounded browser account administration for Phase 8D.7
Allow other-account add/password and forced delete/role commands through
shared dispatcher and handler policy. Keep self-target,
generated-secret,
key, bootstrap, and recovery workflows blocked.

Revalidate currentness after password prompts and before database API
admission. Document that admitted mutations may finish after disconnect,
while subsequent stale operations must reject.

Add policy, transaction-failure, cleanup, and targeted-revocation
regressions. Record completed review, passing host tests and firmware
build, with target validation and M2 acceptance still pending.
2026-09-07 10:03:45 +02:00
Commander1024 326119812f Extend browser admin lifecycle actions
Support browser reboot and HTTPS stop through deferred control, plus
exact
`web certificate rotate --force` handoff to the dispatcher. Add typed
request
validation and focused boundary and lifecycle coverage.
2026-09-07 09:36:38 +02:00
Commander1024 17520b15b7 Configure clangd for ESP-IDF development
Enable PlatformIO compilation database generation and configure
toolchain discovery for Clang-based editors.
2026-09-07 09:35:37 +02:00
Commander1024 0b86fd9c70 Close 8D.6 With Validation Sign-Off 2026-09-06 21:30:26 +02:00
Commander1024 f15491f233 Place admin controls before terminal selector 2026-09-06 20:37:39 +02:00
Commander1024 71f588360a Fix admin ticket validation format mismatch 2026-09-06 20:32:00 +02:00
Commander1024 e6db5428eb Add browser Serial/Admin terminal switching
Keep the serial connection and lease intact while providing a separate,
bounded admin terminal with explicit open and close controls. Fence
retained
terminal state across sessions and add fit-readiness retries with
regression
coverage.
2026-09-06 19:46:38 +02:00
Commander1024 aeb2043396 feat: add bounded admin WebSocket backend (Phase 8D.5)
- Require current admin cookie sessions, Origin checks and single-use
  tickets
- Reuse the shared console with session-aware authorization and slot
  allocation
- Add HTTPD-owned I/O, bounded buffering and revocation cleanup
- Prevent LRU eviction of serial clients and stale admin socket closure
- Reject unsupported web-shell mutations before side effects
- Add host regressions, a smoke client and resource accounting

Validated by user sign-off after a 15-minute full-client soak at 230400
baud, with a few broker drops under heavy output. Browser UI remains
for Phase 8D.6; numeric memory reserves remain open.
2026-09-06 14:41:41 +02:00
Commander1024 e5dce12ed4 Close Phase 8D.4 validation
The user confirmed successful empty-Enter and soak testing, closing
8D.4 while leaving numeric reserve gates open and advancing the next
planned work to 8D.5.
2026-09-06 11:23:58 +02:00
Commander1024 117c694cd4 Add SSH Console Ownership Boundary (Phase 8D.4)
Implement transport-qualified session identity and immutable owner
adapters
for SSH console lifecycle and output-drain operations. Add focused host
tests
covering admission, stale identities, deferred actions, completion
races,
prompts, backpressure, and slot reuse. Update Phase 8D documentation and
current-state tracking.
2026-09-06 09:07:11 +02:00
Commander1024 f9ee6eec9c Record Phase 8D.3 M1 sign-off 2026-09-06 08:38:41 +02:00
Commander1024 5a609fa40b Replace Web Basic Auth With Cookie Sessions
Add bounded login challenges, CSRF/origin enforcement, logout, and
session-bound WebSocket admission. Isolate private HTTPD access behind a
version-guarded adapter and add focused host coverage. Also let empty
admin
SSH input reach the normal console handler.
2026-09-05 23:55:05 +02:00
Commander1024 4435a7fddd Add Standalone Login Page Renderer
Add a hash-bound, no-store login document with focused C and Node host
tests. Keep rendering inert until the 8D.3 authentication cutover.
2026-09-05 18:45:12 +02:00
Commander1024 00f226dc59 Add allocation-free web auth parsers
Implement strict origin, cookie, and login JSON parsing with
fail-closed validation and output wiping. Add focused host contract
tests
and document the preparatory 8D.3 parser split.
2026-09-05 18:22:15 +02:00
Commander1024 a62a655ac1 Bind Serial Transports To Web Sessions 2026-09-05 18:01:39 +02:00
Commander1024 93eef0e676 Added docs. 2026-09-05 17:16:17 +02:00
Commander1024 27c54c0a92 Phase 8D.1 implemented and validated. 2026-09-05 17:15:22 +02:00
Commander1024 d4991658b1 Phase 8D validation completed 2026-09-05 16:11:23 +02:00
Commander1024 d8999cd4a9 Updated memory baselines 2026-09-05 15:20:09 +02:00
Commander1024 02fdeee345 Record Phase 8D baseline and browser contract 2026-09-05 13:08:17 +02:00
Commander1024 af89dd1bd9 Define staged Phase 8D delivery plan 2026-09-05 12:46:27 +02:00
Commander1024 31d3561d47 Document integrated web administration scope 2026-09-01 23:55:48 +02:00
Commander1024 6318f0c770 Unify browser connection controls (Connect / Disconnect) 2026-09-01 22:34:02 +02:00
Commander1024 10b6d65fa9 Mark Phase 8C hardware validation complete 2026-09-01 22:08:48 +02:00
Commander1024 4449131079 Refine Wi-Fi, mDNS, and terminal lifecycles
- Stage disabled station profile edits without restarting the radio
- Make mDNS initialization failure-isolated and reannounce in place
- Document deferred admin actions and explicit browser disconnect
  behavior
2026-08-31 04:29:54 +02:00
Commander1024 3feb3b5916 Add configurable Wi-Fi station mDNS hostname support. Mini-Feature #1 2026-08-31 04:14:38 +02:00
Commander1024 06bf33b3cf Add Explicit Session Exit Controls Mini-Features ä2+3 2026-08-31 04:02:19 +02:00
Commander1024 c37fab67db Avoid Wi-Fi restarts for disabled profiles Fix #3 2026-08-31 03:51:24 +02:00
Commander1024 d9bd86bb84 Move RS-232 status under debug. Fix #2 from list. 2026-08-31 03:45:38 +02:00
Commander1024 2aafff25be Keep USB line coding diagnostic only. Fix cdc connect reconfiguring UART
1
2026-08-31 03:40:55 +02:00
Commander1024 6ad6c00d68 Harden cleanup and reduce internal RAM use. Phase 8C nearly validated
and somewhat stable.
2026-08-31 03:33:43 +02:00
Commander1024 868e9ebc23 Remove outdated startup and SSH banners 2026-08-30 23:20:52 +02:00
Commander1024 26e8bbe905 Revert "Remove SSH console startup instructions"
This reverts commit ec56d20b3e.
2026-08-30 23:14:18 +02:00
Commander1024 ec56d20b3e Remove SSH console startup instructions 2026-08-30 23:11:39 +02:00
Commander1024 5ecd88e19f Align UART completion with SSH behavior 2026-08-30 23:07:59 +02:00
Commander1024 35a6f32e8b Display SSH completion candidates for ambiguous prefixes 2026-08-30 23:00:01 +02:00
Commander1024 21d1b12f31 Share completion and add SSH line editing 2026-08-30 22:45:58 +02:00
Commander1024 2f383cd283 Refresh agent documentation for current behavior 2026-08-30 22:45:33 +02:00
Commander1024 7b87375980 Align Documentation With Current Firmware Behavior 2026-08-30 19:11:39 +02:00
Commander1024 f227a2026f Add Project Guidance For Agents 2026-08-30 18:55:11 +02:00
Commander1024 c2c11fee4e Expand admin SSH command capabilities
Add per-session history, tab completion, interactive prompts, and
bounded input handling. Support deferred lifecycle and host-key actions
after output drains, and document the expanded administration workflow.
2026-08-30 18:34:01 +02:00
Commander1024 0a1bbd6782 Add Serialized SSH Administrative Console 2026-08-30 18:07:02 +02:00
Commander1024 44e3962444 Complete Local Display and Button Interface Validation 2026-08-30 12:27:09 +02:00
Commander1024 302b9065bd Mark Phase 8B complete and add Phase 8E
Document target-hardware validation for role-aware authentication and
outline the planned web session login and logout architecture.
2026-08-30 12:23:01 +02:00
Commander1024 7a4769fdd8 Allow SSH keys on multiple accounts
Update validation and duplicate checks to scope key uniqueness per
account, and clarify the console error message. Document cross-account
key assignment and authentication testing.
2026-08-30 12:11:16 +02:00
Commander1024 c7d0d59f3e Cache web authentication results
Add a short-lived, HMAC-keyed cache for validated principals and
invalidate entries when principals become stale. Improve duplicate SSH
key
errors and enable Ed25519 streaming verification.
2026-08-30 12:02:44 +02:00
Commander1024 b7999043ca Mark Phase 7 As Target-Hardware Validated 2026-08-30 07:53:51 +02:00
Commander1024 bb8e385354 Separate user administration test documentation 2026-08-30 01:35:26 +02:00
Commander1024 0c058b6a8f Implement role-aware HTTPS and SSH authentication 2026-08-30 01:31:05 +02:00
Commander1024 cd235445c7 Add role-based user database administration 2026-08-30 00:56:18 +02:00
Commander1024 0c7b763bec Add role-based user administration phase 2026-08-30 00:05:58 +02:00
Commander1024 57a18be9bd Add project logo to authenticated web UI 2026-08-29 21:25:25 +02:00
Commander1024 96e5792273 Add bounded OLED boot identity animation 2026-08-29 21:05:22 +02:00
Commander1024 276559536b Add persistent OLED UI settings and recovery 2026-08-29 20:18:08 +02:00
Commander1024 16c0c02389 Add Wi-Fi next-profile rotation control 2026-08-29 19:43:21 +02:00
Commander1024 371c0ab896 Add local OLED recovery controls 2026-08-29 19:31:57 +02:00
Commander1024 e291e29357 Implement Phase 7C local status dashboard
Add fixed status icons, OLED dimming and wake behavior, lowercase
glyphs, and access-point metadata tracking. Update validation guidance,
roadmap status, and provide an icon layout mockup with licensing.
2026-08-29 19:05:06 +02:00
Commander1024 c10e7d24a8 Implement Read-Only OLED Status UI 2026-08-29 15:29:19 +02:00
Commander1024 4aa5ca80da Add Panel-Aware Local OLED Display Service 2026-08-29 14:37:44 +02:00
Commander1024 ec9ca5e6d2 Add OLED and button diagnostics 2026-08-29 14:13:34 +02:00
Commander1024 bb04e0ba79 Document Phase 7 display hardware and implementation plan 2026-08-27 16:46:08 +02:00
288 changed files with 164491 additions and 1539 deletions
+10
View File
@@ -0,0 +1,10 @@
CompileFlags:
Add:
- -isystem
- /home/mscholz/.platformio/packages/toolchain-xtensa-esp-elf/xtensa-esp-elf/include
Remove:
- -mlongcalls
- -mdisable-hardware-atomics
- -fstrict-volatile-bitfields
- -fno-tree-switch-conversion
- -fno-shrink-wrap
+17
View File
@@ -0,0 +1,17 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"lsp": {
"clangd": {
"binary": {
"path": "/usr/bin/clangd",
"arguments": [
"--background-index",
"--query-driver=/home/mscholz/.platformio/packages/**/bin/*"
]
}
}
}
}
+44
View File
@@ -0,0 +1,44 @@
# Agent instructions
## Start with project memory
1. Read `docs/agent/code-map.md` before broad repository exploration.
2. Read the relevant sections of `docs/agent/architecture.md` and `docs/agent/design-decisions.md` before changing cross-cutting behavior.
3. Read `docs/agent/current-state.md` when resuming work or investigating recent changes.
4. Use these files to identify the smallest relevant source set before searching or reading code.
5. Verify stored knowledge against implementation whenever it may be stale or correctness depends on exact behavior. Source code is authoritative.
6. Do not repeatedly scan unrelated modules. Prefer targeted symbol searches and representative header/implementation reads.
7. Update durable agent documentation only when architecture, contracts, ownership, or module responsibilities genuinely change.
8. During long-running tasks, keep `docs/agent/current-state.md` current and update it before handoff or context compaction.
9. Keep temporary debugging notes and speculative hypotheses out of `architecture.md` and `design-decisions.md`; use `current-state.md` instead.
10. Treat `GPT-logs/` as non-authoritative history. Confirm any useful claim against current source.
11. Avoid `managed_components/`, `third_party/`, generated `src/web_assets_data.*`, compressed assets, minified libraries, `compile_commands.json`, `dependencies.lock`, and broad `sdkconfig.*` inspection unless the task specifically requires them.
## Project constraints
- This is ESP-IDF firmware for one physical UART1/MAX3243 RS-232 port shared through USB CDC, HTTPS/WebSocket, and SSH.
- Preserve the broker model: exactly one writer, multiple isolated observers.
- Preserve UART0 as the administrative recovery path and native USB as network-independent UART1 access when network services fail.
- Keep serial transport binary-transparent; do not add in-band control sequences.
- Treat bounded queues, buffers, task ownership, generation tokens, and failure isolation as correctness properties, not incidental implementation details.
- Never expose passwords, private keys, Wi-Fi secrets, ticket values, or verifier material through routine status, logs, completion, or the local display.
- Do not regenerate embedded web assets unless the task explicitly requires it. See `web_assets/SOURCES.md` for provenance and generation policy.
## Build and device commands
The normal build, verified from `platformio.ini` and `README.md`, is:
```sh
pio run
```
Upload and monitor commands documented by the project are:
```sh
pio run --target upload
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.
+4
View File
@@ -7,8 +7,12 @@ add_compile_definitions(
WC_RNG_SEED_CB WC_RNG_SEED_CB
NO_WOLFSSL_ESP32_CRYPT_AES NO_WOLFSSL_ESP32_CRYPT_AES
NO_WOLFSSL_ESP32_CRYPT_HASH NO_WOLFSSL_ESP32_CRYPT_HASH
WOLFSSL_ED25519_STREAMING_VERIFY
) )
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
set(PROJECT_VER "0.1.0") set(PROJECT_VER "0.1.0")
project(esp32_serial_swiss_army_knife) project(esp32_serial_swiss_army_knife)
# Narrow mDNS 1.12.0 correctness fix; see tests/mdns_membership/README.md.
include(${CMAKE_CURRENT_LIST_DIR}/cmake/mdns_membership.cmake)
+35 -10
View File
@@ -10,17 +10,34 @@ ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one
- ESP32-S3-WROOM-1-N16R8 module - ESP32-S3-WROOM-1-N16R8 module
- 16 MB flash - 16 MB flash
- 8 MB octal PSRAM - 8 MB octal PSRAM
- [Adafruit MAX3243 full-pinout male RS-232 breakout](https://www.adafruit.com/product/6253) - 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 ## Development status
Hardware characterization, the serial core, session broker, and native USB CDC-ACM are complete. The Wi-Fi foundation is implemented and substantially validated, with several smoke tests still pending. The project is currently in the **SSH-Phase**; later work includes interface hardware, security hardening, OTA, BLE, enterprise Wi-Fi, WireGuard evaluation, and optional storage features. See the [Roadmap](docs/roadmap.md) for validation details and roadmap. 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:** 8A8C 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.
## Documentation ## Documentation
- [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes. - [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
- [Electrical tests](docs/electrical_tests.md): MAX3243, UART loopback, and session-broker verification procedures. - [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures.
- [Command reference](docs/command_reference.md): UART0 administration, serial, broker, USB, Wi-Fi, web, SSH, and diagnostic commands. - [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.
## Flash partition layout ## Flash partition layout
@@ -28,7 +45,7 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
| Partition | Offset | Size | Purpose | | Partition | Offset | Size | Purpose |
|---|---:|---:|---| |---|---:|---:|---|
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, shared admin credential, HTTPS identity, and SSH host-key data | | `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 | | `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data | | `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys | | `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
@@ -39,7 +56,7 @@ The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the cus
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. Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
The table reserves OTA and storage space; it does not implement OTA downloads, rollback confirmation, core-dump handling, NVS encryption, or filesystem mounting. 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 ### One-time migration from the default partition table
@@ -51,7 +68,7 @@ pio run --target upload
pio device monitor -b 115200 pio device monitor -b 115200
``` ```
This removes saved serial configuration and all other flash contents. The firmware recreates NVS with safe defaults. Subsequent ordinary uploads do not need a full erase. 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.
## Build ## Build
@@ -59,6 +76,8 @@ This removes saved serial configuration and all other flash contents. The firmwa
pio run 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.
## Upload and monitor ## 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 development console, then run:
@@ -72,12 +91,18 @@ The firmware provides an interactive UART0 console at `serial-tool>`. 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. 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.
Serial configuration and Wi-Fi edits remain in RAM until explicitly saved with `serial save` or `wifi save`. Retrieve generated web and SSH credentials only from the physical UART0 console with `web credentials show`. 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.
## Security notes ## Security notes
The HTTPS interface uses a device-specific self-signed certificate and HTTP Basic authentication over TLS; there is no plaintext HTTP or TCP serial listener. SSH supports password-authenticated interactive shell sessions only. The shared admin password, HTTPS private key, SSH private key, and Wi-Fi credentials are currently stored in plaintext application-owned NVS blobs. The reserved `nvs_key` partition does not enable encryption. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete. 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.
## License ## 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 `wolfssl/wolfssl` `5.8.2~1` and `wolfssl/wolfssh` `1.4.20`; review upstream security releases before production use. 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.
+44
View File
@@ -0,0 +1,44 @@
# Only the lwIP backend needs this fix. Keep the managed component immutable.
function(project_mdns_membership_overlay)
if(CONFIG_MDNS_NETWORKING_SOCKET)
return()
endif()
idf_component_get_property(mdns_dir espressif__mdns COMPONENT_DIR)
idf_component_get_property(mdns_lib espressif__mdns COMPONENT_LIB)
idf_component_get_property(mdns_version espressif__mdns COMPONENT_VERSION)
if(NOT mdns_version STREQUAL "1.12.0")
message(FATAL_ERROR "mDNS membership overlay requires component version 1.12.0; review upstream")
endif()
idf_build_get_property(python PYTHON)
set(helper "${CMAKE_CURRENT_LIST_DIR}/mdns_membership.py")
set(original "${mdns_dir}/mdns_networking_lwip.c")
set(overlay "${CMAKE_BINARY_DIR}/mdns_membership/mdns_networking_lwip.c")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${helper}" "${original}" "${mdns_dir}/idf_component.yml" "${overlay}")
execute_process(COMMAND "${python}" "${helper}" "${mdns_dir}" "${overlay}"
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error)
if(NOT result EQUAL 0)
message(FATAL_ERROR "mDNS membership overlay failed: ${output}${error}")
endif()
get_target_property(sources ${mdns_lib} SOURCES)
get_target_property(source_dir ${mdns_lib} SOURCE_DIR)
set(replaced 0)
set(updated_sources)
foreach(source IN LISTS sources)
get_filename_component(absolute "${source}" ABSOLUTE BASE_DIR "${source_dir}")
if(absolute STREQUAL original)
list(APPEND updated_sources "${overlay}")
math(EXPR replaced "${replaced} + 1")
else()
list(APPEND updated_sources "${source}")
endif()
endforeach()
if(NOT replaced EQUAL 1)
message(FATAL_ERROR "Expected exactly one mDNS lwIP target source, found ${replaced}; review upstream CMake")
endif()
set_property(TARGET ${mdns_lib} PROPERTY SOURCES "${updated_sources}")
message(STATUS "mDNS 1.12.0: using guarded build-local multicast membership fix")
endfunction()
project_mdns_membership_overlay()
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Generate the narrowly guarded mDNS 1.12.0 membership overlay; never edit upstream."""
import argparse
import hashlib
from pathlib import Path
import re
SOURCE_SHA256 = "adc139fa504a925ab644f21f8dce3659927f534e390a176b72b0ae3206c6a3ea"
OLD_DEINIT = """ s_interfaces[tcpip_if].proto &= ~(ip_protocol == MDNS_IP_PROTOCOL_V4 ? PROTO_IPV4 : PROTO_IPV6);
if (s_interfaces[tcpip_if].proto == 0) {
s_interfaces[tcpip_if].ready = false;
join_group(tcpip_if, ip_protocol, false);
"""
NEW_DEINIT = """ int proto = (ip_protocol == MDNS_IP_PROTOCOL_V4 ? PROTO_IPV4 : PROTO_IPV6);
if (!(s_interfaces[tcpip_if].proto & proto)) {
return;
}
join_group(tcpip_if, ip_protocol, false);
s_interfaces[tcpip_if].proto &= ~proto;
if (s_interfaces[tcpip_if].proto == 0) {
s_interfaces[tcpip_if].ready = false;
"""
OLD_INIT = """ err = pcb_init();
if (err) {
return err;
}
"""
NEW_INIT = """ err = pcb_init();
if (err) {
join_group(tcpip_if, ip_protocol, false);
return err;
}
"""
def generate(component: Path, output: Path) -> None:
manifest = (component / "idf_component.yml").read_text()
if re.findall(r"^version:\s*(\S+)\s*$", manifest, re.MULTILINE) != ["1.12.0"]:
raise ValueError("mDNS membership overlay requires exactly version 1.12.0; review upstream")
original = (component / "mdns_networking_lwip.c").read_bytes()
if hashlib.sha256(original).hexdigest() != SOURCE_SHA256:
raise ValueError("mDNS networking source SHA-256 mismatch; review upstream, do not bypass guard")
patched = original.decode("utf-8")
for old, new in ((OLD_DEINIT, NEW_DEINIT), (OLD_INIT, NEW_INIT)):
if patched.count(old) != 1:
raise ValueError("mDNS membership replacement must match exactly once")
patched = patched.replace(old, new, 1)
result = patched.encode("utf-8")
output.parent.mkdir(parents=True, exist_ok=True)
if not output.exists() or output.read_bytes() != result:
output.write_bytes(result)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("component", type=Path)
parser.add_argument("output", type=Path)
args = parser.parse_args()
try:
generate(args.component, args.output)
except (OSError, ValueError) as error:
parser.exit(1, f"mDNS membership overlay: {error}\n")
+12 -1
View File
@@ -29,6 +29,16 @@ dependencies:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com/
type: service type: service
version: 3.0.3 version: 3.0.3
espressif/mdns:
component_hash: 3ba256ac95e07c274be53cbd73f06cb846c403b61e8fbdf1be57bdb79db7a63e
dependencies:
- name: idf
require: private
version: '>=5.0'
source:
registry_url: https://components.espressif.com/
type: service
version: 1.12.0
espressif/tinyusb: espressif/tinyusb:
component_hash: a72b7d67472914ab76309340fd50d578b31e310963d45ad0f81144bde3314752 component_hash: a72b7d67472914ab76309340fd50d578b31e310963d45ad0f81144bde3314752
dependencies: dependencies:
@@ -66,9 +76,10 @@ dependencies:
direct_dependencies: direct_dependencies:
- espressif/esp_tinyusb - espressif/esp_tinyusb
- espressif/led_strip - espressif/led_strip
- espressif/mdns
- idf - idf
- wolfssl/wolfssh - wolfssl/wolfssh
- wolfssl/wolfssl - wolfssl/wolfssl
manifest_hash: 34e93560f67a22e2a43647a4663b450e07ecd9fa3fdc2ac38343c1a76b513f5d manifest_hash: 4b6fa5a7e06122f9194adfc282a4baf4247d8a114b905e65f6397503e59f0898
target: esp32s3 target: esp32s3
version: 2.0.0 version: 2.0.0
+242
View File
@@ -0,0 +1,242 @@
# Architecture
## Purpose and system shape
This ESP32-S3 firmware exposes one MAX3243-backed UART1 RS-232 port through three bounded transport families:
- native USB CDC-ACM, which is local and unauthenticated;
- authenticated HTTPS with a browser WebSocket terminal;
- authenticated SSH.
UART0 remains a separate trusted administration and recovery console. A local OLED and three buttons provide status and a deliberately limited control surface. Persistent application configuration and security material are stored in versioned NVS blobs.
The central data-path invariant is one serial writer with multiple observers:
```text
USB CDC ---------\
WebSocket --------> session broker <--> serial service <--> UART1 <--> MAX3243
SSH role=user ---/ |
+-- one writer lease
+-- bounded output per observer
SSH role=admin ------> shared administration dispatcher <------ UART0
(does not join the broker)
```
## 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).
## Startup and initialization
`app_main()` in `src/main.c` is the composition root. The implemented order matters:
1. Report PSRAM and initialize the sole project-owned application DRBG before Wi-Fi or other radio use.
2. Initialize boot-critical RGB LED state, RS-232 ownership/static-safe hardware, diagnostics, and the shared administration dispatcher resources. RGB LED initialization is currently guarded by `ESP_ERROR_CHECK` and is therefore boot-fatal.
3. Attempt optional OLED initialization and a bounded boot animation. Display failure is nonfatal; a working display can delay later recovery services by about five seconds.
4. Initialize button diagnostics and load local-UI and serial configurations, falling back to RAM defaults on load failure.
5. Initialize the serial service, session broker, and permanent USB transport task. UART1 is not started automatically here.
6. Load/generate HTTPS material, then initialize the independent user database, committing an empty database when storage is missing. User-database failure makes network authentication fail closed.
7. Initialize the HTTPS runtime, SSH host-key material, and permanent SSH owner task.
8. Load Wi-Fi configuration and the independent mDNS hostname configuration, persist generated first-boot Wi-Fi defaults when appropriate, initialize the nonfatal mDNS configuration service and Wi-Fi manager, and start Wi-Fi when configured for boot. The Wi-Fi manager owns subsequent mDNS announcement transitions.
9. Start HTTPS and SSH only when their startup gates pass. The Wi-Fi portion requires valid configuration and successful manager initialization and, when enabled at boot, successful submission of its asynchronous start request; it does not require association, an IP address, or reachability. HTTPS additionally requires its own security/runtime readiness; SSH independently requires its own security/runtime readiness, not HTTPS identity readiness. This reflects `main.c` after accepted legacy-credential cleanup.
10. Start the local status/control task if button initialization succeeded.
11. Construct ESP-IDF's UART REPL to initialize `esp_console`, but do not start the stock REPL task. Register command groups, install completion, and start the custom UART frontend that feeds the shared dispatcher.
Several core initializers use `ESP_ERROR_CHECK`; optional display and network/security paths generally log failure while retaining UART0 administrative recovery and network-independent UART1 access through USB. SSH starts before command registration, so role-`user` sessions can be admitted in that interval while role-`admin` sessions are rejected until the administration frontend is ready.
## Serial service and physical ownership
`serial_service` owns the UART1 driver while running. It exposes bounded RX/TX streams and a task that:
- continuously drains UART RX, even if UART event notification is incomplete;
- moves pending TX to the UART FIFO without blocking indefinitely on CTS;
- discards and accounts queued traffic during shutdown/reconfiguration.
`rs232_port_owner` separately protects the physical UART/MAX3243 resource:
- `NONE`: available;
- `PHASE0`: hardware diagnostics own it;
- `SERVICE`: serial service owns it;
- `FAULT`: cleanup could not establish a safe state; reboot is required.
The owner is cooperative rather than an interceptor for UART/GPIO APIs. Active diagnostic commands claim `PHASE0`, and the running service claims `SERVICE`; boot-time static-safe GPIO initialization and service-owned restoration of that static mode are explicit exceptions. Unsafe cleanup keeps the transceiver disabled and marks a fault rather than attempting continued operation.
Serial configuration is a working RAM value. Applying it while running performs a stop/restart and attempts rollback on failure. Stop/reconfiguration discards and accounts serial-service RX/TX streams and task-local pending TX, but does not disconnect broker clients or clear their writer lease, events, or already-fanned output. An open USB session retries service start after a stop; existing WebSocket and role-`user` SSH sessions do not independently restart it. Persistence is explicit through save/reset commands.
## Session broker and data flow
The permanent `session_broker` task is the intended sole consumer/producer of serial-service data.
### RS-232 to clients
```text
UART RX -> serial-service RX stream -> broker task
-> independent bounded output stream for every connected client
-> USB / WebSocket / SSH transport output
```
The broker drains serial input even with no clients. A full client output stream drops only that client's copy and updates drop counters; it does not block UART reception or other clients.
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).
### Clients to RS-232
```text
transport input -> broker write check -> serial-service TX stream -> UART TX
```
Only the generation-safe client ID holding the current writer lease may enqueue input. All connected clients, including the writer, observe UART output. Normal requests acquire the lease only when free; disconnect releases it. Administrative APIs can force reassignment or compare-and-release an expected writer.
Broker events are advisory bounded notifications. Transports reconcile against authoritative snapshots because an event queue can overflow. `DTR_ON_CONNECT` follows whether any broker client is connected, not writer ownership.
The broker currently enters nonblocking serial read/write APIs while holding its mutex and takes the serial-service state mutex during first-connect/last-disconnect DTR changes. Keep this ordering acyclic: serial-service code must not call broker APIs while holding its state mutex.
## Transport architecture
### USB CDC
`usb_cdc_transport` has a permanent transport task and TinyUSB callbacks. Attached plus host DTR asserted is treated as open. Opening starts the serial service if necessary, creates the `usb-cdc` broker client, and opportunistically requests writer ownership; otherwise USB observes.
TinyUSB callbacks enqueue/copy data and state; the transport task owns broker lifecycle and forwarding. The line-coding callback records the latest host setting for diagnostics only. It never reconfigures UART1: physical framing and speed remain controlled by the explicit serial configuration, regardless of USB writer ownership.
### HTTPS, WebSocket, and web serial
`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).
`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_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_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).
### 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_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).
### 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).
### 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).
`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.
Authentication uses user-database passwords or stored Ed25519/ECDSA-P256 public keys. Public-key lookup authorizes a username/key pair, while wolfSSH verifies signed proof of possession. SSH host identity is a separate persisted P-256 key managed by `ssh_security`.
Routing follows the authenticated role:
- `user`: start the serial service if necessary, then create a broker-backed binary-transparent serial stream and opportunistically request writer ownership;
- `admin`: bounded administration console, with no broker client or writer lease.
A shell request is required, but project code does not explicitly require a PTY. Exec and subsystem requests are rejected, and there is no project SFTP, SCP, agent-forwarding, or TCP-forwarding route.
## Authentication, authorization, and revocation
`user_database` is a fixed-capacity, mutex-protected store: at most eight accounts and three authorized keys per account. Accounts have `user` or `admin` role, random account ID, and authentication generation. Passwords are salted PBKDF2-HMAC-SHA256 verifiers; plaintext passwords are not retained in the database.
Network code holds copied, secret-free principals rather than pointers into database records. Principal currentness requires matching username, account ID, role, and authentication generation. Password, role, or key changes increment the generation; deletion/recreation also changes the account ID.
Revocation has two layers:
1. after a database mutation commits, the command layer makes best-effort targeted WebSocket/SSH revocation calls; notification failure does not roll back the mutation;
2. transports periodically and at sensitive boundaries recheck principal currentness, providing authoritative fail-safe closure if notification fails.
The final administrator cannot be deleted or demoted. UART0 establishes the first administrator through normal `user add <username> admin` and owns explicit unavailable-database recovery to empty. Recovery refuses a healthy database. No bootstrap API or command remains. Authenticated admin SSH can run the operational registry but is denied recovery; other secret-bearing commands are remotely available unless their handlers deny them.
NVS is not encrypted. Password verifiers improve password storage, but Wi-Fi credentials and TLS/SSH private keys remain recoverable under physical flash extraction.
## 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_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).
## Console architecture
UART0 and admin SSH share canonical command implementations:
```text
UART0 linenoise frontend --\
> fixed request queue -> one dispatcher -> esp_console_run()
admin SSH line editor ----/ |
+-> registered *_console handlers
```
`admin_ssh_console` creates the dispatcher before network services but marks command dispatch ready only after ESP-IDF console registration and successful UART frontend task creation. An admin SSH connection during that boot window is rejected rather than racing an incomplete registry.
The dispatcher is the sole caller of `esp_console_run()`, serializing UART0 and all admin SSH commands. This is required because the console registry is treated as non-reentrant, but it also means a long command or interactive prompt blocks all administration entry routes.
The transport-neutral boundary retains `admin_ssh_console_open_owned()` and adds available-slot admission for runtime SSH/browser owners: copied transport-qualified slot/session/generation identity plus a firmware-lifetime immutable owner adapter. The existing two console slots are shared, not multiplied per frontend; active/executing slots cannot be replaced. Owners serialize per-session input, consume output and enforce transport liveness; completion scratch is claimed nonblockingly across owners. The existing control task calls drain/lifecycle adapters outside console locks. SSH uses generation-checked published snapshots, principal copies and its assigned console index, never wolfSSH from the control task. `SELF_CLOSE` is owner-relative; legacy SSH actions remain SSH-specific and unsupported owner actions are rejected. Dispatcher-side owner `is_current` checks run outside console locks, with full identity recheck after validation. Commands revalidate immediately before the runner; prompts revalidate before publication and after waits (250 ms polling plus check/scheduling latency), rejecting revoked submitted input and stale wakes. SSH preserves close intent through external-close consumption. Consumed output is wiped. These checks do not cancel arbitrary executing handlers or replace owner-side input/output and lifecycle validation.
For SSH, standard output/error is redirected to the invoking session's bounded output ring. `console_input` routes visible or hidden prompts to UART0 or the active SSH session. `exit` and Ctrl+D on an empty admin SSH line use bounded deferred self-disconnect after their acknowledgement drains; role-`user` SSH remains a binary-transparent serial stream. Session tokens include slot and generation so late queued work cannot attach to a reused SSH slot. Only the SSH owner task moves ring output through wolfSSH.
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).
## Wi-Fi and persistence
`wifi_config` owns a fixed-width versioned NVS schema with four prioritized station profiles and AP policy `off`, `fallback`, or `always`. Missing configuration generates per-device defaults including a random AP password. Invalid stored data is generally left untouched while RAM defaults are used.
`wifi_manager` is a permanent task with one bounded command/event queue. ESP-IDF callbacks only copy compact events into the queue. The task owns association, DHCP/address deadlines, profile failover, AP policy, retries/backoff, next-profile requests, and the mDNS announcement lifecycle. One-second reconciliation reads authoritative driver/netif state in TCP/IP context, enables STA SLAAC and creates link-local addresses without restarting DAD. IPv4 or preferred IPv6 (including link-local-only) establishes ONLINE and may retire the fallback AP; ONLINE does not imply a default route or Internet access. DHCPv4 option 12 carries the full `sak-<suffix>` from permanent bounded hostname storage installed before connection; rename updates subsequent exchanges without restarting DHCP. The bounded snapshot retains up to three preferred numeric IPv6 addresses from the same observation as its availability flags; compile-time capacity checks reject larger lwIP address-slot settings. `wifi status` formats these addresses and their scope. Web Network settings encode up to three fixed-width strings from the same snapshot and display link-local/ULA/GUA groups within a 2304-byte JSON bound; the overview/OLED retains availability-only reporting.
`mdns_service` initializes the responder at most once after either-family STA readiness. Initialization failure is latched rather than retried because partial upstream low-memory initialization is not safely recoverable; mDNS failure is nonfatal. HTTPS/SSH owners publish availability under a short portMUX, and only the Wi-Fi owner reconciles the two STA-only DNS-SD records and address families, including while offline. Component calls run outside project service mutexes. Missing-family disable is reasserted every pass and present-family enable/reprobe every 30 seconds because upstream actions can silently drop and expose no readiness acknowledgement. This is eventual convergence, not instantaneous withdrawal: a transient stale/zero-A window can occur after DHCP loss, and AAAA follows upstream valid-address semantics including deprecated addresses. A version/hash-guarded build-local mDNS 1.12.0 source overlay balances per-family multicast membership and join cleanup; managed source stays immutable. Tests and upgrade/removal contract: `tests/mdns_membership/README.md`. ESP-IDF Wi-Fi storage is RAM-only; the application blob is authoritative, and edits require explicit save. Edits to disabled station profiles are staged in RAM without restarting the radio; enabling/disabling a profile or changing enabled station/AP policy restarts it asynchronously. Start/stop—including local controls—intentionally update the RAM `enabled_at_boot` field. Working-configuration copies contain PSKs and must be securely wiped; routine status and the local UI use secret-free snapshots.
Persistent namespaces/blobs include:
- `serial/config`;
- `wifi_app/config`;
- `mdns_cfg/config`;
- `local_ui/config`;
- `web_sec/material`;
- `user_db/database`;
- `ssh_sec/material`.
Configuration modules generally choose RAM defaults without erasing incompatible storage. Security-material modules fail closed on malformed existing material and require explicit reset. OTA slots, coredump space, an NVS-key partition, and storage are reserved in `partitions.csv`; OTA, NVS encryption, coredump handling, and filesystem mounting are not implemented.
## 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_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).
## Local UI and hardware boundaries
`board_pins.h` centralizes project-assigned RS-232, diagnostic, RGB LED, and local-UI hardware resources; UART0 GPIOs remain local to `main.c`, and native USB uses platform wiring. `local_display` solely owns I2C0, the SSD1315-compatible OLED, its static framebuffer, and display mutex. Display frames belong to the initiating task. Dirty-page commits and I2C transactions are bounded.
When button GPIO initialization succeeds, `local_status_ui` starts a firmware-lifetime low-priority task that polls/debounces buttons, renders copied public snapshots, implements aging/wake behavior, and invokes a constrained set of public service APIs for local controls. It collects snapshots before opening a display frame, so service/broker locks are not held across I2C. It never parses CLI output, becomes a broker client, edits credentials, or assigns a writer; emergency action can only release the expected current writer.
The task can run with an absent OLED, and a fresh button press can request one bounded panel reprobe after successful I2C bus setup. Failed I2C bus creation is not recoverable through that path. The `display` configuration commands depend on the UI task. Long confirmation holds protect disruptive local actions, and stuck buttons are quarantined.
Hardware diagnostics are synchronous console commands. RS-232 tests own the physical port exclusively and restore safe GPIO state; OLED tests reuse the display service rather than taking independent I2C ownership.
## Concurrency and lifecycle constraints
- Broker, USB, web-transport, Wi-Fi, and SSH owner tasks are firmware-lifetime tasks; the local-UI task is also firmware-lifetime when button initialization allowed it to start. Stopping a service generally stops its runtime/listener, not the owner task.
- Bounded queues, stream buffers, work bursts, and drop counters are part of slow-client and watchdog isolation.
- Transport slot generations and account authentication generations solve different stale-reference problems; preserve both.
- Library/hardware ownership is centralized: serial task owns UART1 while running, display service owns I2C/framebuffer, the SSH owner task owns post-initialization wolfSSH runtime calls, and the console dispatcher owns `esp_console_run()`.
- Password authentication performs PBKDF2 outside the user-database mutex and revalidates afterward. Some password mutation paths currently derive verifiers while holding the mutation lock; do not generalize the authentication locking pattern without checking the exact path.
- Avoid holding service/database/broker locks across I2C, network sends, or other potentially long operations unless the existing contract explicitly requires it. Preserve the existing broker-before-serial lock order.
- Serial RX/TX stream payloads, broker per-client payloads, the transactional user-database candidate, and selected cryptographic allocations prefer PSRAM with internal fallback. The live user database, FreeRTOS control structures, UART driver buffers, and task stacks remain internal where deterministic/cache-disable access matters.
- Ping's 21-event payload (4,200 bytes) and the public user-console snapshot (2,156 bytes) are lazy PSRAM-only allocations retained for firmware lifetime, with no internal fallback. Queue control and synchronization stay internal; only ping or user status/list/show fails on allocation failure, never registration, mutations or UART0 recovery. Commands retain dispatcher/gate serialization; ping callbacks are task-context producers. The entire user snapshot is wiped after each display attempt. Never introduce ISR/cache-off access or free payloads while callbacks can retain them. Regression coverage: `tests/admin_console_boundary/psram_ping.py` and `accounts.py`.
- The build disables wolfSSL ESP32 AES/SHA acceleration, and the HTTPS path uses software AES for PSRAM-backed records. This preserves the validated workaround for uncoordinated mbedTLS/wolfSSL hardware-crypto locks and a prior mbedTLS external-RAM DMA watchdog stall.
## Legacy credential removal storage boundary
`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).
+208
View File
@@ -0,0 +1,208 @@
# Code map
This is a semantic map, not a complete file inventory. Start here, then read the listed headers and only the implementation paths relevant to the task.
## Bootstrap and system composition
**Responsibility:** establish startup order, recovery behavior, configuration loading, service dependencies, and command registration.
- Files: `src/main.c`, `src/CMakeLists.txt`, root `CMakeLists.txt`, `platformio.ini`, `partitions.csv`, `src/idf_component.yml`; inspect targeted settings in `sdkconfig.defaults` when crypto, PSRAM, HTTPS/HTTPD, USB, or socket capacity matters
- Entry point: `app_main()`
- Called by: ESP-IDF runtime
- Dependencies: every subsystem initializer
- Lifecycle constraint: optional display/network failures should not remove UART0 administrative recovery or USB UART1 access; the custom administration frontend starts only after command registration.
## Small Phase 9 operating baseline
- `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.
## Secure randomness
**Responsibility:** provide the sole project-owned, mutex-serialized application DRBG, seeded before Wi-Fi/radio use.
- Files: `src/secure_random.{h,c}`
- Interfaces: `secure_random_init()`, random-byte helpers, `secure_wipe()`
- Called by: HTTPS material, SSH keys, users, Wi-Fi defaults, tickets, the HTTPS authentication cache, and the wolfCrypt seed callback
- Constraint: initialization order is security-significant; the DRBG deliberately avoids post-radio reseeding and fails closed at its generation limit. Do not add independent weak RNGs or radio-dependent early entropy paths.
## Physical RS-232 and serial service
**Responsibility:** protect the MAX3243/UART resource, own UART1 while running, buffer binary RX/TX, apply serial configuration, and expose status/counters.
- Files: `src/rs232_port_owner.{h,c}`, `src/serial_service.{h,c}`, `src/serial_config.{h,c}`, `src/serial_console.{h,c}`
- Interfaces: owner claim/release/fault; serial init/start/stop/read/write/configuration/snapshots; versioned NVS load/save
- Normal data caller: `session_broker`; USB, WebSocket, role-`user` SSH, console, and local UI also call serial lifecycle/configuration APIs as appropriate
- Dependencies: ESP-IDF UART driver, `board_pins.h`, NVS
- Ownership: the cooperative owner arbitrates active diagnostics (`PHASE0`) against the service (`SERVICE`); boot-time static-safe GPIO initialization and service-owned static-mode restoration are explicit exceptions. Unsafe cleanup marks `FAULT` until reboot.
- Lifecycle: stop/reconfiguration discards serial-service RX/TX and task-local pending bytes, but leaves broker clients, writer ownership, events, and already-fanned output intact. The 16 KiB RX and 8 KiB TX payloads prefer PSRAM; their FreeRTOS controls and UART driver storage remain internal.
## Session broker
**Responsibility:** mediate all transport access to the serial service; provide one writer lease and multiple isolated observers.
- Files: `src/session_broker.{h,c}`, `src/session_console.{h,c}`
- Interfaces: connect/disconnect, request/release/force writer, nonblocking read/write/event APIs, snapshots and counters; `session_broker_get_management_snapshot()` and `session_broker_assign_writer_current()` atomically project/compare target and lease generation. Exhausted 29-bit client generations retire; saturated lease generation preserves ordinary recovery. Tests: `tests/session_broker_diagnostics/run.py`.
- Called by: USB, web serial, role-`user` SSH, console tests, local UI snapshots/actions
- Dependencies: `serial_service`
- 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`.
## Native USB CDC
**Responsibility:** adapt TinyUSB CDC host state/data to one broker client.
- Files: `src/usb_cdc_transport.{h,c}`, `src/usb_console.{h,c}`
- Interfaces: `usb_cdc_transport_init()`, snapshots/counters, queued writer request/release
- Called by: startup, TinyUSB callbacks, console/local UI
- Dependencies: TinyUSB, broker, serial service
- Flow: `USB host <-> USB task <-> broker`
- Lifecycle: permanent owner task; broker client exists only while attached with host DTR asserted.
- Constraint: callbacks retain the latest host line coding only for diagnostics; it never reconfigures UART1. UART1 framing and speed remain controlled by the serial configuration and its explicit persistence commands.
## Web and WebSocket serial
**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).
- 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).
- 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
- Called by: startup, ESP-IDF HTTPS server, user administration revocation, console/local UI
- Dependencies: user database, secure random, broker, successful Wi-Fi manager initialization at boot, mbedTLS/HTTPS server; actual network reachability is an operational prerequisite, not an initializer invariant
- Flow: `browser -> HTTPS login/cookie session -> CSRF-protected ticket -> cookie/Origin/ticket admission -> WebSocket -> web transport -> broker`
- Ownership: HTTPD owns socket send/close work; transport task owns broker mediation; two fixed WebSocket slots and four outstanding tickets.
- Security constraints: Basic/cache removed; four absolute one-hour cookie sessions revalidate principal currentness. Four pre-login challenges (120 s), five credential attempts/60 s globally, no live session/challenge/ticket eviction. Origin/CSRF required for mutations; Origin/cookie/ticket before upgrade. Disconnect pauses reconnect but retains login; Sign out invalidates its session. Authored loader changes must update their hard-coded CSP hashes atomically.
- Session-store boundary: admitted HTTPS start initializes records; auth-init failure gates HTTPS. Failed start/accepted stop disables and wipes state. Tickets/slots require nonzero non-reused session IDs; session/account/global revocation invalidates store records before socket cleanup. RNG/SHA/database calls run outside short portMUX sections; ID/expiry/epoch checks reject stale work. Run `python3 tests/web_session_store/run.py` and its `--serial` integration mode.
- Asset constraint: `web_assets_data.c` is checked-in generated input to the build; do not hand-edit or regenerate casually.
### Browser admin backend
- 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`.
- 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.
## 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).
| Domain | API / canonical owner | Focused source tests |
|---|---|---|
| Serial | `web_serial_settings.{c,h}` / `serial_service` | `tests/web_cookie_auth/run.py --serial-settings`, `tests/admin_console_boundary/run.py` |
| Accounts / keys | `web_account_settings.{c,h}` / `user_database` | `tests/web_cookie_auth/run.py --accounts`, `tests/admin_console_boundary/accounts.py` |
| Network | `web_network_settings.{c,h}` / `wifi_manager`, `mdns_service` | `tests/web_cookie_auth/run.py --network`, `tests/web_network_settings/run.py` |
| Display | `web_display_settings.{c,h}` / `local_status_ui` | `tests/web_cookie_auth/run.py --display` |
| Broker | `web_broker_settings.{c,h}` / `session_broker` | `tests/web_cookie_auth/run.py --broker`, `tests/session_broker_diagnostics/run.py` |
| SSH | `web_ssh_settings.{c,h}` / `ssh_transport`, `ssh_security` | `tests/web_cookie_auth/run.py --ssh`, `tests/ssh_management/run.py`, `tests/ssh_management/security.py` |
| HTTPS / reboot | `web_lifecycle_settings.{c,h}` / `web_server`, `web_security` | `tests/web_cookie_auth/run.py --lifecycle`, `tests/web_admin_transport/server_lifecycle.py` |
Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtures / `layout.py`. These are navigation pointers, not claims of test execution.
## SSH
**Responsibility:** authenticate SSH, route users to serial and administrators to the command dispatcher, and own wolfSSH lifecycle.
- Files: `src/ssh_transport.{h,c}`, `src/ssh_security.{h,c}`, `src/ssh_console.{h,c}`
- Interfaces: init/start/stop, session snapshots/disconnect/revocation, host-key replacement, counters; `ssh_transport_get_management_snapshot()` / `ssh_transport_manage_current()` fence lifecycle and exact session admission. `ssh_transport_replace_identity()` reserves service before task-bound security identity across stop/commit/restart, retains context until all slots retire and rejects orphan starts. Tests: `tests/ssh_management/run.py`, `tests/ssh_management/security.py`, `tests/ssh_management/runtime.py`.
- Called by: startup, network clients, user revocation, console/local UI
- Dependencies: user database, broker, admin SSH console, secure random, wolfSSH/wolfSSL; boot start gate requires Wi-Fi and SSH security/runtime readiness, independently of HTTPS identity readiness (verified in `main.c` after accepted legacy cleanup).
- Flow: role `user` -> broker; role `admin` -> `admin_ssh_console`
- Ownership: after caller-side library initialization, one task pinned to core 1 owns runtime wolfSSH contexts/sessions; two fixed generation-tagged slots.
- Security constraint: an interactive shell request is required; exec and subsystems are rejected, and no project file-transfer or forwarding route exists. PTY is not explicitly required.
## Users, authentication, and authorization
**Responsibility:** persist bounded accounts, verify passwords/SSH keys, issue secret-free principals, and enforce account invariants.
- Files: `src/user_database.{h,c}`, `src/user_console.{h,c}`; `src/admin_command_gate.{h,c}` is currently a narrow recursive wrapper used only by the `user` command handler, not the global command serializer
- Interfaces: credential-independent init/empty recovery, authenticate, principal-currentness, account/password/role/key mutations, snapshots
- Called by: web and SSH authentication/currentness checks and console administration
- Dependencies: NVS, secure random, mbedTLS cryptography; after a committed command-layer mutation, best-effort web/SSH revocation calls supplement authoritative transport currentness checks
- Ownership: database mutex protects the internal live record and PSRAM-preferred transactional candidate; password authentication runs PBKDF2 outside the mutex and revalidates afterward, while mutation locking must be checked per operation.
- Authorization: UART0 establishes the first administrator through normal `user add` and exclusively owns unavailable-database recovery to empty (healthy database refused); current admins may use admin SSH for other commands unless handler policy denies them. HTTPS serial/status permits both roles; administration requires `admin`.
- Constraint: final administrator cannot be deleted or demoted; transport principals must be rechecked after mutations.
## Administration console infrastructure
**Responsibility:** provide one canonical command registry and serialized execution for UART0, admin SSH and browser admin.
- Files: `src/admin_ssh_console.{h,c}`, `src/console_input.{h,c}`, `src/console_completion.{h,c}`, `src/system_console.{h,c}`, `src/network_console.{h,c}` and all `*_console.{h,c}` modules
- Entry points: `admin_ssh_console_init()`, `admin_ssh_console_start_uart_frontend()`, command registration functions
- Called by: startup, UART0 frontend, role-`admin` SSH transport, browser admin transport
- Dependencies: ESP-IDF console/linenoise, all command handlers, user-principal currentness
- Flow: `UART0/admin SSH/browser admin -> bounded request queue -> one dispatcher -> esp_console_run()`
- Ownership: dispatcher is sole `esp_console_run()` caller; the SSH owner exclusively performs post-initialization wolfSSH runtime calls.
- Lifecycle: remote session tokens include slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Admin SSH `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort output draining.
- Constraint: one slow command or prompt serializes all administration. Admin SSH is unavailable until command registration and UART frontend creation complete; supported deferred actions wait only for a bounded application-buffer drain heuristic.
## Wi-Fi
**Responsibility:** persist station/AP policy and own asynchronous ESP-NETIF/Wi-Fi state transitions.
- Files: `src/wifi_config.{h,c}`, `src/wifi_manager.{h,c}`, `src/wifi_console.{h,c}`, `src/mdns_config.{h,c}`, `src/mdns_service.{h,c}`, `src/mdns_console.{h,c}`, `src/network_console.{h,c}`
- Interfaces: config defaults/validate/load/save; manager init/start/stop/apply/reconnect/next-profile/snapshot
- Dependencies: secure random for default AP password, NVS, ESP-NETIF/Wi-Fi/events, Espressif mDNS, lwIP diagnostics
- Lifecycle: permanent manager task and bounded queue; callbacks enqueue compact events only. One-second owner reconciliation handles DHCP hostname updates, preferred IPv4/IPv6 readiness, missed events and mDNS records/families. `ONLINE` includes preferred link-local-only IPv6 and may retire fallback AP without IPv4; snapshots retain IPv6 availability flags plus up to three preferred numeric addresses from the same observation. `wifi status` and web Network settings render those addresses; overview/OLED remain availability-only. The typed runtime adds `ipv6_addresses` (up to three fixed-width strings); backend and UI share a 2304-byte response bound. Regression coverage includes byte order, address clearing and the compile-time slot-capacity bound.
- Phase 12: `mdns_service_set_https_available()` / `mdns_service_set_ssh_available()` publish under a short portMUX; only the Wi-Fi owner calls component APIs. STA DNS-SD has two records; family repair re-probes available families every 30 seconds because upstream action admission has no reliable acknowledgement. AAAA uses valid (including deprecated) upstream addresses. DHCPv4 uses full `sak-<suffix>` before connection and in later exchanges after rename; permanent raw-netif hostname storage is updated only in TCP/IP context.
- Focused tests: `tests/wifi_phase12`, `tests/mdns_phase12`, `tests/ssh_phase12`, `tests/mdns_membership`; HTTPS IPv6 authority cases live in `tests/web_auth_parse` and cookie suites. Build-local `cmake/mdns_membership.*` replaces only reviewed mDNS 1.12.0 networking source to balance multicast membership; version/hash drift fails configuration. No managed source is edited.
- Constraint: application NVS is authoritative (`WIFI_STORAGE_RAM`); working edits are not persisted until save. Start/stop, including local controls, intentionally update the RAM `enabled_at_boot` field. Working-config copies contain PSKs and must be tightly scoped and wiped; routine status/local UI must use secret-free snapshots.
## Local display and controls
**Responsibility:** own OLED I2C/framebuffer operations and present status plus constrained button actions.
- Files: `src/local_display.{h,c}`, `src/local_status_ui.{h,c}`, `src/local_boot_animation.{h,c}`, `src/local_ui_config.{h,c}`, `src/local_ui_console.{h,c}`
- Interfaces: display init/frame/draw/commit/snapshot; UI start/activity/config; generation-checked settings projection/update and explicit persistence reservation; versioned NVS settings
- Called by: startup, local UI task, diagnostics, display console
- Dependencies: copied snapshots/public APIs from serial, broker, USB, Wi-Fi, web, SSH
- Ownership: `local_display` solely owns I2C0 and framebuffer mutex; a frame belongs to its initiating task.
- Lifecycle: the low-priority task is firmware-lifetime only if button GPIO initialization succeeds; it still runs with an absent panel so a press can reprobe after successful I2C bus setup. Failed bus creation is not recoverable by that reprobe, and `display` configuration commands depend on the UI task.
- Constraint: collect service snapshots before I2C; local UI never joins broker or handles secrets. All configuration writers honor the UI owner's zero-wait reservation; NVS runs outside timing critical sections. Reset commits defaults before RAM publication, including CLI; buttons/diagnostic holds update activity, not configuration generation.
## Hardware and diagnostics
**Responsibility:** centralize board wiring and provide bounded electrical tests with safe cleanup.
- Files: `src/board_pins.h`, `src/rs232_hw_test.{h,c}`, `src/local_ui_hw_test.{h,c}`, `src/status_led.{h,c}`
- Documentation: `docs/wiring.md`, `docs/electrical_tests.md`
- Called by: startup and `debug` commands
- Dependencies: physical RS-232 owner, serial/display services, ESP-IDF GPIO/UART/I2C/LED drivers
- Ownership: RS-232 diagnostics refuse to run while the service owns the port; display diagnostics reuse `local_display`.
- Constraint: wiring and voltage assumptions are safety-relevant; verify target hardware before running diagnostics. RGB LED initialization is currently boot-fatal, and its colors report diagnostic state rather than aggregate firmware health.
## Where should I look?
| Task | Start here |
|---|---|
| Change boot order or failure behavior | `src/main.c`, then affected subsystem `init/start` contracts |
| Change serial framing, flow control, or persistence | `serial_config.*`, `serial_service.*`, `serial_console.*` |
| Change writer/observer policy | `session_broker.*`, then all three transports |
| Debug missing or duplicated serial bytes | `serial_service.c` -> `session_broker.c` -> relevant transport task |
| Change USB open/DTR or line coding | `usb_cdc_transport.*` |
| Change browser terminal protocol | `web_serial_transport.*`, `web_ui.c`, `web_server.c` |
| Change HTTPS endpoints/authentication | `web_server.*`, `web_security.*`, `user_database.*` |
| Change SSH login or role routing | `ssh_transport.*`, `ssh_security.*`, `user_database.*` |
| Add or change a command | relevant `*_console.c`, `console_completion.c`, `admin_ssh_console.c` policy/deferred handling |
| Change account roles/passwords/keys | `user_database.*`, `user_console.c`, transport revocation APIs |
| Change Wi-Fi policy or profile persistence | `wifi_manager.*`, `wifi_config.*`, `wifi_console.c` |
| Change station mDNS hostname or persistence | `mdns_service.*`, `mdns_config.*`, `mdns_console.c`, then `wifi_manager.c` |
| Change OLED rendering or buttons | `local_status_ui.c`, `local_display.*`, `local_ui_config.*` |
| Change board GPIO or electrical tests | `board_pins.h`, hardware test module, `docs/wiring.md` |
| Change embedded browser assets | `web_assets/SOURCES.md`, generator, then generated data only as an explicit regeneration task |
| Investigate memory/watchdog regressions | broker/web/SSH bounded loops, allocation placement, root `CMakeLists.txt`, relevant roadmap Phase 6 history |
+71
View File
@@ -0,0 +1,71 @@
# Current project state
Working memory, not an implementation timeline. Source is authoritative; begin with [code map](code-map.md), [architecture](architecture.md) and [decisions](design-decisions.md).
## 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 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.
- Boot TLS -0x004C receive errors and auth-failure counts have no demonstrated cause. Snapshots are non-atomic and counts are not an aligned interval. This handoff changes documentation only; no new build/test/upload/erase/device operation/commit; hardware directory untouched. Prior latest build94,444B RAM/1,854,485B flash and UI174+CSP remain historical validation.
## Follow-up — IPv6 addresses in web Network settings
- User confirms direct IPv6 ping/HTTPS and Avahi AAAA lookup for `sak-1024.local` succeed. Client NSS uses `mdns4_minimal [NOTFOUND=return]`; systemd-resolved explicit mDNS reports no eligible networks. This supports a client resolver integration issue, not failed firmware AAAA publication on the tested Avahi path. The user subsequently enabled dual-family mDNS through Fedora authselect and confirmed success; see acceptance above.
- Added bounded runtime `ipv6_addresses` (<=3 fixed-width lowercase eight-hextet strings) to existing admin Network snapshot, grouped as link-local/ULA/GUA in Settings. Overview/OLED remain flags-only. No new netif call, allocation, task or snapshot storage. Backend/client JSON bound 2048→2304 (+256B response stack); maximum-escaped fixture with all three addresses is 2067B. Strict UI validation/text-only output and existing session fences preserved; generated assets untouched.
- `pio run` PASS **94,444 B linked RAM / 1,854,485 B flash**, +0/+800 versus prior CLI-address build. Network cookie/owner regressions and UI174+CSP PASS. Initial cookie fixture had an obsolete 2048B output buffer; fixed to use the production bound and rerun PASS. Empty/full address lists, byte order, bad counts, malformed/injected UI entries and clearing covered. No upload or hardware operations; hardware directory untouched.
## Follow-up — browser Wi-Fi controls and IPv6 diagnosis
- User requests browser Admin shell Wi-Fi parity with SSH/typed settings. Removed only the Wi-Fi status-only policy gate; canonical settings/lifecycle/persistence/diagnostics and hidden secret prompts now work. mDNS remains status-only and unrelated restrictions stay intact. Disruptive commands may cut off the response; owner admission is not peer acknowledgement or cancellation on disconnect.
- User now confirms the CLI displays an IPv6 address, direct IPv6 ping works, and the web interface opens using IPv6. This validates those reported unicast operations, not all Phase 12 checks. `ping sak-1024.local -6` fails on the client with “Die Adressfamilie für Hostnamen wird nicht unterstützt.” That earlier client failure was subsequently resolved through Fedora authselect dual-family mDNS; Avahi AAAA lookup and normal IPv6 hostname access succeeded. Do not reopen this as an established firmware discovery defect.
- Added three bounded preferred numeric addresses to manager snapshot, copied/cleared with the same TCP/IP observation, and labelled output in `wifi status`. Web/OLED schema unchanged. +48 bytes per snapshot/settings copy and shared static storage; compile rejects >3 lwIP slots. No new allocation/task/socket. Latest `pio run` PASS **94,444 B RAM / 1,853,685 B flash** (+48/+660 vs initial Phase12). No upload/device operations.
- PASS: browser policy, new actual Wi-Fi secret-handler/browser-prompt fixture (cancellation/revocation/wiping/history/role guards), account/lifecycle boundary, preferred-address lifecycle/byte-order/rendering/capacity, Network settings, cookie Network and UI171+CSP, diff check. Broader `admin_console_boundary/run.py` passes console/certificate then fails existing SSH-adapter compilation due missing `web_firmware_update_reserve_reboot` fake; left unrelated fixture unchanged. ASan/UBSan unavailable at host link. Hardware directory untouched.
## Phase 12 implementation history — superseded by acceptance above
- User authorized the agreed DHCPv4 hostname / dual-stack SLAAC / STA DNS-SD baseline. Another agent owns `hardware/`; this work did not read or edit it. Do not commit or revert that agent's work. No upload, erase or device operation performed.
- Wi-Fi owner applies full `sak-<suffix>` before DHCP and on rename for future exchanges, using permanent TCP/IP-owned storage to preserve the existing 59-byte hostname maximum beyond IDF's setter limit. Preferred IPv6 link-local/ULA/GUA can establish ONLINE without IPv4; link-local-only can retire fallback AP. One-second reconciliation handles stale/missed events. Web/OLED expose availability; the follow-up above adds actual preferred addresses to CLI output.
- HTTPS default listener verified dual-stack; shared authority parser now strictly canonicalizes bracketed IPv6 without zone IDs. SSH uses one explicitly dual-stack listener with scope-safe peers, preserving two slots. Service owners publish availability through short portMUX sections; Wi-Fi owner reconciles two DNS-SD records and address families. No new task/socket/broker slot/serial payload buffer, dependency version, partition or generated asset change.
- 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).
## 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.
- 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).
- **Scope decision (2026-09-18):** User removed the BLE transport/provisioning proposal entirely because it no longer fits the project concept. BLE is not planned; retain the existing USB, HTTPS/WebSocket and SSH transport scope. This earlier scope decision did not authorize implementation; Phase 12 was subsequently authorized and implemented as recorded above. Do not resurrect Phase 9 patches. This update changed documentation only; no build, test, upload, erase, device operation or commit was performed.
## Session logging scope decision
- User removed the filesystem-backed session recording proposal entirely. On-device session logging/transcripts are out of scope: users capture relevant output in their SSH or web terminal instead. Avoid adding RAM pressure, flash-write interference or potentially incomplete recordings to the serial path. The reserved `storage` partition remains unchanged and unmounted; this decision does not authorize partition migration.
## Accepted state — 2026-09-18
- **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:** 8AC 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.
- 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
- Previously accepted combined binary WS send: CPU160MHz / 230400 baud full mix including browser admin. The earlier Phase 8 telemetry had very low internal/DMA lifetime minima (2,052/460 B); these are nonblocking headroom follow-ups, not approved reserves or proof of simultaneous allocation failure. Full table, capture workload and counter limits are preserved in the roadmap. The latest Phase 12 full-mix minima are 8,528/772 B, with the same evidence limits.
- TLS `-0x004C` means generic NET_RECV_FAILED, not OOM. Historical authentication/admission symptoms do not establish a cause. Do not invent fault, soak, timing or power-loss passes.
- Credentials remain unencrypted; old flash contents are not erased. Intermittent trusted-network operation reduces exposure, not physical-extraction risk. Upstream upgrades are separate deliberate tasks, not an endless local backport programme.
- Phase 10 is complete by the explicit acceptance above; detailed unreported regression scenarios remain unevidenced, not completion blockers. Device operations, branch/reset, commits and dependency upgrades remain outside this documentation task.
## Contracts to preserve
- One UART1 writer, isolated observers and binary transparency. UART0 is administrative recovery; native USB is network-independent UART1, not administration or uninterrupted reboot.
- Typed operations carry original-login IDs to the existing dispatcher. Owner-reserved generations fence stale/ABA changes; revocation/timeout does not cancel already admitted work. HTTPS commits before stop/restart; SSH stops before commit/restart; committed identity is not rolled back on lifecycle failure. Lost acknowledgement means uncertainty, not automatic replay.
- Preserve private IDF HTTPD version guards, one outstanding owner-work reservation through failed destruction, SSH context lifetime until slots retire, bounded queues/buffers and secret-free metadata. Canonical recovery survives conditional-token exhaustion.
- Removed Phase 8 scope stays removed: dedicated typed network diagnostics and ordinary browser-session/USB controls. No implicit browser-shell parity or browser identity reset/recovery/export.
+193
View File
@@ -0,0 +1,193 @@
# Durable design constraints and decisions
Only constraints supported by implementation or current project documentation belong here. When original rationale is unknown, the entry describes the observable constraint without inventing intent.
## Display configuration has an owner reservation separate from button activity
**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).
## 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).
## One broker mediates all production serial transports
**Decision:** USB CDC, WebSocket, and role-`user` SSH access UART1 through `session_broker`; transports do not independently own the serial service.
**Rationale/evidence:** The broker is initialized after the serial service and all transport implementations connect broker clients. It is the normal serial RX consumer and TX gate. Project documentation requires one writer and multiple observers.
**Consequence for future changes:** New serial transports must become broker clients. Do not bypass writer checks or consume `serial_service` RX directly. `serial_service_start()` is not idempotent, so admission code must reconcile check/start races as the existing transports do. Broker paths enter serial-service APIs while holding the broker mutex; preserve that lock order and do not call back into the broker while holding the serial state mutex. Preserve binary transparency and avoid in-band ownership control.
**Relevant files:** `src/session_broker.{h,c}`, `src/serial_service.{h,c}`, `src/usb_cdc_transport.c`, `src/web_serial_transport.c`, `src/ssh_transport.c`
## Slow clients are isolated by bounded per-client storage
**Decision:** UART RX is drained and copied into independent bounded broker output streams; a full observer loses only its own copy.
**Rationale/evidence:** `session_broker` accounts per-client dropped bytes instead of blocking fan-out. The roadmap records slow-client isolation as a project-wide constraint.
**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).
**Relevant files:** `src/session_broker.c`, `src/session_broker.h`, `docs/roadmap.md`
## Physical UART ownership and logical writer ownership remain separate
**Decision:** `rs232_port_owner` controls whether diagnostics or the serial service may manipulate UART/MAX3243 hardware; the broker separately controls which connected client may write.
**Rationale/evidence:** The code has explicit `NONE`, `PHASE0`, `SERVICE`, and `FAULT` hardware states plus broker client/writer IDs.
**Consequence for future changes:** A writer lease never authorizes direct UART/GPIO access. Active hardware tests must claim `PHASE0`; the production service must claim `SERVICE`. Boot-time static-safe GPIO setup and service-owned static-mode restoration are explicit exceptions to this cooperative gate. Ambiguous cleanup must keep the transceiver safe and require reboot rather than clearing fault casually.
**Relevant files:** `src/rs232_port_owner.{h,c}`, `src/rs232_hw_test.c`, `src/serial_service.c`, `src/session_broker.c`
## Resource IDs are generation-safe
**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.
## 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).
## UART0 is the physical recovery authority
**Decision:** UART0 remains independent of UART1 and networking. The first administrator is created with normal `user add` on UART0; explicit unavailable-user-database recovery to empty is UART0-only and refuses healthy storage. No bootstrap command/API remains.
**Rationale/evidence:** `main.c` configures UART0 separately; command policy and user handlers deny these operations remotely. README/roadmap identify UART0 as the trusted recovery console.
**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`
## Admin SSH and user SSH are different routes
**Decision:** A role-`user` SSH session becomes a broker serial client. A role-`admin` session enters the administration console and never obtains a broker client/writer lease.
**Rationale/evidence:** Role routing is explicit after SSH authentication. The administrative shell is intended for command execution, not multiplexed serial data.
**Consequence for future changes:** Do not silently give administrators both streams or infer that higher privilege means UART1 ownership. A route-switch feature would require explicit protocol, lifecycle, and authorization design.
**Relevant files:** `src/ssh_transport.c`, `src/admin_ssh_console.{h,c}`, `src/session_broker.c`
## One dispatcher executes the canonical command registry
**Decision:** UART0 and admin SSH submit complete lines to one fixed queue; one task is the sole caller of `esp_console_run()`.
**Rationale/evidence:** The implementation treats ESP-IDF console execution as non-reentrant and removes the need for separate remote command implementations.
**Consequence for future changes:** Register one canonical handler rather than creating a second SSH dispatcher. Long commands/prompts block all administration, so keep handlers bounded or explicitly asynchronous. Preserve output routing and remote principal checks.
**Relevant files:** `src/admin_ssh_console.c`, `src/main.c`, `src/console_input.c`, all `src/*_console.c`
## Selected self-affecting admin SSH actions use bounded deferred control
**Decision:** SSH self-close/reboot/lifecycle/host-key actions use existing bounded application-drain control; browser shell stop/reboot and typed certificate handoff use owner adapters, not command replay. Drain is not peer receipt. Crypto/NVS browser rotation runs on the existing dispatcher, not the small control stack; executing slots remain reserved across self-detach.
**HTTPS typed ACK:** Successful synchronous send return precedes one nonreused-ID HTTPD callback to the existing dispatcher. No captured request/fd/reusable operation pointer or lifecycle wait on HTTPD. Lost accepted work retains one reservation until callback or successful destruction; never release speculatively on timeout. Original-login/current-admin/dequeue deadline checks precede admission. Reserved restart may deliberately invalidate that login; subsequent revocation is not cancellation. Reboot invokes canonical `esp_restart()` outside locks without self-console cleanup.
**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).
## 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).
## 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).
## 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).
## 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).
## Security material and configuration use bounded, versioned NVS records
**Decision:** Application settings, users, and identities use separate fixed/versioned NVS blobs. Serial, Wi-Fi, mDNS-hostname, and local-UI working edits are RAM-only until explicitly saved. User mutations and HTTPS/SSH identity changes commit directly as part of the operation. Invalid ordinary configuration generally selects RAM defaults without erasing storage; malformed security material fails closed and needs explicit reset.
**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).
**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`
## NVS is persistence, not a physical security boundary
**Decision:** The current firmware stores Wi-Fi credentials and TLS/SSH private keys in unencrypted application NVS. The reserved NVS-key partition does not enable encryption.
**Rationale/evidence:** `partitions.csv`, README security notes, and current code show no NVS-encryption setup. Original rationale for deferring encryption is outside the implementation; the observable limitation is explicit.
**Consequence for future changes:** Do not claim resistance to flash extraction. Logical NVS replacement can leave old plaintext credentials in flash and is not secure erasure; no factory erase is required by this cleanup. Older v1-only firmware cannot read v2 HTTPS material. Avoid increasing stored secret exposure. Enabling encryption requires migration/recovery planning, not just changing the partition table.
**Relevant files:** `partitions.csv`, `README.md`, `src/web_security.c`, `src/ssh_security.c`, `src/wifi_config.c`
## Wi-Fi callbacks enqueue; the manager owns policy
**Decision:** ESP event callbacks copy bounded event data into the Wi-Fi manager queue. A permanent manager task performs driver operations, profile/AP policy, deadlines, reconciliation, and station mDNS announcement transitions. mDNS initializes at most once, remains allocated across transient disconnects, and treats failure as nonfatal. IPv4 or preferred IPv6 including link-local-only establishes ONLINE; route/Internet reachability is not implied. Full DHCP hostname storage and IPv6 state access belong to TCP/IP context. HTTPS/SSH owners only publish availability with short critical sections; Wi-Fi owns DNS-SD record and family reconciliation. Public mDNS actions are not acknowledged, so absent-family disables repeat every pass and healthy enables re-probe on a slower 30-second repair cadence. Record/address withdrawal is eventual, and upstream AAAA includes deprecated-but-valid addresses.
**Rationale/evidence:** Callback paths avoid blocking, NVS, and policy work. Manager deadlines consult authoritative driver/netif state so dropped events are recoverable.
**Consequence for future changes:** Keep callbacks short and nonblocking. Add state transitions to the manager rather than directly invoking Wi-Fi policy from consoles, UI, or callbacks. Preserve queue-drop observability.
**Relevant files:** `src/wifi_manager.{h,c}`, `src/wifi_config.{h,c}`, `src/mdns_service.{h,c}`, `src/mdns_config.{h,c}`. `cmake/mdns_membership.*` is a narrowly reviewed exception for mDNS 1.12.0 multicast join/leave imbalance: patch only a build-local networking source with strict version/hash guards, never the managed source. Dependency upgrades must review/remove the overlay; do not bypass its guards or restore the abandoned Phase 9 patches.
## Optional local UI cannot become a core dependency
**Decision:** The OLED/display may fail without stopping serial, UART0, USB, or networking. The UI consumes copied snapshots and calls public APIs; it never parses CLI output or joins the broker.
**Rationale/evidence:** `main.c` logs display failures and continues. `local_status_ui` collects snapshots before display frames and exposes limited confirmed controls.
**Consequence for future changes:** Keep OLED/I2C work bounded and outside service locks. Do not put credentials or core ownership into UI state. A missing display must remain nonfatal.
**Relevant files:** `src/main.c`, `src/local_display.{h,c}`, `src/local_status_ui.c`, `src/local_ui_config.c`
## Hardware and library access has designated owners
**Decision:** The serial task owns UART1 while active, `local_display` owns I2C/framebuffer access, the SSH owner task owns runtime wolfSSH contexts/calls after caller-side library initialization, and the console dispatcher alone runs registered commands.
**Rationale/evidence:** These constraints are enforced by module structure, mutex/task assertions, and transport indirection. Original rationale varies; the observable effect is serialized library/hardware access.
**Consequence for future changes:** Cross-task requests should use existing queues/public APIs. Do not make post-initialization wolfSSH calls, mutate display frames, or run console handlers from arbitrary tasks.
**Relevant files:** `src/serial_service.c`, `src/local_display.c`, `src/ssh_transport.c`, `src/admin_ssh_console.c`
## Software cryptography settings are a validated concurrency workaround
**Decision:** wolfSSL ESP32 AES/SHA acceleration is disabled, and HTTPS uses software AES for PSRAM-backed TLS records. Internal task stacks are retained where cache-disable safety matters.
**Rationale/evidence:** Root `CMakeLists.txt` disables wolfSSL hardware crypto. The roadmap reports a reproduced watchdog stall involving mbedTLS external-RAM hardware-AES DMA, uncoordinated mbedTLS/wolfSSL hardware locks, and a successful software-crypto concurrency retest; no standalone execution record is checked in.
**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
## Embedded web assets are checked-in generated artifacts
**Decision:** Vendored xterm assets are compressed and embedded ahead of the normal firmware build; `src/web_assets_data.c` is compiled directly.
**Rationale/evidence:** `src/CMakeLists.txt` lists generated data as a source, and `web_assets/SOURCES.md` documents pinned versions, hashes, and deterministic gzip inputs.
**Consequence for future changes:** Edit authored web UI separately. Changes to its inline bootstrap loader must update the hard-coded CSP hash atomically and preserve the response security policy. When dependency assets change, follow the documented provenance/generation process and review generated diffs; do not hand-edit arrays or regenerate assets during unrelated work.
**Relevant files:** `web_assets/SOURCES.md`, `web_assets/generate_embedded_assets.py`, `src/web_assets_data.{h,c}`, `src/web_ui.c`
+96 -16
View File
@@ -1,14 +1,58 @@
# Command reference # Command reference
Use these commands from the UART0 `serial-tool>` administration console. Run `help` for the available root commands and `<group> help` for a group summary. Configuration changes are RAM-only unless explicitly saved. 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.
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.
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.
## System ## System
| Command | Description | | Command | Description |
|---|---| |---|---|
| `memory` | Show free memory, minimum free memory, and largest blocks for internal RAM, DMA-capable RAM, and PSRAM. | | `memory` | Show free memory, minimum free memory, and largest blocks for internal RAM, DMA-capable RAM, and PSRAM. |
| `reboot` | Drain console output briefly and restart the ESP32. | | `reboot` | Drain console output briefly and restart the ESP32; refused while a firmware upload or another reboot owns exclusion. |
| `status` | Show quick MAX3243 signal state. | | `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.
## Role-based users
| Command | Description |
|---|---|
| `user status` / `user list` | Show database generation, capacity, administrator count, and all secret-free account summaries. |
| `user show <username>` | Show one account's role, ID, authentication generation, and SSH-key fingerprints. |
| `user add <username> <user|admin>` | Create an account using a bounded no-echo password and confirmation prompt. |
| `user add <username> <user|admin> --generate` | Create an account with a generated password displayed once. |
| `user delete <username> --force` | Delete an account; the final administrator is protected. |
| `user role <username> <user|admin> --force` | Change a role; the final administrator cannot be demoted. |
| `user password <username>` | Set and confirm a new password without echo. |
| `user password <username> --generate` | Replace a password with a generated value displayed once. |
| `user key add <username>` | Prompt on UART0 or authenticated admin SSH for one bounded OpenSSH public-key line. |
| `user key add <username> <type> <base64>` | Import a key non-interactively; intended for authenticated admin SSH and also accepted on UART0. |
| `user key delete <username> <0..2> --force` | Delete one key by the index shown by `user show`. |
| `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 1264 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.
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.
`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.
## Local display
| Command | Description |
|---|---|
| `display status` | Show the runtime aging settings and OLED service state. |
| `display set dim-seconds <0..86400>` | Set the RAM inactivity delay before contrast drops to `1`; `0` disables dimming. |
| `display set off-seconds <0..86400>` | Set the RAM inactivity delay before the OLED switches off; `0` disables automatic off. |
| `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.
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).
## Serial service ## Serial service
@@ -28,7 +72,7 @@ Defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control,
| Command | Description | | Command | Description |
|---|---| |---|---|
| `broker status` / `broker clients` | Show broker state or connected clients. | | `broker status` / `broker clients` | Show broker state or connected clients. |
| `broker counters` / `broker clear-counters` | Show or clear broker counters. | | `broker counters` / `broker clear-counters` | Show global totals plus active-client ID/type/pending/HWM/UART/queued/read/dropped; clear counters and seed HWM from current pending bytes without draining output. |
| `broker connect <name>` / `broker disconnect <client-id>` | Create or remove a console test client. | | `broker connect <name>` / `broker disconnect <client-id>` | Create or remove a console test client. |
| `broker request-writer <client-id>` / `broker release-writer <client-id>` | Request or relinquish the single writer lease. | | `broker request-writer <client-id>` / `broker release-writer <client-id>` | Request or relinquish the single writer lease. |
| `broker force-writer <client-id|none>` | Administratively assign or clear the writer lease. | | `broker force-writer <client-id|none>` | Administratively assign or clear the writer lease. |
@@ -36,7 +80,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 read <client-id> [maximum-bytes]` | Read queued serial output for a client. |
| `broker events <client-id>` | Show ownership and connection events 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. 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).
## Native USB CDC-ACM ## Native USB CDC-ACM
@@ -47,7 +91,7 @@ Each client has a generation-safe ID. There can be one writer and multiple obser
| `usb counters` / `usb clear-counters` | Show or clear USB counters. | | `usb counters` / `usb clear-counters` | Show or clear USB counters. |
| `usb request-writer` / `usb release-writer` | Request or release USB writer ownership. | | `usb request-writer` / `usb release-writer` | Request or release USB writer ownership. |
Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, starts UART1 if needed, and requests writer ownership. It becomes an observer if another client is writer. USB data is binary-transparent. Supported host line coding is 1101000000 baud, 7/8 data bits, none/odd/even parity, and 1/2 stop bits; settings apply only while USB owns the writer lease and are not saved automatically. Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, starts UART1 if needed, and requests writer ownership. It becomes an observer if another client is writer. USB data is binary-transparent. The host's CDC line coding is shown by `usb status` for diagnostics only; it does not alter UART1. Configure physical baud rate, framing, flow control, and DTR explicitly with `serial` commands and persist them with `serial save`.
## Wi-Fi ## Wi-Fi
@@ -55,6 +99,7 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|---|---| |---|---|
| `wifi status` / `wifi profiles` | Show Wi-Fi state or configured station profiles. | | `wifi status` / `wifi profiles` | Show Wi-Fi state or configured station profiles. |
| `wifi start` / `wifi stop` / `wifi reconnect` | Start, stop, or reconnect Wi-Fi. | | `wifi start` / `wifi stop` / `wifi reconnect` | Start, stop, or reconnect Wi-Fi. |
| `wifi next-profile` | Queue a switch to the enabled station profile after the currently active profile in priority order; wraps safely. |
| `wifi profile set <slot> <priority> <mixed|wpa3> <ssid>` | Set a station profile. | | `wifi profile set <slot> <priority> <mixed|wpa3> <ssid>` | Set a station profile. |
| `wifi profile secret <slot>` | Set a profile password through a no-echo prompt. | | `wifi profile secret <slot>` | Set a profile password through a no-echo prompt. |
| `wifi profile enable|disable|delete <slot>` | Manage a station-profile slot. | | `wifi profile enable|disable|delete <slot>` | Manage a station-profile slot. |
@@ -67,7 +112,20 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
| `wifi nslookup <host>` | Resolve and display unique IPv4/IPv6 addresses. | | `wifi nslookup <host>` | Resolve and display unique IPv4/IPv6 addresses. |
| `wifi traceroute <host> [max-hops]` | Run IPv4 ICMP traceroute with up to 30 hops. | | `wifi traceroute <host> [max-hops]` | Run IPv4 ICMP traceroute with up to 30 hops. |
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Passwords are not displayed by ordinary status output. `ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. Edits to a disabled profile's SSID, priority, security mode, or secret are staged in RAM and do not interrupt the current Wi-Fi connection. Enabling or disabling a profile, changing an enabled profile, or changing AP policy/configuration applies the new radio policy and may reconnect Wi-Fi. Use `wifi save` to persist working changes. Passwords are not displayed by ordinary status output.
## mDNS
| Command | Description |
|---|---|
| `mdns status` | Show the configured `sak-<suffix>.local` hostname and announcement state. |
| `mdns suffix <value>` | Set a 155-character lowercase hostname suffix in RAM. |
| `mdns save` / `mdns load` | Save the working suffix to its independent NVS record or load it. |
| `mdns defaults` / `mdns reset` | Restore the MAC-derived suffix in RAM, or restore and persist it. |
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. Existing ping selects the first usable resolver result without family racing, and traceroute remains IPv4-only. 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 pending device validation.
## HTTPS web terminal ## HTTPS web terminal
@@ -76,14 +134,16 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
| `web` / `web help` | Show web-service command usage. | | `web` / `web help` | Show web-service command usage. |
| `web status` | Show HTTPS and WebSocket state. | | `web status` | Show HTTPS and WebSocket state. |
| `web start` / `web stop` | Start or stop HTTPS service. | | `web start` / `web stop` | Start or stop HTTPS service. |
| `web counters` / `web clear-counters` | Show or clear web counters. | | `web counters` / `web clear-counters` | Show or clear ordinary HTTPS/serial WebSocket counters, independently of performance capture. |
| `web credentials show` | Display the physical-console-only shared admin credentials. | | `web diagnostics enable\|disable\|show\|clear` | Independent opt-in admission tracing and post-TLS occupancy; not serial throughput timing. |
| `web credentials rotate --force` | Replace credentials and disconnect web and SSH sessions. | | `web performance enable\|disable\|show\|clear` | Default-disabled per-slot binary TX timing/count aggregates. Disable freezes; enable resumes; clear preserves enabled state. Each control operation fences in-flight samples with a new epoch. All actions print a snapshot. |
| `web certificate info` | Display certificate identity and fingerprint. | | `web certificate info` | Display certificate identity and fingerprint. |
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. | | `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 missing, incompatible, or damaged web material. | | `web reset --force` | Explicitly replace HTTPS certificate/private key only, including missing, incompatible or damaged material; never changes users. |
HTTPS listens on port 443 only. Authenticate to `https://<device-address>/` as `admin` with the credential shown on UART0. The device serves its vendored xterm.js terminal without Internet access. Browser sessions use binary WebSocket frames and follow the broker's one-writer rule. 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.
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.
## SSH serial transport ## SSH serial transport
@@ -92,19 +152,26 @@ HTTPS listens on port 443 only. Authenticate to `https://<device-address>/` as `
| `ssh` / `ssh help` | Show SSH command usage. | | `ssh` / `ssh help` | Show SSH command usage. |
| `ssh status` | Show service state and resource information. | | `ssh status` | Show service state and resource information. |
| `ssh start` / `ssh stop` | Start or stop the SSH server. | | `ssh start` / `ssh stop` | Start or stop the SSH server. |
| `ssh sessions` | List active SSH sessions and broker roles. | | `ssh sessions` | List active SSH sessions with account, user role, authentication method, route, broker role where applicable, and admin-worker state. |
| `ssh disconnect <session-id>` | Disconnect one SSH session. | | `ssh disconnect <session-id>` | Disconnect one SSH session. |
| `ssh counters` / `ssh clear-counters` | Show or clear SSH counters. | | `ssh counters` / `ssh clear-counters` | Show or clear SSH counters. |
| `ssh host-key info` | Display the OpenSSH host-key fingerprint. | | `ssh host-key info` | Display the OpenSSH host-key fingerprint. |
| `ssh host-key rotate --force` | Replace the persistent SSH host key. | | `ssh host-key rotate --force` | Replace the persistent SSH host key. |
| `ssh reset --force` | Explicitly replace invalid or missing SSH material. | | `ssh reset --force` | Explicitly replace invalid or missing SSH material. |
SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a separate host key. It accepts password-authenticated interactive shell/PTY sessions only; it does not provide public-key authentication, `exec`, SFTP, SCP, forwarding, or subsystems. Verify the fingerprint from `ssh host-key info` before accepting an SSH host key. 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.
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 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.
`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.
## Hardware diagnostics ## Hardware diagnostics
| Command | Description | | Command | Description |
|---|---| |---|---|
| `debug status` | Show MAX3243 driver, receiver, VLD, and shutdown states. It requires UART1 to be stopped. |
| `debug transceiver <enable|disable>` | Enable or shut down the MAX3243. | | `debug transceiver <enable|disable>` | Enable or shut down the MAX3243. |
| `debug drivers <tx 0|1> <dtr 0|1> <rts 0|1>` | Drive static TX, DTR, and RTS logic levels for measurement. | | `debug drivers <tx 0|1> <dtr 0|1> <rts 0|1>` | Drive static TX, DTR, and RTS logic levels for measurement. |
| `debug loopback-a` / `debug loopback-b` | Test MAX3243 driver/receiver loopback configurations. | | `debug loopback-a` / `debug loopback-b` | Test MAX3243 driver/receiver loopback configurations. |
@@ -112,5 +179,18 @@ SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a sep
| `debug uart-loopback <baud> [8N1|8E1|8O1|8N2|7E1|7O1] [bytes]` | Run a parameterized UART loopback test. | | `debug uart-loopback <baud> [8N1|8E1|8O1|8N2|7E1|7O1] [bytes]` | Run a parameterized UART loopback test. |
| `debug uart-suite` | Test supported baud rates and frame formats. | | `debug uart-suite` | Test supported baud rates and frame formats. |
| `debug cts-flow-test` / `debug rts-flow-test` | Verify hardware transmit gating or receive backpressure. | | `debug cts-flow-test` / `debug rts-flow-test` | Verify hardware transmit gating or receive backpressure. |
| `debug display status` | Show the current display diagnostic state. |
| `debug display probe` | Probe the expected OLED addresses 7-bit `0x3c` and `0x3d`, initially using 100 kHz I²C. The tested module responds at `0x3c`. |
| `debug display scan --force` | Scan usable 7-bit addresses `0x08``0x77` at 100 kHz; use only on this dedicated local-UI bus. |
| `debug display init [address]` | Initialize the OLED at 7-bit `0x3c`/`0x3d`, or their 8-bit write/read aliases: `0x78`/`0x79` and `0x7a`/`0x7b`. |
| `debug display off` | Turn off the initialized OLED. |
| `debug display pattern <clear|fill|checker|grid|corners|layout>` | Draw a full-screen electrical and geometry test pattern; `layout` renders separate status- and content-panel text. |
| `debug display row <0..63>` | Draw the selected one-pixel display row for addressing and color-boundary checks. |
| `debug display contrast <0..255>` | Set the OLED contrast to the specified bounded value. |
| `debug display invert <on|off>` | Enable or disable OLED pixel inversion. |
| `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 130 seconds; the default is 10 seconds. |
Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. Diagnostics refuse to use UART1 until `serial stop` releases it. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed. 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).
+187 -3
View File
@@ -1,8 +1,192 @@
# Electrical tests # Electrical tests
These procedures verify the MAX3243 breakout, UART1 data path, hardware flow control, and session broker. They are manual tests: the firmware never starts one automatically. These manual procedures verify the OLED and buttons, MAX3243 breakout, UART1 data path, hardware flow control, and session broker. The firmware does not start diagnostic commands automatically, but normal boot does initialize the OLED, play the bounded identity animation, and start the local status/control UI when the hardware responds.
> **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. > **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
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.
### 1. Power-off wiring checks
Disconnect both USB connectors and every other power source before checking or changing wiring.
1. Confirm OLED `VCC` goes only to `3V3`, OLED `GND` goes to `GND`, SDA goes to GPIO11, and SCL goes to GPIO12.
2. Check for an unintended short between `3V3` and `GND`, and verify ground continuity between the OLED and ESP32 board.
3. Determine whether the OLED module has SDA/SCL pull-ups and verify that any module-mounted or external pull-ups terminate at 3.3 V, never 5 V. Add suitable external pull-ups to `3V3` only if the module does not provide them; account for parallel resistance if more than one set is fitted.
4. Confirm each button is wired between its input and `GND`: previous/back GPIO10, select/confirm GPIO13, and next GPIO14. With a meter, each button should be open when released and near zero ohms to `GND` when pressed.
5. Check that no button shorts two GPIOs together and that SDA and SCL are not swapped or shorted.
### 2. Powered idle checks and address probe
Apply power and allow normal boot to complete. A connected, responsive OLED is normally initialized automatically and may already show the status UI.
1. Measure OLED `VCC` relative to `GND`; it should be approximately 3.3 V.
2. Measure idle SDA on GPIO11 and idle SCL on GPIO12. Both should be near 3.3 V. Power down immediately if either bus line rises toward 5 V; correct the OLED supply or pull-up wiring before continuing.
3. Run `debug display status` and record whether normal boot initialized the display.
4. Run `debug display probe`. Confirm that it tests only 7-bit `0x3c` and `0x3d` at the initial 100 kHz bus rate. The tested module should acknowledge at `0x3c` (8-bit `0x78` write / `0x79` read).
If the expected address does not respond, treat the result as a nonfatal hardware finding. On this dedicated local-UI bus, `debug display scan --force` may identify an unexpected address before further investigation. Otherwise leave the serial core running, power down, and recheck 3.3 V power, common ground, SDA/SCL order, solder joints, and pull-ups. Do not scan a bus shared with unrelated I²C devices.
### 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):
```text
debug display init 0x3c
```
Then run:
```text
debug display pattern clear
debug display pattern fill
debug display pattern checker
debug display pattern grid
debug display pattern corners
```
Confirm that clear and fill affect the full 128×64 area, checker and grid have regular spacing without shifted or wrapped columns, and all four corner markers are visible in the correct locations. Display diagnostics pause the periodic status UI for 30 seconds so the selected pattern remains observable. Record any unexpected mirroring, rotation, clipping, or column offset for correction.
### 4. Rendered status/content layout
Run the Phase 7B panel-layout pattern:
```text
debug display pattern layout
```
Confirm that the status text is entirely in the yellow 128×16 panel (rows 015), content text is entirely in the blue 128×48 panel (rows 1663), and the physical black divider between them remains visible. Record any text crossing the divider, clipping, incorrect panel color, or divider obscuration.
### 5. Row 15/16 color-boundary test
Clear the display, illuminate row 15, and record its physical color and position:
```text
debug display pattern clear
debug display row 15
```
Repeat for row 16:
```text
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.
### 6. Contrast, inversion, and display-off checks
With a visible pattern loaded, exercise the bounded contrast range and confirm that brightness changes without bus errors. On the validated module, the useful visible brightness range begins at `1` and extends through `255`:
```text
debug display contrast 0
debug display contrast 64
debug display contrast 128
debug display contrast 255
```
Then verify inversion toggles all displayed pixels and can be restored:
```text
debug display invert on
debug display invert off
```
Finally run `debug display off` and confirm the panel turns off cleanly. Use `debug display status`, then `debug display init 0x3c` (or equivalently `0x78` or `0x79`) before further display tests.
### 7. Button checks
With all buttons released, run `debug buttons status`. Confirm previous/back GPIO10, select/confirm GPIO13, and next GPIO14 report released/high due to their internal pull-ups; each should report pressed/low while held to `GND`.
Run `debug buttons test` for the default 10-second interval. During the test, press and release each button separately with a deliberate short press, then repeat with a sustained long press. Confirm that the correct button and short/long classification are reported exactly once per intended action.
> **Live-UI caution:** The normal local status/control task continues to process button input during this diagnostic. Begin on a status page, watch the OLED, and cancel any Controls confirmation that opens. Do not leave a disruptive action selected while testing long holds.
Repeat with an explicit duration, for example:
```text
debug buttons test 30
```
Use the longer run to check:
- **Debounce:** press with normal switch bounce and make several deliberately quick taps; one physical press must not produce a burst of duplicate press/release or short/long events.
- **Long press:** hold each button long enough for the diagnostic to classify it as long, then release it; it must not also create an unintended short-press action.
- **Stuck button:** hold one button before starting the test and keep it held. The input must remain identified as pressed/stuck without blocking checks of the other buttons, and the bounded diagnostic must still exit after the selected duration.
- **Recovery:** release the held button and confirm `debug buttons status` returns to released/high without a reboot.
`debug buttons test [seconds]` accepts 1 through 30 seconds and defaults to 10 seconds when omitted. Record unexpected event duplication, missed transitions, incorrect GPIO mapping, false long presses, or a test that fails to terminate.
### 8. Status pages
After boot, the OLED starts on the **OVERVIEW** page. A short previous/back press on GPIO10 and a short next press on GPIO14 must wrap through these status pages:
1. **OVERVIEW** — serial, broker, USB, HTTPS/WebSocket, SSH, and Wi-Fi summary.
2. **RS232 MODEM** — framing, modem inputs, byte counters, queue depth, drops, and faults.
3. **BROKER** — connected clients, current writer marker, pending output, and drop/event counters.
4. **NETWORK SERVICES** — Wi-Fi state/RSSI/IP/AP state and HTTPS, WebSocket, and SSH service state.
Confirm that each page remains entirely within the blue content panel. The yellow panel must retain the same fixed icon positions while pages change: serial, segmented Wi-Fi strength, USB, WebSocket, SSH, broker clients, and alert. Active icons are solid with a value below; inactive icon slots remain completely blank and omit their value. Confirm that page text uses readable upper/lowercase, separates items and values with colons, and displays the active station SSID or AP SSID with its channel on the Overview and Network pages.
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:
1. At five minutes, display contrast drops to `1` while the contents remain present.
2. At ten minutes, the OLED switches off.
3. The first debounced press of any button wakes the OLED at contrast `127` without changing the current page.
4. A subsequent previous/next press navigates normally and restarts the inactivity timers.
`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)
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.
Stopping a service, Wi-Fi reconnect, Wi-Fi next-profile, writer revocation, and reboot open a confirmation page. Verify that:
1. Previous/back cancels the confirmation without changing the selected service or writer.
2. A short Select press does not execute the action.
3. Only one continuous two-second Select hold executes the action once; releasing it does not repeat the action.
4. Any simultaneous button chord is ignored and cannot confirm an action.
5. An untouched confirmation expires to the menu after 30 seconds.
6. A dimmed/off OLED consumes the first button press for wake only; its later release and hold must not navigate or confirm an action.
For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then use the status pages to observe the asynchronous state change. For `WiFi:next profile`, configure at least two enabled station profiles, cancel once with Previous/back, then hold Select for two seconds; verify the current station disconnects, the next enabled profile is attempted, and the Overview/Network `WiFi:<SSID> Ch:<channel>` value (or `wifi status`) changes. Repeat until the selection wraps to the first profile. HTTPS and SSH starts must fail cleanly when neither station nor AP networking is available. Writer revocation must only release the current writer—never assign a replacement—and the UI must never appear as a broker client. For reboot, observe `Restarting...`, then verify all normal boot services and UART0 recovery return.
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
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:
```text
display status
display set dim-seconds 5
display set off-seconds 10
display save
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:
| Fault/stress | Procedure | Required result |
|---|---|---|
| OLED absent at boot | Power down, remove the OLED, then boot. | UART0, serial, USB, Wi-Fi, HTTPS/WebSocket, and SSH startup remain independent; no reset or probe loop occurs. |
| Reattach | Power down before reconnecting loose wiring, boot, and press one button. If using a connector explicitly suitable for live removal, avoid shorts and reconnect before pressing. | One bounded reprobe initializes the panel; the wake press does not navigate or execute an action. |
| NACK/timeout | With safe test wiring, interrupt the display during refresh or hold one bus line low briefly, then release it. | One bounded frame fails and marks the OLED unavailable; no watchdog reset, repeated log flood, serial loss, or broker ownership change occurs. A later new press can recover after the bus is healthy. |
| Stuck button | Hold each button continuously for at least ten seconds, then operate each of the other buttons. | The held input is quarantined, the other controls remain usable, and no action repeats. Release and debounce the held input; it must rearm without rebooting. |
| 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.
## Configuration A: data and handshake pairs ## Configuration A: data and handshake pairs
@@ -92,7 +276,7 @@ Each output should be negative. Exact voltages depend on supply, load, meter, an
## Recommended order ## Recommended order
1. With no DE-9 jumpers, run `status`. 1. With no DE-9 jumpers and after `serial stop`, run `debug status`.
2. Measure output polarity with `debug drivers 0 0 0` and `debug drivers 1 1 1`. 2. Measure output polarity with `debug drivers 0 0 0` and `debug drivers 1 1 1`.
3. Power down, install configuration A, power up, and run `debug loopback-a`. 3. Power down, install configuration A, power up, and run `debug loopback-a`.
4. Run `debug valid-test` with configuration A still installed. 4. Run `debug valid-test` with configuration A still installed.
+65
View File
@@ -0,0 +1,65 @@
# Bounded ordinary HTTPS idle retention
Current owner/SDK contract. The user accepted idle cleanup as working; [overall acceptance](roadmap.md#phase8-acceptance-evidence) records current status and evidence limits. No individual unreported soak, fault or reserve result is inferred.
## Evidence and scope
User-authorized response to the admission capture: post-TLS occupancy **6/6, ordinary4 / serial2 / admin0**, ordinary connection ages **5074 seconds**; admin ticket returned in **14 ms**, no subsequent TLS/upgrade observed, ticket unconsumed. These ages measure connection lifetime, not time since the last request. They support investigating retained ordinary sockets, not claiming every captured socket was idle. Installed HTTPD excludes its listening fd from `select` while full with LRU disabled. New connections can therefore wait before TLS or upgrade even when ticket issuance was fast.
This policy releases **expired idle ordinary HTTPS connections**, not arbitrary ordinary requests to make room. The policy uses the existing six sockets, LRU-disabled operation, one-second receive/send and five-second TLS-handshake timeouts without expanding task stacks or queue capacities. Current overall handler capacity is 39. No new task. UART0 recovery, independent USB access, serial broker ownership, tickets, authentication and both WebSocket transports are unchanged.
## Usage and timeout semantics
- Automatically enabled for every successfully started HTTPS server, including when diagnostic tracing is disabled and either optional transport is unavailable. No new console command, setting or credential migration.
- **15 seconds of observed ordinary idle retention**, checked by a **one-second ESP timer** that requests work on HTTPD. The three existing five-second browser status-poll intervals leave room for normal keepalive reuse instead of a TLS handshake per request.
- A new post-TLS connection that has sent no request gets the same full idle window. Time spent establishing TLS does not consume it.
- The first owner sweep observing a new connection or a changed successful-request completion marker starts a fresh window. The marker covers every ordinary route, including login, assets, status, typed settings, tickets and keepalive errors that return successfully—not just diagnostic wrappers. Failed requests are deleted by the normal SDK path instead.
- HTTPD pending bytes, TLS pending bytes or a readable TCP fd reset the observation window. Negative TLS pending results and `select` errors conservatively reset it too. No bytes are read or discarded by the probe.
- At the threshold, after current SDK classification and zero-time readiness checks, HTTPD calls `shutdown(current_fd, SHUT_RDWR)`. A successful shutdown is latched; a failed call retries on the next probe. The SDK's subsequent read/delete path owns socket close, TLS destruction, diagnostic close notification and freeing the slot. The probe never calls `close`, overrides TLS cleanup, or queues a session-close pointer.
- **WebSockets (serial and admin), closing WebSockets and async requests are exempt.** The check uses actual SDK flags, not diagnostic metadata. Successful explicit 101/classification and request cleanup finish before the owner can sweep, so there is no ordinary-idle interval during upgrade admission.
- Under an available owner and timely successful work delivery, expect 15 seconds plus the initial observation delay (nominally up to one second), the next probe delay (nominally up to one second), and SDK read/cleanup latency. This is a conservative sampled idle policy, **not a strict wall-clock timeout or admission SLA**.
### Deliberate limitations
1. Parsing an incomplete request, synchronous response sends, leftover-body purge and TLS handshakes serialize on HTTPD. The probe cannot interrupt them. Existing per-read/per-send timeouts remain; a peer trickling input can extend overall processing beyond one timeout. This change does not provide a slowloris deadline or solve TLS/memory/global-socket pressure.
2. Four continuously active ordinary connections plus two WebSockets can still fill all six slots. They are not evicted. Likewise, an admission attempt immediately after fresh saturation can hit the browser's existing 15-second timeout before the conservative window expires; the user may still need to retry. Older idle retained sockets are eligible on the next delivered probe.
3. As with any HTTP keepalive timeout, bytes arriving **after** the last readiness check can race a shutdown. Already executing HTTPD requests/responses are protected; future client intent cannot be predicted. A client may need a new connection. No new application-level mutation retry/replay is added. Response completion here means synchronous HTTPD completion, not proof of peer receipt/TCP acknowledgement.
4. `httpd_queue_work` with `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` disabled uses loopback UDP. A reported queue error releases the reservation and retries next tick. **A successful send is not an execution acknowledgement**: an accepted-but-lost control message leaves one reservation pending, disables further probes, and requires successful HTTPS stop/restart to restore probing. There is no speculative reservation timeout: it could accumulate delayed callbacks and violate the one-probe bound. This is explicitly regression-tested, not hidden behind a hard retention guarantee. HTTPD blockage or queue loss also cannot be repaired by raising sockets/LRU/timeouts here.
5. A failed stop leaves cleanup detached and ownership retained for a later stop retry; it does not restart probes on a partially stopped server. Timer allocation/start failure gates HTTPS start with its error rather than silently starting without the policy. UART0/USB recovery is unaffected.
## Exact installed SDK audit
All SDK references below are under `~/.platformio/packages/framework-espidf/components/`; installed framework is ESP-IDF **5.5.0** (`3.50500.0`). Production private access remains solely in `src/web_httpd_adapter.c`, with the existing compile-time version guard extended to require idle-lifecycle re-audit.
- `esp_http_server/src/httpd_main.c`: `httpd_server` selects the listener only with capacity or LRU enabled. Control work runs **before** current data sessions, then accept. `httpd_process_session` skips async sessions and synchronously runs `httpd_sess_process`; errors delete the session before any subsequent owner work. Accept invokes TLS synchronously through `open_fn`.
- `esp_http_server/src/httpd_sess.c`: `httpd_sess_process` calls `httpd_req_new`, then `httpd_req_delete`, and only after both return success assigns `session->lru_counter = ++hd->lru_counter`. This happens even with LRU disabled. New sessions zero the slot, including the marker; the global counter resets when all sessions are deleted. Application calls to `httpd_sess_update_lru_counter` are confined to verified serial-WebSocket send work (`web_serial_transport.c`), which the sweep exempts. This marker is not a timestamp and not an fd-generation token.
- `esp_http_server/src/httpd_parse.c`: `httpd_req_new` synchronously parses and invokes the URI handler. `httpd_req_delete` drains any remaining body; cleanup clears `hd_req_aux.sd` and request pointers. A return from a diagnostic handler wrapper or a response-send call is earlier than this boundary. The sweep requires HTTPD's thread identity and no current `hd_req_aux.sd`, and skips `for_async_req`. Current ordinary handlers do not use async requests, out-of-owner sends, or unfinished chunked responses. Re-audit that contract if introduced.
- `esp_http_server/include/esp_http_server.h` has no global synchronous post-request-cleanup hook. Its event notifications are not such a hook: `esp_http_server_dispatch_event` posts to the event loop. `HTTP_SERVER_EVENT_SENT_DATA` is emitted by `httpd_resp_send` and per `httpd_resp_send_chunk`, **before** handler return/body purge/cleanup. Send/receive overrides belong to HTTPS and do not expose a safe completion hook. URI matching and error handlers likewise cannot supply an all-route post-cleanup boundary.
- `esp_https_server/src/https_server.c`: successful `httpd_ssl_open` stores the transport context and installs TLS send/recv/pending functions before synchronous `HTTPD_SSL_USER_CB_SESS_CREATE`. The application callback invalidates any old row for that fd even if the TLS pointer, socket-slot address and counter value were reused. If fd lookup unexpectedly fails, all observations reset conservatively. The close callback remains the diagnostic observer; SDK destruction remains intact. `httpd_ssl_pending` calls `esp_tls_get_bytes_avail` without consuming data; errors can post an existing SDK error event.
- `httpd_sess_trigger_close` resolves fd to a raw reusable `sock_db *`, then queues `httpd_sess_close`. Its zero-counter/LRU guard does not prove the same connection still occupies that slot. **Not used by this policy.** Direct owner shutdown retains the slot until SDK read cleanup and has no deferred fd/pointer argument that could later close a replacement.
- `httpd_queue_work` uses `cs_send_to_ctrl_sock` / `sendto` in `esp_http_server/src/util/ctrl_sock.c`. The actual generated config leaves `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` undefined/off, selecting the nonblocking queue mode. The idle initializer explicitly rejects builds with that blocking option on. Successful `httpd_stop` waits for `THREAD_STOPPED`, frees HTTPD and ends possible old callback execution; failed stop is not a retirement boundary.
## Ownership and bounded storage
`src/web_httpd_idle.{c,h}` owns one persistent ESP timer, six static observation rows, lifecycle gate/generation and queued/submitting flags. It uses no request data, secret, dynamic per-connection allocation, payload buffer, new task or additional socket. Timer callback performs only short metadata locking and at most one queue submission; all private session access, readiness and shutdown run on HTTPD.
`src/web_server.c` serializes lifecycle. Prepare initializes rows **before** SSL startup; the TLS callback and sweep thereafter share the same owner. Attach publishes a nonzero, nonwrapping `uintptr_t` server generation, passed by value as opaque work argument—not a mutable shared descriptor or raw fd. At most one queued/executing probe is reserved; a separate submitting flag remains set until `httpd_queue_work` returns even if work already finished. That closes the callback-before-submit-return race.
Detach first prevents submissions, then waits at most one second for any submitting call to return. Fence timeout forbids SSL destruction, retaining the handle for retry. An already executing sweep may finish safely while stop waits for HTTPD. Only successful SDK stop retires a discarded queued reservation; restart gets a new generation even if the server handle is reused. A stale generation cannot sweep or clear a newer reservation. No counter wrap or generic off-owner session-list query is accepted.
`src/web_httpd_adapter.{c,h}` defines the six-row bound and 15-second policy and performs the version-pinned owner sweep. `src/CMakeLists.txt` adds only the new module. Diagnostics remains unchanged internally; server composes idle identity reset followed by existing diagnostic publication.
## Regression and resource limits
`tests/web_httpd_idle/run.py` exercises production lifecycle/sweep paths, installed SDK request cleanup, host socketpair/readiness/shutdown/fd reuse and deterministic TLS/timer/queue doubles. `tests/web_admin_transport/server_lifecycle.py` checks server composition. Historical host validation passed; these references do not claim a new run or real target scheduler/TLS timing.
Bounded storage: six observation rows (144 bytes), one persistent timer (installed non-profiled layout 32 bytes before allocator overhead), lifecycle/generation flags and one reserved probe. SDK control UDP/mailbox allocation is transient and separate. No per-connection payload/task/stack/socket growth. Actual HTTPD/timer margins and runtime overhead/reserve floors require target measurement, not host sizes. See [latest firmware resources](roadmap.md#phase8-acceptance-evidence).
## Regression procedure — not execution evidence
1. Start with diagnostic capture disabled: establish two serial WebSockets and ordinary HTTPS fetches. Verify idle ordinary sockets disappear after the observation window while both serial clients/lease remain unchanged. Enable capture only as needed to compare close/open occupancy; ages remain connection ages, not idle timestamps.
2. Reproduce the original ordinary4/serial2 full-slot case; wait beyond the idle window, then issue/open admin. Record client ticket/TLS/upgrade timings and occupancy without recording tickets/cookies. Verify no repeat reload loop is needed for already-old idle saturation. Separately test fresh saturation and acknowledge the existing 15-second browser timeout limit.
3. Leave status polling active for several minutes: no five-second TLS reconnect churn; both WebSockets and binary serial data/broker isolation survive. Repeat Settings/account-key operations, large assets, login/logout and two browser contexts. Compare with polling paused to distinguish genuinely idle slots.
4. Slow incomplete headers/bodies, pipelined requests, slow response readers and slow/failed TLS handshakes: no probe-driven close of an executing response or admitted WebSocket. Record owner delays; do not infer an overall request deadline from the unchanged one-second receive timeout.
5. Repeated close/reopen/fd reuse, full-mix stop/start and certificate rotation via supported UART0/SSH/browser lifecycle paths. Confirm UART0 and USB remain available, queued work never affects replacement connections, failed-stop retries retain ownership, and no start allocates a second server.
6. Capture settled/loaded/post-cleanup internal/DMA/PSRAM values and HTTPD/ESP-timer minimum-free stack. Soak at the accepted full client mix. Investigate control queue loss separately if probing appears stuck; successful stop/start is the safe recovery, not an eviction/capacity increase.
+87
View File
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="620" viewBox="0 0 960 620" role="img" aria-labelledby="title description">
<title id="title">Phase 7 dual-color OLED overview-screen mockup</title>
<desc id="description">An enlarged mockup of the verified 128 by 64 pixel local status display. The upper sixteen yellow rows and lower forty-eight blue rows are separate rendered panels, divided by a narrow physical black separator. The yellow panel shows service, Wi-Fi, client, writer, and alert status. The blue panel shows IP address, RS-232 configuration, modem signals, traffic counters, and page navigation. Three buttons below are labeled previous, OK, and next.</desc>
<defs>
<filter id="yellowGlow" x="-20%" y="-40%" width="140%" height="180%">
<feGaussianBlur stdDeviation="1.7" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<filter id="blueGlow" x="-20%" y="-40%" width="140%" height="180%">
<feGaussianBlur stdDeviation="1.5" result="blur"/>
<feMerge><feMergeNode in="blur"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<linearGradient id="bezel" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#253747"/>
<stop offset="0.5" stop-color="#14212c"/>
<stop offset="1" stop-color="#091017"/>
</linearGradient>
<radialGradient id="button" cx="42%" cy="35%" r="70%">
<stop offset="0" stop-color="#566777"/>
<stop offset="0.65" stop-color="#273440"/>
<stop offset="1" stop-color="#111920"/>
</radialGradient>
<pattern id="pixels" width="5" height="5" patternUnits="userSpaceOnUse">
<path d="M5 0H0V5" fill="none" stroke="#ffffff" stroke-opacity="0.022" stroke-width="0.45"/>
</pattern>
</defs>
<rect width="960" height="620" fill="#0b1118"/>
<text x="480" y="35" fill="#d7e3ed" font-family="sans-serif" font-size="20" font-weight="600" text-anchor="middle">Phase 7 — 128×64 local status display</text>
<rect x="55" y="55" width="850" height="470" rx="25" fill="url(#bezel)" stroke="#456074" stroke-width="3"/>
<circle cx="82" cy="82" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="878" cy="82" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="82" cy="498" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<circle cx="878" cy="498" r="9" fill="#081016" stroke="#667b8b" stroke-width="2"/>
<!-- The screen is exactly 2:1. Its 320-pixel height represents 64 rows at 5x scale. -->
<rect x="160" y="95" width="640" height="320" rx="3" fill="#010406" stroke="#718696" stroke-width="2"/>
<rect x="160" y="95" width="640" height="80" fill="#1a1600"/>
<rect x="160" y="175" width="640" height="240" fill="#00121a"/>
<rect x="160" y="95" width="640" height="320" fill="url(#pixels)"/>
<line x1="160" y1="175" x2="800" y2="175" stroke="#77848c" stroke-opacity="0.38"/>
<g font-family="DejaVu Sans Mono, Liberation Mono, Consolas, monospace" font-size="27" font-weight="600" letter-spacing="0.2">
<g fill="#ffd52e" filter="url(#yellowGlow)">
<text x="177" y="131">SER● WIFI▮▮▮ U1 W1 S1</text>
<text x="177" y="168">WR:SSH CLIENTS:3 !:0</text>
</g>
<g fill="#20c9ff" filter="url(#blueGlow)">
<text x="177" y="211">IP 192.168.122.42</text>
<text x="177" y="251">RS232 230400 8N1</text>
<text x="177" y="291">FLOW:NONE VLD:1</text>
<text x="177" y="331">DCD0 DSR1 CTS1 RI0</text>
<text x="177" y="371">RX:1.23M TX:633</text>
<text x="177" y="408"> OVERVIEW 1/4 </text>
</g>
</g>
<g font-family="sans-serif" font-size="14" fill="#9fb0bd">
<text x="812" y="112">row 0</text>
<text x="812" y="171">row 15</text>
<text x="812" y="190">row 16</text>
<text x="812" y="414">row 63</text>
<text x="148" y="445" text-anchor="end">128 px</text>
<text x="480" y="445" text-anchor="middle">Verified panels: yellow rows 015 · black divider · blue rows 1663</text>
</g>
<g>
<circle cx="325" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<circle cx="480" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<circle cx="635" cy="493" r="37" fill="url(#button)" stroke="#728898" stroke-width="2"/>
<g fill="#d7e3ed" font-family="sans-serif" font-size="25" font-weight="700" text-anchor="middle">
<text x="325" y="502"></text>
<text x="480" y="501" font-size="18">OK</text>
<text x="635" y="502"></text>
</g>
<g fill="#91a4b2" font-family="sans-serif" font-size="14" text-anchor="middle">
<text x="325" y="551">previous / hold: back</text>
<text x="480" y="551">select / hold: confirm</text>
<text x="635" y="551">next</text>
</g>
</g>
<text x="480" y="593" fill="#718594" font-family="sans-serif" font-size="13" text-anchor="middle">Mockup only — glyph metrics remain to be finalized; the physical color boundary is hardware-verified.</text>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

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

After

Width:  |  Height:  |  Size: 6.4 KiB

+209 -119
View File
@@ -1,10 +1,11 @@
# Implementation roadmap # Implementation roadmap
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. A phase is complete only when its implementation, documentation, build validation, and relevant hardware tests have passed. 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 ## Status legend
- **Complete** — implemented and validated on the target hardware. - **Complete** — implemented and accepted, with target-hardware validation or an explicit phase-specific user waiver recorded below.
- **In progress** — implementation or validation is actively underway, but the overall phase is not complete.
- **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks. - **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks.
- **Planned** — accepted project direction, not yet implemented. - **Planned** — accepted project direction, not yet implemented.
- **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment. - **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment.
@@ -36,14 +37,14 @@ These constraints apply across all phases:
| 5A | Authenticated HTTPS administration foundation | **Complete** | | 5A | Authenticated HTTPS administration foundation | **Complete** |
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** | | 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
| 6 | Authenticated SSH serial transport | **Complete** | | 6 | Authenticated SSH serial transport | **Complete** |
| 7 | Local display and button interface | **Planned** | | 7 | Local display and button interface | **Complete** |
| 8 | Security and production hardening | **Planned** | | 8 | Role-based users and administrative access | **Complete** |
| 9 | Authenticated, rollback-capable OTA | **Planned** | | 9 | Small intermittent-use security baseline | **Complete (user signoff 2026-09-18; new hardware check waived)** |
| 10 | BLE serial transport and provisioning evaluation | **Planned** | | 10 | Simple admin web firmware upload | **Complete (explicit user acceptance 2026-09-18; upload and normal operation verified)** |
| 11 | Advanced network integration | **Under evaluation** | | 12 | Dual-stack networking and local service discovery | **Complete (explicit user validation; fresh boot and full client mix at 230400 baud)** |
| 12 | Optional filesystem-backed features | **Under evaluation** |
## Completed foundation
## Completed phases
### Phase 0 — RS-232 hardware characterization ### Phase 0 — RS-232 hardware characterization
@@ -55,7 +56,7 @@ Implemented and hardware-validated:
- Hardware CTS transmit gating and RTS receive-backpressure tests. - Hardware CTS transmit gating and RTS receive-backpressure tests.
- Exclusive RS-232 port ownership and fault-safe cleanup. - Exclusive RS-232 port ownership and fault-safe cleanup.
- RGB status indication for diagnostic state. - RGB status indication for diagnostic state.
- Potentially disruptive tests isolated under the `debug` CLI submenu; top-level `status` retained. - 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).
@@ -91,7 +92,7 @@ Implemented and hardware-validated:
- Automatic broker connection when the host opens the port with DTR asserted. - Automatic broker connection when the host opens the port with DTR asserted.
- Opportunistic writer acquisition with observer fallback. - Opportunistic writer acquisition with observer fallback.
- Binary-transparent bidirectional serial data. - Binary-transparent bidirectional serial data.
- Supported host line-coding application while USB owns the writer lease. - Host line-coding visibility for diagnostics; UART1 remains under explicit serial configuration control.
- Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls. - Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls.
### Phase 4 — Wi-Fi foundation ### Phase 4 — Wi-Fi foundation
@@ -107,6 +108,11 @@ Implemented and hardware-validated:
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`. - Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
- Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`. - Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`.
Post-validation enhancements implemented; regression validation pending:
- Edits to disabled station profiles are staged without restarting the active radio; enable-state changes and edits affecting enabled radio policy apply asynchronously.
- Configurable STA-only `sak-<suffix>.local` mDNS hostname with an independent versioned NVS record, live hostname updates, automatic withdrawal/restoration across STA IPv4 transitions, and nonfatal failure isolation. The responder initializes at most once and keeps its task stack internal while general metadata prefers PSRAM.
A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP. A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP.
### Phase 5A — Authenticated HTTPS foundation ### Phase 5A — Authenticated HTTPS foundation
@@ -114,7 +120,7 @@ A WPA3-required profile correctly rejects an AP that does not advertise the requ
Implemented and hardware-validated: Implemented and hardware-validated:
- HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS. - HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS.
- Persistent random shared `admin` credential. - Persistent random shared `admin` credential, later retained only as migration/recovery material by Phase 8.
- Persistent device-specific ECDSA P-256 certificate and private key. - Persistent device-specific ECDSA P-256 certificate and private key.
- Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery. - Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery.
- Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths. - Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths.
@@ -134,148 +140,232 @@ Implemented and hardware-validated:
- Viewport-constrained terminal fitting without recursive page growth. - Viewport-constrained terminal fitting without recursive page growth.
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software. - Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
## Latest completed phase Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected.
### Phase 6 — Authenticated SSH serial transport ### Phase 6 — Authenticated SSH serial transport
The SSH implementation is complete and hardware-validated. It provides: Implemented and target-hardware validated:
- wolfSSH on TCP port 22 while HTTPS remains on mbedTLS. - wolfSSH on TCP port 22 with a separate persistent ECDSA P-256 host key.
- Password authentication using the shared `admin` credential. - Two bounded interactive shell/PTY sessions; no `exec`, file transfer, forwarding, or subsystems.
- A separate persistent ECDSA P-256 SSH host key and OpenSSH-compatible fingerprint. - Authenticated, binary-transparent broker forwarding with opportunistic writer acquisition and observer fallback.
- Interactive shell/PTY channels only. - Bounded handshake attempts/deadlines, receive work, buffers, and a single wolfSSH owner task pinned to CPU 1.
- No `exec`, subsystem, SFTP, SCP, agent forwarding, or TCP forwarding. - UART0 lifecycle, session, counter, and host-key administration.
- Two fixed session/handshake slots. - PSRAM-preferred allocations with internal fallback and internal task stacks.
- Three password attempts and a 15-second handshake deadline. - Bounded broker/WebSocket work, software mbedTLS AES, and software wolfCrypt AES/SHA to avoid the validated shared-hardware-crypto/PSRAM watchdog failure while retaining ESP-IDF SHA/MPI acceleration.
- No broker client before successful authentication and shell negotiation. - Concurrent USB, WebSocket, and SSH operation validated with USB and SSH alternately holding the writer lease.
- Binary-transparent serial forwarding with no in-band writer-control protocol.
- Opportunistic writer acquisition and observer behavior through the common broker.
- Credential-rotation revocation and UART0 session/host-key administration.
- A single bounded wolfSSH owner task pinned to CPU 1.
- wolfSSL/wolfSSH allocations that prefer PSRAM with internal-memory fallback.
- A per-call wolfSSH receive budget to prevent pipelined input from monopolizing the owner task.
- Bounded broker and WebSocket work bursts that guarantee scheduler/idle progress during sustained serial traffic.
- Software AES for PSRAM-backed mbedTLS records plus software wolfCrypt AES/SHA, preventing mbedTLS and wolfSSL from independently reconfiguring shared ESP32 crypto hardware.
- Internal task stacks retained for safety during flash/NVS cache-disable windows.
- Heap and SSH stack telemetry through `memory` and `ssh status`.
SSH login, host-key verification, serial traffic, Web Serial coexistence, and USB CDC coexistence have been validated. During concurrency testing, the previously failing three-client arrangement used USB CDC and SSH as writer in turn while the remaining transports observed.
Early runs showed healthy heap values but CPU 0 idle-task watchdog timeouts, disproving the initial memory-pressure hypothesis. One trace exposed an unbounded broker scheduling path. A later, decisive trace repeatedly stopped inside mbedTLS's external-RAM hardware-AES DMA wait while HTTPD encrypted a WebSocket record. The broker and WebSocket producer now use bounded active bursts, mbedTLS software AES avoids the hanging PSRAM DMA path, and wolfSSL AES/SHA hardware acceleration is disabled because its private locks do not coordinate with ESP-IDF's crypto lock.
#### Phase 6 hardware validation
The final target-hardware retest covered:
1. USB CDC, one WebSocket terminal, and one SSH terminal connected concurrently.
2. SSH as writer with USB CDC and WebSocket observing.
3. USB CDC as writer with SSH and WebSocket observing.
4. Serial traffic with active readers and writer-token release/reassignment.
5. Continued HTTPS/WebSocket, SSH, USB CDC, serial-service, and UART0 responsiveness.
The software-crypto build no longer reproduces the HTTPD watchdog stall. This validates that the failure was a shared hardware-crypto/PSRAM DMA problem rather than heap exhaustion. Phase 6 is complete; these concurrent arrangements remain regression tests for future transport, TLS, memory-placement, and ESP-IDF changes.
## Planned phases
The order below is the current plan. Detailed requirements should be finalized at the start of each phase, and optional features must not weaken the completed serial and recovery paths.
### Phase 7 — Local display and buttons ### Phase 7 — Local display and buttons
Add a standalone local status/control interface without making it a dependency of the serial core. Implemented and target-hardware validated:
Planned work: - Optional 128×64 SSD1315-compatible I²C OLED on GPIO11/12 and active-low buttons on GPIO10/13/14; assignments remain centralized in `board_pins.h`.
- Display service with a static framebuffer, bounded dirty-page commits, and separate yellow status rows 015 plus blue content rows 1663.
- Low-priority status/control task built from copied public snapshots; no service lock is held across I²C and the UI never becomes a broker client.
- Overview, RS-232, broker, and network/service pages with no credential or key material.
- Confirmation-protected local controls for service lifecycle, Wi-Fi reconnect/profile rotation, writer release, display off, and reboot; the UI cannot assign a writer or edit secrets/configuration.
- Persistent dim/off settings, bounded boot animation, consumed wake presses, stuck-button quarantine, and one-shot confirmations.
- Missing, disconnected, or unresponsive display hardware remains nonfatal and can recover through one bounded reprobe.
- Concurrent serial, UART0, USB, WebSocket, SSH, and injected display/button fault behavior validated. See [Electrical tests](electrical_tests.md) and [Hardware wiring](wiring.md).
- Select the actual display, button arrangement, electrical interface, and remaining GPIO assignment before implementation. ### Phase 8 — Role-based users and administrative access — Complete
- Add a board-profile abstraction where required instead of scattering new pin assumptions.
- Show concise serial, Wi-Fi, service, client-count, and writer-owner status.
- Provide safe button actions for navigation and common lifecycle or writer-control operations.
- Require deliberate confirmation for destructive or security-sensitive actions.
- Avoid displaying passwords or private-key material during ordinary operation.
- Keep UART0 fully functional as the authoritative recovery interface if the display or buttons fail.
- Bound display refresh and event processing so they cannot interfere with UART or network transport timing.
Completion requires electrical validation, operation with all current transports, and documented recovery behavior. Implemented and accepted: a bounded, persistent user system replaces the single shared network credential. Both roles can use the authenticated web serial/status interface. Over SSH, `user` routes to the broker-backed serial stream while `admin` routes exclusively to the administration shell and receives no broker client or writer lease. UART0 remains the physical recovery and bootstrap authority.
### Phase 8 — Security and production hardening Completed implementation (8A8C target-hardware validated; 8D explicitly accepted at 8D.22 on 2026-09-13):
Harden stored credentials, boot integrity, update trust, failure diagnostics, and dependency maintenance. This phase must be staged carefully because some ESP32 eFuse operations are irreversible. 1. **Phase 8A — User database and UART0 administration — Complete**
- Versioned NVS database for up to eight `user`/`admin` accounts, random account IDs and authentication generations, final-admin protection, and three Ed25519/P-256 keys per account.
- Salted PBKDF2-HMAC-SHA256 password verifiers, bounded no-echo entry, one-time generated passwords, and secret-free account/key status.
- Historical migration/bootstrap supported the initial cutover. Current firmware instead commits missing storage empty, provisions the first admin with normal UART0 `user add`, and provides UART0-only unavailable-database recovery; legacy credential/bootstrap commands are removed.
- Migration, bootstrap, CRUD, persistence, and command basics target-hardware validated; the full fault-injection matrix remains regression coverage. NVS remains unencrypted and offline guessing remains possible.
2. **Phase 8B — Role-aware HTTPS and SSH authentication — Complete**
- HTTPS and SSH authenticate through the common database and copied secret-free principals; unavailable user storage fails closed. The initial HTTPS Basic path was replaced by cookie authentication in 8D.
- One-time principal-bound WebSocket tickets and ongoing principal-currentness checks prevent stale admission or input.
- Account mutations request targeted WebSocket/SSH revocation; authentication generations provide fail-safe invalidation while unrelated accounts remain connected.
- Password/key login, ticket behavior, targeted revocation, recovery, and concurrent transport operation are target-hardware validated. Legacy credentials were subsequently removed without changing established TLS identity or valid users.
3. **Phase 8C — SSH administrative shell — Complete**
- Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream.
- UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed.
- The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs.
- Transport-neutral bounded prompts now support interactive user passwords/keys and Wi-Fi secrets over admin SSH without exposing hidden input or allowing another command while a prompt is active. Ping callbacks enqueue typed bounded events and the dispatcher alone formats their output. Four-entry per-session history and whole-line Tab completion are RAM-only and wiped on disconnect.
- Authenticated administrators receive the operational registry, including HTTPS material rotation/reset, reboot, ping, and SSH lifecycle/session/host-key mutation. Self-terminating reboot and SSH actions are deferred until acknowledgement output drains, block further shell input, and execute through existing synchronous owner APIs from a separate bounded control task. First-admin provisioning and explicit `user recover --force` remain physical-UART0 operations (`user bootstrap` was later removed); admin SSH also rejects generating a replacement password for its own account.
- `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort application-buffer draining. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
- Target-hardware validation passed for route separation, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation. Stress at 460800 baud with SSH and WebSocket clients caused substantial expected packet drops and slower display controls, but did not exhaust memory or require lowering the supported baud-rate range.
4. **Phase 8D — Integrated web administration — Complete**
- User explicitly signed off tested firmware at **8D.22 on 2026-09-13**, superseding earlier per-slice pending acceptance/review gates. Cookie login/logout replaces Basic; bounded digest-only sessions, same-origin/CSRF checks, principal currentness and targeted revocation protect serial and admin routes.
- Admin-only Serial/Admin selection and Settings preserve connected terminal observation and serial writer ownership. The browser shell shares the serialized UART0/admin-SSH dispatcher, with bounded output, explicit admission and retained frontend restrictions—not unrestricted shell parity.
- Typed Serial, Accounts/password/authorized-key, Network/Wi-Fi/mDNS, Display, Broker and SSH settings; confirmed HTTPS stop/restart/reboot and HTTPS/SSH identity rotation. Canonical owners compare reserved service/identity/target generations, preserve commit/failure semantics and expose no stored secrets. Admitted work is not cancelled by later logout/timeout; uncertain mutations are never automatically replayed.
- Accessible Serial/Wi-Fi/client/writer contextual controls reuse existing controllers. Writer transfer is explicit and generation-safe; one writer, isolated observers, binary transparency, UART0 administrative recovery and network-independent native USB UART1 remain invariants.
- Scope removed by user: **8D.15** dedicated typed network diagnostics (shell diagnostics retained) and the unimplemented **8D.19** ordinary browser-session/native-USB controls (SSH controls retained). Browser identity reset/recovery/export is excluded; canonical recovery remains available.
- Acceptance evidence and legacy storage compatibility are condensed below. [Web administration](web_administration.md) retains current API/owner/recovery contracts; [regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) retain practical checks, not claims of execution.
Planned work: #### Phase8 acceptance evidence
- Define development, test, and production security profiles. **8D.22 signoff, 2026-09-13:** the user explicitly accepted the tested firmware, superseding earlier per-slice target, review and integration gates for the retained scope above. Earlier scoped acceptance covered browser login/logout, shared administration, Serial/account/Network presentation and legacy cleanup. Prior host regression suites and independent reviews were reported passing; this consolidation asserts no new build or test execution.
- Enable and validate NVS encryption for Wi-Fi, HTTPS, and SSH material.
- Evaluate flash encryption and PSRAM encryption for the target hardware and threat model.
- Enable secure boot with a documented signing-key and recovery process.
- Define secret-safe core-dump handling; do not expose credentials or session material in diagnostics.
- Audit key and session-material zeroization across application, mbedTLS, wolfSSL, and wolfSSH allocations.
- Add authentication throttling, lockout/backoff, and security-event counters where appropriate.
- Review TLS/SSH algorithms, certificate policy, password policy, and browser security headers.
- Review current wolfSSL/wolfSSH and ESP-IDF security releases instead of treating integration versions as permanent pins.
- Recheck all third-party licenses and preserve source/license notices.
- Document provisioning, key rotation, factory reset, backup, recovery, and device decommissioning.
Security features must be validated first on expendable hardware before any irreversible production configuration is recommended. Prior final Phase 8 build: **PASS, 100,556 B linked RAM / 1,828,573 B flash, CPU 160 MHz**. The combined binary WebSocket-send fix was separately accepted at **160 MHz / 230400 baud with the full client mix including browser admin**; retain combined send and bounded failed-send isolation, not the earlier frequency-only experiment. Ordinary HTTPS idle cleanup was accepted, not every possible admission failure or owner delay.
### Phase 9 — Authenticated OTA and rollback Latest reported Phase 8 telemetry (bytes):
Use the existing dual 4 MiB application slots and `otadata` partition to add safe firmware updates. | Sample / capability | Free | Minimum free | Largest block |
|---|---:|---:|---:|
| Boot internal 8-bit | 59,808 | 58,840 | 31,744 |
| Boot internal DMA | 52,052 | 51,084 | 31,744 |
| Boot PSRAM | 8,196,968 | 8,183,972 | 8,126,464 |
| Loaded after burst, internal 8-bit | 31,508 | 2,052 | 18,432 |
| Loaded after burst, internal DMA | 23,752 | 460 | 18,432 |
| Loaded after burst, PSRAM | 8,136,624 | 8,065,972 | 7,995,392 |
Planned work: Loaded SSH minimum-free stack: **15,028 B**. This sample had two SSH sessions (serial/admin), two serial WebSockets and USB, with SSH as writer. Browser admin had been used then closed; it was **not active in this capture**. Web send/queue/protocol and SSH IO errors were zero; one SSH handshake failure and one session revocation were reported without an established cause.
- Accept only authenticated, integrity-checked firmware images compatible with the selected secure-boot policy. **Limits, not reopened acceptance gates:** missing latest broker/serial counters prevent an exact zero-drop or byte-integrity inference. No latest browser-admin-active full-mix, individual fault-injection, exact duration, soak, cleanup-cycle or reserve-floor result is inferred. Internal/DMA lifetime minima remain a nonblocking transient-headroom follow-up, not approved reserves or proof of allocation failure: capability pools overlap and per-region minima may be non-simultaneous, so do not sum them. HTTPD/dispatcher stack margins, correlated peaks and allocation-reserve policy remain unmeasured/unapproved here. SDK TLS `-0x004C` means NET_RECV_FAILED, not an OOM diagnosis; stale cookies as the cause of two boot authentication failures remain unconfirmed.
- Support an update path that does not expose a plaintext management endpoint.
- Stream downloads with bounded RAM use and explicit progress/error reporting.
- Preserve serial, Wi-Fi, HTTPS, and SSH configuration across successful updates.
- Mark new firmware pending until a post-boot health check succeeds.
- Roll back automatically after failed boot or health confirmation.
- Define power-loss behavior for every update stage.
- Keep UART0 recovery and wired re-flashing documented and functional.
- Add version/compatibility checks and reject accidental downgrade unless an explicit policy allows it.
OTA is complete only after successful update, interrupted-update, invalid-image, rollback, and configuration-persistence tests on hardware. #### Phase8 legacy credential compatibility
### Phase 10 — BLE Cleanup was accepted **2026-09-08**, with unchanged certificate fingerprint and usable preexisting test users. This is not an unreported provisioning, recovery, power-loss or all-key pass.
Evaluate and, if resource limits permit, add BLE as another bounded transport or provisioning path. - **Users:** missing `user_db/database` is committed as an empty v1 database, with no legacy account/password import. First-admin provisioning is physical UART0 `user add <username> admin` (optional `--generate`); entered passwords use hidden confirmation and generated passwords appear once. UART0-only `user recover --force` rebuilds only an unavailable database empty, refuses healthy databases (including empty ones), and leaves other configuration and TLS/SSH identities untouched; follow with normal `user add`.
- Valid existing v1 user bytes load without rewrite or changes to accounts, roles, IDs, authentication generations, verifiers or authorized keys; migrated `user` accounts are not promoted. Private `v1_admin_marker` preserves the old wire byte and is derived from admin count on mutation, not a new role/schema/public bootstrap state. Public bootstrap and legacy migration/synchronization APIs are removed; final-admin protection, conditional mutations, principal currentness and targeted revocation remain.
- **TLS:** at unchanged `web_sec/material`, a private reader validates the **1,392-byte v1** layout and migrates to **1,340-byte TLS-only v2**, preserving exact private-key DER, certificate DER, SHA-256 fingerprint and material generation—not rotating identity. The candidate is validated and committed before publication; temporary credential-bearing input is wiped. Malformed/unknown material, read/crypto failures and migration write/commit failures fail closed without regeneration or overwrite; truly missing material may be generated normally.
- `web credentials show`, `web credentials rotate --force` and `user bootstrap` are removed. `web certificate info` / `web certificate rotate --force` retain frontend policy. `web reset --force` replaces TLS identity only, not accounts/passwords/verifiers; replacement/restart may close browser sessions and require renewed certificate trust/login, without revoking unrelated SSH sessions. No public legacy credential reader, display, rotation or authentication path remains.
- **Downgrade/security:** older v1-only firmware cannot read v2 HTTPS storage; user-database v1 compatibility does not make HTTPS downgrade safe or restore removed credentials. Logical NVS replacement is **not secure flash wiping**: historical plaintext legacy credentials may remain, Wi-Fi secrets and TLS/SSH private keys remain unencrypted, and PBKDF2 verifiers permit offline guessing. No erase is required or authorized by cleanup.
- Prior host tests used NVS fault doubles and a generated identity wrapped in the historical layout, **not an independently captured old-device fixture**; they do not prove flash durability or power-loss behavior. Focused runners: `tests/web_security/run.py`, `tests/admin_console_boundary/accounts.py`, `tests/admin_ssh_policy/run.py`. See [legacy-removal regressions](user_administration_tests.md#current-legacy-removal-regression-procedure).
Planned work: ### Phase 9 — Security and production hardening
- Decide whether BLE is primarily a serial transport, a provisioning interface, or both. **Complete — user signoff 2026-09-18; new hardware check explicitly waived.** Reduced scope: an intermittently powered wireless serial cable on a trusted network, not a permanently exposed Internet service. The earlier extensive Phase 9 implementation was abandoned in favor of the accepted pre-Phase-9 baseline. Short exposure reduces risk; it does not make upstream defects harmless or certify this firmware secure.
- Require authenticated pairing/bonding appropriate to the device threat model.
- Integrate serial access through the existing broker rather than bypassing writer ownership.
- Preserve binary transparency through explicit framing and MTU-aware partial transfers.
- Bound connections, queues, retries, and advertising behavior.
- Measure coexistence with 2.4 GHz Wi-Fi, HTTPS, WebSocket, SSH, USB CDC, and UART service.
- Store bond/provisioning data under the hardened secret-storage policy.
- Provide UART0 controls and a physical recovery path for clearing BLE state.
BLE remains subordinate to stable Wi-Fi and serial operation; it should be omitted if coexistence or memory costs cannot be bounded acceptably. Small implementation:
### Phase 11 — Advanced network integration - Keep the baseline PlatformIO/ESP-IDF and managed-library versions and upstream sources. No generated dependency patches, custom SSH state machine, crypto-policy replacement, allocator hooks, or SDK migration in this phase.
- Explicitly disable core dumps and select silent panic reboot through standard `sdkconfig.defaults` options. Keep ordinary logging; reduced crash diagnostics are a deliberate trade-off. Existing generated configurations must be checked because defaults do not override saved selections.
- Follow the [operational checklist](security_operations.md): unique credentials, trusted identity verification, no Internet forwarding, networking off/power off after use, and existing rotation/recovery procedures.
- Review upstream maintenance releases occasionally and before planned upgrades. Evaluate reachable issues against actual use; any stock dependency upgrade is a separate build/device-tested task, not a reason to accumulate local backports. Preserve existing license notices.
These features are candidates, not current commitments: **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.
- mDNS advertisement for the certificates device-specific `.local` name and selected services. **Host/build evidence (2026-09-18):** normal `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the original 20241119 toolchains: **94,196 B linked RAM / 1,828,565 B flash**. Generated configuration confirms no dumps and silent panic reboot; compilation inputs contain none of the abandoned Phase 9 source overlays or wolf crypto guards. No device operation or target validation was performed.
- Enterprise Wi-Fi support, subject to credential-storage and certificate-validation design.
- IPv6 behavior and diagnostics beyond the current basic support. **Acceptance (2026-09-18):** the user signed off this small Phase 9 and explicitly waived a new whole-phase hardware check of boot, UART0 recovery, native USB serial, HTTPS/SSH login and normal serial sharing. Application code is unchanged, **but crash-diagnostic defaults changed**; this is a waiver, not evidence of target or panic-path validation. The prior build/configuration evidence above remains the validation record; no new hardware test was performed. A separately controlled panic check with synthetic secrets remains optional, not a completion gate; do not add a production panic endpoint or erase flash. Disabling dumps does not erase old contents.
- WireGuard feasibility evaluation, including RAM, CPU, licensing, key storage, routing, and recovery impact.
### Phase 10 — Simple admin web firmware upload
**Complete — explicit user acceptance on 2026-09-18: firmware upload works and normal operation is verified.** The admin **Settings → HTTPS / Reboot** firmware card uploads the locally built `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. Install this OTA-enabled firmware by wire first, then use web application uploads. There is no release server, automatic download or update service. [Firmware update and recovery](#phase10-update-and-recovery) below covers operation; [regression guidance](#phase10-regression-guidance) covers future checks.
Implemented scope:
- Raw `POST /api/firmware` requires exact `Content-Type: application/octet-stream`, known exact `Content-Length`, cookie, same-origin Origin and `X-CSRF-Token`; no multipart/JSON, Basic authentication or ordinary `user` access. Reject queries, ambiguous headers, chunked transfer and `Expect`; UI code leaves browser-managed headers alone. Admission precedes body/flash work; session/principal currentness is rechecked before boot selection. The card confirms upload/reboot and shows progress/results without automatic retries. HTTPD capacity is 40 method/path handlers.
- Standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) stream one upload through a 4 KiB buffer into the inactive slot (two 4 MiB app slots). A reboot task with a 2 KiB stack is preallocated before erase. Lifecycle/identity reservations and ordinary software-reboot gating exclude competing work; no custom flash protocol or whole-image RAM buffer.
- Accepts an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Browser hints require a nonempty `.bin` of at most 4 MiB; the server bounds raw length against actual destination capacity before trusting HTTPD's narrowed length. Checks completeness, target/header and exact SDK image length/integrity, including mandatory appended SHA-256, before selection. The digest is not publisher authentication; neither it nor filename checks prove board compatibility or a working application.
- Before selection, rejected/incomplete uploads and receive/write/validation failures abort any live OTA handle without selecting the candidate; the inactive slot may be erased/partially written. Boot-metadata failure (`firmware_commit_failed`) needs inspection and carries SDK transactional uncertainty. `200 {"ok":true,"rebooting":true}` means validation/selection succeeded; successful synchronous send schedules restart after 500 ms, retaining reservations, but proves neither browser receipt nor boot success. Response failure after selection schedules no restart, releases reservations for manual reboot and latches further admissible uploads to 409 `firmware_selected_reboot_required`; the latch survives HTTPS stop/start until device reset.
- Synchronous HTTPD receive/flash blocks other HTTPD work: browser sessions can stall/drop. The 120-second total receive-loop and ten-second stall checks use the existing one-second socket timeout, not an absolute deadline: synchronous SDK erase/write/validation and scheduling are not preempted. The browser's 180-second timeout cannot cancel committed work. UART0/native USB remain independent paths, not guarantees of uninterrupted serial timing during flash; reboot interrupts all transports. Software exclusion cannot prevent physical reset, power loss or panic.
- **Preserve NVS:** write only the inactive application slot and the OTA selection metadata. Do not erase the chip, rewrite the partition table/bootloader, or touch `nvs`, `nvs_key`, PHY, storage or other data partitions. Existing users, passwords, Wi-Fi/serial settings and HTTPS/SSH identities remain stored, as with an application-only wired update without erase. This preserves stored bytes; the uploaded firmware must still understand the existing schemas and must not itself erase/migrate them incompatibly.
- Keep wired USB-to-UART flashing documented as recovery if the uploaded application does not boot or no longer serves the web UI. Basic image validation cannot prevent a valid but broken application from requiring wired recovery.
**Not in scope:** secure boot, image signatures/signing-key management, anti-rollback/version-downgrade enforcement, automatic rollback/post-boot health-confirmation machinery, remote release discovery, partition migration or NVS backup/restore. Use upstream OTA support without dependency patches. The administrator is responsible for selecting trusted, compatible firmware, including when deliberately installing an older build.
**Historical integration build and host evidence (parent-reported):** `pio run` **PASS**, **94,220 B linked RAM / 1,847,645 B flash**. Against Phase 9's 94,196 B / 1,828,565 B, this is **+24 B RAM / +19,080 B flash**. Final backend 88 cases plus the pinned SDK begin/abort failure-contract test, UI 169 groups, server lifecycle 44, admin transport 25, console lifecycle, SSH runtime and cookie lifecycle checks passed. Additional cookie base/admin/display/lifecycle, HTTPD idle cleanup 18, and SSH management/runtime/security checks passed; the extra cookie `--admin` run initially lacked a reboot symbol in its fixture, corrected in test-only code before passing. Host doubles are not target evidence. The later concise-UI fix was copy-only; its latest reported UI regression passed all **169 groups**, with no rebuild after the text change. The build figures above remain historical; this documentation update ran no build or tests.
**Acceptance (2026-09-18):** after the firmware upload implementation and concise-UI fix, the user confirmed: “That works perfectly. And the usual operation is also verified.” Phase 10 is complete by this explicit acceptance of working upload and verified normal operation. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or wired-recovery passes. The [regression guidance](#phase10-regression-guidance) below is reusable future guidance, not an acceptance blocker.
#### Phase10 update and recovery
1. Keep **USB-to-UART** available for UART0 administration/flashing; native USB CDC is network-independent UART1 access, not administration. Install the updater by wire first: older firmware without the route cannot install its own first web updater. The custom OTA layout is required; [one-time old-layout migration](../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).
3. Save desired RAM-only settings and record nonsecret configuration/public HTTPS/SSH fingerprints. Arrange a maintenance window and stable power/network; sign in as `admin` over trusted HTTPS, open **Settings → HTTPS / Reboot → Firmware update**, choose the file and confirm **Upload and reboot…**.
4. Wait for validation/reboot, not just 100% transmitted bytes. Restore network reachability, reload and explicitly sign in; verify the running application, saved configuration and identities before another upload.
5. A lost response, timeout, page close, sign-out or browser abort proves neither cancellation nor failure. The UI locks uncertain outcomes without replay; reload clears only browser locks, not the device latch. Inspect through UART0 and deliberately reboot if appropriate: any later reset can boot an already selected image.
6. For initial wired installation or recovery when boot/HTTPS fails, connect USB-to-UART, select a known-good compatible checkout, run `pio run --target upload`, then `pio device monitor -b 115200`. **Do not erase for ordinary updates or recovery**; there is no automatic recovery flashing/rollback.
Replies: **400** malformed/incompatible/incomplete input; **401/403** authentication/Origin/CSRF or `admin_required`; **408** `firmware_timeout`; **409** selected-image latch; **413/415** destination capacity/content type; **500** `firmware_write_failed`/`firmware_commit_failed`; **503** busy/unavailable/resources. Unread rejected bodies close rather than drain; bounded secret-free JSON may never reach a disconnected browser.
#### Phase10 regression guidance
These are reusable checks, **not recorded passes or outstanding acceptance gates**. Record only performed outcomes; host flash/network/scheduling doubles are not hardware evidence. Focused host suites: `python3 tests/web_firmware_update/run.py` and `python3 tests/web_ui_session/run.py` (the SDK begin/abort case covers a live handle published on erase failure).
- Exercise wired-first install, known-good upload/reboot/new build and explicit reconnect; compare saved users/roles, password/key login, Wi-Fi/serial/display/hostname settings and public HTTPS/SSH fingerprints without recording secrets.
- Interrupt/cancel receive; reject corrupt, truncated, wrong-target, non-application and oversized inputs without unintended selection/restart, then deliberately perform a valid update. Reject unauthenticated/ordinary-user, wrong-Origin and missing/wrong-CSRF requests before flash. Never attempt power interruption at commit without wired recovery ready.
- Exercise competing upload, HTTPS lifecycle/identity and ordinary reboot exclusion; HTTPD requests may wait rather than promptly return busy. With controlled post-selection response-failure injection, verify no automatic restart, subsequent 409 (also after HTTPS restart), then manual reboot; an arbitrary disconnect does not establish this fault case.
- Observe web stalls/drops and serial/network recovery, single-writer isolation, UART0 recovery and native USB UART1 access without networking; demonstrate known-good wired recovery without erase and recheck configuration/identities. Do not claim uninterrupted traffic.
## Current and planned phases
**Phases 8, 9, 10 and 12 are complete** for their accepted scopes. Phase 9 includes the explicit new-hardware-check waiver above; Phase 10 includes explicit user acceptance of upload and normal operation. Phase 12 includes explicit user validation and the fresh-boot/full-client evidence below. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak, recovery or reserve measurement was performed for completed phases.
### Phase 12 — Advanced network integration
**Complete by explicit user validation:** “I just validated Phase 12 successfully.” Accepted baseline:
- Dual-stack local access and DNS-SD discovery: advertise available HTTPS and SSH services using the shared `sak-<suffix>.local` hostname, with appropriate IPv4 `A` and IPv6 `AAAA` records. Preserve IPv4 access and verify IPv6 support throughout HTTPS, WebSocket and SSH. Clients choose address-family preference and fallback; DNS-SD cannot mandate IPv6 preference. Certificate-name integration and trust remain separate concerns.
- Send the configured device hostname (`sak-<suffix>`, without `.local`) through DHCPv4 Host Name option 12 so a suitably configured DHCP/DNS server can publish the lease address in its own DNS zone. Set the STA netif hostname before DHCP starts, define how hostname changes reach subsequent DHCP exchanges, and verify the transmitted option and server-side DNS registration. DNS publication and the DNS domain remain server policy; mDNS does not provide this integration.
- IPv6 baseline: link-local addressing and SLAAC-provided ULA/global addresses, with mDNS/DNS-SD for local discovery; no DHCPv6 requirement. Preserve link-local interface scope and keep advertisements synchronized with usable addresses and service availability. Registration of SLAAC addresses in infrastructure DNS is a separate optional feature, outside this baseline; SLAAC does not provide automatic hostname registration.
- Validate IPv4-only, IPv6-only and dual-stack operation, including reconnects, address changes and service lifecycle changes. Discovery remains local-link by default; preserve UART0 and native USB recovery regardless of network state.
The device is not intended to become a general-purpose router. Captive-portal interception, unauthenticated DNS redirection, NAPT, and a plaintext serial listener remain out of scope unless the project requirements are explicitly revised. The device is not intended to become a general-purpose router. Captive-portal interception, unauthenticated DNS redirection, NAPT, and a plaintext serial listener remain out of scope unless the project requirements are explicitly revised.
### Phase 12 — Optional filesystem-backed features #### Implementation and operational boundaries
The `storage` partition is reserved but not currently mounted. Possible uses must be justified individually: - The Wi-Fi owner installs the full configured hostname before STA connection and updates it after hostname edits. DHCPv4 option 12 uses `sak-<suffix>`, not `.local`. Rename does not force DHCP restart: the next DHCP exchange carries the new name, and DNS registration/cache cleanup remain server policy. Existing hostname limits and NVS formats are unchanged.
- STA enables SLAAC and IPv6 link-local creation; either IPv4 or a preferred IPv6 address establishes `ONLINE`. **Link-local-only counts as online**, prevents IPv4-only timeout/failover, and can retire the fallback AP after the existing stability interval. This indicates local address availability, not Internet reachability or a default route. Web, CLI and OLED distinguish absent IPv4 from IPv6 availability. A diagnostic follow-up adds up to three preferred numeric IPv6 addresses to the snapshot, copied and cleared with the same observation; `wifi status` and browser Network settings print them in link-local/ULA/GUA groups; overview/OLED status retains availability flags. The settings JSON is bounded to 2304 bytes, with no extra netif calls on HTTPD.
- HTTPS/WebSocket use IDF's existing dual-stack listener. SSH explicitly uses one dual-stack listener, retaining the existing two-session limit. Bracketed IPv6 HTTPS authorities are canonicalized and bound to the existing Host/Origin/session checks; scoped IPv6 literal URLs are rejected. Prefer the `.local` hostname for link-local browser access, subject to client resolver support. No certificate regeneration, new trust mechanism, or automatic IPv6 preference is introduced.
- STA-only mDNS advertises `_https._tcp:443` and `_ssh._tcp:22` according to listener availability, without TXT metadata. Record and address-family changes converge through the existing Wi-Fi owner, including offline reconciliation; service setters do no component work. The responder is not restarted for ordinary changes. Initialization failure remains latched/nonfatal.
- mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently.
- `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set.
- `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added.
- Existing diagnostic limits remain: hostname ping selects the resolver's first usable result (not Happy Eyeballs), scoped link-local ping is not newly supported, and traceroute remains IPv4-only. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning are outside this baseline.
- LittleFS-backed web assets to decouple large static files from the application image. #### Validation evidence and acceptance
- Bounded diagnostic logs with explicit retention and secret-redaction rules.
- Import/export of non-secret configuration.
- Optional user files needed by later administration features.
Before enabling storage, define corruption recovery, wear limits, quotas, atomic update behavior, compatibility across OTA slots, and whether encryption is required. Serial operation and recovery must not depend on a mountable filesystem. `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the guarded mDNS source compiled: **94,396 B linked RAM / 1,853,025 B flash**, +176 B RAM / +5,380 B flash versus the recorded Phase 10 build. These are static link sizes, not runtime heap or stack headroom. Initial target compilation rejected lock-free bool atomics; the implementation now uses short portMUX sections. Existing SDK Kconfig notes remain.
Host checks passed: Wi-Fi/address/hostname reconciliation, mDNS lifecycle/family repair, actual patched multicast-membership functions and CMake guards, SSH dual-stack/management/runtime/security, HTTPS authority parser (689 cases on host and actual installed lwIP parser), cookie authorization variants, Network settings, browser UI/CSP (171 groups), HTTPS lifecycle (45 groups and eight status projections), firmware upload (88 cases plus SDK contract), broker diagnostics, and session-store/serial integration. Network-settings host fakes were updated after the new netif dependency exposed a compilation failure; the rerun passed. These are not packet-level or hardware evidence.
Follow-up after the user's live status report: both preferred-address flags were `yes`, while plain client `ping` selected IPv4. Source verification confirms the flags require actual preferred addresses; this is not evidence of failed SLAAC or verified IPv6 reachability. `wifi status` now prints the addresses for diagnosis. Latest follow-up `pio run` passed: **94,444 B linked RAM / 1,853,685 B flash** (+48 / +660 versus the initial Phase 12 build). Browser Admin console now permits canonical Wi-Fi commands, including disruptive edits and hidden prompts, as explicitly requested. Focused policy/browser Wi-Fi prompt, address snapshot/rendering, Network/cookie and UI regressions passed. The broader console-boundary suite passes its console/certificate stages but has a pre-existing SSH-adapter fixture compilation failure for missing `web_firmware_update_reserve_reboot`; no production change was made to hide it.
Latest address-display build: `pio run` **94,444 B linked RAM / 1,854,485 B flash**; Network API/owner checks and **174 browser groups plus CSP** passed. These remain build/host results, separate from the user's device evidence below.
##### User-reported device acceptance
User explicitly validated Phase 12 after confirming numeric IPv6 ping/HTTPS, Avahi AAAA resolution and normal IPv6 hostname access following the Fedora `authselect` mDNS correction. The earlier hostname failure was a client resolver configuration issue, not missing device IPv6 addresses. The acceptance capture adds:
- **Fresh boot:** HTTPS, SSH and mDNS running without reported startup failures; zero SSH sessions, serial stopped, no broker clients, USB attached but not host-open. The configured UART profile is **230400 baud, 8N1, RTS/CTS, DTR active, RTS threshold 96**.
- **Full client mix:** two authenticated public-key SSH sessions with IPv6 ULA peers (one serial observer, one admin console); two serial WebSockets (one writer, one observer); one active browser admin WebSocket; native USB host-open as an observer. Four broker clients, exactly one writer, all reported pending/event counts zero. Serial running with RX-available/TX-pending zero, CTS asserted and valid RS-232 voltage at observation time.
- SSH: two successful handshakes, zero handshake/authentication failures or timeouts, zero stream I/O failures and zero session revocations. Stream counters **RX 68 / accepted 65 / rejected 3 / TX 161,209 bytes**; two broker writer revocations were reported. The rejected bytes are retained as evidence, not silently described as zero loss or assigned an unverified cause.
- Web serial: two connections, **27 accepted RX frames/bytes**, zero rejected RX frames/bytes; **782 binary TX frames / 318,230 bytes**, 11 control frames / 948 bytes. Send/queue/protocol/close failure counters zero. Two writer requests were denied; the final broker snapshot still shows one writer. Browser admin: one connection, **106 RX / 3,721 TX bytes**, zero send/queue/protocol/authorization/backpressure failures. No response errors reported.
- HTTPS and SSH both report running, not transitioning, `ESP_OK`; mDNS reports expected announcement and `ESP_OK`. Expected announcement alone is not packet-level service-record proof; the earlier successful Avahi lookup separately establishes the reported AAAA lookup.
All heap figures are bytes, copied from the user's observations:
| Observation | Heap capability | Free | Lifetime minimum-free | Largest block |
|---|---|---:|---:|---:|
| Fresh boot | Internal 8-bit | 65,880 | 64,912 | 31,744 |
| Fresh boot | Internal DMA | 58,124 | 57,156 | 31,744 |
| Fresh boot | External PSRAM | 8,196,732 | 8,187,980 | 8,126,464 |
| Full client mix | Internal 8-bit | 35,408 | 8,528 | 22,528 |
| Full client mix | Internal DMA | 27,652 | 772 | 22,528 |
| Full client mix | External PSRAM | 8,111,952 | 8,072,612 | 7,995,392 |
SSH owner stack: **20,480 B** configured, **18,476 B minimum-free** at fresh-boot observation and **16,284 B minimum-free** under the full mix.
**Evidence limits:** minimum-free is the conservative sum of matching heap regions' lifetime minima, not a simultaneous free-space measurement or guaranteed allocation reserve. Internal 8-bit and DMA capabilities overlap and must not be added as independent pools. The **772 B DMA lifetime minimum remains a headroom watch item**, not an observed allocation failure or acceptance blocker. Boot output contains two TLS `-0x004C` receive errors and unauthenticated-request failures; the capture does not establish their cause or connect them to memory exhaustion. Command snapshots are non-atomic; differing cumulative request/authentication counts must not be treated as one aligned measurement interval. No UART overflow/per-observer drop counters, soak duration, byte-for-byte capture integrity, exhaustive fault injection, DHCP packet capture/server-zone update, or every address-family/service transition is established by this excerpt. User acceptance is recorded without inventing those passes.
##### Reusable regression guidance
The following procedures are retained for future regression testing, **not outstanding acceptance blockers or a claim that every item was executed**. Keep UART0 and USB recovery available:
1. On IPv4, IPv6-only and dual-stack STA networks, exercise HTTPS login, serial/admin WebSockets, SSH user/admin access and one-writer/observer isolation. Test hostname access plus IPv4 and unscoped ULA/GUA IPv6 literals; verify SSH host keys and HTTPS identity rather than trusting discovery.
2. Capture DHCP DISCOVER/REQUEST option 12 at boot, rename/renew and reconnect, including a maximum-length suffix. With a configured DHCP/DNS server, verify the resulting zone entry and its server-controlled update/removal behavior.
3. Browse `_https._tcp` and `_ssh._tcp` and inspect A/AAAA/SRV answers over both families. Stop/restart each service; rename; drop/reacquire DHCP while IPv6 survives; change/deprecate/expire RA prefixes; disconnect/reconnect. Confirm eventual withdrawal/restoration, valid-address AAAA semantics and no persistent zero-A response. Exercise repeated family transitions and, where practical, missed-event/action repair.
4. Check link-local-only readiness and the fallback-AP transition explicitly. Confirm the client preserves interface scope; do not infer browser link-local success from `ONLINE` alone. Test multicast filtering/AP isolation separately from firmware address readiness.
5. Repeat the previously accepted 230400-baud mixed USB/WebSocket/SSH workload with discovery and address changes. Capture current/minimum internal, DMA and PSRAM availability, task stack margins, UART overflow and per-client drop counters. No new runtime memory reserve or maximum-baud performance guarantee is claimed.
Phase 12 is complete by the explicit user validation above. This acceptance update changes documentation only; the agent did not run a new build, test, upload, erase or device operation.
## Cross-phase release gates ## Cross-phase release gates
Every phase should satisfy the following before being marked complete: Every phase should satisfy the following before being marked complete, with any user-waived check explicitly recorded in that phase rather than reported as passed:
1. A clean release build for the ESP32-S3 N16R8 target. 1. A clean release build for the ESP32-S3 N16R8 target.
2. No new compiler or project diagnostics attributable to the change. 2. No new compiler or project diagnostics attributable to the change.
@@ -294,10 +384,10 @@ The following are not implemented merely because flash partitions or library sup
- NVS, flash, or PSRAM encryption. - NVS, flash, or PSRAM encryption.
- Secure boot or production eFuse provisioning. - Secure boot or production eFuse provisioning.
- OTA download, image confirmation, or rollback policy. - Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is complete in Phase 10; these advanced update features remain deferred.
- Core-dump collection or secret-safe core-dump processing. - Core-dump collection or secret-safe core-dump processing.
- Filesystem mounting. - Filesystem mounting.
- Public-key SSH user authentication.
- SFTP, SCP, SSH `exec`, forwarding, or subsystems. - SFTP, SCP, SSH `exec`, forwarding, or subsystems.
- General routing, NAPT, captive-portal interception, or unauthenticated serial access. - General routing, NAPT, captive-portal interception, or unauthenticated serial access.
+100
View File
@@ -0,0 +1,100 @@
# Security operations — intermittent hobby use
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.
## Provision once, locally
- Use the physical **UART0 administration console** on a trusted computer.
Native USB CDC is a separate UART1 serial-data path, not the admin console.
- Create the first administrator with `user add <username> admin`.
Use a unique password; the prompt accepts 1264 printable ASCII characters.
Add a separate `user` account for ordinary serial use, especially over SSH:
SSH `admin` opens administration, whereas SSH `user` opens UART1.
- Use unique Wi-Fi credentials, not passwords reused from other services.
Configure station profiles using the [Wi-Fi command reference](command_reference.md#wi-fi);
enter their passwords with `wifi profile secret <slot>`.
If using the device AP, set its password with `wifi ap secret`.
If the AP is unnecessary, select `wifi ap policy off`.
Run `wifi save` to persist working Wi-Fi settings.
- Before entering network credentials into a client, check device identity locally:
run `web certificate info` and `ssh host-key info` on trusted UART0.
Compare the SHA-256 certificate fingerprint in the browser certificate viewer
and the SSH host-key fingerprint in the SSH client with those local values.
Record the verified fingerprints; do not blindly dismiss certificate or changed-key warnings.
- Keep passwords in a password manager. Generated account passwords (`--generate`)
are displayed once; avoid terminal recording or sharing that output.
## Each use
- Use only a trusted LAN or the device's password-protected AP.
Do not configure router port forwarding or otherwise expose HTTPS/SSH to the Internet.
A trusted network reduces exposure; it does not make every attached client trustworthy.
- Check unexpected identity warnings against UART0 before logging in.
Keep UART0, native USB, and the connected RS-232 equipment physically controlled:
local serial access is not protected by network account authentication.
- When finished, sign out of the browser and close SSH clients.
Browser serial **Disconnect** alone is not sign-out.
- Prefer powering the device off when unused. If it must remain powered for USB
serial use, run `wifi stop` from UART0 and check `wifi status` after the queued
operation completes. This stops network access, not native USB UART1 access.
To keep Wi-Fi disabled at the next boot, follow with `wifi save`.
`wifi start` enables it again in RAM; save again only if that boot policy is wanted.
Do not rely on a temporary stop surviving reboot without saving.
## Change credentials or identity when needed
Use trusted UART0 for these operations to avoid losing a remote session mid-change.
Rotate after suspected disclosure, a lost client key, or a change of owner/access;
there is no need for a calendar-driven rotation ritual for this use case.
| What changed | Built-in operation |
|---|---|
| Account password | `user password <username>` (no-echo prompt). |
| Authorized SSH client key | `user show <username>` to identify its index; `user key delete <username> <0..2> --force`, then `user key add <username>` to enter the replacement public key. |
| Account no longer needed | `user delete <username> --force`; the final administrator is protected. |
| Wi-Fi password | Change the router credential as appropriate, then `wifi profile secret <slot>`; for the device AP use `wifi ap secret`. Finish with `wifi save`. |
| HTTPS device identity | `web certificate rotate --force`. |
| SSH device identity | `ssh host-key rotate --force`. |
Account changes invalidate that account's network sessions; identity replacement
interrupts the affected service. Check command results, then verify new fingerprints
through UART0 before updating browser trust or SSH known-host entries and reconnecting.
Changing a password does not remove authorized SSH keys; review both after compromise.
Identity rotation is not an account-password change. If an acknowledgement is lost,
inspect current state on UART0 rather than blindly repeating the operation.
## Recovery and backup limits
- Keep UART0 available for forgotten passwords, Wi-Fi mistakes, and network-service
recovery. Native USB can still carry UART1 data when networking fails, but cannot
repair accounts or identities; neither path promises uninterrupted service through reboot.
- For a healthy database, use normal UART0 account commands to repair access.
`user recover --force` is **only** for an unavailable user database: it destroys
that database's contents and rebuilds it empty, refuses a healthy database, and
requires creating a new administrator afterward. It is not a forgotten-password reset.
- `web reset --force` replaces HTTPS certificate/private-key material only;
`ssh reset --force` replaces SSH host-key material. Neither resets accounts or Wi-Fi.
These are scoped recovery operations, not a whole-device factory reset.
- No supported configuration/credential/identity backup-export command was found
in the application console. Keep a private reprovisioning record of settings,
account names/roles, client public keys, and verified fingerprints; store passwords
separately in a password manager. Do not expect status output to recover secrets
or private keys. Reprovisioning may require accepting a newly verified device identity.
- Physical extraction or firmware replacement is **not protected by this checklist**.
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
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.
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.
+239
View File
@@ -0,0 +1,239 @@
# User administration and authentication tests
Reusable procedures, **not execution evidence**. [Phase 8 is accepted](roadmap.md#phase8-acceptance-evidence); overall 8D.22 user sign-off does not assert that every checklist item below ran. Historical 8A/B sections describe their original baselines only; current cookie authentication, removed bootstrap/credentials and UART0-only recovery override them. Use current contracts and the integrated regression section for present behavior. Never log generated/entered passwords, cookies/tickets or private/verifier material.
## Current Broker management regression procedure — 8D.16
The [Broker contract](web_administration.md#broker-and-contextual-controls) defines current API, generations, wrap safety and bounds. Retain these checks for future regressions; acceptance is recorded separately.
- Verify admin-only detailed client rows against non-consuming UART0 snapshots/counters; direct normal-user management GET/POST denied. Observe zero/one/full client mix without reading UART payloads as a probe.
- Opening, selecting, cancelling, refreshing and navigating must leave writer and both terminals unchanged. Explicit full-page Refresh clears selection; contextual refresh retains selected target/version and sticky stale/absence latches until deliberate reselection. Native confirmation must identify exact target and snapshot writer.
- Assign among USB/SSH/two browser serial clients with both admins connected. Disconnect/reuse target while confirmation is open; race writer release/reacquire, competing requests, shell force and local release. Stale confirmation must fail without changing the intervening lease; Refresh/reselect/confirm explicitly.
- Test separate-login result isolation, same-login tab replacement, pending capacity, lost ACK/result, bounded polling/manual recovery, logout/expiry/revocation and HTTPS stop/restart. Never infer cancellation from connection loss or replay automatically.
- At signed-off 160 MHz/230400 baud, check full-mix binary traffic/drop isolation, UART0/USB recovery and optional-route failure isolation. Capture exact revision/mix and internal/DMA/PSRAM plus HTTPD/dispatcher margins; host tests/build do not approve reserves or target throughput for an unmeasured workload.
## Current Network settings regression procedure — 8D.12/8D.13
The [Network contract](web_administration.md#network) defines the current byte codec, owner/persistence semantics and uncertainty. Retain these regression procedures without inferring individual execution from overall sign-off.
- Verify admin-only Settings/Network and direct-route normal-user denial, current cookie/principal, body/query/framing/Origin/CSRF checks, unavailable/contended snapshots and generation races against CLI/local controls.
- Round-trip UTF-8 and arbitrary SSID bytes through text/hex, including NUL/BOM/non-UTF-8 and 32-byte boundaries. No saved PSK or length may appear in responses, status/logs/completion/local display. Keep omits credentials; Replace never accepts blank; disabled-STA Clear (including disable+clear) works; enabled-STA/AP clear is denied even when AP policy is off. Check transient-input expiry and context/session clearing.
- Distinguish RAM Apply, explicit Save, stored-only Wi-Fi Load and reboot persistence. Missing/invalid/failing Wi-Fi storage must not install generated defaults or new AP secrets. Exercise stale generations, queue failure/drop accounting and NVS failures without secret logging. No Wi-Fi reset/default/export action exists.
- Prepare UART0 and USB before confirming disruptive actions. Test Start/Stop (including RAM boot policy), Reconnect/Next, AP policies and stopped no-ops. The selected profile is an edit target, not explicit connection selection; Next uses canonical priority/wrap. Cancel confirmations and exercise lost ACK/401/disconnect, manual Check Result/Refresh, another-tab result replacement and no automatic replay. `accepted` is not online; delivery before disconnection is not guaranteed. Reconnect via STA/AP; UART0 administers recovery, USB preserves independent UART1 access.
- Test mDNS generation/Set/Save/Load/Defaults, live/offline reannouncement, next STA IP, init/live failure isolation and `applied_not_queued`. Verify actual client DNS and changed-hostname browser trust/login, not merely `announced`. Confirm no unintentional Wi-Fi secret reset.
- With USB/two web serial/SSH serial and both admin routes, verify hidden output draining and writer/observer preservation through Settings navigation. Separate actual network-disruption losses from serial/broker regressions. Exercise optional Network route/timer failure and stop/restart without taking down unrelated routes. Browser-shell restrictions remain unchanged.
- Capture boot/full-mix internal/DMA/PSRAM free/minimum/largest blocks and memory floors during TLS/Network operations; timer heap/slot costs, repeated-operation cleanup/soak and **HTTPD/dispatcher stack margins** remain required. Record exact revision/client mix and nonsecret counters, including broker and manager queue drops. Host tests do not establish target reserve or hard scheduling/cancellation guarantees.
## Current legacy-removal regression procedure
The legacy bootstrap/credential/reconciliation instructions in the phase baselines below are **historical only**, superseded by [legacy credential removal](roadmap.md#phase8-legacy-credential-compatibility) and the current [command reference](command_reference.md). Basic authentication is also historical; current HTTPS uses cookie login. The current overrides apply to later Phase 8C/browser procedures too: no `user bootstrap` or `web credentials` command remains, first-admin creation uses normal UART0 `user add`, and recovery rebuilds empty. Never treat the checklist below as evidence of execution.
1. On a disposable controlled NVS image with only `user_db/database` missing, boot and confirm an empty database is persisted with zero accounts/admins and no imported credential. Reboot and confirm it remains empty. Keep physical UART0 attached; do not factory-erase the device for this test.
2. Run `user add maint admin` on UART0, check hidden password confirmation and cancellation, then confirm account/password persistence after reboot. Separately exercise `user add operator user --generate` and secure one-time display. Final-administrator delete/demotion must still fail. Existing SSH own-password generation restrictions and typed browser generated-password support remain unchanged.
3. Load a valid existing v1 user image and verify accounts, roles, IDs, auth generations, verifiers and keys are unchanged, including a formerly migrated role-`user` account. No bootstrap status should appear and no account should be silently promoted. No web material change may synchronize a verifier.
4. On a disposable malformed user image, confirm authentication fails closed without automatically overwriting storage. On UART0 run `user recover --force`, confirm empty storage, then `user add maint admin`. Recovery must refuse healthy databases, including healthy empty storage, and be unavailable through SSH/browser. Verify serial/Wi-Fi configuration and TLS/SSH identities remain intact. Inject read/write/commit failures where available and check failure isolation and complete committed records, not partial live mutations.
5. Upgrade valid 1,392-byte v1 `web_sec/material`; verify persisted 1,340-byte TLS-only v2 and exact certificate/key DER, fingerprint and generation retention in a controlled fixture without logging private data. Reboot and confirm identity continuity. Malformed/unknown/cryptographically invalid records and migration read/write/commit failures must fail closed without fallback replacement. Do not interpret a failed commit as proof that no flash write occurred.
6. Confirm help/completion/status expose no legacy credential/bootstrap operation or secret. Removed commands must reject without mutation. Exercise retained user generation and `web certificate rotate --force` through supported frontends. `web reset --force` must change TLS only, require new certificate trust/login after HTTPS restart, leave user credentials/generations unchanged, and not revoke unrelated SSH sessions.
7. Recheck UART0/native USB availability and broker one-writer/isolated-observer behavior with network authentication unavailable. Record only nonsecret counters and telemetry. Older v1-only firmware cannot read HTTPS v2; logical NVS replacement is not secure flash erasure and no factory erase is required.
Acceptance and prior evidence limits are recorded in [legacy compatibility](roadmap.md#phase8-legacy-credential-compatibility) and [overall acceptance](roadmap.md#phase8-acceptance-evidence); the preceding procedure is not an execution log.
## Historical Phase 8A baseline — role-based database and UART0 administration
Keep UART0 physically attached throughout these tests. At the Phase 8A baseline, HTTPS and SSH still used the legacy shared credential while the new database and public keys were prepared for the Phase 8B cutover.
### 1. First-boot migration and persistence
Upgrade a device that already has working HTTPS/SSH credentials and no `user_db` blob. After boot:
1. Run `user status` and `user show admin`.
2. Confirm exactly one `admin`-named account exists with role `user`, `bootstrapped=no`, no displayed password/salt/verifier, and no public keys.
3. Confirm the pre-upgrade credential still authenticates to HTTPS and SSH and that serial terminal behavior is unchanged.
4. Reboot and confirm `user status` reports the same account ID and authentication generation.
5. Confirm routine boot logs, `web status`, and `ssh status` contain no user password, verifier, salt, or raw public-key blob.
A transient failure reading the legacy web credential must not create or commit an empty user database. If a valid `user_db` blob already exists, it must still load without the legacy credential being readable.
### 2. Administrator bootstrap
Run `user bootstrap`, verify that typed password characters are not echoed, enter a 1264 character printable password twice, and confirm `user show admin` now reports role `admin` and a higher authentication generation. Reboot and confirm `bootstrapped=yes` and one administrator persist. On a separate resettable test image, exercise `user bootstrap --generate`; record the generated password securely for the test, verify it is displayed once, then clear the terminal scrollback after use.
During Phase 8A, verify the newly bootstrapped password does **not** replace the legacy HTTPS/SSH login yet and the existing network credential continues to work. This separation is intentional until Phase 8B. A second `user bootstrap` must be rejected without changing the account.
### 3. Account CRUD, capacity, and invariants
Use generated test passwords where practical:
```text
user add operator user --generate
user add maint admin --generate
user list
user show operator
user role operator admin --force
user role operator user --force
user password operator --generate
```
Confirm unique valid usernames are accepted; uppercase, empty, oversized, or punctuation-invalid usernames are rejected. Confirm duplicate users are rejected. Fill all eight slots and verify a ninth account is rejected without altering existing records; delete a non-final account and confirm its slot can be reused with a new account ID.
With two administrators, demote or delete one and confirm the operation persists. Then attempt both deletion and demotion of the final administrator and confirm both are rejected. Before bootstrap on a separate migration run, verify `user delete admin --force` is rejected so legacy synchronization remains recoverable. `Ctrl-C` during either hidden password prompt must cancel without changing the account or leaving stale input in the next console command.
### 4. SSH public-key import and fingerprints
Generate disposable Ed25519 and ECDSA P-256 OpenSSH keys on a trusted host. For each supported type, run `user key add <username>`, paste the single `.pub` line, and confirm `user show <username>` reports the type, stable slot index, and expected `SHA256:` fingerprint without displaying the Base64 blob or comment.
Verify all of the following fail without changing the account generation or key list:
- Unsupported RSA, certificate, or unknown key types.
- Truncated/invalid Base64, mismatched outer and embedded types, trailing blob data, malformed Ed25519 lengths, and an invalid/off-curve ECDSA point.
- Adding the same key again to the same account.
- Adding a fourth key to an account that already has three.
- Deleting an empty/out-of-range slot.
Assign the same public key to a second account and confirm it is accepted, receives an independent slot, and authenticates as the username selected by the SSH client.
Then exercise `user key delete <username> <index> --force` and `user key clear <username> --force`, reboot, and confirm the exact remaining fingerprints persist. Phase 8A stores these keys but does not yet accept SSH key login.
### 5. Legacy credential rotation boundary and reboot reconciliation
Before administrator bootstrap on a resettable test image:
1. Run `web credentials rotate --force`.
2. Confirm the console reports that the pre-bootstrap migrated verifier was synchronized.
3. Verify the new legacy credential works for HTTPS and SSH, reboot, and confirm `user show admin` retains the synchronized authentication generation.
4. Where NVS fault injection or controlled power interruption is available, interrupt after the web credential commits but before the user blob commits. On the next boot, confirm reconciliation updates the migrated verifier rather than committing an empty database or losing network recovery.
After bootstrap, rotate the legacy credential again. Confirm the console explicitly says it remains separate from user passwords, existing network sessions are revoked, the new legacy credential works, and the bootstrapped administrator generation/password are not silently replaced. Apply the same checks to `web reset --force`, additionally confirming HTTPS certificate recovery/restart behavior remains intact.
### 6. Explicit database recovery and storage failures
Using a disposable device or test NVS image, inject a wrong-size/version or malformed `user_db` blob and reboot. Confirm normal database initialization fails without erasing shared NVS, but the `user` command is still registered. `user status` must report the database unavailable. Run:
```text
user recover --force
user status
user bootstrap --generate
```
Confirm recovery explicitly replaces only `user_db`, imports the current legacy credential as role `user`, and leaves serial, Wi-Fi, HTTPS identity, SSH host key, and legacy network login intact. Confirm recovery is rejected while a valid database is initialized. Simulate NVS write/commit exhaustion or failure during create, password, role, key, and recovery operations; after reboot, each operation must expose either the complete old blob or complete new blob, never a partially live mutation.
Because NVS is not yet encrypted, this test validates logical verifier storage rather than resistance to physical extraction. PBKDF2 verifiers remain susceptible to offline guessing, and append-oriented NVS may retain historical plaintext legacy credentials until a full partition/flash erase.
### 7. Phase 8A regression
With the maximum supported mix of USB CDC, WebSocket, and SSH serial clients active, repeat user list/show and several UART0 mutations. Confirm password prompts remain physical-UART-only, no user operation acquires a broker client or writer lease, existing Phase 8A network authentication and revocation behavior remains functional, and serial/broker counters show no unexplained loss. Record `memory`, `web counters`, and `ssh counters` before and after the run.
## Historical Phase 8B baseline — role-aware HTTPS and SSH authentication
These procedures preserve the Phase 8B regression baseline. Complete the Phase 8A bootstrap first and retain two disposable accounts, one `user` and one `admin`. At that baseline both roles received the serial/status interface; current Phase 8C firmware instead routes SSH administrators to the administration shell.
### 1. HTTPS passwords and fail-closed behavior
For both roles, authenticate to `/`, `/api/status`, and the browser terminal with the account password. Confirm the full 16-character username and a 64-character password work, while absent, malformed, oversized, wrong-user, and wrong-password Basic headers fail without exposing which component was wrong. After bootstrap, confirm the value from `web credentials show` no longer authenticates unless it independently equals a current user password.
On a disposable malformed-database image, confirm HTTPS returns authentication-service unavailable and never falls back to the legacy credential. Recover through physical UART0 only. Verify `web status` reports user-database authentication and active WebSocket sessions show account, user role, and password method without secrets.
### 2. SSH password and public-key login
For both `user` and `admin`, verify password SSH login reaches the same broker-backed serial stream as before. Confirm `ssh sessions` shows the account, user role, password method, and writer/observer role. An administrator must not receive UART0 commands yet.
Import one Ed25519 key and one ECDSA P-256 key through `user key add`, then verify each matching private key authenticates without a password and `ssh sessions` reports `public-key`. Confirm an unregistered key, wrong username, unsupported RSA key, certificate key, and three failed completed attempts are rejected. Normal client probe-then-sign behavior must consume one completed attempt, not two; unsigned probes alone must never create an authenticated session or broker client.
### 3. WebSocket ticket binding
Authenticate separately as two users and request tickets. Confirm each ticket is accepted exactly once, expires after 30 seconds, and cannot be reused or altered. Change one account after ticket issuance but before WebSocket connection; its ticket must fail closed without creating a broker client, while the other account's ticket still works. Repeat with delete followed by recreation of the same username and confirm the old ticket remains invalid.
### 4. Targeted session revocation
Connect simultaneous WebSocket and SSH sessions for two different users and place one affected session in writer state. One mutation at a time, exercise password change, role change, SSH-key add/delete/clear, and account deletion. Required results:
1. Outstanding tickets and active sessions for the changed username are revoked promptly and any writer lease is released.
2. The stale session cannot inject additional serial bytes or reacquire writer ownership.
3. The unrelated user's WebSocket/SSH sessions remain connected and retain their expected broker roles.
4. New authentication succeeds only with the updated password/key/role state.
5. Deleting and recreating the same username does not revive old tickets or sessions.
Run `web status`, `ssh sessions`, `web counters`, `ssh counters`, and `broker clients` around each mutation. Also rotate `web credentials rotate --force` after administrator bootstrap and confirm it changes only recovery material: role-based HTTPS/SSH sessions remain connected and user passwords are unchanged. `web reset --force` restarts HTTPS because the TLS identity changes, but must not revoke unrelated SSH sessions.
### 5. Concurrency regression
With USB CDC, two role-based network users, one WebSocket terminal, one SSH terminal, and UART1 traffic active, alternate writer ownership and mutate one account. Confirm binary transparency, observer isolation, bounded authentication/handshake behavior, UART0 responsiveness, and no unexpected disconnect of the unaffected user. Record memory, broker, web, SSH, and serial counters before and after. Repeat after reboot to verify passwords, roles, keys, and authentication methods persist.
## Phase 8C SSH administrative shell
**Validation status: passed on target hardware.** The full matrix below passed, including concurrent USB/WebSocket/user-SSH/admin-SSH operation. At 460800 baud with SSH and WebSocket clients in parallel, substantial packet drops and slower display controls were observed under load, but memory exhaustion could not be provoked and no lower baud-rate cap is planned.
Use one disposable `admin` and one disposable `user`. Keep UART0 attached throughout. The SSH server still accepts only shell sessions: `exec`, subsystem/SFTP/SCP, forwarding, and unauthenticated connections must remain rejected.
### 1. Route separation and normal shells
1. Connect as the normal user and confirm the existing broker-backed UART1 serial stream, broker client, and writer/observer behavior are unchanged.
2. Connect as the administrator and confirm the `admin@serial-tool>` prompt appears. Run `help`, `memory`, `serial status`, `wifi status`, `web status`, `broker status`, and `broker clients`. Compare representative output with UART0 and confirm both routes execute the same registered command implementations.
3. From UART0 run `ssh sessions` and `broker clients`. The user session must show `route=broker`; the admin session must show `route=admin-console`, `broker=0`, `broker-role=n/a`, and no writer lease. The admin session must not start UART1 or alter broker client/writer counts merely by connecting.
### 2. Bounded command processing
Exercise printable input, backspace, Ctrl-C, CR/LF, an empty line, and a line longer than the documented limit. Confirm the command line is bounded, overflow is discarded through a clear diagnostic, and a new prompt remains usable. Run `help`, `user list`, and `broker clients` in a normal ANSI terminal and confirm every line starts in column zero: canonical LF output must be normalized to CRLF without doubling handlers that already emit CRLF.
Run at least five distinct commands, then use Up/Down to navigate the four-entry per-session history, return to a saved draft with Down, and confirm older entries are bounded out. Verify history does not survive reconnect and is not shared with a second administrator. Exercise Tab on root and nested prefixes such as `us`, `user l`, `wifi ap sh`, `wifi next`, and `ssh host-key i`; confirm the same hints are offered by UART0 and SSH. For an ambiguous prefix, confirm both routes print the same matching candidates and redraw the unchanged input line instead of cycling candidates; unique/common prefixes must redraw cleanly without inserting escape-sequence bytes into the command. Type a command, use Left/Right to insert and delete characters in its middle, then use Home/End and the terminal's Pos1/Ende keys; verify cursor placement, Backspace, and Delete change exactly the intended byte before submission.
Run an unsupported command and confirm it is rejected without affecting UART0 or the serial broker. Run the full root `help` output to exercise output-ring draining. With the SSH client temporarily unable to read output, confirm the worker applies input backpressure rather than accepting an unbounded command/output backlog; inspect `ssh counters` for admin-console admission and input-backpressure values.
### 3. Remote account administration
Run `user list`, `user show <name>`, `user add <name> user --generate`, `user password <name> --generate`, `user role <name> admin --force`, and the key delete/clear operations from the administrative shell. Confirm generated passwords appear once only on that authenticated channel, affected account sessions are revoked, and unrelated sessions remain connected.
Import both supported key types through the remote form:
```text
user key add <username> ssh-ed25519 <base64-blob>
user key add <username> ecdsa-sha2-nistp256 <base64-blob>
```
Confirm the full ECDSA P-256 command is accepted, fingerprints appear in `user show`, a duplicate on the same account is rejected, and the same key can be imported for a second account. Verify subsequent private-key SSH login uses the selected SSH username.
### 4. Interactive administration, lifecycle actions, and revocation
Confirm `user bootstrap` and `user recover --force` remain unavailable from SSH and continue to work through physical UART0. Also confirm that an administrator cannot run `user password <own-username> --generate` over its own SSH session; entered replacement passwords and generated passwords for other accounts remain available. From admin SSH, exercise manually entered user passwords and public keys, Wi-Fi station/AP secret entry, AP secret display, legacy web recovery credential display/rotation, HTTPS certificate rotation/reset, and both `ping` and `wifi ping`. Hidden characters must not echo or enter command history; visible key input must support Backspace and Ctrl-C; ping lines must remain ordered and correctly attributed to the invoking SSH session.
Exercise `ssh disconnect` for another session and the current session. For the other-session case, confirm the acknowledgement drains, the target closes, and the source shell returns. Separately test `reboot`, `ssh stop`, `ssh host-key rotate --force`, and `ssh reset --force`: each must acknowledge scheduling, stop accepting another command, drain output, and then close/reboot as appropriate. Reconnect after key replacement and verify the new fingerprint. Simulate an unread SSH output window and confirm the destructive action cancels after its bounded drain timeout rather than remaining pending forever.
While an administrative command is queued or running, use UART0 to change that admin's role/password/key or delete it. Confirm no second remote administrative command runs after the mutation, the SSH session is revoked promptly, and queued output is not delivered to a reused SSH slot. Repeat with a different account mutation and confirm the administrator remains connected.
Finally, issue commands concurrently from UART0 and admin SSH, including `user list`, long `help` output, and one UART0 interactive password or key prompt while an SSH command waits. Confirm the single dispatcher serializes all `esp_console_run()` calls, UART0 retains its line editing/history/completion, prompt input is consumed only from UART0, outputs are not mixed between transports, and there is no stack overflow, corrupted argument parsing, database damage, or broker disruption.
## Integrated web administration regression procedure
Current retained-scope checks for future changes. [8D.22 sign-off](roadmap.md#phase8-acceptance-evidence) closes the phase; these are **not additional acceptance conditions or claims of individual execution**. Record exact revision, workload/client mix, duration, counters and heap/stack evidence for any new run. Do not treat host doubles as target timing/power-loss proof or reserve approval. Prepare UART0 recovery/native USB before disruptive tests and do not erase persisted data without explicit approval.
### 1. Integrated login and authorization
Authenticate as both roles through the same-origin login page, explicitly log out, and switch accounts without clearing browser state or relying on an HTTP Basic credential cache. Verify bounded session capacity, expiry, login throttling, malformed cookies, CSRF rejection, Origin rejection, password/role/key invalidation, deletion/recreation, and unrelated-account isolation. A normal user must be unable to discover through navigation or invoke directly any admin ticket, admin WebSocket, typed mutation API, quick setting, detailed client list, or writer-transfer operation.
### 2. Serial/admin terminal switching and lease preservation
As an administrator, connect the browser serial terminal, acquire the writer lease, send and observe serial data, then switch repeatedly between **Serial terminal** and **Admin shell**. The visible terminal contents and input route must change, but `broker clients`, the displayed browser client ID, and active writer ID must remain unchanged. Request control/Release control and writer/observer state must stay visible in both modes. While Admin shell is selected, have a normal user request the writer lease and confirm the retained browser lease prevents unintended takeover. Only explicit Release control, confirmed writer transfer, Disconnect, logout, revocation, expiry, or connection failure may release it.
Verify the browser admin shell executes the canonical registry through the single dispatcher, preserves bounded history/completion/prompts and backpressure, and does not itself become a second broker client. First-admin provisioning remains UART0-only, remote recovery is rejected, and the removed bootstrap command is unavailable everywhere. Closing only the admin-console route must leave the browser serial client and its lease intact.
### 3. Quick settings and client popovers
For an administrator, open the **Serial** and **Wi-Fi** card popovers by hover, keyboard focus, and click/tap. Confirm each path exposes the same controls, opening alone performs no mutation, Escape/outside click closes cleanly, current values refresh safely, common edits have clear apply-versus-save behavior, and links reach the full Settings section. Saved Wi-Fi secrets must not appear by default or enter routine DOM text, logs, URLs, or status responses.
Open **Broker clients** and confirm its secret-free list matches authoritative broker snapshots: client ID, transport/name, writer/observer role, and bounded queue/drop state. Open **Active writer**, choose another currently connected eligible client, confirm the transfer explicitly, and verify exactly one writer remains. Disconnect or recycle the target before confirmation and confirm its stale generation/ID is rejected without releasing or reassigning the current writer. Hover/focus alone must never transfer ownership.
### 4. Typed settings and destructive operations
Exercise user/password/role/key management, serial settings and persistence, Wi-Fi profiles/AP policy/secrets, retained SSH service/session controls, display settings, HTTPS lifecycle/reboot and HTTPS/SSH rotation through typed bounded APIs. Dedicated typed network diagnostics and ordinary browser-session/native-USB management are excluded; exercise existing shell diagnostics only through permitted frontends. Compare resulting subsystem state with the equivalent canonical CLI behavior without routing API requests through command strings. Generated passwords appear once in no-store responses; destructive or self-terminating HTTPS/SSH/reboot/security actions require explicit confirmation and explain the expected connection loss.
### 5. Concurrency and failure isolation
Run USB, browser serial, browser admin shell, user SSH, admin SSH, UART0, and active UART1 traffic concurrently. Alternate explicit writer transfers while issuing administrative commands and opening/closing popovers. Verify one writer, isolated observers, bounded memory/queues, principal revocation, no mixed admin output, no hidden lease loss during terminal switching, and continued UART0/native-USB recovery if web-session or admin-console initialization fails.
### 6. Service identity, lost acknowledgement and recovery
Confirm public fingerprints and both service/identity versions before HTTPS/SSH rotation. Race canonical CLI changes with open confirmations; stale operations must reject before mutation. For HTTPS, exercise send-return/ID-callback admission, accepted-but-lost work and failed destruction without accumulating callbacks; restart must retain owner reservation while deliberately invalidating logins. For SSH, failed stop must skip mutation/start and retained context must survive until every slot retires. Inspect precommit/postcommit partial effects without assuming an error means no change; never replay automatically. Verify new trust via UART0, fresh HTTPS login after restart, stopped-rotate versus CLI-reset behavior, and continued unrelated-service recovery. Browser identity Reset/recovery/export is not part of the workflow.
### 7. Display, API bounds and optional failures
Exercise Display generation conflicts, explicit RAM/Save/Load/Defaults/Reset, absent panel with available UI task and concurrent button activity. Reset storage failure must leave RAM unchanged. Across all typed domains check body/receive/schema limits, optional registration failures, one-slot/login isolation, queue expiry versus admitted execution, saturation and stale IDs. Secret timers cancel/wipe only non-executing work; logout/deadline cannot recall admitted commits. Compare Serial service discards with preserved broker clients/lease/output, rather than claiming uninterrupted data through explicit reconfiguration.
+138
View File
@@ -0,0 +1,138 @@
# Web administration contracts
Current implementation contracts; [Phase 10 firmware upload is complete by explicit user acceptance on 2026-09-18](roadmap.md#phase-10--simple-admin-web-firmware-upload), confirming upload and normal operation, not specific fault or recovery checks. Phase status and executed-evidence limits belong in the [roadmap](roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance record](roadmap.md#phase8-acceptance-evidence), not in implementation timelines. [Regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) describe checks, not results. Source is authoritative; start with the [code map](agent/code-map.md).
## Authentication and admission
- HTTPS-only, same-origin login replaces Basic authentication entirely. Four digest-only opaque sessions have absolute one-hour expiry; four pre-login challenges last 120 seconds. Login is globally bounded to five credential attempts per 60 seconds. Live sessions/challenges/tickets are not evicted to admit another client.
- Raw tokens travel only in the host-only `__Host-` cookie with Secure, HttpOnly, SameSite=Strict, Path=/ and explicit lifetime; no Domain. Session records retain copied secret-free principals, CSRF state and nonzero, nonreused originating-session IDs. Passwords, raw cookies/tickets, verifiers and private keys never enter routine snapshots/logs/completion/display.
- Mutation admission requires current cookie/principal, strict Origin and CSRF validation; administration additionally requires current `admin`. Normal users retain serial/status but cannot invoke administration directly. Authentication POST fetches use CORS mode with fixed same-origin URLs and same-origin credentials: do not accept Origin `null` to compensate for browser no-referrer behavior.
- Session-store initialization is part of admitted HTTPS start; authentication failure gates HTTPS. Failed start/accepted stop disables and wipes session state. Logout invalidates only the originating session before socket cleanup; account mutation invalidates that account's sessions/tickets, including deletion/recreation, without revoking unrelated accounts. Currentness checks remain authoritative if best-effort notifications fail.
- Four serial tickets and two admin tickets are digest-only, single-use, 30-second, session/principal-bound records. Cookie/Origin/ticket/currentness and transport admission precede explicit WebSocket 101. Store RNG/SHA/database calls run outside short spinlocks; IDs/expiry/epochs fence stale publication without nested store/transport locks.
- HTTPD remains bounded to six sockets, two serial WebSockets, one admin WebSocket and 40 method/path handlers; LRU eviction is disabled. Sessions, sockets, tickets and the two shared remote-console slots are separate capacity limits. Optional settings/admin failures preserve unrelated routes where their initialization contract permits; UART0 and native USB remain independent of web readiness.
`web_httpd_adapter` alone accesses private IDF 5.5.0 HTTPD state. It rejects duplicate/ambiguous headers, postpones 101 until admission, and wipes consumed header scratch while preserving right-aligned unread bytes. Optional Settings registration stages descriptor/name allocations before publishing either, avoiding the pinned public registration failure path. Re-audit these private boundaries on SDK upgrades and same-version SDK patches: the version guard does not detect patches that retain the same version number. HTTPD response headers are pointer-backed, not copied; both `Set-Cookie` value buffers must remain valid and distinct through response send. Do not reuse or wipe those buffers before sending completes. Do not enable header/ticket debug logging. Auth documents, scripts and sensitive responses are no-store with CSP/no-referrer/frame-denial protections; authored loader changes require matching CSP hashes. Generated assets are not a normal documentation/build output.
## Terminal and console ownership
Switching Serial/Admin/Settings never creates a second serial client, requests/releases the writer lease or reconnects serial. Hidden connected terminals keep draining; only selected terminal input is sent. Both browser terminals have separate 5,000-line scrollback and 64 KiB callback-accounted pending-output bounds with visible browser-drop counts. Admin input is bounded to 4 KiB admission and 512-byte frames. Admin open/reopen is explicit; admin close is isolated. Disconnect pauses serial reconnect but retains login; logout/expiry/revocation closes affected sockets normally.
The document binds terminal state to its first validated username/role/session-stable CSRF tuple. A different identity requires a clean document before showing retained output. Pagehide fences sockets/work and hides scrollback until same-session revalidation; admin is not automatically reopened. Fit readiness caches only success and uses at most three generation-fenced animation-frame retries per external request.
`admin_ssh_console` is the sole `esp_console_run()` caller for UART0, admin SSH and browser admin. Two fixed remote-console slots are shared across SSH/browser, not two per transport. Queue records carry copied principals and transport-qualified generation tokens. Owner currentness is checked outside console locks before commands and during prompts (250 ms polling plus validation/scheduling latency), then identity is rechecked. This is operation admission, not cancellation or rollback of arbitrary executing handlers. Consumed output and retired prompt/history state are wiped.
HTTPD alone owns browser-admin socket IO and its 1,552-byte PSRAM-only payload. A 20 ms timer queues at most one owner poll, not a new task. Close uses current-owner shutdown, not queued reusable `sock_db *` pointers. Detach fences submitters; queued state retires only after successful HTTPD destruction. Failed destruction retains ownership and prevents unsafe restart/reuse.
### Browser-shell policy
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
- Browser `wifi` uses the canonical command handler, including bare `wifi` (status), profile/AP settings, persistence, lifecycle and network diagnostics. Profile/AP secret entry uses the shared hidden prompt: no input echo, history or completion of secret bytes; cancellation, disconnect and failed currentness discard the input. Explicit `wifi ap show-secret` reveals the AP password only in the invoking admin terminal; routine status and completion do not reveal credentials. Browser `mdns` still permits only exact `mdns status`.
- 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 086400 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 03 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 155 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 863 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.
+32
View File
@@ -0,0 +1,32 @@
# Web admission diagnostics
Current default-disabled instrumentation, not an admission fix or an allocation-failure detector. `src/web_diagnostics.{c,h}` observes public synchronous HTTPS create/close callbacks and the four serial/admin ticket/upgrade handlers. It adds no owner task, queued probe or socket-capacity change. See [ordinary idle cleanup](https_idle_cleanup.md) for the independent cleanup policy and [acceptance evidence](roadmap.md#phase8-acceptance-evidence) for user reports.
## Usage and interpretation
Use UART0 (preferred during network stalls) or authenticated admin SSH:
```text
web diagnostics clear
web diagnostics enable
web diagnostics show
```
Enable before reproducing the failed third connection. At failure, run `web diagnostics show` promptly, alongside existing `memory`/`web counters`/browser Network timing evidence; repeat the snapshot if admission remains stalled. Then `web diagnostics disable` freezes event retention (live occupancy continues updating); `web diagnostics show` prints retained history, and `web diagnostics clear` erases history/counters without changing live sockets or enable state. The setting is RAM-only and defaults off after boot. Existing browser-shell policy deliberately denies these commands, even though shared completion offers the fixed forms. There is no HTTP diagnostic endpoint.
- Six connection records are maintained from boot even with capture disabled, so enabling on an already loaded server does not mislabel existing sockets. Each successful TLS connection gets a monotonically increasing, non-wrapping, firmware-lifetime 64-bit `conn` sequence independent of fd, cookie, ticket, broker or user identity. Clear, disable and HTTPS restart do not reset it. Internal TLS object identity is used only during synchronous cleanup; no pointer is exported or dereferenced by the console.
- Snapshot output gives its boot-relative `snapshot_us`, post-TLS occupancy split ordinary/serial-WS/admin-WS and each live fd/connection sequence/open time/age. `kind=0/1/2` means ordinary/serial/admin. Ordinary includes every successful TLS socket not yet observed as upgraded, including idle keep-alives, assets/login/settings and sockets not yet used for HTTP. Classification is actual public `httpd_ws_get_fd_info()` state after upgrade-handler return, **not inferred from `ESP_OK`**. No URI string is inspected.
- The fixed 32-entry overwrite ring records successful TLS open, TLS transport-context close, and enter/result for serial/admin ticket and upgrade handlers. `t_us` is boot-relative observation time; result `dt_us` measures the underlying handler only (including its work/IO, excluding entry resource sampling); close `dt_us` is successful-TLS connection lifetime. Open/enter durations are zero. Open/result occupancy includes the connection; close occupancy excludes it. Event IDs survive clear; `overwritten` counts evicted retained records since clear. `unmatched` counts duplicate create/unmatched close; `lost` counts untrackable creates (getter failure, metadata/sequence exhaustion). Nonzero anomaly counters mean occupancy is not trustworthy as complete evidence. Counters saturate.
- `rc` is the exact handler return, **not HTTP status or ticket issuance outcome**: sending a 401/403/503 can return `ESP_OK`. Match browser HTTP status and existing rejection/issuance counters; there is no ticket-value correlation, request ID, authentication identity, header/body/query logging, or allocation-failure attribution. Upgrade success is visible in the occupancy classification. Ring overwrite or clear/toggle during an operation can leave unpaired enter/result records; do not invent a duration for a missing result.
- Each retained event samples free/largest bytes for internal 8-bit, internal DMA and PSRAM 8-bit, plus the current HTTPD task's minimum-free stack **in ESP-IDF bytes**. Capability scans run outside the diagnostic lock. Samples are sequential, not an atomic heap snapshot; overlapping internal/DMA pools must not be added. Stack watermark includes diagnostic call overhead and is not dispatcher margin. `show` does not query a live task handle: resources are historical event samples, not fresh heap values at show time. Use `memory` for current system heap.
- Console snapshots copy only local diagnostic metadata under a short portMUX, never inspect HTTPD session internals or wait for its owner. Printing occurs outside the lock. At most 32 ID-qualified rows and six live records are printed, even during churn; concurrent clear/overwrite is reported as “no longer retained”. Capture epochs reject samples crossing enable/disable/clear, and sequence checks prevent a stale upgrade result from reclassifying a reused fd. No queued diagnostic work exists, hence no outstanding probe or stale queue lifetime to retire on restart. Minimal connection bookkeeping and upgrade classification remain active while disabled; heap scans and event recording do not. Instrumentation still has CPU/static RAM cost, not zero perturbation.
## SDK audit and explicit blind spots
Audited installed PlatformIO ESP-IDF **5.5.0**, `components/esp_https_server/src/https_server.c` (`httpd_ssl_open`, `httpd_ssl_close`), `components/esp_http_server/src/httpd_sess.c` (`httpd_sess_delete`) and `httpd_main.c`. HTTPS performs synchronous TLS creation before the configured `open_fn` and `user_cb` create callback. It installs a transport-context destructor; that destructor invokes the public close callback before deleting TLS/freeing the context. HTTPD's default close closes the fd, then clears contexts, then frees its session slot. The diagnostic close therefore marks a cleanup observation, not a FIN timestamp or causal close reason. It uses the stored fd, not a getter on an already-closed socket. **Neither `open_fn` nor `close_fn` is replaced**, preserving all existing HTTPS cleanup ownership and failure behavior. Callbacks execute synchronously under the existing HTTPD lifecycle; successful stop finishes cleanup before restart, while failed/partial stop retains remaining live metadata. No asynchronous fd-only events are consumed, avoiding event-delay/fd-reuse ambiguity.
This deliberately bounded first slice does **not** measure TCP connect/accept/listen backlog, pending clients when IDF stops accepting at capacity, handshake begin/duration/failure, TLS allocation failure or aggregate lwIP socket pressure. The public configured open hook is post-TLS too; adding it would not fix these blind spots. No owner-queued client-list probe is added. Occupancy is an owner-published **successful-TLS lower bound**, not the complete HTTPD session table while a handshake is in progress. A clean **6/6** snapshot supports established-connection saturation at that instant (three WS + three ordinary is directly distinguishable); fewer than six does not exonerate admission/TLS/global socket pressure. Correlate time with browser evidence and existing secret-free TLS errors; do not call this pre-TLS tracing or claim the root cause is proven. Existing capacities, receive/send/handshake timeouts and accepted admission issue remain unchanged.
## Regression entry points and limits
`python3 tests/web_diagnostics/run.py` covers bounded metadata/ring behavior and SDK guards; `python3 tests/web_admin_transport/server_lifecycle.py` covers lifecycle composition. These are reusable host commands, not a new execution claim. Doubled TLS/scheduling and host tests cannot establish target admission latency, reserve floors or fault causes. Capture on UART0 alongside `memory`, `web counters`, browser timings and non-consuming broker/serial counters, without cookies/tickets/headers. Generic TLS NET_RECV_FAILED (`-0x004C`) and NET_CONN_RESET (`-0x0050`) are not OOM diagnoses.
+77
View File
@@ -0,0 +1,77 @@
# Web serial throughput diagnostics
## Current contract and accepted fix
Default-disabled instrumentation measures broker fan-out and binary WebSocket TX, not CPU usage, peer receipt or pure scheduler latency. The user separately accepted the combined binary header/payload-send fix at **160 MHz / 230400 baud with full mix including browser admin**. [Acceptance evidence](roadmap.md#phase8-acceptance-evidence) distinguishes that report from the latest capture and unmeasured reserves. The old frequency-only experiment is not the retained solution.
`web_httpd_ws_send_binary` is an IDF-5.5.0-pinned owner-only session-override send with a bounded 516-byte header/payload scratch copy and 512-byte payload. Generation validation and one outstanding item per slot remain. A non-full send installs a reject-only override before close/shutdown, preventing SDK automatic PONG/CLOSE from reentering TLS with different arguments after incomplete output, even if shutdown fails. No retry/replay; HTTPD owns TLS destruction. Text/control/admin retain the SDK sender. One API call does not promise one TLS record/packet or peer receipt. Keep CPU160, priorities, scheduling and 4096-byte broker/512-byte web payload bounds unless separately changing them with evidence.
## Broker accounting
`broker counters` retains global totals and adds one row per active client: generation-safe ID, type, pending bytes, output high-water mark (HWM), UART bytes considered for that client, queued bytes, read bytes and dropped bytes. `broker clients` helps map the IDs to active transports; `web performance show` supplies each web slot's broker ID.
- HWM is peak output occupancy, bounded by 4,096 bytes. `broker clear-counters` clears totals but seeds each active HWM with current pending occupancy, not zero. It does not drain queues or change ownership.
- `read` means handed to a transport, not received or rendered by a peer. Queued counts successful copies; UART counts bytes considered while the client was connected.
- Disconnected per-client rows disappear and slot/generation reuse resets them. Global counters retain disconnected traffic until cleared, including accepted-but-unread output discarded on disconnect. Thus global drops need not mean only overflow, and queued plus dropped need not equal UART fan-out after disconnect discards.
- Capture before disconnect. **Do not use `broker read` for observation:** it consumes data intended for that transport. Snapshot/counter commands do not consume serial data.
## Independent web performance capture
Use physical UART0 for the least intrusive capture. Commands are also available through the authenticated admin SSH registry. `web performance enable|disable|show|clear` is separate from admission tracing (`web diagnostics enable|disable|show|clear`) and from ordinary `web counters|clear-counters`.
Performance capture defaults disabled. Enable resumes aggregates; disable freezes aggregates; clear resets aggregates while preserving enable state. Each control operation advances a nonwrapping epoch and fences in-flight samples. Exhaustion fails closed. Slot generation and broker identity also fence reuse; this is not a disconnected-session archive.
Each active one of two fixed slots shows fd, generation, broker ID, pending/measured-pending/executing state and current-epoch `pending_age_us`. Age starts at reservation-path entry and includes an executing send. If `measured_pending=0`, zero age is unavailable, not evidence of immediate service. Disabling invalidates current-epoch pending age; capture a live `show` only if investigating a stuck queue, accepting its perturbation.
Only serial **binary TX** is measured. Text/control frames are not samples, though they can delay subsequent binary work. No payloads, passwords, keys, cookies, tickets, verifier material or other authentication secrets are retained or printed.
| Printed timing | Exact interpretation |
|---|---|
| `queue->callback-entry` | Reservation-path entry to HTTPD callback entry, timestamped before the callback takes the transport lock. Includes submission-path work, not just HTTPD queue residence; owned callbacks retired without sending can count. |
| `send-call` | Time around the actual synchronous HTTPD-owner send API call. Return is not peer acknowledgement, browser receipt or rendering. |
| `completion->first-drain-attempt-return` | Successful send-call completion to return of the first subsequent broker read attempt, including empty/error attempts. |
| `completion->next-nonempty-drain-return (includes idle)` | Completion to the next successful nonempty broker read return, possibly after empty attempts and source idle time. |
| `completion->first-attempt-nonempty-return` | Subset where the first subsequent read returns data. Excludes observed empty attempts, but does not prove backlog existed at send completion. |
Each timing reports count, sum in microseconds, integer estimated average (`sum/count`, zero for no samples) and maximum. Completion intervals are not scheduler-only: they include transport work, broker read/mutex time and intervening control-frame work, and may include idle time even in the first-attempt-nonempty subset. They do not isolate a particular task, TLS operation or network cause.
`queued_frames/bytes` counts transport reservations, including reported queue failures; `queue_errors` records those failures. `sent_frames/bytes` counts successful send-call returns. `send_errors` includes owner-context rejection, whereas send-call timings require an actual API call. `retired` records measured callbacks retired without sending. Saturating aggregates set `saturated=1`; totals and averages then cease to be reliable. Epoch fencing intentionally prevents old work contaminating a new capture, so do not demand equality across toggles or in-flight clears.
## Reproducible UART0 capture
1. Record firmware/build identity, baud/framing/flow control, source burst size, browser versions, client IDs/roles and full-mix topology. Establish all serial clients and both admin routes first. Quiet the UART source and allow queues and connection activity to settle. Do not reconnect/login during the trace.
2. Optionally stop independent admission tracing before the timed capture. Then reset in this order, while quiet:
```text
web diagnostics disable
web performance clear
web performance enable
broker clear-counters
serial clear-counters
web clear-counters
```
The first command is optional; record whether admission tracing was enabled. Resets are sequential, not an atomic cross-service snapshot.
3. Send the known burst once, stop the source and wait for transport queues to drain. Avoid repeated console/status printing during the burst. A quiet final broker pending count of zero alone does not prove browser receipt; retain peer byte counts too.
4. Stop performance capture first, then capture all counters **before disconnecting any client**:
```text
web performance disable
web performance show
broker counters
serial counters
web counters
broker clients
```
Disable already prints the performance snapshot; `show` is an explicit repeatable frozen-aggregate record. Keep connection counts and source/peer totals with the sample.
5. Only after saving evidence, change to one browser, keeping the other full-mix participants and serial settings equivalent. Quiet/settle and repeat the complete reset/burst/drain/stop/capture sequence. Do not compare a fresh capture to lifetime counters.
6. Separately compare equivalent performance-enabled and performance-disabled bursts with fresh ordinary counter resets. Keep performance disabled for the latter; do not treat retained performance aggregates as that run's measurements. Keep admission tracing state equal and record it.
At 230400 baud, 8N1, 4,096 bytes represents approximately **177.8 ms** of continuous input retention from an empty broker queue, ignoring flow-control pauses. Compare per-client drops/HWM with latency maxima against this scale, not as a hard deadline or proof of cause; occupancy, idle gaps and timings cover different boundaries. For future regressions, gather per-client attribution and controlled comparisons before changing buffers, priorities, scheduling or CPU.
## Resources and regression references
Two fixed diagnostic records plus nonwrapping epoch/timestamp/slot fences and broker HWM fields add bounded storage, no new instrumentation allocations/tasks/queues/payloads. Disabled is not zero overhead: gate branches remain and broker occupancy/HWM is always active. Enabled timing and short aggregate locks have runtime costs not established by host tests. The combined-send local scratch adds HTTPD stack use; actual reserve remains a target measurement.
`tests/session_broker_diagnostics/run.py` covers counters and generation-safe management; `tests/web_serial_performance/run.py` covers production send/drain paths, epochs, retirement, SDK wire compatibility and failed-send/control-reply isolation. `tests/admin_ssh_policy/run.py` covers SSH-allowed/browser-denied tracing commands. Prior focused/broad host validation and firmware builds passed; no tests were run during documentation consolidation. These harnesses do not prove real TLS/scheduler overhead, peer-byte integrity or long-soak behavior. Use the capture procedure above for future attribution, retaining explicit workload/duration/counter provenance.
+32 -1
View File
@@ -100,9 +100,40 @@ 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. 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
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 015 and blue rows 1663, divided by a narrow physical black separator:
| Device connection | ESP32-S3 connection | Electrical behavior | Purpose |
|---|---:|---|---|
| OLED `VCC` | `3V3` | 3.3 V power only | OLED power and I²C pull-up rail |
| OLED `GND` | `GND` | Common ground | OLED return and I²C reference |
| OLED `SDA` | GPIO11 | I²C data | Display data |
| OLED `SCL` | GPIO12 | I²C clock | Display clock |
| Previous/back button | GPIO10 to `GND` | Active-low input with internal pull-up | Previous item or back |
| Select/confirm button | GPIO13 to `GND` | Active-low input with internal pull-up | Select or confirm |
| Next button | GPIO14 to `GND` | Active-low input with internal pull-up | Next item |
```text
ESP32-S3-DevKitC-1 N16R8 128×64 I²C OLED
──────────────────────── ────────────────
3V3 ────────────> VCC
GND ────────────> GND
GPIO11 / SDA <───────────> SDA
GPIO12 / SCL ────────────> SCL
GPIO10 ───── previous/back button ───── GND
GPIO13 ───── select/confirm button ──── GND
GPIO14 ───── next button ────────────── GND
```
> **OLED voltage warning:** Power OLED `VCC` from `3V3`, not 5 V. Many OLED modules connect their SDA/SCL pull-up resistors directly to `VCC`; powering such a module from 5 V could expose the ESP32-S3 GPIOs to unsafe I²C levels. Power down before attaching or removing loose OLED wiring. Only perform live-removal fault tests with a connector designed to avoid shorts and unintended pin sequencing.
Before applying power, verify whether the module already includes SDA and SCL pull-ups and where they terminate. Any module-mounted or external I²C pull-ups must go to 3.3 V. If pull-ups are absent, add suitable external pull-ups from SDA and SCL to `3V3`; if they are present, account for their parallel resistance before adding more. The buttons normally need no external pull-ups because firmware enables the ESP32 internal pull-ups.
## Electrical verification ## Electrical verification
See [Electrical tests](electrical_tests.md) for safe loopback wiring, polarity checks, UART flow-control verification, and session-broker loopback testing. 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.
## Future hardware profiles ## Future hardware profiles
+3
View File
@@ -0,0 +1,3 @@
Import("env")
env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=True)
+7
View File
@@ -0,0 +1,7 @@
# KiCad per-user state, locks and automatic backups
*.kicad_prl
*.lck
~*.lck
*-backups/
*.bak
__pycache__/
+116
View File
@@ -0,0 +1,116 @@
(kicad_symbol_lib (version 20231120) (generator "kicad_symbol_editor")
(symbol "HW678_N16R8" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 34.29 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "HW678_N16R8" (at 0 31.75 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:HW678_2x22_Provisional" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "HW678_N16R8_0_1" (rectangle (start -17.78 29.21) (end 17.78 -29.21) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "HW678_N16R8_1_1"
(pin power_out line (at -22.86 26.67 0) (length 5.08) (name "3V3" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at -22.86 24.13 0) (length 5.08) (name "3V3" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 21.59 0) (length 5.08) (name "EN" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 19.05 0) (length 5.08) (name "GPIO4" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 16.51 0) (length 5.08) (name "GPIO5" (effects (font (size 1.016 1.016)) )) (number "5" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 13.97 0) (length 5.08) (name "GPIO6" (effects (font (size 1.016 1.016)) )) (number "6" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 11.43 0) (length 5.08) (name "GPIO7" (effects (font (size 1.016 1.016)) )) (number "7" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 8.89 0) (length 5.08) (name "GPIO15" (effects (font (size 1.016 1.016)) )) (number "8" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 6.35 0) (length 5.08) (name "GPIO16" (effects (font (size 1.016 1.016)) )) (number "9" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 3.81 0) (length 5.08) (name "GPIO17" (effects (font (size 1.016 1.016)) )) (number "10" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 1.27 0) (length 5.08) (name "GPIO18" (effects (font (size 1.016 1.016)) )) (number "11" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -1.27 0) (length 5.08) (name "GPIO8" (effects (font (size 1.016 1.016)) )) (number "12" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -3.81 0) (length 5.08) (name "GPIO3" (effects (font (size 1.016 1.016)) )) (number "13" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -6.35 0) (length 5.08) (name "GPIO46" (effects (font (size 1.016 1.016)) )) (number "14" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -8.89 0) (length 5.08) (name "GPIO9" (effects (font (size 1.016 1.016)) )) (number "15" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -11.43 0) (length 5.08) (name "GPIO10" (effects (font (size 1.016 1.016)) )) (number "16" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -13.97 0) (length 5.08) (name "GPIO11" (effects (font (size 1.016 1.016)) )) (number "17" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 -16.51 0) (length 5.08) (name "GPIO12" (effects (font (size 1.016 1.016)) )) (number "18" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -19.05 0) (length 5.08) (name "GPIO13" (effects (font (size 1.016 1.016)) )) (number "19" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -21.59 0) (length 5.08) (name "GPIO14" (effects (font (size 1.016 1.016)) )) (number "20" (effects (font (size 1.016 1.016)) )))
(pin passive line (at -22.86 -24.13 0) (length 5.08) (name "5V" (effects (font (size 1.016 1.016)) )) (number "21" (effects (font (size 1.016 1.016)) )))
(pin power_out line (at -22.86 -26.67 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "22" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 26.67 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "23" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 24.13 180) (length 5.08) (name "GPIO43" (effects (font (size 1.016 1.016)) )) (number "24" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 21.59 180) (length 5.08) (name "GPIO44" (effects (font (size 1.016 1.016)) )) (number "25" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 19.05 180) (length 5.08) (name "GPIO1" (effects (font (size 1.016 1.016)) )) (number "26" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 16.51 180) (length 5.08) (name "GPIO2" (effects (font (size 1.016 1.016)) )) (number "27" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 13.97 180) (length 5.08) (name "GPIO42" (effects (font (size 1.016 1.016)) )) (number "28" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 11.43 180) (length 5.08) (name "GPIO41" (effects (font (size 1.016 1.016)) )) (number "29" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 8.89 180) (length 5.08) (name "GPIO40" (effects (font (size 1.016 1.016)) )) (number "30" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 6.35 180) (length 5.08) (name "GPIO39" (effects (font (size 1.016 1.016)) )) (number "31" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 3.81 180) (length 5.08) (name "GPIO38" (effects (font (size 1.016 1.016)) )) (number "32" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 1.27 180) (length 5.08) (name "GPIO37" (effects (font (size 1.016 1.016)) )) (number "33" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -1.27 180) (length 5.08) (name "GPIO36" (effects (font (size 1.016 1.016)) )) (number "34" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -3.81 180) (length 5.08) (name "GPIO35" (effects (font (size 1.016 1.016)) )) (number "35" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -6.35 180) (length 5.08) (name "GPIO0" (effects (font (size 1.016 1.016)) )) (number "36" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -8.89 180) (length 5.08) (name "GPIO45" (effects (font (size 1.016 1.016)) )) (number "37" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -11.43 180) (length 5.08) (name "GPIO48" (effects (font (size 1.016 1.016)) )) (number "38" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -13.97 180) (length 5.08) (name "GPIO47" (effects (font (size 1.016 1.016)) )) (number "39" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -16.51 180) (length 5.08) (name "GPIO21" (effects (font (size 1.016 1.016)) )) (number "40" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -19.05 180) (length 5.08) (name "GPIO20" (effects (font (size 1.016 1.016)) )) (number "41" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -21.59 180) (length 5.08) (name "GPIO19" (effects (font (size 1.016 1.016)) )) (number "42" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 -24.13 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "43" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 -26.67 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "44" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "MAX3243_Module_Reference" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 22.86 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "MAX3243_Module_Reference" (at 0 20.32 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "MAX3243_Module_Reference_0_1" (rectangle (start -15.24 17.78) (end 15.24 -17.78) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "MAX3243_Module_Reference_1_1"
(pin power_in line (at -20.32 13.97 0) (length 5.08) (name "Vin" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin power_in line (at -20.32 11.43 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 8.89 0) (length 5.08) (name "DCD" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 6.35 0) (length 5.08) (name "RX" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 3.81 0) (length 5.08) (name "TX" (effects (font (size 1.016 1.016)) )) (number "5" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 1.27 0) (length 5.08) (name "DTR" (effects (font (size 1.016 1.016)) )) (number "6" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -1.27 0) (length 5.08) (name "DSR" (effects (font (size 1.016 1.016)) )) (number "7" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 -3.81 0) (length 5.08) (name "RTS" (effects (font (size 1.016 1.016)) )) (number "8" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -6.35 0) (length 5.08) (name "CTS" (effects (font (size 1.016 1.016)) )) (number "9" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -8.89 0) (length 5.08) (name "RI" (effects (font (size 1.016 1.016)) )) (number "10" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -11.43 0) (length 5.08) (name "VLD" (effects (font (size 1.016 1.016)) )) (number "11" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 -13.97 0) (length 5.08) (name "OFF_N" (effects (font (size 1.016 1.016)) )) (number "12" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "OLED_I2C_26mm" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "DS" (at 0 12.7 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "OLED_I2C_26mm" (at 0 10.16 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:OLED_26mm_I2C_Provisional" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "OLED_I2C_26mm_0_1" (rectangle (start -10.16 7.62) (end 10.16 -7.62) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "OLED_I2C_26mm_1_1"
(pin power_in line (at -15.24 3.81 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin power_in line (at -15.24 1.27 0) (length 5.08) (name "VCC" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin input line (at -15.24 -1.27 0) (length 5.08) (name "SCL" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -15.24 -3.81 0) (length 5.08) (name "SDA" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "SW_NO" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "SW" (at 0 7.62 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "SW_NO" (at 0 5.08 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "SW_NO_0_1" (polyline (pts (xy -2.54 0) (xy 2.54 1.524)) (stroke (width 0.254) (type default)) (fill (type none)))(circle (center -2.54 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none)))(circle (center 2.54 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none))))
(symbol "SW_NO_1_1"
(pin passive line (at -5.08 0 0) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 5.08 0 180) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "R" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "R" (at 0 6.096 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "R" (at 0 3.556 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Resistor, axial DIN0207 footprint; exact MPN pending" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "R_0_1" (rectangle (start -2.54 1.016) (end 2.54 -1.016) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "R_1_1"
(pin passive line (at -5.08 0 0) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 5.08 0 180) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
)
)
)
@@ -0,0 +1,56 @@
(footprint "HW678_2x22_Provisional"
(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")
(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))))
(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))
(pad "4" thru_hole circle (at 0.00 7.62) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "5" thru_hole circle (at 0.00 10.16) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "6" thru_hole circle (at 0.00 12.70) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "7" thru_hole circle (at 0.00 15.24) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "8" thru_hole circle (at 0.00 17.78) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "9" thru_hole circle (at 0.00 20.32) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "10" thru_hole circle (at 0.00 22.86) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "11" thru_hole circle (at 0.00 25.40) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "12" thru_hole circle (at 0.00 27.94) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "13" thru_hole circle (at 0.00 30.48) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "14" thru_hole circle (at 0.00 33.02) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "15" thru_hole circle (at 0.00 35.56) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "16" thru_hole circle (at 0.00 38.10) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "17" thru_hole circle (at 0.00 40.64) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "18" thru_hole circle (at 0.00 43.18) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "19" thru_hole circle (at 0.00 45.72) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "20" thru_hole circle (at 0.00 48.26) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "21" thru_hole circle (at 0.00 50.80) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "22" thru_hole circle (at 0.00 53.34) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "23" thru_hole circle (at 25.40 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "24" thru_hole circle (at 25.40 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "25" thru_hole circle (at 25.40 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "26" thru_hole circle (at 25.40 7.62) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "27" thru_hole circle (at 25.40 10.16) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "28" thru_hole circle (at 25.40 12.70) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "29" thru_hole circle (at 25.40 15.24) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "30" thru_hole circle (at 25.40 17.78) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "31" thru_hole circle (at 25.40 20.32) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "32" thru_hole circle (at 25.40 22.86) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "33" thru_hole circle (at 25.40 25.40) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "34" thru_hole circle (at 25.40 27.94) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "35" thru_hole circle (at 25.40 30.48) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "36" thru_hole circle (at 25.40 33.02) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "37" thru_hole circle (at 25.40 35.56) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "38" thru_hole circle (at 25.40 38.10) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "39" thru_hole circle (at 25.40 40.64) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "40" thru_hole circle (at 25.40 43.18) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "41" thru_hole circle (at 25.40 45.72) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "42" thru_hole circle (at 25.40 48.26) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "43" thru_hole circle (at 25.40 50.80) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "44" thru_hole circle (at 25.40 53.34) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
)
@@ -0,0 +1,34 @@
(footprint "MAX3243_Reference_Provisional"
(version 20260206)
(generator "pcbnew")
(layer "F.Cu")
(descr "PROVISIONAL female 5988 CAD reference / VERIFY MALE 6253 geometry and pad map. Top view DE9 toward -Y; origin JP2 pin 1. Female reference: 1 Vin, 2 GND, 3 DCD, 4 RX, 5 TX, 6 DTR, 7 DSR, 8 RTS, 9 CTS, 10 RI, 11 VLD, 12 OFF. PROVISIONAL connector fit: carrier pads 1.70 mm / drill 1.00 mm. No verified DE9 envelope or courtyard. CAD-derived outline: Limor Fried/Ladyada for Adafruit Industries, CC BY-SA 3.0; see rs232-footprint-notes.md")
(tags "MAX3243 module female5988 reference male6253 provisional THT")
(attr through_hole)
(property "Reference" "REF**" (at 13.97 -23) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
(property "Value" "MAX3243_Reference_Provisional" (at 13.97 -20.5) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
(fp_text user "PROVISIONAL" (at 13.97 -16) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
(fp_text user "FEMALE CAD REF" (at 13.97 -14) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))))
(fp_text user "VERIFY MALE" (at 13.97 -12.3) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))))
(fp_text user "1" (at 0 1.7) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.12))))
(fp_line (start 29.845 0.127) (end 29.845 -26.67) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(fp_line (start 29.845 -26.67) (end -1.905 -26.67) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(fp_line (start -1.905 -26.67) (end -1.905 0.127) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(fp_arc (start -1.905 0.127) (mid -1.161051224 1.923051224) (end 0.635 2.667) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(fp_line (start 0.635 2.667) (end 27.305 2.667) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(fp_arc (start 27.305 2.667) (mid 29.101051224 1.923051224) (end 29.845 0.127) (stroke (width 0.1) (type solid)) (layer "F.Fab"))
(pad "1" thru_hole rect (at 0 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "2" thru_hole circle (at 2.54 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "3" thru_hole circle (at 5.08 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "4" thru_hole circle (at 7.62 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "5" thru_hole circle (at 10.16 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "6" thru_hole circle (at 12.7 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "7" thru_hole circle (at 15.24 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "8" thru_hole circle (at 17.78 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "9" thru_hole circle (at 20.32 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "10" thru_hole circle (at 22.86 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "11" thru_hole circle (at 25.4 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "12" thru_hole circle (at 27.94 0) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "" np_thru_hole circle (at 1.905 -8.763) (size 3 3) (drill 3) (layers "*.Cu" "*.Mask"))
(pad "" np_thru_hole circle (at 26.035 -8.763) (size 3 3) (drill 3) (layers "*.Cu" "*.Mask"))
)
@@ -0,0 +1,21 @@
(footprint "OLED_26mm_I2C_Provisional"
(version 20260206)
(generator "pcbnew")
(layer "F.Cu")
(descr "User-verified front/display view, header at top; origin top-left PCB corner; 1 GND, 2 VCC, 3 SCL, 4 SDA; 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 13 -2) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
(property "Value" "OLED_26mm_I2C_Provisional" (at 13 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
(fp_text user "PROVISIONAL CONNECTOR FIT" (at 13 30) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
(fp_rect (start 0 0) (end 26 26) (stroke (width 0.1) (type solid)) (fill no) (layer "F.Fab"))
(fp_rect (start 1 4.5) (end 25.5 21) (stroke (width 0.1) (type dash)) (fill no) (layer "F.Fab"))
(pad "1" thru_hole rect (at 9.19 1.50) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "2" thru_hole circle (at 11.73 1.50) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "3" thru_hole circle (at 14.27 1.50) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "4" thru_hole circle (at 16.81 1.50) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
(pad "" np_thru_hole circle (at 1.75 1.75) (size 2 2) (drill 2) (layers "*.Cu" "*.Mask"))
(pad "" np_thru_hole circle (at 24.25 1.75) (size 2 2) (drill 2) (layers "*.Cu" "*.Mask"))
(pad "" np_thru_hole circle (at 1.75 24.25) (size 2 2) (drill 2) (layers "*.Cu" "*.Mask"))
(pad "" np_thru_hole circle (at 24.25 24.25) (size 2 2) (drill 2) (layers "*.Cu" "*.Mask"))
)
+261
View File
@@ -0,0 +1,261 @@
# KiCad 10 module carrier — schematic and routed PCB draft
Open **`serial-carrier.kicad_pro`** in KiCad 10, then open
**`serial-carrier.kicad_sch`**. A readable export is **`serial-carrier.pdf`**.
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 carrier draft.** It is **not fabrication-ready**.
See **`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Ω
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.
## Files and use
| File | Purpose |
|---|---|
| `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 |
| `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 |
| `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 |
| `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
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.
**Normal validation from the repository root:**
```sh
python3 hardware/PCB/validate_schematic.py
```
This checks the current schematic without regenerating it, exports its netlist,
runs KiCad ERC, checks all 17 connected nets and all 15 firmware GPIO assignments,
and refreshes PDF/SVG review exports. It requires Python 3 and `kicad-cli`.
The expected-net snapshot records the initial design contract; intentional wiring
changes need a deliberate update to the checks and snapshot.
**Do not casually rerun `generate_schematic.py`:** it overwrites the schematic,
symbol library, library tables and expected-net snapshot, discarding subsequent
manual edits to those files. It preserves existing project settings and does not
modify footprints. The generated KiCad files are the editable design artifacts.
## Schematic conventions and design decisions
- U1 numbering is project-specific: viewed from the component side, antenna up
and USB down, left row is 122 and right row is 2344, both top-to-bottom.
These numbers map directly to the local HW678 footprint, not to GPIO numbers
or the official DevKit header reference designators. Signal order was transcribed
from the user-supplied component-side board-label photos; the user confirmed
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 14 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.
- No-connect crosses mean unused on the carrier, not disconnected internally.
Native USB GPIO19/20, UART0 GPIO43/44, memory-conflicting GPIO3537 and the 5 V
header are not connected to carrier circuitry.
- U1 pin 1 (3V3) and pin 22 (GND) model the onboard supply as power outputs for
ERC; the other duplicate rail pins are passive. This does not verify the
regulator's capacity or USB power isolation. GPIO9 is modeled as a programmable
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 R1R3, **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,
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.
DIN0207 axial footprints are selected; exact resistor MPNs remain pending.
- External I²C pull-ups are **not assigned guessed values**. Verify OLED onboard
pull-ups, resistance and rail before electrical sign-off and add appropriately
sized carrier pull-ups if needed. The OLED must be powered from 3.3 V.
## Validation status
KiCad CLI 10.0.6 loaded the schematic and exported PDF, SVG and XML netlist.
ERC reported **zero errors/warnings**, with no project exclusions added. Exact
net memberships, all 44 U1 pins and firmware GPIO assignments were checked.
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.
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
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.
No firmware build or device tests were run: this PCB work did not modify firmware.
## Evidence and components
Repository sources: `../../docs/wiring.md`, `../../src/board_pins.h`,
`../../docs/electrical_tests.md`, and `../../README.md`.
| 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 | Published female CAD pin map and geometry verified; male mapping/fit remains provisional. Reference footprint used on PCB only; schematic assignment remains blank. |
| 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. |
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.
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. No male-specific module footprint is claimed verified.
## Verified logical connections for the existing firmware profile
This table uses signal names, **not physical connector positions**. The schematic
and its local symbols additionally capture the project's physical pad numbering.
The wiring guide warns that compatible ESP32 boards may have different layouts.
| ESP32 connection | Peripheral connection |
|---|---|
| 3V3 | RS-232 Vin; existing I²C OLED VCC |
| GND | RS-232 GND; OLED GND; one contact of each button |
| GPIO17 | RS-232 TX |
| GPIO18 | RS-232 RX |
| GPIO15 | RS-232 RTS |
| GPIO16 | RS-232 CTS |
| GPIO7 | RS-232 DTR |
| GPIO5 | RS-232 DSR |
| GPIO4 | RS-232 DCD |
| GPIO6 | RS-232 RI |
| GPIO8 | RS-232 VLD |
| GPIO9 | RS-232 OFF (active-low shutdown; open-drain drive) |
| GPIO11 | Existing I²C OLED SDA |
| GPIO12 | Existing I²C OLED SCL |
| GPIO10 | Previous/back switch to GND |
| GPIO13 | Select/confirm switch to GND |
| GPIO14 | Next switch to GND |
## Electrical constraints from the repository
- 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Ω
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.
- Preserve access to both development-board USB connectors and account for the
antenna keepout using the exact board/module documentation.
## User-confirmed design direction
- ESP32 purchase: https://www.amazon.de/dp/B0F3XMYYQY
- 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.
- 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-supplied mechanical evidence
- ESP32 listing pictures show HW-678 V0.0.0; user confirms matching markings and
actual shield S3-N16R8. Disregard the listing picture's N8R2 marking for memory
identification and its erroneous USB labels: native USB is GPIO19 D / GPIO20 D+.
- ESP32 raw measurements: header first-to-last centres initially 52.6 mm, revised
to 52.93 mm; row spacing measured 25.00 mm. User subsequently confirmed both
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.
- 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,
confirmed on actual module. Raw first-to-last span revised from 7 mm to 7.49 mm;
user confirms standard breadboard fit and agrees nominal pitch is 2.54 mm
(7.62 mm span).
- OLED mounting-hole diameter measured as 2 mm. User approved the latest edge
measurements as superseding the earlier 22.16 mm hole-spacing measurement:
centres are 1.75 mm from adjacent edges, giving 22.50 mm spacing in both axes.
- User verified the template's PCB outline and mounting-hole placement against
the actual OLED module: both fit perfectly. Header-pin centres were subsequently
corrected to 1.50 mm from the top PCB edge, superseding 0.75 mm. The header row
is therefore 0.25 mm nearer the top edge than the upper mounting-hole centres.
- User measured the display outline margins: top 4.5 mm, bottom 5 mm, left 1 mm,
right 0.5 mm. On the 26 mm PCB this defines a 24.5 × 16.5 mm rectangle from
(1, 4.5) to (25.5, 21), shown dashed in the template. This records the supplied
display boundary, not an independently verified active-pixel area or case cutout.
- User subsequently confirmed the revised template is "about a perfect fit",
validating the PCB outline, mounting-hole placement, corrected 1.50 mm header
position and display outline against the actual module. This is a user-reported
mechanical fit check, not electrical or fabrication validation.
- User explicitly approved a horizontally centred header, superseding the earlier
approximate 7.33 mm hole-to-pin measurements.
- Approved coordinates in mm, viewed from the display side with the header at
the top, origin at the top-left PCB corner, X right and Y down:
GND (9.19, 1.50), VCC (11.73, 1.50), SCL (14.27, 1.50), SDA (16.81, 1.50).
Mounting-hole centres: (1.75, 1.75), (24.25, 1.75), (1.75, 24.25),
(24.25, 24.25), each diameter 2 mm. Board outline: (0, 0) to (26, 26).
- `oled-fit-check.svg` is a 1:1 mechanical template of that geometry. Print at
100%, without fit-to-page, and verify its 20 mm scale before checking the module.
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
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.
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.
+142
View File
@@ -0,0 +1,142 @@
# Adafruit MAX3243 breakout: authoritative CAD header research
Research date: 2026-09-20. Scope: source retrieval and pad/geometry extraction only; no project schematic or layout modified.
## Critical product distinction
**Product 5988 is the DE-9 female version. The male version is product 6253.** The product-5988 page's male variant link goes to `/product/6253`; each product description explicitly identifies its connector gender. Both link to the same Learn guide. Do not describe the downloaded CAD as verified male-specific CAD: its connector is `X2`, value `DE9-F`, package `F09HP`, and the repository README identifies product 5988.
The exact header map below is proven for the published 5988 board. It is useful reference for the family, but identical male-board geometry/pad numbering has **not** been established by a male-specific CAD file or physical measurement. Confirm the actual 6253 module before releasing a male carrier footprint. No inference from connector gender or a generic DE-9 footprint is substituted for that check.
## Provenance and downloaded originals
Authoritative chain:
1. [Adafruit Learn downloads](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads) links to the Eagle repository below, a TI datasheet, fab print and 3D models.
2. [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB).
3. Retrieved `main` commit **`63c6200bb3ef17d491089cfa6441b1810d79bb20`**, committed 2024-08-12T22:42:27Z. All four local originals were downloaded from this immutable revision, not a moving branch.
4. [Product 5988, female](https://www.adafruit.com/product/5988), [product 6253, male](https://www.adafruit.com/product/6253), and [Learn pinouts](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/pinouts) corroborate identity and signal names.
Local files in `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` |
| `license.txt` | `075dad5e5fc96c27014fabc269f4f5732909cffd178a486f546d982b6cf86b74` |
Raw URL prefix for each filename (URL-encode spaces):
`https://raw.githubusercontent.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB/63c6200bb3ef17d491089cfa6441b1810d79bb20/`
**License:** upstream `license.txt` is **Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)**. README attribution: “Designed by Limor Fried/Ladyada for Adafruit Industries.” README explicitly requires all its preceding text in redistribution; the full original README and license are therefore retained. Preserve these notices when redistributing CAD and comply with applicable attribution/share-alike obligations for adaptations. The CAD-derived content of this report is attributed to that design and provided under CC BY-SA 3.0.
The guide also links [5988 3D CAD](https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter). Its directory listing contains `.f3d`, `.step`, `.stl`, and `.jpg`; these were **not downloaded or used for measurements**, and their licensing was not independently assessed. No male-specific 3D model was verified in this research.
## Exact header numbering and net map
Source: Eagle XML `drawing/board/elements/element[@name='JP2']`, embedded `microbuilder` package `1X12_ROUND_76MIL`, and `drawing/board/signals/signal/contactref`. Cross-checked against schematic `JP2`, deviceset `HEADER-1X12`, selected device `76MIL`: symbol pin N maps directly to physical pad N.
**Viewing convention:** component/top view, DE-9 pointing toward the top of the page (+Y), logic header along the bottom. X increases rightward, Y upward. The origin is the lower-left bounding-box corner of the bare PCB (the corner itself is rounded). These are Eagle board coordinates in **millimetres**, not a mating-face view and not a bottom view.
```text
DE-9 / +Y
+-----------------------+
| |
| o o | module mounting holes
| |
| 1 2 3 ... 12 | JP2, y = 2.667 mm
+-----------------------+
Vin OFF
+X ->
```
| JP2 physical pad | Label / function | CAD net | X (mm) | Y (mm) | MAX3243 IC1 pad | DE-9 X2 pad via level shifter |
|---:|---|---|---:|---:|---:|---:|
| 1 | Vin | VCC | 1.905 | 2.667 | 26 | — |
| 2 | GND | GND | 4.445 | 2.667 | 25 | 5 (direct ground) |
| 3 | DCD | DCD | 6.985 | 2.667 | 15 | 1 |
| 4 | RX | RXD | 9.525 | 2.667 | 16 | 2 |
| 5 | TX | TXD | 12.065 | 2.667 | 13 | 3 |
| 6 | DTR | DTR | 14.605 | 2.667 | 14 | 4 |
| 7 | DSR | DSR | 17.145 | 2.667 | 17 | 6 |
| 8 | RTS | RTS | 19.685 | 2.667 | 12 | 7 |
| 9 | CTS | CTS | 22.225 | 2.667 | 19 | 8 |
| 10 | RI | RI | 24.765 | 2.667 | 18 | 9 |
| 11 | VLD | !INVALID | 27.305 | 2.667 | 21 | — |
| 12 | OFF | !FORCEOFF | 29.845 | 2.667 | 22 | — |
**Left-to-right order: `Vin GND DCD RX TX DTR DSR RTS CTS RI VLD OFF`.** This is physical pad order, not the order of rows in the project's wiring table.
At the logic header, TX/DTR/RTS are inputs to the MAX3243; RX/DCD/DSR/CTS/RI are outputs from it. Schematic gates explicitly map TX to T2IN, DTR to T1IN, RTS to T3IN, RX to R4OUT, DCD to R5OUT, DSR to R3OUT, CTS to R1OUT and RI to R2OUT. RS-232 and logic signal nets are separated by the chip; matching signal names do not mean direct continuity.
OFF is **active low** (`!FORCEOFF`, IC1 pad 22) with 10 kΩ pull-up R1 to VCC. The Learn pinout page agrees. The upstream README and shop prose incorrectly say high shuts it down; do not propagate that prose error. VLD is `!INVALID`, high on valid input detection. Use 3.3 V Vin for this project's ESP32 interface, as already required by `docs/wiring.md`.
## Exact published PCB geometry
All figures below are nominal CAD dimensions, not measured tolerances.
### Logic header JP2
- Single row, **12 plated through-hole pads at 2.54 mm (0.100 inch) pitch**.
- Element origin `(15.875, 2.667)`; no element rotation or mirroring (R0).
- Package local pad centres: `x = -13.970 + 2.540 × (N - 1)`, `y = 0`, N = 1…12.
- Board coordinates: `x = 1.905 + 2.540 × (N - 1)`, `y = 2.667`.
- First-to-last centre span **27.940 mm (1.100 inch)**.
- All holes: **1.000 mm drill**, explicit **1.9304 mm (76 mil) round copper diameter**. Each pad has `rot="R90"`; that does not rotate the header row or change a round pad's outline.
- Both end pad centres are **1.905 mm** from the corresponding vertical board edge.
- Row is **2.667 mm (0.105 inch)** above the lower bounding-box edge.
- No square pad-1 marker is defined by this package: identify pad 1 using Vin and the stated view, not assumed square copper.
### Bare-board outline and mounting holes
Source: `drawing/board/plain/wire[@layer='20']` and mounting-hole element/package definitions.
- Bare PCB bounding box: **31.750 mm X × 29.337 mm Y** (1.250 × 1.155 inches), X = 0…31.750, Y = 0…29.337.
- Straight top edge `(0,29.337)` to `(31.750,29.337)`; square top corners.
- Bottom corners: **2.540 mm radius**, defined by 90° arcs `(0,2.540)``(2.540,0)` and `(29.210,0)``(31.750,2.540)`.
- Bottom straight edge: X = 2.540…29.210 at Y = 0.
- Module mounting holes: `U$2` at **(3.810, 11.430)** and `U$3` at **(27.940, 11.430)**.
- Both use `MOUNTINGHOLE_3.0_PLATEDTHIN`, pad `P$1`, **3.000 mm drill / 3.600 mm copper diameter**. They are plated pads, not anonymous NPTH holes, and neither appears in the board's signal contactrefs.
- Hole centre spacing **24.130 mm (0.950 inch)**; hole row is **8.763 mm** above the header row.
For a carrier footprint using header pad 1 as `(0,0)` with Y still upward:
- Header pads: `(2.540 × (N - 1), 0)`.
- Mounting hole centres: **(1.905, 8.763)** and **(26.035, 8.763)**.
- Bare-board bounding box: X = **1.905…29.845**, Y = **2.667…26.670**; retain corner arcs described above.
If the destination CAD uses screen coordinates with Y downward, transform Y deliberately; do not silently mirror physical pad numbering. The placement side and whether the module faces up or down must be fixed before generating a carrier footprint.
### DE-9 on the downloaded female CAD (not male qualification)
`X2` origin `(15.875,17.907)`, R0, package `F09HP`, value `DE9-F`.
| X2 pad | Board X (mm) | Board Y (mm) | CAD net |
|---|---:|---:|---|
| 1 | 10.3886 | 16.637 | RS_DCD |
| 2 | 13.1318 | 16.637 | RS_RXD |
| 3 | 15.8750 | 16.637 | RS_TXD |
| 4 | 18.6182 | 16.637 | RS_DTR |
| 5 | 21.3614 | 16.637 | GND |
| 6 | 11.7602 | 19.177 | RS_DSR |
| 7 | 14.5034 | 19.177 | RS_RTS |
| 8 | 17.2466 | 19.177 | RS_CTS |
| 9 | 19.9898 | 19.177 | RS_RI |
| G1 | 3.3528 | 17.907 | no signal contactref |
| G2 | 28.3972 | 17.907 | no signal contactref |
Signal holes have 1.016 mm drill and 1.778 mm octagonal copper; shell/mechanical pads G1/G2 have 3.302 mm drill and 5.080 mm copper, spaced 25.0444 mm. Shell pads are not shown tied to GND by signal contactrefs; DE-9 pin 5 is grounded. These are **PCB solder-tail coordinates**, not front-face pin coordinates.
The connector package's layer-21 drawing reaches local Y = 17.907, hence board Y = 35.814, beyond the board's Y = 29.337. This is library drawing evidence, **not a validated mechanical envelope**. Do not size an enclosure from silkscreen. Product pages list assembled dimensions of 33.6 × 32.0 × 14.1 mm (5988) and 33.7 × 31.8 × 14.2 mm (6253), with no coordinate/tolerance definition; these do not replace a male-specific mechanical drawing, and differ from the female library drawing extent.
## Validation and next-use boundary
- Parsed the actual Eagle board and schematic XML successfully using Python's standard-library XML parser.
- Joined package pad names to board signal contactrefs, applied JP2's unrotated element translation, and cross-checked schematic header numbering/net assignments.
- Extracted outline arcs, mounting-hole package dimensions, and connector pad data directly; no dimensions inferred from product photos.
- Recorded SHA-256 hashes of the downloaded originals.
- No firmware build or hardware tests performed: this task changed research/reference files only.
- **Ready:** exact published 5988 header map, pad centres, board outline and hole coordinates for CAD reference.
- **Still required for a male-module carrier release:** establish that the purchased module is 6253, verify its header order and mounting geometry against male-specific CAD or the physical module, and verify connector overhang, mating clearance, module height and mounting orientation. The shared guide alone is insufficient proof of identical male geometry.
+8
View File
@@ -0,0 +1,8 @@
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
1 References Quantity Description Manufacturer MPN or identification Footprint Status
2 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
3 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
4 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
5 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
6 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
7 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
8 Assembly hardware TBD Module headers and optional beta-test sockets plus mounting hardware TBD TBD Not yet selected; not included in electrical component count
+107
View File
@@ -0,0 +1,107 @@
# Button selection: Omron B3F-1000
Verified 2026-09-20 against the Omron datasheet and installed KiCad libraries. This research covers three active-low GPIO-to-GND buttons.
**Integration decision:** the A0 schematic now implements B3F-1000 switches with one **2.2 kΩ, 1%, at least 0.125 W external pull-up** per input. The lower-current/internal-pull-up-only discussion below records the selection rationale, not an unselected schematic option. Firmware is unchanged. Exact resistor MPN, supply tolerance/budget and stock remain release checks.
## Schematic-ready recommendation
**B3F-1000 fits the installed footprint mechanically and its normally-open contact topology matches the circuit. Low-current operation with internal pull-ups needs the qualification below.**
For each of the three buttons:
- Value / manufacturer part number: **B3F-1000**, Omron; standard silver-plated contacts, ivory flat plunger, no separate ground terminal.
- Symbol: **`Switch:SW_Push`** (normally open, two passive pins).
- Explicitly assign footprint: **`Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x`**. The installed symbol's default footprint field is empty.
- Symbol pin **1 → GPIO**, symbol pin **2 → GND**. The switch is nonpolar; swapping these two nets is electrically equivalent.
| Function | Pin 1 net | Pin 2 net |
|---|---|---|
| Previous/back | GPIO10 | GND |
| Select/confirm | GPIO13 | GND |
| Next | GPIO14 | GND |
GPIO assignments verified in `src/board_pins.h`. The PCB README specifies normally-open buttons to GND with internal pull-ups. Released = high; pressed = low. No voltage source connects directly through the switch: any external pull-up goes through a resistor to 3.3 V.
## Exact terminal and pad mapping
The installed footprint has **four physical holes but only two distinct electrical pad numbers: `1,1,2,2`**, not four uniquely numbered pads. The installed `SW_Push` symbol has pin 1 at the left and pin 2 at the right in its default orientation; pin numbers are hidden by the symbol.
Unrotated footprint, front/top view; local coordinates in mm, positive Y downward:
```text
6.5 mm
KiCad pad 1 (0,0) ----------- KiCad pad 1 (6.5,0)
Omron terminal 4 Omron terminal 3
NO
contact 4.5 mm
Omron terminal 2 Omron terminal 1
KiCad pad 2 (0,4.5) --------- KiCad pad 2 (6.5,4.5)
```
The horizontal connections above are permanent internal connections; pressing joins the two rows. Omron datasheet **page 4**, upper-left **“Terminal Arrangement/Internal Connections (Top View)”**, explicitly shows terminals **43 common** and **21 common**. This maps to KiCad's upper pair numbered 1 and lower pair numbered 2 when aligned as shown. **Omron terminal 1 is not KiCad pad 1 in this orientation.** The manufacturer's separate bottom-view numbering sketch must not be mistaken for a top view. Omron states terminal numbers are not marked on the switches. A 180° insertion swaps the two electrical sides without affecting this nonpolar circuit.
Both pads numbered 1 receive the symbol pin-1 net; both pads numbered 2 receive pin-2's net. Do not attach a four-unique-pin symbol to this footprint without an explicit remapping. Connecting GPIO and GND to two terminals within the same common pair would permanently ground the GPIO.
The footprint sets `duplicate_pad_numbers_are_jumpers no`; do not assume PCB routing software will treat the internal metal as a routing jumper. Route/check the duplicate-pad nets normally.
## Manufacturer dimensions and ratings
Source: Omron B3F datasheet, ordering table p. 1, ratings/operating characteristics p. 3, B3F-1000 drawing p. 4. Dimensions below are mm.
| Item | Datasheet specification |
|---|---|
| Body | 6 ±0.2 × 6 ±0.2 |
| Overall height above seating plane | 4.3 ±0.2 |
| Plunger diameter | 3.5 |
| Terminal row spacing | 4.5 ±0.2 |
| Formed terminal span at hole-entry region | 6.5 ±0.5 |
| Maximum-width dimension shown across formed leads | 7.7 ±0.5 |
| Lead projection below seating plane | 3.5 |
| Lead thickness / width callouts | 0.3 / 0.7 |
| Recommended PCB hole-centre grid | 6.5 ±0.1 × 4.5 ±0.1 |
| Recommended PCB holes | Four, diameter 1 ±0.1; reference drawing for PCB thickness 1.6 |
| General dimensional tolerance | ±0.4 unless otherwise specified |
| Contact form | SPST-NO |
| Rated resistive load, standard silver version | **150 mA at 324 VDC** |
| Minimum applicable load | **10 µA at 1 VDC, reference value** |
| Initial contact resistance | 100 mΩ maximum |
| Bounce | 5 ms maximum |
| Operating force | 0.98 ±0.29 N (100 ±30 gf) |
| Release force | 0.2 N minimum |
| Pretravel | 0.25 +0.2/0.1 |
| Durability for B3F-1000, 0.98 N | 1,000,000 operations minimum |
| Protection / washing | IP00 / washing not possible |
### Installed footprint comparison
Inspected `/usr/share/kicad/footprints/Button_Switch_THT.pretty/SW_TH_Tactile_Omron_B3F-100x.kicad_mod` (file format version `20260206`, generator version `10.0`) and `/usr/share/kicad/symbols/Switch.kicad_sym`.
- Hole centres: `(0,0)`, `(6.5,0)`, `(0,4.5)`, `(6.5,4.5)` — match Omron's nominal PCB grid.
- All four plated through-hole pads: circular copper diameter **1.7**, drill **1.0**, front/back copper and mask — nominal drill matches Omron's reference hole diameter. Confirm the fabricator's finished-hole tolerance when ordering.
- Fab body: `(0.25,-0.75)` to `(6.25,5.25)`, **6 × 6**, matching nominal body dimensions.
- Courtyard: `(-1.1,-1.1)` to `(7.6,5.6)`, **8.7 × 6.7**. This is a library placement boundary, not an enclosure or finger-access clearance specification.
- Footprint description specifies H4.3 mm. No physical sample fit, enclosure-height check, or independent 3D-model measurement was performed.
## Low-current qualification / selection decision
**Do not claim internal-pull-up operation meets the standard part's full rated-load range.** At 3.3 V, pull-up resistance greater than 3.3 kΩ gives less than 1 mA contact current. For illustration only, 45 kΩ would give about 73 µA; that is not a verified resistance specification for this board's GPIOs.
Omron's 10 µA / 1 V minimum is explicitly a **reference value**, not an unconditional low-current reliability guarantee. B3F-1000 is a reasonable prototype choice, but internal-pull-up-only contact reliability should be qualified in the intended environment and over life.
For a conservative silver-contact design within the stated rated load, consider **one external 2.2 kΩ pull-up to 3.3 V per button** (about 1.5 mA while pressed); verify supply/resistor tolerances and GPIO limits before adopting. This recommendation was subsequently adopted in the A0 carrier schematic; see the integration decision above. If low current is important, the datasheet also lists **B3F-1002-G** with gold contacts in the same p. 4 dimensional group, rated 100 µA50 mA at 324 VDC, but with a different operating force (1.76 ±0.49 N); weak internal pull-ups are not automatically within that rated range either. It has not been selected or stock-checked here.
Retain firmware debouncing; a mechanical switch is not bounce-free. The device is unsealed (IP00), and the datasheet prohibits washing.
## Supply evidence
**Live stock not verified; no supply assurance.** On 2026-09-20 a request to the [DigiKey B3F-1000 product URL](https://www.digikey.com/en/products/detail/omron-electronics-inc-emc-div/B3F-1000/33150) returned HTTP 403 / an anti-bot challenge. No quantity, price, lead time, or lifecycle status could be established from that response. Confirm the exact MPN and available quantity with a distributor before BOM release; datasheet availability is not stock evidence.
## Evidence and remaining checks
- [Authoritative Omron datasheet](https://omronfs.omron.com/en_US/ecb/products/pdf/en-b3f.pdf), retrieved 2026-09-20.
- [Local PDF](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.
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.
+105
View File
@@ -0,0 +1,105 @@
# Provisional carrier module footprints
These project-local footprints are in `Carrier.pretty`. They are mechanical
starting points, **not fabrication-ready connector selections**. Both are now
assigned to their matching symbols in the native schematic through the project
library tables and used in the routed PCB draft. The assembled module 3D models
are not provided. See `pcb-draft-notes.md` for placement/routing and
`rs232-footprint-notes.md` for the additional provisional RS-232 reference footprint.
## Shared pad/drill provenance and limits
Reference inspected locally: installed KiCad library footprint
`Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical`, file
`/usr/share/kicad/footprints/Connector_PinHeader_2.54mm.pretty/PinHeader_1x04_P2.54mm_Vertical.kicad_mod`
(format version `20260206`; local KiCad `10.0.6-1.fc44`).
Both footprints use that reference's **1.70 × 1.70 mm plated through-hole pads,
1.00 mm drills, rectangular pad 1, and circular remaining pads**, on
`*.Cu` and `*.Mask`, with unused copper layers retained. Nominal pitch is 2.54 mm.
Only the reference's pad/drill convention is reused, not its connector body,
courtyard, or 3D model. HW678 pad 23 is circular: only overall pad 1 is rectangular.
**PROVISIONAL connector fit — awaiting the selected actual pin/socket datasheet.**
Breadboard fit supports centre spacing, not drill size, finished-hole tolerance,
annular-ring suitability, socket body clearance, mating height, or solderability.
Validate those against the actual final headers and beta-test sockets and the PCB
fabricator's process before release. No electrical nets or ESP32 GPIO assignments
are inferred. No courtyards are supplied: module/connector assembly clearances
remain incomplete, so automated courtyard checks cannot establish fit.
Coordinates below are millimetres, unrotated on the carrier front side, X right
and Y down. Views are not mirrored. Placement on the back side or viewing the
module from its solder side requires the appropriate KiCad transform; do not
silently reverse the numbering.
## `Carrier:OLED_26mm_I2C_Provisional`
Origin: top-left corner of the module PCB, viewed from the front/display side,
header at the top. User-verified geometry from the current request and the later
mechanical-evidence section of `README.md`:
| Pad | Signal | X | Y |
|---|---|---:|---:|
| 1 | GND | 9.19 | 1.50 |
| 2 | VCC | 11.73 | 1.50 |
| 3 | SCL | 14.27 | 1.50 |
| 4 | SDA | 16.81 | 1.50 |
- PCB boundary: (0, 0) to (26, 26), solid rectangle on `F.Fab`.
This depicts the module, not the carrier's routed edge; no `Edge.Cuts` is used.
- Four unnumbered, non-plated through holes, diameter 2.00 mm, at
(1.75, 1.75), (24.25, 1.75), (1.75, 24.25), (24.25, 24.25).
Hole centres are 1.75 mm from adjacent edges, with 22.50 mm spacing in both axes.
These transfer the verified module hole geometry to the carrier; they do not
select screws, inserts, bosses, or hardware clearance envelopes.
- Display boundary: (1, 4.5) to (25.5, 21), 24.5 × 16.5 mm, dashed on `F.Fab`.
This is the supplied display boundary, not a verified active-pixel area or case cutout.
- Pad-to-signal mapping is documented here and in the footprint description;
footprint pads themselves have numbers, not assigned nets.
The approved geometry supersedes the earlier 22.16 mm hole-spacing measurement:
22.50 mm hole spacing, Y=1.50 mm header centres, and the centred header coordinates
above apply. See README for the user-reported successful template fit check.
## `Carrier:HW678_2x22_Provisional`
Component/top view: antenna up, USB connectors down. Origin is the top-left header
centre (pad 1), **not a PCB corner**.
- Left row: pads 1 through 22, top to bottom, X=0.
- Right row: pads 23 through 44, top to bottom, X=25.40.
- Y=0 through 53.34 in 2.54 mm increments for each row (21 intervals).
- User breadboard fit verifies nominal 2.54 mm pitch and 25.40 mm row spacing;
these supersede the earlier raw caliper dimensions in README.
- Numbering follows the project's explicit convention, not a generic dual-row
connector's alternating or counterclockwise numbering. Physical numbers do
**not** establish GPIO, supply, or ground assignments.
- Only pads and annotation text are present. **No PCB outline, antenna keepout,
courtyard, USB envelope, mounting holes, or 3D model is guessed.** Text positions
are annotations, not dimensional evidence. Outline registration, antenna extent,
connector offsets/overhang, and assembly height remain unknown.
## Validation and release gates
Validation performed with KiCad 10.0.6 `pcbnew.FootprintLoad`: both footprints
parsed successfully. Assertions passed for pad counts, all numbered pad positions,
pad shapes/types/sizes/drills, OLED NPTH positions and diameters, both OLED `F.Fab`
rectangle bounds, and absence of courtyards, `Edge.Cuts`, and keepout zones.
HW678 was also checked to contain no graphical shapes. The local Python binding
emitted three startup `PROPERTY_ENUM` assertions (also seen on a version-only
import); these did not prevent loading or the checks from passing.
Repeat the retained footprint regression checks from the repository root with:
```sh
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
```
This requires the system Python with KiCad's `pcbnew` binding. It checks the
native-loaded pad counts, coordinates, pad types, sizes, drills and geometry. Parsing is
not DRC, electrical verification, connector fit verification, or fabrication approval.
Print both footprints at 1:1 and check the real assemblies, especially HW678 row
numbering/orientation. Before layout release, select and verify connectors, map
HW678 pins from reliable board evidence, and establish module envelopes,
antenna restrictions, USB access, mounting hardware, and courtyards.
+6
View File
@@ -0,0 +1,6 @@
(fp_lib_table (version 7)
(lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.pretty")(options "")(descr "Provisional module carrier footprints; see footprint-notes.md"))
(lib (name "Button_Switch_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Button_Switch_THT.pretty")(options "")(descr "KiCad standard THT switches"))
(lib (name "Resistor_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Resistor_THT.pretty")(options "")(descr "KiCad standard axial resistors"))
(lib (name "MountingHole")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr "KiCad standard mounting holes"))
)
+221
View File
@@ -0,0 +1,221 @@
#!/usr/bin/env python3
"""Create the compact placement draft from a fresh KiCad XML netlist.
Requires system Python + pcbnew. Refuses to overwrite an existing PCB unless
--overwrite is explicit. This is a draft generator, not a manufacturing tool.
Does not modify the schematic or footprint libraries. KiCad SaveBoard also saves
PCB design settings into the project; close the project before regeneration.
"""
import argparse
import json
from pathlib import Path
import re
import subprocess
import xml.etree.ElementTree as ET
import pcbnew as p
ROOT = Path(__file__).resolve().parent
PCB = ROOT / "serial-carrier.kicad_pcb"
LIBS = Path("/usr/share/kicad/footprints")
ORIGIN = (100, 100)
OUTLINE = [(0,0), (82,0), (82,80), (36,80), (36,61), (0,61)]
PLACEMENT = {
"U1": ("Carrier:HW678_2x22_Provisional", 8, 4),
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 30),
"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),
}
MOUNTS = [(3.5,3.5), (3.5,56), (78,4), (78,76)]
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
RF_RESERVE = [(10,0), (31,0), (31,13), (10,13)]
def mm(v):
return p.FromMM(v)
def xy(x,y):
return p.VECTOR2I(mm(x+ORIGIN[0]), mm(y+ORIGIN[1]))
def text(board, value, x, y, layer=p.F_SilkS, size=1):
item = p.PCB_TEXT(board)
item.SetText(value)
item.SetPosition(xy(x,y))
item.SetLayer(layer)
item.SetTextSize(p.VECTOR2I(mm(size),mm(size)))
item.SetTextThickness(mm(0.15))
board.Add(item)
return item
def polygon(zone, points):
poly=zone.Outline()
poly.NewOutline()
for x,y in points:
pt=xy(x,y)
poly.Append(pt.x,pt.y)
def load(lib_id):
lib,name=lib_id.split(":",1)
folder=ROOT / "Carrier.pretty" if lib=="Carrier" else LIBS / (lib+".pretty")
fp=p.FootprintLoad(str(folder),name)
if fp is None:
raise RuntimeError("Cannot load "+lib_id)
fp.SetFPIDAsString(lib_id)
return fp
def quiet_footprint(fp):
# Retain all mechanical geometry, but replace sprawling research annotation
# text with concise board annotations. Libraries are never modified.
for graphic in list(fp.GraphicalItems()):
if isinstance(graphic,p.PCB_TEXT):
fp.Remove(graphic)
fp.Value().SetVisible(False)
fp.Reference().SetTextSize(p.VECTOR2I(mm(1),mm(1)))
fp.Reference().SetTextThickness(mm(0.15))
fp.Reference().SetLayer(p.F_SilkS)
def copy_schematic_fields(fp, component):
for field in component.findall("fields/field"):
name=field.get("name")
if name in ("Reference", "Value", "Footprint"):
continue
fp.SetField(name, field.text or "")
fp.GetField(name).SetVisible(False)
def configure_draft_netclasses():
# SaveBoard creates the project defaults. Align future interactive routing
# with the draft's physical minimum widths and its explicit power routing.
path=ROOT/"serial-carrier.kicad_pro"
project=json.loads(path.read_text())
settings=project["net_settings"]
default=next(c for c in settings["classes"] if c["name"]=="Default")
default.update(clearance=0.25, track_width=0.25, via_diameter=0.7, via_drill=0.3)
power=dict(default, name="Power", priority=0, track_width=0.5, diff_pair_width=0.25)
settings["classes"]=[c for c in settings["classes"] if c["name"]!="Power"]+[power]
patterns=[v for v in settings.get("netclass_patterns",[]) if v["pattern"] not in ("/+3V3","/GND")]
settings["netclass_patterns"]=patterns+[{"netclass":"Power","pattern":name} for name in ("/+3V3","/GND")]
path.write_text(json.dumps(project,indent=2)+"\n")
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument("--overwrite",action="store_true")
args=parser.parse_args()
if PCB.exists() and not args.overwrite:
parser.error("PCB exists; preserve manual edits or explicitly use --overwrite")
if any(ROOT.glob("~serial-carrier.*.lck")):
parser.error("Project is open in KiCad; close it before regenerating PCB/settings")
netfile=ROOT / "validation/pcb-source-netlist.xml"
subprocess.run(["kicad-cli","sch","export","netlist","--format","kicadxml","-o",str(netfile),str(ROOT/"serial-carrier.kicad_sch")],check=True)
source=ET.parse(netfile).getroot()
components={c.get("ref"):c for c in source.findall("components/comp")}
assert set(components)==set(PLACEMENT), "Schematic component set changed; review layout"
schematic=ROOT.joinpath("serial-carrier.kicad_sch").read_text()
root_uuid=re.search(r'\(uuid "?([a-f0-9-]{36})"?\)',schematic).group(1)
board=p.BOARD()
board.SetCopperLayerCount(2)
settings=board.GetDesignSettings()
settings.SetBoardThickness(mm(1.6))
settings.m_MinClearance=mm(0.25)
settings.m_CopperEdgeClearance=mm(0.5)
settings.m_HoleClearance=mm(0.25)
settings.m_TrackMinWidth=mm(0.25)
settings.m_ViasMinSize=mm(0.7)
# SaveBoard persists these proposed draft rules into the project settings.
title=board.GetTitleBlock()
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier draft")
title.SetRevision("A0 DRAFT")
title.SetComment(0,"NOT FOR FABRICATION - male module / USB / antenna geometry unverified")
pad_nets={}
for source_net in source.findall("nets/net"):
net=p.NETINFO_ITEM(board,source_net.get("name"))
board.Add(net)
for node in source_net.findall("node"):
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")
fp=load(lib_id)
fp.SetReference(ref)
fp.SetValue(comp.findtext("value"))
path=p.KIID_PATH()
path.push_back(p.KIID(root_uuid))
path.push_back(p.KIID(comp.findtext("tstamps")))
fp.SetPath(path)
fp.SetSheetfile("serial-carrier.kicad_sch")
fp.SetSheetname("serial-carrier")
fp.SetPosition(xy(x,y))
board.Add(fp)
quiet_footprint(fp)
copy_schematic_fields(fp,comp)
if ref=="U1":
fp.Reference().SetPosition(xy(20.7,25))
elif ref=="U2":
fp.Reference().SetPosition(xy(56.97,18))
elif ref=="DS1":
fp.Reference().SetPosition(xy(56,49))
elif ref.startswith("SW"):
fp.Reference().SetPosition(xy(x+3.25,y-2))
else:
fp.Reference().SetLayer(p.F_Fab)
fp.Reference().SetPosition(xy(x+3.81,y-1.6))
for pad in fp.Pads():
key=ref,pad.GetNumber()
if pad.GetNumber():
net,node=pad_nets[key]
pad.SetNet(net)
pad.SetPinFunction(node.get("pinfunction", ""))
pad.SetPinType(node.get("pintype", "passive"))
for i,(x,y) in enumerate(MOUNTS,1):
fp=load("MountingHole:MountingHole_3.2mm_M3")
fp.SetReference("H"+str(i))
fp.SetValue("M3 mounting proposal / 3.2mm")
fp.SetBoardOnly(True)
fp.SetExcludedFromBOM(True)
fp.SetExcludedFromPosFiles(True)
fp.SetPosition(xy(x,y))
board.Add(fp)
quiet_footprint(fp)
fp.Reference().SetVisible(False)
for a,b in zip(OUTLINE,OUTLINE[1:]+OUTLINE[:1]):
edge=p.PCB_SHAPE(board)
edge.SetShape(p.SHAPE_T_SEGMENT)
edge.SetStart(xy(*a)); edge.SetEnd(xy(*b))
edge.SetLayer(p.Edge_Cuts); edge.SetWidth(mm(0.05))
board.Add(edge)
reserve=p.ZONE(board)
reserve.SetIsRuleArea(True)
layers=p.LSET(); layers.AddLayer(p.F_Cu); layers.AddLayer(p.B_Cu)
reserve.SetLayerSet(layers)
reserve.SetZoneName("PROVISIONAL RF RESERVE - verify actual antenna clearance")
reserve.SetDoNotAllowTracks(True); reserve.SetDoNotAllowVias(True)
reserve.SetDoNotAllowZoneFills(True); reserve.SetDoNotAllowPads(True)
reserve.SetDoNotAllowFootprints(False) # the module itself spans this region
polygon(reserve,RF_RESERVE)
board.Add(reserve)
text(board,"RF RESERVE",20.5,7,p.Dwgs_User,1.2)
text(board,"VERIFY ANTENNA",20.5,10,p.Dwgs_User,1)
text(board,"ESP32 HW678",20.7,29)
text(board,"USB ACCESS - VERIFY",18,64,p.Dwgs_User,1)
text(board,"DE-9 OUT / VERIFY MALE",56.97,1.5,p.Dwgs_User,1)
text(board,"MAX3243 REF",56.97,21)
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,"Carrier envelope only; module overhang and connector clearance not qualified",41,-2.5,p.Dwgs_User,0.9)
board.BuildConnectivity()
p.SaveBoard(str(PCB),board)
configure_draft_netclasses()
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
print("Placement draft only. Board-only U2 footprint is a female-CAD reference, not male qualification.")
if __name__=="__main__":
main()
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""Generate the initial native KiCad carrier schematic (Python stdlib only).
Explicit bootstrap/regeneration tool: overwrites the generated schematic, symbol
library and library tables. Do not run over subsequent manual KiCad edits without
reviewing/backing them up. It does not regenerate footprints or the fit template.
"""
import json
from pathlib import Path
import uuid
ROOT = Path(__file__).resolve().parent
PROJECT = "serial-carrier"
NS = uuid.UUID("b6f75dd1-a843-42f2-9067-57c995968145")
def uid(key):
return str(uuid.uuid5(NS, key))
def q(value):
return json.dumps(str(value), ensure_ascii=False)
def n(value):
return f"{value:.4f}".rstrip("0").rstrip(".") if value else "0"
def effects(size=1.27, extra=""):
return f"(effects (font (size {size} {size})) {extra})"
def prop(name, value, x=0, y=0, hidden=False):
return f'(property {q(name)} {q(value)} (at {n(x)} {n(y)} 0) {effects(extra="(hide yes)" if hidden else "")})'
# Physical module numbering is a project convention, not ESP32 GPIO numbering:
# component side, antenna up / USB down; left row 1..22, right row 23..44.
left = ["3V3", "3V3", "EN", "GPIO4", "GPIO5", "GPIO6", "GPIO7", "GPIO15", "GPIO16", "GPIO17", "GPIO18", "GPIO8", "GPIO3", "GPIO46", "GPIO9", "GPIO10", "GPIO11", "GPIO12", "GPIO13", "GPIO14", "5V", "GND"]
right = ["GND", "GPIO43", "GPIO44", "GPIO1", "GPIO2", "GPIO42", "GPIO41", "GPIO40", "GPIO39", "GPIO38", "GPIO37", "GPIO36", "GPIO35", "GPIO0", "GPIO45", "GPIO48", "GPIO47", "GPIO21", "GPIO20", "GPIO19", "GND", "GND"]
GPIO_NET = {4:"RS_DCD",5:"RS_DSR",6:"RS_RI",7:"RS_DTR",8:"RS_VALID",9:"RS_OFF_N",10:"BTN_PREVIOUS",11:"OLED_SDA",12:"OLED_SCL",13:"BTN_SELECT",14:"BTN_NEXT",15:"RS_RTS",16:"RS_CTS",17:"RS_TX",18:"RS_RX"}
OUTPUTS = {7, 12, 15, 17}
INPUTS = {4, 5, 6, 8, 10, 13, 14, 16, 18}
def gpio_type(name):
if not name.startswith("GPIO"):
return "input" if name == "EN" else "passive"
number = int(name[4:])
if number == 9:
# Model the programmable MCU pad, not a discrete open-collector device.
# The required firmware open-drain mode and module pull-up are noted on sheet.
return "bidirectional"
return "output" if number in OUTPUTS else "input" if number in INPUTS else "bidirectional"
# Pin tuples: physical number, name, electrical type, local X, local Y, angle.
MCU = []
for side, names in enumerate((left, right)):
for index, name in enumerate(names):
number = index + 1 + side * 22
kind = "power_out" if number in (1, 22) else gpio_type(name)
MCU.append((str(number), name, kind, -22.86 if side == 0 else 22.86, 26.67-index*2.54, 0 if side == 0 else 180))
# Published Adafruit 5988 CAD reference; male 6253 correspondence is provisional.
RS_NAMES = ["Vin", "GND", "DCD", "RX", "TX", "DTR", "DSR", "RTS", "CTS", "RI", "VLD", "OFF_N"]
RS = [(str(i+1), name, "power_in" if i < 2 else "input" if name in ("TX","DTR","RTS","OFF_N") else "output", -20.32, 13.97-i*2.54, 0) for i,name in enumerate(RS_NAMES)]
OLED = [(str(i+1),name,kind,-15.24,3.81-i*2.54,0) for i,(name,kind) in enumerate((("GND","power_in"),("VCC","power_in"),("SCL","input"),("SDA","bidirectional")))]
TWO = [("1","~","passive",-5.08,0,0),("2","~","passive",5.08,0,180)]
SPECS = {
"HW678_N16R8": (MCU, 17.78, 29.21, "U", "Carrier:HW678_2x22_Provisional", "HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained"),
"MAX3243_Module_Reference": (RS, 15.24, 17.78, "U", "", "Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE"),
"OLED_I2C_26mm": (OLED, 10.16, 7.62, "DS", "Carrier:OLED_26mm_I2C_Provisional", "User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA"),
"SW_NO": (TWO, 2.54, 2.54, "SW", "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", "Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"),
"R": (TWO, 2.54, 1.016, "R", "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", "Resistor, axial DIN0207 footprint; exact MPN pending")
}
def symbol_definition(name, embedded=False):
pins, halfwidth, halfheight, ref, footprint, description = SPECS[name]
out = [f'(symbol {q("Carrier:"+name if embedded else name)} (pin_names (offset 0.762)) (in_bom yes) (on_board yes)',
prop("Reference", ref, 0, halfheight+5.08), prop("Value", name, 0, halfheight+2.54),
prop("Footprint", footprint, hidden=True), prop("Datasheet", "", hidden=True), prop("Description",description,hidden=True)]
if name == "SW_NO":
shape = '(polyline (pts (xy -2.54 0) (xy 2.54 1.524)) (stroke (width 0.254) (type default)) (fill (type none)))'
shape += ''.join(f'(circle (center {x} 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none)))' for x in (-2.54,2.54))
else:
shape = f'(rectangle (start {-halfwidth} {halfheight}) (end {halfwidth} {-halfheight}) (stroke (width 0.254) (type default)) (fill (type background)))'
out.append(f'(symbol {q(name+"_0_1")} {shape})')
out.append(f'(symbol {q(name+"_1_1")}')
for number,label,kind,x,y,angle in pins:
length = abs(x)-halfwidth
out.append(f'(pin {kind} line (at {n(x)} {n(y)} {angle}) (length {n(length)}) (name {q(label)} {effects(1.016)}) (number {q(number)} {effects(1.016)}))')
out.extend([')', ')'])
return '\n'.join(out)
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')
# Preserve project settings on regeneration.
project_file = ROOT / (PROJECT + ".kicad_pro")
if not project_file.exists():
project_file.write_text(json.dumps({"meta":{"filename":project_file.name,"version":3}},indent=2)+"\n")
root_uuid = uid("root")
items = [f'(kicad_sch (version 20250114) (generator "eeschema") (uuid {q(root_uuid)}) (paper "A3")',
'(title_block (title "ESP32 Serial Swiss Army Knife - Module Carrier") (date "2026-09-20") (rev "A0 DRAFT") (comment 1 "Not fabrication-ready: provisional module footprints and male RS-232 mapping"))',
'(lib_symbols '+'\n'.join(symbol_definition(name,True) for name in SPECS)+')']
expected = {}
def wire(x1,y1,x2,y2,key):
items.append(f'(wire (pts (xy {n(x1)} {n(y1)}) (xy {n(x2)} {n(y2)})) (stroke (width 0) (type default)) (uuid {q(uid("wire:"+key))}))')
def label(net,x,y,key):
items.append(f'(label {q(net)} (at {n(x)} {n(y)} 0) {effects(1.016,"(justify left bottom)")} (uuid {q(uid("label:"+key))}))')
def note(text,x,y,size=1.27):
items.append(f'(text {q(text)} (at {n(x)} {n(y)} 0) {effects(size,"(justify left top)")} (uuid {q(uid("text:"+text))}))')
def place(name,ref,value,x,y,nets,extra=None):
pins,hw,hh,_,footprint,description = SPECS[name]
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():
instance.append(prop(k,v,x,y,True))
instance.extend(f'(pin {q(p[0])} (uuid {q(uid(ref+":"+p[0]))}))' for p in pins)
instance.append(f'(instances (project {q(PROJECT)} (path {q("/"+root_uuid)} (reference {q(ref)}) (unit 1)))))')
items.append('\n'.join(instance))
for number,_,_,px,py,angle in pins:
ax,ay=x+px,y-py
net=nets.get(number)
key=ref+":"+number
if net is None:
items.append(f'(no_connect (at {n(ax)} {n(ay)}) (uuid {q(uid("nc:"+key))}))')
else:
end=ax-12.7 if angle==0 else ax+12.7
wire(ax,ay,end,ay,key)
label(net,end,ay,key)
expected.setdefault(net,[]).append([ref,number])
mcu_nets={}
for number,name,*_ in MCU:
if name == "3V3":
mcu_nets[number]="+3V3"
elif name == "GND":
mcu_nets[number]="GND"
elif name.startswith("GPIO") and int(name[4:]) in GPIO_NET:
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("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"})
note(function,302.26,y+21.59,1.016)
note("MODULE CARRIER - INITIAL SCHEMATIC",15.24,15.24,2.54)
note("Native KiCad 10 schematic. Named wire labels join matching nets on this sheet.\nRead hardware/PCB/README.md before layout or fabrication.",15.24,24.13)
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("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("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("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)
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')
print("Generated native KiCad schematic, project-local symbol library and tables.")
+37
View File
@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100mm" height="90mm" viewBox="0 0 100 90">
<title>OLED module mechanical fit check — front view</title>
<desc>26 mm square PCB, four 2 mm mounting holes at 1.75 mm edge offsets. Centred four-pin header at 2.54 mm pitch, 1.50 mm from top edge. Display outline at (1, 4.5), measuring 24.5 × 16.5 mm. Header crosses indicate centres only, not drill sizes.</desc>
<g font-family="sans-serif" font-size="3" fill="black">
<text x="5" y="7">OLED mechanical fit check — front view</text>
<text x="5" y="12">Print at 100%; disable fit-to-page.</text>
<text x="5" y="17">Verify the scale below before use.</text>
</g>
<g transform="translate(35 27)" stroke="black" stroke-width="0.15" fill="none">
<rect width="26" height="26"/>
<rect id="display-outline" x="1" y="4.5" width="24.5" height="16.5" stroke-dasharray="0.6 0.4"/>
<circle cx="1.75" cy="1.75" r="1"/>
<circle cx="24.25" cy="1.75" r="1"/>
<circle cx="1.75" cy="24.25" r="1"/>
<circle cx="24.25" cy="24.25" r="1"/>
<path id="header-centres" d="M8.79 1.5h.8 M9.19 1.1v.8 M11.33 1.5h.8 M11.73 1.1v.8 M13.87 1.5h.8 M14.27 1.1v.8 M16.41 1.5h.8 M16.81 1.1v.8"/>
<g stroke="none" fill="black" font-family="sans-serif" font-size="1.5" text-anchor="middle">
<text x="9.19" y="-1">GND</text>
<text x="11.73" y="-1">VCC</text>
<text x="14.27" y="-1">SCL</text>
<text x="16.81" y="-1">SDA</text>
<text x="13" y="7">Dashed: display outline</text>
<text x="13" y="10">24.5 × 16.5 mm</text>
<text x="13" y="13">26 × 26 mm PCB</text>
<text x="13" y="16">Holes: Ø2 mm</text>
<text x="13" y="19">Hole spacing: 22.50 mm</text>
</g>
</g>
<g stroke="black" stroke-width="0.15" fill="none">
<path d="M35 65h20 M35 63v4 M55 63v4"/>
</g>
<g font-family="sans-serif" font-size="2.5" fill="black">
<text x="39" y="71">20 mm scale</text>
<text x="5" y="79">Header crosses mark pin centres, not drill sizes.</text>
<text x="5" y="84">Mechanical reference only — not a fabrication file.</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

+173
View File
@@ -0,0 +1,173 @@
# Compact two-layer PCB — A0 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.**
No Gerbers, production drills or manufacturing archive were generated.
## Size and placement
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
- **Two copper layers**, F.Cu and B.Cu. All components mount on the front.
- Proposed **1.6 mm** carrier thickness; fabrication stackup/process not selected.
- Stepped outline: a **36 × 19 mm lower-left cutaway** leaves cable space below
the ESP32 while the OLED/buttons occupy the deeper right-hand section.
- Module and mating-plug overhangs are not included in the carrier dimensions.
The final complete assembly/enclosure envelope still needs measurement.
Coordinates in the following table are local millimetres from the carrier's
upper-left corner, X right/Y down, viewed from the component side. In the KiCad
file add (100, 100) mm to obtain absolute board coordinates. All footprints are
unrotated; their origins are defined in the footprint notes, not necessarily
component body corners.
| Reference | Local origin (X, Y) | Placement |
|---|---|---|
| U1 | (8, 4) | ESP32 left header pin 1; antenna toward upper edge, USB toward cutaway |
| U2 | (43, 30) | MAX3243 header pin 1; DE-9 toward upper edge |
| DS1 | (43, 36) | OLED module upper-left PCB corner; display faces up |
| R1 / R2 / R3 | (43 / 54 / 65, 34) | Axial pull-ups in the gap between RS-232 and OLED |
| SW1 / SW2 / SW3 | (43 / 55 / 67, 68) | Back, select, next below display; switch pad 1 origins |
| H1 | (3.5, 3.5) | Proposed case mounting hole |
| H2 | (3.5, 56) | Proposed case mounting hole |
| H3 | (78, 4) | Proposed case mounting hole |
| H4 | (78, 76) | Proposed case mounting hole |
Outline vertices: (0,0), (82,0), (82,80), (36,80), (36,61), (0,61), closed.
The board area is **58.76 cm²**; its bounding rectangle is 65.6 cm².
H1H4 are proposed **Ø3.2 mm NPTH M3 clearance holes**, not a previously agreed
case specification. They are marked board-only and excluded from the electrical
BOM. The OLED and RS-232 module mounting holes are additional, separate holes.
Screw heads, spacers, heat-set inserts, tool access and underside lead clearance
must be checked against the final case and assembly heights. No insert dimensions
or module stand-off heights are assumed.
## Routing and electrical status
The PCB contains actual copper, not just ratsnest placement:
- All **38 required physical-pad spanning connections** across **17 connected
nets** routed, including both duplicated pads of each switch contact.
- **569 segments**, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
- **20 through vias**, **0.70 mm copper / 0.30 mm drill**.
- **No copper pours**; ground is explicitly routed. This is an initial routing
solution, not a reviewed return-current/EMI or power-integrity design.
- Proposed minimum different-net clearance **0.25 mm** and copper-edge clearance
**0.50 mm**. These are selected draft design rules, not fabrication qualification.
- Project netclasses align interactive routing defaults with the draft: Default
uses 0.25 mm tracks / 0.25 mm clearance, while Power matches `/+3V3` and `/GND`
with 0.50 mm tracks. Both use 0.70 / 0.30 mm vias.
- Schematic pad-net assignments, component values and UUID linkage are retained.
U1's intentionally unused GPIO/USB/UART0/5 V header pads remain unused.
The bounded draft router prioritizes connectivity and conservative geometric
clearance, not optimal trace topology. Do not treat DRC passage as evidence of
current-carrying capacity, supply stability, I²C signal quality or EMC compliance.
## Explicit provisional mechanical choices
### ESP32 and RF
A two-layer **no-tracks/no-vias/no-pads/no-zone-fill rule area** reserves local
X=10…31 mm, Y=0…13 mm toward the antenna. This is a deliberate carrier design
allocation, **not a measured antenna envelope or a manufacturer RF keepout**.
The module itself may span the rule area; footprint placement is not forbidden.
No carrier copper enters the reserve, but copper runs close to its boundary.
U1's exact PCB/antenna/USB offsets are still unmeasured. The intended antenna
overhang and USB access must be checked with the actual board and a printed
placement template. Move U1, change the edge or enlarge the reserve if required.
Missing RF clearance cannot be signed off from the current footprint alone.
### RS-232
U2 uses the new **`Carrier:MAX3243_Reference_Provisional`** footprint **on the PCB
only**. Its bare-module outline/header/holes are adapted from published female
5988 CAD for the intended male 6253 module. The user reports identical dimensions,
but the male header map and assembled DE-9 envelope are still unverified.
See `rs232-footprint-notes.md` and its CC BY-SA attribution.
The schematic footprint remains intentionally blank, preserving its caution
and avoiding edits to the schematic that was open in KiCad. Consequently there
is **one known schematic-parity warning**: U2 footprint differs from the empty
schematic assignment. This is not excluded/suppressed, and is not a net mismatch.
Once the male footprint is qualified, explicitly assign it in the schematic
before relying on Update PCB from Schematic to manage U2's footprint.
### OLED and controls
OLED geometry, including its display boundary, is the user-fit-verified geometry.
The module schematic/library files are unchanged. The new overall placement still
needs an assembly fit check: adjacent resistor bodies, module underside parts,
header/socket bodies, button actuation and finger/plunger clearance are not
represented by complete assembly courtyards. In particular the axial resistors
are tightly packed between the module outlines; confirm clearance and height.
## 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
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
100%, disable fit-to-page, and confirm the outer width is 82 mm before use.
It includes only known module outlines; no guessed U1 body or DE-9 envelope.
## Validation and limits
Run from the repository root with the system Python containing KiCad bindings:
```sh
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
```
The first script exports a **fresh saved-schematic netlist** and checks the actual
PCB pad assignments, component linkage, dimensions, layer count, nominal thickness,
mounting holes, track/via sizes and KiCad DRC. It never rewrites the PCB or project.
It reports the single expected U2 parity warning explicitly, and fails on any
other parity issue, DRC violation or unconnected item. Reports are retained in
`validation/pcb-parity-drc.json` and `pcb-validation-summary.json`.
The second independently checks continuous copper/hole/edge geometry and original
placement/net/UUID preservation against the initial unrouted snapshot. It needs
locally installed NumPy and Shapely in addition to pcbnew. It is not a substitute
for current-schematic net validation and is specific to this initial layout.
Results with KiCad **10.0.6**:
- **0 physical DRC errors/warnings; 0 unconnected items.**
- All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**.
- Independent minimum different-net/copper-to-NPTH gap **0.275 mm**; minimum
copper/NPTH-to-board-edge gap **0.525 mm**.
- All 9 electrical footprints, 4 board-only mounts, outline and RF area retained.
- Startup `PROPERTY_ENUM` assertions from the local pcbnew binding are still
emitted, but the checks complete successfully.
DRC uses the existing default ignored checks: `missing_courtyard`,
`track_not_centered_on_via`, `tuning_profile_track_geometries`,
`footprint_filters_mismatch`, `footprint_type_mismatch`. No new exclusions were
added to hide problems. **Missing module courtyards therefore remain an explicit
manual release gate**. The draft rules do not represent a chosen fabricator's
complete capability set. No electrical/hardware/fabrication tests were performed.
## Regeneration and project settings
Normal use is to **edit the native PCB in KiCad**, not regenerate it. The optional
`generate_pcb_draft.py` recreates the *unrouted* initial placement and discards
manual PCB work only with explicit `--overwrite`; it refuses an open-project lock.
KiCad's initial SaveBoard populated the previously minimal `.kicad_pro` with PCB
settings and draft clearance/width rules. The schematic was not modified. If the
project was already open, reopen it to load the saved PCB settings before review.
`route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
existing tracks requires explicit `--route --overwrite --replace-routes`. Do not
run that over manual routing without a backup. It is a bounded initial-draft
router, **not** a general-purpose autorouter. See `validation/pcb-routing-summary.md`
for algorithm limits and commands. These scripts do not need downloaded packages
or services in the current environment and create no manufacturing outputs.
Before fabrication: qualify all module pin maps/footprints and connectors,
complete assembly courtyards and RF/USB/DE-9 checks, review power/return paths,
verify the existing USB power circuit and OLED pull-ups, confirm procurement and
case hardware, rerun ERC/DRC, and perform an independent hardware design review.
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
## Adafruit RS-232 Full Pinout Level-Shifter Breakout PCB
<a href="http://www.adafruit.com/products/5988"><img src="assets/5988.jpg?raw=true" width="500px"><br/>
Click here to purchase one from the Adafruit shop</a>
PCB files for the Adafruit RS-232 Full Pinout Level-Shifter Breakout.
Format is EagleCAD schematic and board layout
* https://www.adafruit.com/product/5988
### Description
If you want to interface with telco, retro, or industrial equipment, you'll probably run into RS-232 interfaces. The Adafruit RS232 Full-Pinout Breakout with 8 Channels of UART to RS-232 Level Shifters is your friend in such cases. It gives you 5 input and 3 output channels of level shifting and takes care of the high/negative voltage generation all in a low-cost breakout board. We use the trusty MAX3243 from TI, a classic chip part of the MAX232 lineage, so you know it will work great for all your RS-232 needs, up to 250Kbps.
RS-232 is what we had before USB: a 9- or 25-pin D-Sub connector that allowed data plus flow control lines. Many folks may remember these interfaces were used for mice, modems, barcode scanners, teletypes, and more. We still find devices sold with RS-232 ports, although many folks use a USB to RS-232 adapter these days.
If you want to use a microcontroller or microcomputer to chat with an RS-232, then thankfully, all you need is a serial port / UART (something just about any microcontroller has) and a level shifter. The level shifter is required because while most UARTs are 0-3.3V or 0-5V logic level, RS-232 requires +-6 to +-10V, yep the signal voltage goes negative! That means a specialized shifter is required to generate extra high and low voltages and safely convert the logic levels.
Sure you could buy a raw MAX232 chip and wire up the necessary capacitors, but this board does it all for you and even comes with a DE-9 connector for plugging in directly into your 'client' device. It can run on 3.3V power and logic, which many older chips can't do. It also can do all 8 data pins, so you can use all of the flow control signals like RTS, CTS, DTR, DSR, DCD and RI.
We also include two separate lines from the MAX3243: 'Valid' and 'Off'. The Valid line output will have logic level high when the chip detects signal voltages from the device its plugged into. So you can use it as a 'connection made' signal. There's also the Off pin, which when set to logic high by the microcontroller will tri-state all the pins for power reduction.
This breakout comes fully assembled with a UART side for low-voltage power/logic level, and a DE-9F RS-232 side for high-voltage signals. We also include some header so you can solder to a breadboard in a few minutes.
### License
Adafruit invests time and resources providing this open source design, please support Adafruit and open-source hardware by purchasing products from [Adafruit](https://www.adafruit.com)!
Designed by Limor Fried/Ladyada for Adafruit Industries.
Creative Commons Attribution/Share-Alike, all text above must be included in any redistribution.
See license.txt for additional details.
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.
+753
View File
@@ -0,0 +1,753 @@
Tactile Switch B3F
Through-hole-mounting Switches
in a Wide Range of Models:
6 × 6 mm, 12 × 12 mm, Side-operated
Models, Gold-plated Contacts, and
Radial Tape
• Extended mechanical/electrical durability: 10 x 106
operations for 12 x 12 mm type and 1 x 106
operations for the 6 x 6 mm type
• Taped radial type, vertical type and high force
types are available.
• Gold plated models available for increased
contact reliability, resistance to corrosive gas and
insulation failure prevention for ion migration in
harsh environments
• B32-series Key Tops mount to models with
projected plungers.
RoHS Compliant
■ List of Models
6 × 6 mm Models
Type Contact Plunger Height Operating Plunger Bags
material force (OF) color Without ground Minimum With ground Minimum
terminal packing unit terminal packing unit
Standard: Silver Flat type 4.3 mm 0.98 N {100 gf} lvory B3F-1000 B3F-1100
B3F-1000 plated 1.47 N {150 gf} Yellow B3F-1002 B3F-1102
Series
2.55 N {260 gf} Orange B3F-1005 B3F-1105
4.9 N {500 gf} Red B3F-1006 ---
5.0 mm 0.98 N {100 gf} Black B3F-1020 B3F-1120
1.47 N {150 gf} Gray B3F-1022 B3F-1122
2.55 N {260 gf} Pink B3F-1025 B3F-1125
4.9 N {500 gf} Blue B3F-1026 ---
5.0 mm 0.98 N {100 gf} Black --- B3F-1110
(7.5-mm pitch)
7.0 mm 0.98 N {100 gf} Black B3F-1060 100 pcs --- 100 pcs
1.47 N {150 gf} Yellow B3F-1062 ---
9.5 mm 0.98 N {100 gf} Black B3F-1070 ---
1.47 N {150 gf} Yellow B3F-1072 ---
2.55 N {260 gf} Orange B3F-1075 ---
7.3 mm 0.98 N {100 gf} lvory B3F-1050 B3F-1150
Projected type
1.47 N {150 gf} Yellow B3F-1052 B3F-1152
2.55 N {260 gf} Orange B3F-1055 B3F-1155
4.9 N {500 gf} Red B3F-1056 ---
1
B3F B3F
Type Contact Plunger Height Operating Plunger Bags
material force (OF) color Without ground Minimum With ground Minimum
terminal packing unit terminal packing unit
Side- Silver 3.15 mm 0.98 N {100 gf} lvory --- B3F-3100
Flat type
operated: plated 1.47 N {150 gf} Yellow --- B3F-3102
B3F-3000
Series 2.55 N {260 gf} Orange --- B3F-3105
3.85 mm 0.98 N {100 gf} Black --- B3F-3120
1.47 N {150 gf} Gray --- B3F-3122
2.55 N {260 gf} Pink --- B3F-3125
Projected type 6.15 mm 0.98 N {100 gf} lvory --- B3F-3150
1.47 N {150 gf} Yellow --- B3F-3152
2.55 N {260 gf} Orange --- B3F-3155
High- Gold 4.3 mm 1.76 N {180 gf} Yellow B3F-1002-G B3F-1102-G
Flat type
reliability plated
gold-plated: 5.0 mm Gray B3F-1022-G B3F-1122-G
B3F-1000-G 7.0 mm Yellow B3F-1062-G 100 pcs --- 100 pcs
Series (see note)
9.5 mm Yellow B3F-1072-G ---
(see note)
Side- Projected type 7.3 mm Yellow B3F-1052-G ---
operated
with highly
reliable
gold-plated:
B3F-3000-G
Series
3.85 mm Gray --- B3F-3122-G
Flat type
(see note)
Note: Bulk Packaged, 100 Switches per bag. Order in multiples of the package quantity.
12 × 12 mm Models
Type Contact Plunger Height Operating Plunger Bags
material (or LED color) force (OF) color Without ground Minimum With ground Minimum
terminal packing unit terminal packing unit
Standard: Silver Flat type 4.3 mm 1.27 N {130 gf} lvory B3F-4000 B3F-4100
B3F-4000 plated 2.55 N {260 gf} Yellow B3F-4005 B3F-4105
Series
Projected type 7.3 mm 1.27 N {130 gf} lvory B3F-4050 B3F-4150
2.55 N {260 gf} Yellow B3F-4055 B3F-4155
Long durability: Silver Flat type 4.3 mm 1.27 N {130 gf} Blue B3F-5000 B3F-5100
100 pcs 100 pcs
B3F-5000 Se- plated
ries Projected type 7.3 mm Blue B3F-5050 B3F-5150
High reliability Gold Flat type 4.3 mm 1.27 N {130 gf} Blue B3F-5001 B3F-5101
gold-plated: plated
B3F-5001 Projected type 7.3 mm Blue B3F-5051 B3F-5151
Series
Note: Bulk Packaged, 100 switches per bag. Order in multiples of the package quantity.
6 × 6 mm Radial Models (Taping Specifications)
Type Contact Plunger Height Operating Plunger Taped Radial
material force (OF) color Without ground Minimum With ground Minimum
terminal packing unit terminal packing unit
Taped Silver Flat type 4.3 mm 0.98 N {100 gf} lvory B3F-6000 B3F-6100
Radial: plated 1.47 N {150 gf} Yellow B3F-6002 B3F-6102
B3F-6000
Series 5.0 mm 0.98 N {100 gf} Black B3F-6020 B3F-6120
1,000 pcs 1,000 pcs
1.47 N {150 gf} Gray B3F-6022 B3F-6122
Projected type 7.3 mm 0.98 N {100 gf} lvory B3F-6050 B3F-6150
1.47 N {150 gf} Yellow B3F-6052 B3F-6152
Note: The switches are tape packaged in units of 1,000 per package. Order in multiples of the package size. Switches are not sold individually.
2
B3F B3F
■ Ratings/Characteristics
Rating (resistive load) 1 to 50 mA, 3 to 24 VDC (B3F-G: 100 μA to 50 mA, 3 to 24 VDC)
Minimum applicable load (reference value) 10 μA at 1 VDC (resistive load)
Ambient operating temperature -25°C to +70°C at 60%RH max. (with no icing or condensation)
Ambient operating humidity 35% to 85% (at +5 to +35°C)
Contact form SPST-NO
Contact resistance (initial value) 100 mΩ max.
Insulation resistance 100 MΩ min. (at 250 VDC with insulation tester)
Dielectric strength 500 VAC, 50/60 Hz for 1 min
Bounce time 5 ms max.
Vibration resistance Malfunction: 10 to 55 Hz, 1.5 mm double amplitude
Shock resistance Destruction: 1,000 m/s2 {approx. 100G} max.
Malfunction: 100 m/s2 {approx. 10G} max.
Durability B3F-1000, B3F-3000, B3F-6000:
1,000,000 operations min (OF: 0.98 N {100 gf}) (B3F-1070: 500,000 operations min)
300,000 operations min (OF: 1.47 N {150 gf})
100,000 operations min (OF: 2.55 N {260 gf})
50,000 operations min (OF: 4.9 N {500 gf})
B3F-4000:
3,000,000 operations min (OF: 1.27 N {130 gf})
1,000,000 operations min (OF: 2.55 N {260 gf})
B3F-5000/5001:
10,000,000 operations min.
B3F-G:
300,000 operations min.
Weight 6 × 6 mm models: approx. 0.25 g
12 × 12 mm models (standard types): approx. 0.85 g
Radial models: approx. 0.25 g
Degree of protection IEC IP00
Washing Not possible
■ Operating Characteristics
6 × 6 mm Models
B3F-1000, B3F-3000, B3F-6000 B3F-G
Operating force (OF) 0.98 N 1.47 N 2.55 N 4.9 N 1.76 N
B3F-1@@0 B3F-1@@2 B3F-1@@5 B3F-10@6 B3F-1@@2-G
Item B3F-3@@0 B3F-3@@2 B3F-3@@5 B3F-3@@2-G
B3F-6@@0 B3F-6@@2
Operating force (OF) 0.98±0.29 N 1.47±0.49 N 2.55±0.69 N 4.9±1.47 N 1.76±0.49 N
{100±30 gf} {150±50 gf} {260±70 gf} {500±150 gf} {180±50 gf}
Releasing force (RF) 0.2 N {20 gf} min. 0.49 N {50 gf}min. 0.49 N {50 gf}min. 0.7 N {70 gf} min. 0.49 N {50 gf}min.
Pretravel (PT) 0.25+0.2/0.1 mm 0.25+0.2/0.1 mm
12 × 12 mm Models
B3F-4000, B3F-5000, B3F-5001
Operating force (OF) 1.27 N 2.55 N
B3F-4@@0 B3F-4@@5
Item B3F-5@@0
B3F-5@@1
Operating force (OF) 1.27±0.49 N 2.55±0.69 N
{130±50 gf} {260±70 gf}
Releasing force (RF) 0.29 N {30 gf} min. 0.49 N {50 gf} min.
Pretravel (PT) 0.3+0.2/0.1 mm
3
B3F B3F
■ Dimensions (Unit: mm)
Note: The numbers used for terminals in the following graphics are indicated in the “Bottom View” diagram below. In this 2 1
diagram, the Switch is rotated so that the terminals are on the right and left-hand sides, and the OMRON logo ap- 4 3
pears the right way up. (Except Side-operated and Radial Models) (Bottom View)
6 × 6 mm Models
Standard, Flat Plunger Type Standard, Flat Plunger Type
(without Ground Terminal) (with Ground Terminal)
B3F-1000, B3F-1002, B3F-1005, B3F-1006 B3F-1100, B3F-1102, B3F-1105
B3F-1020 (See note.), B3F-1022 (See note.), B3F-1120 (See note.), B3F-1122 (See note.)
B3F-1025 (See note.), B3F-1026 (See note.) B3F-1125 (See note.)
B3F-1002-G, B3F-1022-G (See note.) B3F-1102-G, B3F-1122-G (See note.)
PCB Processing Dimensions PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
6±0.2 (PCB thickness, t=1.6) 6±0.2 (PCB thickness, t=1.6)
6±0.2 4.5±0.2 6±0.2 4.5±0.2
4.5±0.1 4.5±0.1
4.1±0.1
1.5 3.5
6.5±0.1 Four, 1±0.1 dia. dia. Five, 1±0.05 dia.
3.5
dia. (See note.) 6.5±0.1
4.3±0.2 3.4
(See note.) Terminal Arrangement/ Terminal Arrangement/Internal
4.3±0.2 3.4 Internal Connections (Top View) Connections (Top View)
3.5
4 3 0.3
3.5 4 3
0.3 0.7
0.7 0.7
6.5±0.5 6.5±0.5
0.3
7.7±0.5 0.7 0.7 2 1 7.7±0.5 2 1
Note: The height of B3F-1120, B3F-1122, and 5
Note: The height of B3F-1020, B3F-1022, B3F-1025,
and B3F-1026 is 5±0.2 mm. B3F-1125 is 5±0.2 mm.
Standard, Flat Plunger Type Standard, Flat Plunger Type
(with Ground Terminal, Pitch: 7.5 mm) (without Ground Terminal)
B3F-1110 B3F-1060, B3F-1062, B3F-1062-G
PCB Processing Dimensions PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
(PCB thickness, t=1.6) (PCB thickness, t=1.6)
6±0.2
6±0.2
6±0.2 4.5±0.1
4.5±0.2 4.5±0.1
6±0.2 4.5±0.2
4.1±0.1
Four, 3.5
1.5 1.2±0.05 dia. dia.
1±0.05 dia. 6.5±0.1
Four,
3.5 7.5±0.1 0.5 max. 1±0.05 dia.
dia.
7±0.2
Terminal Arrangement/Internal 3.4
5±0.2
3.4
Connections (Top View) Terminal Arrangement/
(1.8)
3.5
4 3 0.3
Internal Connections
3.5 (Top View)
0.3 6.5±0.5
7.7±0.5 0.7 0.7
4 3
0.7 0.7 0.7 2 1
7.5±0.5
0.3 5
9±0.5
2 1
Note: Unless otherwise specified, a tolerance of ±0.4 mm applies to all dimensions. No terminal numbers are indicated on the Switches.
4
B3F B3F
Standard, Flat Plunger Type Standard, Projected Plunger Type
(without Ground Terminal) (without Ground Terminal)
B3F-1070, B3F-1072, B3F-1075, B3F-1072-G B3F-1050, B3F-1052
B3F-1055, B3F-1056
PCB Processing Dimensions B3F-1052-G PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
(PCB thickness, t=1.6) (PCB thickness, t=1.6)
6±0.2 6±0.2
6±0.2 4.5±0.2 4.5±0.1 6±0.2 4.5±0.2 4.5±0.2
3.5
dia. 6.5±0.1 Four,
3 6.5±0.1 Four, 2.4 × 2.4±0.1 3.5 dia. 1±0.05 dia.
dia. 1±0.05 .dia
0.5 max. Terminal Arrangement/Internal 1.8±0.1 Terminal Arrangement/Internal
9.5 Connections (Top View) Connections (Top View)
7.3±0.2
3.4 4.3±0.2 3.4
4 3 4 3
(1.8)
3.5 3.5
0.3 0.3
6.5±0.5 2 1 6.5±0.5 2 1
7.7±0.5 0.7 0.7 0.7 0.7
7.7±0.5
Standard, Projected Plunger Type Side-operated, Flat Plunger Type
(with Ground Terminal) B3F-3100, B3F-3102, B3F-3105
B3F-1150, B3F-1152, B3F-1155
PCB Processing Dimensions PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
(PCB thickness, t=1.6) 7.3 (PCB thickness, t=1.6)
6±0.2
Two, 1.5 ±0.05 dia.
6.25
6±0.2 4.5±0.2 4.5±0.1
2.5±0.1
4.1±0.1
3.5 dia.
1.5 Five, 1 ±0.05 dia. 4.5±0.1
2.4 × 2.4±0.1 3.5 dia. 7±0.1 Two,
6.5±0.1 1±0.05 dia.
7.4
1.8±0.1 Terminal Arrangement/ Terminal Arrangement/
4
7.3±0.2 Internal Connections Internal Connections
4.3±0.2 3.4 (Top View) (Top View)
3.5
4 3 4
3.5 0.3 3
0.7 0.3 1
0.3 4.5±0.5 1 2
2 1 2.25
0.7 0.7 0.7 7±0.5 2.5±0.5
6.5±0.5 5 3.15±0.2
0.3 8.7
7.7±0.5
Side-operated, Flat Plunger Type (Height: 3.85 mm) Side-operated, Projected Plunger Type
B3F-3120, B3F-3122, B3F-3125, B3F-3122-G B3F-3150, B3F-3152, B3F-3155
PCB Processing Dimensions PCB Processing Dimensions
7.3 7.3 (Reference Only) (Top View)
(Reference Only) (Top View)
(PCB thickness, t=1.6) (PCB thickness, t=1.6)
Two, 1.5 ±0.05 dia. 6.25 Two, 1.5 ±0.05 dia.
6.25
2.5±0.1 2.5±0.1
3.5 dia.
4.5±0.1 4.5±0.1
3.5 dia.
7±0.1 Two, 7±0.1 Two,
1±0.05 dia. 1±0.05 dia.
7.4 Terminal Arrangement/ 7.4 2.4 × 2.4±0.1 Terminal Arrangement/
4 Internal Connections Internal Connections
4
(Top View) (Top View)
3.5
4 3.5 4
3 3
0.3
0.7 0.3 1 0.3
4.5±0.5 1 2 0.7 0.3 1 1 2
4.5±0.5 1.8±0.1
2.25
7±0.5 2.5±0.5 2.25
3.85±0.2 7±0.5 2.5±0.5
8.7 6.15±0.2
8.7
Note: Unless otherwise specified, a tolerance of ±0.4 mm applies to all dimensions. No terminal numbers are indicated on the Switches.
5
B3F B3F
12 × 12 mm Models
Standard, Long-durability, Standard, Long-durability,
and High-reliability Models and High-reliability Models
Flat Plunger Type Flat Plunger Type
(without Ground Terminal) (with Ground Terminal)
B3F-4000, B3F-4005, B3F-4100, B3F-4105,
B3F-5000, B3F-5001 B3F-5100, B3F-5101
PCB Processing Dimensions PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
(PCB thickness, t=1.6) (PCB thickness, t=1.6)
Two, 1.8±0.05 dia.
Two, 1.8±0.05 dia. (for positioning boss)
12±0.2 (for positioning boss) 12±0.2
5±0.1 9±0.1
5±0.1 9±0.1
12±0.2 5±0.2 12±0.2 5±0.2
6.9±0.1
Five,
Four, 1.2±0.05 dia.
12.5±0.1 1.2±0.05 dia.
1.6 12.5±0.1
7.1 dia.
Terminal Arrangement/ Terminal Arrangement/
7.1 dia.
Internal Connections Internal Connections
4.3±0.2
(Top View) (Top View)
3.5 4.3±0.2 3.5
3.5 4 3 4 3
3.5
1.6 dia. 1.6 dia.
0.3 1 1 0.9 0.3 1 1
12.5±0.5 9±0.1 2 1 12.5±0.5 6.9 2 1
13.8±0.5 13.8±0.5 9±0.1 5
Standard, Long-durability, Standard, Long-durability,
and High-reliability Models and High-reliability Models
Projected Plunger Type Projected Plunger Type
(without Ground Terminal) (with Ground Terminal)
B3F-4050, B3F-4055, B3F-4150, B3F-4155,
B3F-5050, B3F-5051 PCB Processing Dimensions B3F-5150, B3F-5151 PCB Processing Dimensions
(Reference Only) (Top View) (Reference Only) (Top View)
(PCB thickness, t=1.6) (PCB thickness, t=1.6)
12±0.2 Two, 1.8±0.05 dia.
Two, 1.8±0.05 dia.
(for positioning boss) (for positioning boss)
12±0.2
12±0.2 5±0.2
5±0.1 9±0.1
5±0.1 9±0.1
12±0.2 5±0.2
6.9±0.1
1.6 Five,
Four, 1.2±0.05 dia.
12.5±0.1 1.2±0.05 dia.
@3.8±0.1 12.5±0.1
@3.8±0.1
7.1 dia. 7.1 dia.
Terminal Arrangement/ 1.8±0.2 Terminal Arrangement/
1.8±0.2
Internal Connections 7.3±0.2 Internal Connections
7.3±0.2
4.3±0.2 3.5 (Top View) 4.3±0.2 3.5 (Top View)
3.5 4 3 3.5 4 3
1.6 dia. 1.6 dia.
0.3 1 1 0.9 0.3 1 1
12.5±0.5 9±0.1 2 1 12.5±0.5 6.9 2 1
13.8±0.5 13.8±0.5 9±0.1
5
Note: Unless otherwise specified, a tolerance of ±0.4 mm applies to all dimensions.
6
B3F B3F
Note: The numbers used for terminals in the following graphics are indicated in the “Bottom View” diagram below. In this 1
diagram, the Switch is rotated so that the terminals are on the right and left-hand sides, and the OMRON logo appears
the right way up.
2
6 mm × 6 mm Radial Types (Taping Specifications): Sold in Units of 1,000 Switches
(Bottom View)
Flat Plunger Type 6±0.3 Surface B
(without Ground Terminal)
B3F-6000, B3F-6002 6±0.3
12.7±1
6.35±1
2 max.
3.5 dia. Surface A
0.5 max.
Support
0.9 tape 9
(1) 4.3 0.5 max. +1
3.4 180
6
0.2 max. 9±0.5
0.5
+1
5-0.2
+0.8 0.3 18-0.5
4 dia. ±0.2
Terminal Arrangement
PCB Processing Dimensions /Internal Connections
12.7±0.3 Carrier tape
(Reference Only) (Top View) (Top View)
Note: The tape is random
(PCB thickness, t=1.6)
between surface A
and surface B.
+0.1 2 1
Two, 1 0 dia. 5±0.1
Flat Plunger Type 6±0.3 12.7±1
6.35±1
(with Ground Terminal) 2 max.
B3F-6100, B3F-6102 6±0.3 2
3.5 dia.
Support 11
0.5 max. tape 0.5 max.
20±0.5
0.9
(1) 4.3 3.4
6
9±0.5
3.1
+1
0.2 dia. 18-0.5
0.5 5±0.2 0.3
+0.8 4±0.2 dia.
5-0.2 0.6
12.7±0.3 Carrier tape
PCB Processing Dimensions Terminal Arrangement
(Reference Only) (Top View) /Internal Connections
(PCB thickness, t=1.6) (Top View)
+0.1 2 1
Three, 1 0 dia. 5±0.1 3
5±0.1
Flat Plunger Type 6±0.3 Surface B
(without Ground Terminal) 12.7±1
B3F-6020, B3F-6022 6.35±1
2 max.
6±0.3
3.5 dia. Surface A
Support 9
0.5 max. tape 0.5 max. +1
180
0.9
(1) 5
3.4 6
9±0.5
+1
0.2 max. 18-0.5
0.5
+0.8
5-0.2 0.3 4±0.2 dia.
Terminal Arrangement Carrier tape
Note: The tape is random PCB Processing Dimensions /Internal Connections
12.7±0.3
between surface A (Reference Only) (Top View) (Top View)
and surface B. (PCB thickness, t=1.6)
+0.1 2 1
Two, 1 0 dia. 5±0.1
Note: Unless otherwise specified, a tolerance of ±0.4 mm applies to all dimensions. No terminal numbers are indicated on the Switches.
7
B3F B3F
Flat Plunger Type 6±0.3
(with Ground Terminal)
B3F-6120, B3F-6122 6±0.3 2
12.7±1
6.35±1
2 max.
3.5 dia.
0.5 max.
0.9 Support 11
(1) 5
3.4 tape 0.5 max.
20±0.5
3.1
0.2 max. 6
9±0.5
0.5 5±0.2 0.3
+1
+0.8
5-0.2 0.6 18-0.5
4±0.2 dia.
PCB Processing Dimensions Terminal Arrangement
(Reference Only) (Top View) /Internal Connections Carrier tape
12.7±0.3
(PCB thickness, t=1.6) (Top View)
+0.1 2 1
Three, 1 0 dia. 5±0.1 3
5±0.1
Projected Plunger Type 6±0.3 Surface B
(without Ground Terminal)
B3F-6050, B3F-6052
6±0.3
12.7±1
3.5 dia. Surface A 6.35±1
2.4 2 max.
0.5 max.
7.3 0.9
(1) 3.4
Support 9
tape 0.5 dia. +1
18 0
0.2 max.
0.5
+0.8 0.3 6
5-0.2 9±0.5
+1
18-0.5
PCB Processing Dimensions Terminal Arrang ement
(Reference Only) (Top View) /Internal Connections 4±0.2 dia.
(PCB thickness, t=1.6) (Top View)
12.7±0.3 Carrier tape
+0.1
Two, 1 0 dia. 5±0.1 2 1
Note: The tape is random between surface A
and surface B.
Projected Plunger Type 6±0.3
(with Ground Terminal)
B3F-6150, B3F-6152
6±0.3 2 12.7±1
6.35±1
2 max.
3.5 dia. 2.4
0.5 max.
7.3 0.9
(1) 3.4 Support 11
tape 0.5 max.
20±0.5
3.1
0.2 max.
6
9±0.5
0.5 5±0.2 0.3
+0.8 +1
5 -0.2 0.6 18-0.5
Terminal Arrangement 4±0.2 dia.
PCB Processing Dimensions
(Reference Only) (Top View) /Internal Connections
(PCB thickness, t=1.6) (Top View) 12.7±0.3 Carrier tape
+0.1
Three, 1 0 dia. 5±0.1 2 1
3
5±0.1
Note: Unless otherwise specified, a tolerance of ±0.4 mm applies to all dimensions. No terminal numbers are indicated on the Switches.
8
B3F B3F
■ Key Tops
B32-series Key Tops are available for projected plungers. Refer to the Datasheet of B32 for details.
■ Precautions
Be sure to read the safety precautions common to all Tactile Switches for correct use.
• Application examples provided in this document are for reference only. In actual applications, confirm equipment functions and safety before using the product.
• Consult your OMRON representative before using the product under conditions which are not described in the manual or applying the product to nuclear control systems, railroad
systems, aviation systems, vehicles, combustion systems, medical equipment, amusement machines, safety equipment, and other systems or equipment that may have a serious
influence on lives and property if used improperly. Make sure that the ratings and performance characteristics of the product provide a margin of safety for the system or
equipment, and be sure to provide the system or equipment with double safety mechanisms.
Note: Do not use this document to operate the Unit.
OMRON Corporation
Electronic and Mechanical Components Company Contact: www.omron.com/ecb Cat. No. A070-E1-08
1014(0207)(O)
9
+359
View File
@@ -0,0 +1,359 @@
Creative Commons Legal Code
Attribution-ShareAlike 3.0 Unported
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
DAMAGES RESULTING FROM ITS USE.
License
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
CONDITIONS.
1. Definitions
a. "Adaptation" means a work based upon the Work, or upon the Work and
other pre-existing works, such as a translation, adaptation,
derivative work, arrangement of music or other alterations of a
literary or artistic work, or phonogram or performance and includes
cinematographic adaptations or any other form in which the Work may be
recast, transformed, or adapted including in any form recognizably
derived from the original, except that a work that constitutes a
Collection will not be considered an Adaptation for the purpose of
this License. For the avoidance of doubt, where the Work is a musical
work, performance or phonogram, the synchronization of the Work in
timed-relation with a moving image ("synching") will be considered an
Adaptation for the purpose of this License.
b. "Collection" means a collection of literary or artistic works, such as
encyclopedias and anthologies, or performances, phonograms or
broadcasts, or other works or subject matter other than works listed
in Section 1(f) below, which, by reason of the selection and
arrangement of their contents, constitute intellectual creations, in
which the Work is included in its entirety in unmodified form along
with one or more other contributions, each constituting separate and
independent works in themselves, which together are assembled into a
collective whole. A work that constitutes a Collection will not be
considered an Adaptation (as defined below) for the purposes of this
License.
c. "Creative Commons Compatible License" means a license that is listed
at https://creativecommons.org/compatiblelicenses that has been
approved by Creative Commons as being essentially equivalent to this
License, including, at a minimum, because that license: (i) contains
terms that have the same purpose, meaning and effect as the License
Elements of this License; and, (ii) explicitly permits the relicensing
of adaptations of works made available under that license under this
License or a Creative Commons jurisdiction license with the same
License Elements as this License.
d. "Distribute" means to make available to the public the original and
copies of the Work or Adaptation, as appropriate, through sale or
other transfer of ownership.
e. "License Elements" means the following high-level license attributes
as selected by Licensor and indicated in the title of this License:
Attribution, ShareAlike.
f. "Licensor" means the individual, individuals, entity or entities that
offer(s) the Work under the terms of this License.
g. "Original Author" means, in the case of a literary or artistic work,
the individual, individuals, entity or entities who created the Work
or if no individual or entity can be identified, the publisher; and in
addition (i) in the case of a performance the actors, singers,
musicians, dancers, and other persons who act, sing, deliver, declaim,
play in, interpret or otherwise perform literary or artistic works or
expressions of folklore; (ii) in the case of a phonogram the producer
being the person or legal entity who first fixes the sounds of a
performance or other sounds; and, (iii) in the case of broadcasts, the
organization that transmits the broadcast.
h. "Work" means the literary and/or artistic work offered under the terms
of this License including without limitation any production in the
literary, scientific and artistic domain, whatever may be the mode or
form of its expression including digital form, such as a book,
pamphlet and other writing; a lecture, address, sermon or other work
of the same nature; a dramatic or dramatico-musical work; a
choreographic work or entertainment in dumb show; a musical
composition with or without words; a cinematographic work to which are
assimilated works expressed by a process analogous to cinematography;
a work of drawing, painting, architecture, sculpture, engraving or
lithography; a photographic work to which are assimilated works
expressed by a process analogous to photography; a work of applied
art; an illustration, map, plan, sketch or three-dimensional work
relative to geography, topography, architecture or science; a
performance; a broadcast; a phonogram; a compilation of data to the
extent it is protected as a copyrightable work; or a work performed by
a variety or circus performer to the extent it is not otherwise
considered a literary or artistic work.
i. "You" means an individual or entity exercising rights under this
License who has not previously violated the terms of this License with
respect to the Work, or who has received express permission from the
Licensor to exercise rights under this License despite a previous
violation.
j. "Publicly Perform" means to perform public recitations of the Work and
to communicate to the public those public recitations, by any means or
process, including by wire or wireless means or public digital
performances; to make available to the public Works in such a way that
members of the public may access these Works from a place and at a
place individually chosen by them; to perform the Work to the public
by any means or process and the communication to the public of the
performances of the Work, including by public digital performance; to
broadcast and rebroadcast the Work by any means including signs,
sounds or images.
k. "Reproduce" means to make copies of the Work by any means including
without limitation by sound or visual recordings and the right of
fixation and reproducing fixations of the Work, including storage of a
protected performance or phonogram in digital form or other electronic
medium.
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
limit, or restrict any uses free from copyright or rights arising from
limitations or exceptions that are provided for in connection with the
copyright protection under copyright law or other applicable laws.
3. License Grant. Subject to the terms and conditions of this License,
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
perpetual (for the duration of the applicable copyright) license to
exercise the rights in the Work as stated below:
a. to Reproduce the Work, to incorporate the Work into one or more
Collections, and to Reproduce the Work as incorporated in the
Collections;
b. to create and Reproduce Adaptations provided that any such Adaptation,
including any translation in any medium, takes reasonable steps to
clearly label, demarcate or otherwise identify that changes were made
to the original Work. For example, a translation could be marked "The
original work was translated from English to Spanish," or a
modification could indicate "The original work has been modified.";
c. to Distribute and Publicly Perform the Work including as incorporated
in Collections; and,
d. to Distribute and Publicly Perform Adaptations.
e. For the avoidance of doubt:
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme cannot be waived, the Licensor
reserves the exclusive right to collect such royalties for any
exercise by You of the rights granted under this License;
ii. Waivable Compulsory License Schemes. In those jurisdictions in
which the right to collect royalties through any statutory or
compulsory licensing scheme can be waived, the Licensor waives the
exclusive right to collect such royalties for any exercise by You
of the rights granted under this License; and,
iii. Voluntary License Schemes. The Licensor waives the right to
collect royalties, whether individually or, in the event that the
Licensor is a member of a collecting society that administers
voluntary licensing schemes, via that society, from any exercise
by You of the rights granted under this License.
The above rights may be exercised in all media and formats whether now
known or hereafter devised. The above rights include the right to make
such modifications as are technically necessary to exercise the rights in
other media and formats. Subject to Section 8(f), all rights not expressly
granted by Licensor are hereby reserved.
4. Restrictions. The license granted in Section 3 above is expressly made
subject to and limited by the following restrictions:
a. You may Distribute or Publicly Perform the Work only under the terms
of this License. You must include a copy of, or the Uniform Resource
Identifier (URI) for, this License with every copy of the Work You
Distribute or Publicly Perform. You may not offer or impose any terms
on the Work that restrict the terms of this License or the ability of
the recipient of the Work to exercise the rights granted to that
recipient under the terms of the License. You may not sublicense the
Work. You must keep intact all notices that refer to this License and
to the disclaimer of warranties with every copy of the Work You
Distribute or Publicly Perform. When You Distribute or Publicly
Perform the Work, You may not impose any effective technological
measures on the Work that restrict the ability of a recipient of the
Work from You to exercise the rights granted to that recipient under
the terms of the License. This Section 4(a) applies to the Work as
incorporated in a Collection, but this does not require the Collection
apart from the Work itself to be made subject to the terms of this
License. If You create a Collection, upon notice from any Licensor You
must, to the extent practicable, remove from the Collection any credit
as required by Section 4(c), as requested. If You create an
Adaptation, upon notice from any Licensor You must, to the extent
practicable, remove from the Adaptation any credit as required by
Section 4(c), as requested.
b. You may Distribute or Publicly Perform an Adaptation only under the
terms of: (i) this License; (ii) a later version of this License with
the same License Elements as this License; (iii) a Creative Commons
jurisdiction license (either this or a later license version) that
contains the same License Elements as this License (e.g.,
Attribution-ShareAlike 3.0 US)); (iv) a Creative Commons Compatible
License. If you license the Adaptation under one of the licenses
mentioned in (iv), you must comply with the terms of that license. If
you license the Adaptation under the terms of any of the licenses
mentioned in (i), (ii) or (iii) (the "Applicable License"), you must
comply with the terms of the Applicable License generally and the
following provisions: (I) You must include a copy of, or the URI for,
the Applicable License with every copy of each Adaptation You
Distribute or Publicly Perform; (II) You may not offer or impose any
terms on the Adaptation that restrict the terms of the Applicable
License or the ability of the recipient of the Adaptation to exercise
the rights granted to that recipient under the terms of the Applicable
License; (III) You must keep intact all notices that refer to the
Applicable License and to the disclaimer of warranties with every copy
of the Work as included in the Adaptation You Distribute or Publicly
Perform; (IV) when You Distribute or Publicly Perform the Adaptation,
You may not impose any effective technological measures on the
Adaptation that restrict the ability of a recipient of the Adaptation
from You to exercise the rights granted to that recipient under the
terms of the Applicable License. This Section 4(b) applies to the
Adaptation as incorporated in a Collection, but this does not require
the Collection apart from the Adaptation itself to be made subject to
the terms of the Applicable License.
c. If You Distribute, or Publicly Perform the Work or any Adaptations or
Collections, You must, unless a request has been made pursuant to
Section 4(a), keep intact all copyright notices for the Work and
provide, reasonable to the medium or means You are utilizing: (i) the
name of the Original Author (or pseudonym, if applicable) if supplied,
and/or if the Original Author and/or Licensor designate another party
or parties (e.g., a sponsor institute, publishing entity, journal) for
attribution ("Attribution Parties") in Licensor's copyright notice,
terms of service or by other reasonable means, the name of such party
or parties; (ii) the title of the Work if supplied; (iii) to the
extent reasonably practicable, the URI, if any, that Licensor
specifies to be associated with the Work, unless such URI does not
refer to the copyright notice or licensing information for the Work;
and (iv) , consistent with Ssection 3(b), in the case of an
Adaptation, a credit identifying the use of the Work in the Adaptation
(e.g., "French translation of the Work by Original Author," or
"Screenplay based on original Work by Original Author"). The credit
required by this Section 4(c) may be implemented in any reasonable
manner; provided, however, that in the case of a Adaptation or
Collection, at a minimum such credit will appear, if a credit for all
contributing authors of the Adaptation or Collection appears, then as
part of these credits and in a manner at least as prominent as the
credits for the other contributing authors. For the avoidance of
doubt, You may only use the credit required by this Section for the
purpose of attribution in the manner set out above and, by exercising
Your rights under this License, You may not implicitly or explicitly
assert or imply any connection with, sponsorship or endorsement by the
Original Author, Licensor and/or Attribution Parties, as appropriate,
of You or Your use of the Work, without the separate, express prior
written permission of the Original Author, Licensor and/or Attribution
Parties.
d. Except as otherwise agreed in writing by the Licensor or as may be
otherwise permitted by applicable law, if You Reproduce, Distribute or
Publicly Perform the Work either by itself or as part of any
Adaptations or Collections, You must not distort, mutilate, modify or
take other derogatory action in relation to the Work which would be
prejudicial to the Original Author's honor or reputation. Licensor
agrees that in those jurisdictions (e.g. Japan), in which any exercise
of the right granted in Section 3(b) of this License (the right to
make Adaptations) would be deemed to be a distortion, mutilation,
modification or other derogatory action prejudicial to the Original
Author's honor and reputation, the Licensor will waive or not assert,
as appropriate, this Section, to the fullest extent permitted by the
applicable national law, to enable You to reasonably exercise Your
right under Section 3(b) of this License (right to make Adaptations)
but not otherwise.
5. Representations, Warranties and Disclaimer
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR
OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY
KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE,
INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY,
FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF
LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS,
WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION
OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU.
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. Termination
a. This License and the rights granted hereunder will terminate
automatically upon any breach by You of the terms of this License.
Individuals or entities who have received Adaptations or Collections
from You under this License, however, will not have their licenses
terminated provided such individuals or entities remain in full
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
survive any termination of this License.
b. Subject to the above terms and conditions, the license granted here is
perpetual (for the duration of the applicable copyright in the Work).
Notwithstanding the above, Licensor reserves the right to release the
Work under different license terms or to stop distributing the Work at
any time; provided, however that any such election will not serve to
withdraw this License (or any other license that has been, or is
required to be, granted under the terms of this License), and this
License will continue in full force and effect unless terminated as
stated above.
8. Miscellaneous
a. Each time You Distribute or Publicly Perform the Work or a Collection,
the Licensor offers to the recipient a license to the Work on the same
terms and conditions as the license granted to You under this License.
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
offers to the recipient a license to the original Work on the same
terms and conditions as the license granted to You under this License.
c. If any provision of this License is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this License, and without further action
by the parties to this agreement, such provision shall be reformed to
the minimum extent necessary to make such provision valid and
enforceable.
d. No term or provision of this License shall be deemed waived and no
breach consented to unless such waiver or consent shall be in writing
and signed by the party to be charged with such waiver or consent.
e. This License constitutes the entire agreement between the parties with
respect to the Work licensed here. There are no understandings,
agreements or representations with respect to the Work not specified
here. Licensor shall not be bound by any additional provisions that
may appear in any communication from You. This License may not be
modified without the mutual written agreement of the Licensor and You.
f. The rights granted under, and the subject matter referenced, in this
License were drafted utilizing the terminology of the Berne Convention
for the Protection of Literary and Artistic Works (as amended on
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
and the Universal Copyright Convention (as revised on July 24, 1971).
These rights and subject matter take effect in the relevant
jurisdiction in which the License terms are sought to be enforced
according to the corresponding provisions of the implementation of
those treaty provisions in the applicable national law. If the
standard suite of rights granted under applicable copyright law
includes additional rights not granted under this License, such
additional rights are deemed to be included in the License; this
License is not intended to restrict the license of any rights under
applicable law.
Creative Commons Notice
Creative Commons is not a party to this License, and makes no warranty
whatsoever in connection with the Work. Creative Commons will not be
liable to You or any party on any legal theory for any damages
whatsoever, including without limitation any general, special,
incidental or consequential damages arising in connection to this
license. Notwithstanding the foregoing two (2) sentences, if Creative
Commons has expressly identified itself as the Licensor hereunder, it
shall have all rights and obligations of Licensor.
Except for the limited purpose of indicating to the public that the
Work is licensed under the CCPL, Creative Commons does not authorize
the use by either party of the trademark "Creative Commons" or any
related trademark or logo of Creative Commons without the prior
written consent of Creative Commons. Any permitted use will be in
compliance with Creative Commons' then-current trademark usage
guidelines, as may be published on its website or otherwise made
available upon request from time to time. For the avoidance of doubt,
this trademark restriction does not form part of the License.
Creative Commons may be contacted at https://creativecommons.org/.
+365
View File
@@ -0,0 +1,365 @@
#!/usr/bin/python3
"""Bounded, board-specific two-layer draft router. Requires pcbnew and numpy.
Dry-run by default. --route --overwrite explicitly permits saving routing;
existing copper additionally requires --replace-routes. Never edits the project,
schematic, footprint libraries or generator. Review actual KiCad DRC afterwards.
This is not a production autorouter or a fabrication qualification.
"""
import argparse
from collections import defaultdict
import hashlib
import heapq
import json
import math
from pathlib import Path
import time
import numpy as np
import pcbnew as p
ROOT = Path(__file__).resolve().parent
PCB = ROOT / 'serial-carrier.kicad_pcb'
STEP = 0.05
NX, NY = int(82 / STEP) + 1, int(80 / STEP) + 1
N = NX * NY
CLEARANCE = 0.25
MARGIN = 0.02
LAYERS = (p.F_Cu, p.B_Cu)
X = 100 + np.arange(NX)[None, :] * STEP
Y = 100 + np.arange(NY)[:, None] * STEP
def mm(v):
return p.FromMM(v)
def point(xy):
return p.VECTOR2I(mm(xy[0]), mm(xy[1]))
def pos(item):
v = item.GetPosition()
return (p.ToMM(v.x), p.ToMM(v.y))
def identity(board):
"""Immutable geometry/linkage snapshot, independent of serialization order."""
footprints = []
for f in board.GetFootprints():
pads = sorted((a.m_Uuid.AsString(), a.GetNumber(), a.GetNetname(),
pos(a), (a.GetSize().x, a.GetSize().y),
(a.GetDrillSize().x, a.GetDrillSize().y),
a.GetShape(), a.GetAttribute(), a.GetLayerSet().FmtHex(),
a.GetOrientationDegrees(), a.GetPinFunction(), a.GetPinType())
for a in f.Pads())
footprints.append((f.m_Uuid.AsString(), f.GetReference(), f.GetValue(),
f.GetFPIDAsString(), f.GetPath().AsString(), pos(f),
f.GetOrientationDegrees(), f.GetLayer(), pads))
edges = sorted((a.m_Uuid.AsString(), a.GetShape(), a.GetStart().x,
a.GetStart().y, a.GetEnd().x, a.GetEnd().y, a.GetWidth())
for a in board.GetDrawings() if a.GetLayer() == p.Edge_Cuts)
zones = sorted((z.m_Uuid.AsString(), z.GetLayerSet().FmtHex(),
z.GetIsRuleArea(), z.GetDoNotAllowTracks(),
z.GetDoNotAllowVias(), z.GetDoNotAllowZoneFills(),
tuple((z.Outline().COutline(0).CPoint(i).x,
z.Outline().COutline(0).CPoint(i).y)
for i in range(z.Outline().COutline(0).PointCount())))
for z in board.Zones())
nets = sorted((n.GetNetCode(), n.GetNetname()) for n in board.GetNetsByNetcode().values())
return (board.GetCopperLayerCount(), sorted(footprints), edges, zones, nets)
def window(x0, y0, x1, y1):
ix0 = max(0, int(math.floor((x0 - 100) / STEP)))
iy0 = max(0, int(math.floor((y0 - 100) / STEP)))
ix1 = min(NX, int(math.ceil((x1 - 100) / STEP)) + 1)
iy1 = min(NY, int(math.ceil((y1 - 100) / STEP)) + 1)
return slice(iy0, iy1), slice(ix0, ix1)
def capsule(mask, a, b, radius):
sy, sx = window(min(a[0], b[0]) - radius, min(a[1], b[1]) - radius,
max(a[0], b[0]) + radius, max(a[1], b[1]) + radius)
xx, yy = X[:, sx], Y[sy, :]
dx, dy = b[0] - a[0], b[1] - a[1]
length2 = dx * dx + dy * dy
t = np.clip(((xx - a[0]) * dx + (yy - a[1]) * dy) / length2, 0, 1) if length2 else 0
mask[sy, sx] |= (xx - a[0] - t * dx)**2 + (yy - a[1] - t * dy)**2 <= radius**2
def pad_obstacle(mask, pad, extra, hole_only=False):
xy = pos(pad)
size = pad.GetDrillSize() if hole_only else pad.GetSize()
rx, ry = p.ToMM(size.x) / 2, p.ToMM(size.y) / 2
if not rx or not ry:
return
if abs(rx - ry) < 1e-6 and (hole_only or pad.GetShape() == p.PAD_SHAPE_CIRCLE):
capsule(mask, xy, xy, rx + extra)
else:
# Bounding rectangle deliberately overestimates non-circular pad shapes.
box = pad.GetBoundingBox()
x0, y0 = p.ToMM(box.GetX()) - extra, p.ToMM(box.GetY()) - extra
x1, y1 = p.ToMM(box.GetRight()) + extra, p.ToMM(box.GetBottom()) + extra
sy, sx = window(x0, y0, x1, y1)
mask[sy, sx] |= (X[:, sx] >= x0) & (X[:, sx] <= x1) & (Y[sy, :] >= y0) & (Y[sy, :] <= y1)
def boundary(radius):
edge = 0.5 + radius + MARGIN
mask = np.broadcast_to((X < 100 + edge) | (X > 182 - edge) |
(Y < 100 + edge) | (Y > 180 - edge), (NY, NX)).copy()
mask |= (X < 136 + edge) & (Y > 161 - edge)
# Expand the RF reservation by copper radius plus numerical safety margin.
r = radius + MARGIN
mask |= (X >= 110 - r) & (X <= 131 + r) & (Y <= 113 + r)
return mask
def masks(board, pads, net, width):
blocked = np.stack([boundary(width / 2)] * 2)
via = boundary(0.35)
for pad in pads:
if pad.GetNetCode() != net or not pad.GetNumber():
for z, layer in enumerate(LAYERS):
if pad.IsOnLayer(layer):
pad_obstacle(blocked[z], pad, CLEARANCE + width / 2 + MARGIN)
pad_obstacle(via, pad, CLEARANCE + 0.35 + MARGIN)
# Even same-net holes must not receive a drilled via.
pad_obstacle(via, pad, CLEARANCE + 0.35 + MARGIN, hole_only=True)
for track in board.GetTracks():
if track.GetNetCode() == net:
if isinstance(track, p.PCB_VIA):
capsule(via, pos(track), pos(track), 0.7 + CLEARANCE + MARGIN)
continue
a = (p.ToMM(track.GetStart().x), p.ToMM(track.GetStart().y))
b = (p.ToMM(track.GetEnd().x), p.ToMM(track.GetEnd().y))
radius = p.ToMM(track.GetWidth(p.F_Cu) if isinstance(track, p.PCB_VIA) else track.GetWidth()) / 2
for z, layer in enumerate(LAYERS):
if isinstance(track, p.PCB_VIA) or track.GetLayer() == layer:
capsule(blocked[z], a, b, radius + CLEARANCE + width / 2 + MARGIN)
capsule(via, a, b, radius + CLEARANCE + 0.35 + MARGIN)
return blocked.reshape(-1), via.reshape(-1)
def node(xy, layer=0):
x, y = (int(round((v - 100) / STEP)) for v in xy)
return layer * N + y * NX + x
def decode(i):
z, q = divmod(i, N)
y, x = divmod(q, NX)
return (100 + x * STEP, 100 + y * STEP), z
def astar(blocked, via, start, target, deadline, max_expansions):
tx = int(round((target[0] - 100) / STEP))
ty = int(round((target[1] - 100) / STEP))
targetq = ty * NX + tx
def h(q):
y, x = divmod(q, NX)
dx, dy = abs(x - tx), abs(y - ty)
return 10 * max(dx, dy) + 4 * min(dx, dy)
dist = np.full(2 * N, 2147483647, dtype=np.int32)
parent = np.full(2 * N, -1, dtype=np.int32)
heap = []
for z in (0, 1):
i = node(start, z)
if not blocked[i]:
dist[i] = 0
heapq.heappush(heap, (h(i % N), 0, i))
expanded = 0
moves = ((1, 0, 10), (-1, 0, 10), (0, 1, 10), (0, -1, 10),
(1, 1, 14), (1, -1, 14), (-1, 1, 14), (-1, -1, 14))
while heap:
_, cost, i = heapq.heappop(heap)
if cost != dist[i]:
continue
z, q = divmod(i, N)
if q == targetq:
path = [i]
while parent[path[-1]] >= 0:
path.append(int(parent[path[-1]]))
return path[::-1], expanded
expanded += 1
if expanded >= max_expansions or (expanded % 1024 == 0 and time.monotonic() >= deadline):
return None, expanded
y, x = divmod(q, NX)
for dx, dy, stepcost in moves:
if not (0 <= x + dx < NX and 0 <= y + dy < NY):
continue
j = i + dy * NX + dx
if blocked[j] or (dx and dy and (blocked[i + dx] or blocked[i + dy * NX])):
continue
# Mild layer preference prevents needless coincident layer choices.
penalty = int((z == 0 and dx == 0) or (z == 1 and dy == 0))
nc = cost + stepcost + penalty
if nc < dist[j]:
dist[j], parent[j] = nc, i
heapq.heappush(heap, (nc + h(j % N), nc, j))
j = (1 - z) * N + q
if not via[q] and not blocked[j]:
nc = cost + int(18 / STEP) # 1.8 mm equivalent cost for a layer change.
if nc < dist[j]:
dist[j], parent[j] = nc, i
heapq.heappush(heap, (nc + h(q), nc, j))
return None, expanded
def add_path(board, path, start, target, net, width):
points = [decode(i) for i in path]
points = [(start, points[0][1])] + points + [(target, points[-1][1])]
# Only merge exactly collinear grid runs; never shortcut obstacle checks.
simple = []
for item in points:
if simple and item == simple[-1]:
continue
while len(simple) >= 2 and item[1] == simple[-1][1] == simple[-2][1]:
a, b, c = simple[-2][0], simple[-1][0], item[0]
cross = (b[0]-a[0])*(c[1]-b[1]) - (b[1]-a[1])*(c[0]-b[0])
dot = (b[0]-a[0])*(c[0]-b[0]) + (b[1]-a[1])*(c[1]-b[1])
if abs(cross) > 1e-8 or dot < 0:
break
simple.pop()
simple.append(item)
for (a, z), (b, zz) in zip(simple, simple[1:]):
if z != zz:
assert a == b
item = p.PCB_VIA(board)
item.SetPosition(point(a))
item.SetWidth(mm(0.7))
item.SetDrill(mm(0.3))
item.SetViaType(p.VIATYPE_THROUGH)
item.SetLayerPair(p.F_Cu, p.B_Cu)
else:
if a == b:
continue
item = p.PCB_TRACK(board)
item.SetStart(point(a))
item.SetEnd(point(b))
item.SetWidth(mm(width))
item.SetLayer(LAYERS[z])
item.SetNetCode(net)
board.Add(item)
def pairs_for_net(pads):
# Minimum spanning tree includes duplicate switch contacts explicitly.
connected = {0}
todo = set(range(1, len(pads)))
pairs = []
while todo:
length, a, b = min((math.dist(pos(pads[a]), pos(pads[b])), a, b)
for a in connected for b in todo)
pairs.append((length, pads[a], pads[b]))
connected.add(b)
todo.remove(b)
return pairs
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--route', action='store_true')
parser.add_argument('--overwrite', action='store_true')
parser.add_argument('--replace-routes', action='store_true')
parser.add_argument('--seconds', type=float, default=240)
parser.add_argument('--max-expansions', type=int, default=1200000)
args = parser.parse_args()
if args.route and not args.overwrite:
parser.error('--route requires --overwrite; back up manual routing first')
if not 0 < args.seconds <= 900 or not 0 < args.max_expansions <= 1500000:
parser.error('bounds: seconds (0,900], max-expansions (0,1500000]')
if ROOT.joinpath('~serial-carrier.kicad_pcb.lck').exists():
parser.error('PCB editor lock exists; close the PCB before routing')
original_bytes = PCB.read_bytes()
board = p.LoadBoard(str(PCB))
before = identity(board)
if board.GetCopperLayerCount() != 2 or len(board.GetFootprints()) != 13:
parser.error('unexpected board structure; review router assumptions')
if len(board.Zones()) != 1 or not board.Zones()[0].GetIsRuleArea():
parser.error('unexpected zones; router supports only the original RF rule area')
outline = [(100, 100), (182, 100), (182, 180), (136, 180), (136, 161), (100, 161)]
expected_edges = {frozenset((a, b)) for a, b in zip(outline, outline[1:] + outline[:1])}
actual_edges = {frozenset(((p.ToMM(e.GetStart().x), p.ToMM(e.GetStart().y)),
(p.ToMM(e.GetEnd().x), p.ToMM(e.GetEnd().y))))
for e in board.GetDrawings() if e.GetLayer() == p.Edge_Cuts}
if actual_edges != expected_edges:
parser.error('outline differs from the board-specific routing envelope')
reserve = board.Zones()[0]
contour = reserve.Outline().COutline(0)
rf_points = {(p.ToMM(contour.CPoint(i).x), p.ToMM(contour.CPoint(i).y))
for i in range(contour.PointCount())}
if (rf_points != {(110, 100), (131, 100), (131, 113), (110, 113)} or
not all(reserve.IsOnLayer(layer) for layer in LAYERS) or
not all((reserve.GetDoNotAllowTracks(), reserve.GetDoNotAllowVias(),
reserve.GetDoNotAllowZoneFills()))):
parser.error('RF rule area differs from the board-specific reservation')
if board.GetTracks() and not args.replace_routes:
parser.error('existing routing protected; --replace-routes required even for preview')
for track in list(board.GetTracks()):
board.Delete(track)
pads = [a for f in board.GetFootprints() for a in f.Pads()]
if any(a.GetOrientationDegrees() % 90 or not all(a.IsOnLayer(l) for l in LAYERS) for a in pads):
parser.error('router requires axis-aligned through-hole pads')
nets = defaultdict(list)
for pad in pads:
if pad.GetNumber() and pad.GetNetCode():
nets[pad.GetNetCode()].append(pad)
work = []
for net, netpads in nets.items():
if len(netpads) > 1:
name = netpads[0].GetNetname()
for length, a, b in pairs_for_net(netpads):
# Short local links first, then the long signal fanout; ground last.
priority = (name == '/GND', length)
work.append((priority, net, name, a, b))
work.sort(key=lambda w: (w[0], w[2]))
start_time = time.monotonic()
deadline = start_time + args.seconds
results = []
for _, net, name, a, b in work:
width = 0.5 if name in ('/+3V3', '/GND') else 0.25
path, expanded = None, 0
if time.monotonic() < deadline:
blocked, via = masks(board, pads, net, width)
path, expanded = astar(blocked, via, pos(a), pos(b), deadline, args.max_expansions)
fallback = False
if path is None and width == 0.5 and time.monotonic() < deadline:
width, fallback = 0.25, True
blocked, via = masks(board, pads, net, width)
path, more = astar(blocked, via, pos(a), pos(b), deadline, args.max_expansions)
expanded += more
label = lambda pad: pad.GetParentFootprint().GetReference() + '.' + pad.GetNumber()
result = dict(net=name, start=label(a), end=label(b), start_mm=pos(a), end_mm=pos(b),
routed=path is not None, width_mm=width, power_fallback=fallback, expansions=expanded)
results.append(result)
if path is not None:
add_path(board, path, pos(a), pos(b), net, width)
print(json.dumps(result), flush=True)
assert identity(board) == before, 'immutable board data changed'
report = dict(draft_only=True, saved=args.route, grid_mm=STEP, clearance_mm=CLEARANCE,
safety_margin_mm=MARGIN, via_diameter_mm=0.7, via_drill_mm=0.3,
elapsed_seconds=time.monotonic()-start_time, connections=results,
routed_tree_edges=sum(r['routed'] for r in results), total_tree_edges=len(results),
tracks=sum(not isinstance(t, p.PCB_VIA) for t in board.GetTracks()),
vias=sum(isinstance(t, p.PCB_VIA) for t in board.GetTracks()),
immutable_identity_sha256=hashlib.sha256(repr(before).encode()).hexdigest(),
input_sha256=hashlib.sha256(original_bytes).hexdigest(),
validation='KiCad DRC must be run separately; tree-edge counts are not DRC connectivity')
if args.route:
if PCB.read_bytes() != original_bytes or ROOT.joinpath('~serial-carrier.kicad_pcb.lck').exists():
raise RuntimeError('PCB changed or editor opened during routing; refusing overwrite')
# Check a serialized candidate before replacing the user's board.
candidate = ROOT / 'validation/pcb-routing-candidate.kicad_pcb'
p.SaveBoard(str(candidate), board)
assert identity(p.LoadBoard(str(candidate))) == before, 'saved identity mismatch'
candidate.replace(PCB)
ROOT.joinpath('validation/pcb-routing-run.json').write_text(json.dumps(report, indent=2)+'\n')
print(json.dumps({k: v for k, v in report.items() if k != 'connections'}, indent=2))
if __name__ == '__main__':
main()
+117
View File
@@ -0,0 +1,117 @@
# MAX3243 carrier — PROVISIONAL female CAD reference / VERIFY MALE
Footprint: `Carrier:MAX3243_Reference_Provisional` in
`Carrier.pretty/MAX3243_Reference_Provisional.kicad_mod`.
Intended only for a <=100 × 100 mm, two-copper-layer carrier PCB draft.
No schematic assignment or edits are made. This footprint does not establish
whole-board fit, connector access, DRC compliance, or fabrication readiness.
## Evidence and scope
The user reports that the male **6253** module has the same dimensions. This is
not confirmation of its pad map. Geometry below is taken from published **female
5988** Eagle CAD, not verified male-specific CAD. Confirm the purchased module's
identity, orientation, header numbering, outline and mounting geometry before
release. Never infer header order from DE-9 gender or mating-face numbering.
Source: [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB),
commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`.
Local original: `reference/Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd`;
SHA-256 `6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`
was rechecked against the file. See `adafruit-research.md` for provenance and
female/male distinctions.
CAD-derived geometry adapted from the design by **Limor Fried/Ladyada for Adafruit
Industries**, under **CC BY-SA 3.0 Unported**:
https://creativecommons.org/licenses/by-sa/3.0/ . The CAD-derived footprint and
these CAD-derived notes use that license. Preserve the full upstream notices in
`reference/README.md` and `reference/license.txt` when redistributing this adaptation.
Changes: coordinate transformation, carrier pad convention, NPTH carrier holes,
and provisional annotations; no module circuitry or connector package copied.
## View, origin and exact nominal geometry
Front/component top view, module facing up, DE-9 toward the top (**negative Y**).
Origin is header JP2 pin 1, not a PCB corner. X increases rightward; Y downward.
Transform Eagle board coordinates in mm with:
`x = X - 1.905; y = 2.667 - Y`.
- Pads 1…12: `(2.54 × (N - 1), 0)`, first-to-last span 27.94 mm.
- Full bare-module outline is on `F.Fab`, **not carrier `Edge.Cuts`**.
Bounds: X = -1.905…29.845, Y = -26.670…2.667 (31.750 × 29.337 mm).
Top corners are square. Bottom corners retain the source 2.540 mm radius arcs.
- Left arc: (-1.905, 0.127) to (0.635, 2.667), centre (0.635, 0.127).
Right arc: (27.305, 2.667) to (29.845, 0.127), centre (27.305, 0.127).
Midpoints use radius/sqrt(2), rounded to nine decimal places in the file;
the arcs are not chamfers or a bounding-box approximation.
- Two unnumbered **3.00 mm NPTH carrier holes**: (1.905, -8.763) and
(26.035, -8.763), spacing 24.130 mm. These transfer module-hole centres and
drill diameter only. The source module holes are actually plated, 3.60 mm
copper / 3.00 mm drill, unconnected in the source signal map. Carrier NPTH
deliberately does not copy that plating or assign electrical connections.
No screw, washer, standoff or tool-access clearance is implied.
## Provisional carrier pads and female reference map
Same convention documented in `footprint-notes.md` for the other carrier modules:
**1.70 × 1.70 mm PTH pads / 1.00 mm drills**, rectangular pad 1, circular pads
2…12, `*.Cu` and `*.Mask`, unused copper layers retained. That convention derives
from the installed KiCad `PinHeader_1x04_P2.54mm_Vertical`; it is not a selected
header/socket qualification. The source module itself uses 1.9304 mm round pads.
Confirm actual pins/sockets, finished-hole tolerances, annular rings and assembly
height before fabrication. Square pad 1 is a carrier marker, not source evidence.
| Pad | X (mm), Y=0 | Female label | Female CAD net |
|---:|---:|---|---|
| 1 | 0.00 | Vin | VCC |
| 2 | 2.54 | GND | GND |
| 3 | 5.08 | DCD | DCD |
| 4 | 7.62 | RX | RXD |
| 5 | 10.16 | TX | TXD |
| 6 | 12.70 | DTR | DTR |
| 7 | 15.24 | DSR | DSR |
| 8 | 17.78 | RTS | RTS |
| 9 | 20.32 | CTS | CTS |
| 10 | 22.86 | RI | RI |
| 11 | 25.40 | VLD | !INVALID |
| 12 | 27.94 | OFF | !FORCEOFF |
**This is the female reference map, NOT a confirmed male map.** Footprint pads
have numbers only, no assigned nets. OFF is active low; VLD indicates valid input
when high. This project's logic interface requires 3.3 V Vin.
## Intentionally omitted / release gates
**No verified DE-9 mechanical envelope is available, so none is drawn.** No
male overhang, shell, mating plug, cable bend, enclosure cutout, height, underside
keepout, mounting-hardware clearance, 3D model or courtyard is invented. Female
library silkscreen and product bounding dimensions are not male clearance data.
The F.Fab outline describes only the bare module PCB, not the assembled envelope.
There is **no fake courtyard**; courtyard checks cannot establish assembly fit.
Short silkscreen labels identify PROVISIONAL / FEMALE CAD REF / VERIFY MALE.
Before layout release: verify male pad map and orientation against the actual
module or authoritative male CAD; perform a 1:1 fit check; select headers and
mounting hardware; verify assembled and mating envelopes; then establish real
clearances/courtyard and check the complete <=100 × 100 mm two-layer layout.
## Validation performed
KiCad **10.0.6** `pcbnew.FootprintLoad` successfully loaded the footprint using
system Python (`/usr/bin/python3 -B`) in a read-only inline check. Assertions passed
for all 12 numbered pad positions against source JP2 package/element coordinates,
female signal-contactref order, pad sizes/drills/shapes and copper/mask presence,
no assigned header nets, both NPTH centres/sizes/drills, and all six transformed
source outline segments including both 90-degree, 2.540 mm radius arcs and centres.
Also checked absence of courtyard, Edge.Cuts, zones and 3D models.
The first check assumed arc endpoint order was preserved; KiCad normalizes arc
orientation. The corrected check matches endpoint pairs independent of order and
verifies radius, angle and centre; it passed without changing geometry. The
binding emitted the same three startup `PROPERTY_ENUM` assertions documented for
other module validation; these did not prevent loading or successful checks.
No hardware fit test, male electrical verification, board DRC or firmware build
was performed. Existing `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.
File diff suppressed because it is too large Load Diff
+352
View File
@@ -0,0 +1,352 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_barcodes": false,
"apply_defaults_to_fp_dimensions": false,
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 1.27
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"border_display_style": 2,
"border_hatch_pitch": 0.5,
"corner_radius": 0.0,
"corner_smoothing": 0,
"fill_mode": 0,
"hatch_gap": 1.5,
"hatch_orientation": 0.0,
"hatch_smoothing_level": 0,
"hatch_smoothing_value": 0.1,
"hatch_thickness": 1.0,
"min_clearance": 0.5,
"min_island_area": 10.0,
"min_thickness": 0.25,
"pad_connection": 1,
"remove_islands": 0,
"thermal_relief_gap": 0.5,
"thermal_relief_spoke_width": 0.5
}
},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_field_mismatch": "warning",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"missing_tuning_profile": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_not_centered_on_via": "ignore",
"track_on_post_machined_layer": "error",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"tuning_profile_track_geometries": "ignore",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.25,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.25,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.25,
"min_via_annular_width": 0.1,
"min_via_diameter": 0.7,
"solder_mask_to_copper_clearance": 0.0,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [],
"zones_allow_external_fillets": false
},
"ipc2581": {
"bom_rev": "",
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": "",
"sch_revision": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"component_class_settings": {
"assignments": [],
"meta": {
"version": 0
},
"sheet_component_classes": {
"enabled": false
}
},
"cvpcb": {
"equivalence_files": []
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "serial-carrier.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.25,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.25,
"tuning_profile": "",
"via_diameter": 0.7,
"via_drill": 0.3,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.25,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.25,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.5,
"tuning_profile": "",
"via_diameter": 0.7,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{ "netclass": "Power", "pattern": "/+3V3" },
{ "netclass": "Power", "pattern": "/GND" }
]
},
"pcbnew": {
"last_paths": {
"idf": "",
"netlist": "",
"plot": "",
"specctra_dsn": "",
"step": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"bus_aliases": {},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"top_level_sheets": [
{
"filename": "serial-carrier.kicad_sch",
"name": "serial-carrier",
"uuid": "00000000-0000-0000-0000-000000000000"
}
]
},
"sheets": [],
"text_variables": {},
"tuning_profiles": {
"meta": {
"version": 0
},
"tuning_profiles_impedance_geometric": []
}
}
+400
View File
@@ -0,0 +1,400 @@
(kicad_sch (version 20250114) (generator "eeschema") (uuid "c86e0d56-4290-5f2b-b790-14926c8cdeea") (paper "A3")
(title_block (title "ESP32 Serial Swiss Army Knife - Module Carrier") (date "2026-09-20") (rev "A0 DRAFT") (comment 1 "Not fabrication-ready: provisional module footprints and male RS-232 mapping"))
(lib_symbols (symbol "Carrier:HW678_N16R8" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 34.29 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "HW678_N16R8" (at 0 31.75 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:HW678_2x22_Provisional" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "HW678_N16R8_0_1" (rectangle (start -17.78 29.21) (end 17.78 -29.21) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "HW678_N16R8_1_1"
(pin power_out line (at -22.86 26.67 0) (length 5.08) (name "3V3" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at -22.86 24.13 0) (length 5.08) (name "3V3" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 21.59 0) (length 5.08) (name "EN" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 19.05 0) (length 5.08) (name "GPIO4" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 16.51 0) (length 5.08) (name "GPIO5" (effects (font (size 1.016 1.016)) )) (number "5" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 13.97 0) (length 5.08) (name "GPIO6" (effects (font (size 1.016 1.016)) )) (number "6" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 11.43 0) (length 5.08) (name "GPIO7" (effects (font (size 1.016 1.016)) )) (number "7" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 8.89 0) (length 5.08) (name "GPIO15" (effects (font (size 1.016 1.016)) )) (number "8" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 6.35 0) (length 5.08) (name "GPIO16" (effects (font (size 1.016 1.016)) )) (number "9" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 3.81 0) (length 5.08) (name "GPIO17" (effects (font (size 1.016 1.016)) )) (number "10" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 1.27 0) (length 5.08) (name "GPIO18" (effects (font (size 1.016 1.016)) )) (number "11" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -1.27 0) (length 5.08) (name "GPIO8" (effects (font (size 1.016 1.016)) )) (number "12" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -3.81 0) (length 5.08) (name "GPIO3" (effects (font (size 1.016 1.016)) )) (number "13" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -6.35 0) (length 5.08) (name "GPIO46" (effects (font (size 1.016 1.016)) )) (number "14" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -8.89 0) (length 5.08) (name "GPIO9" (effects (font (size 1.016 1.016)) )) (number "15" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -11.43 0) (length 5.08) (name "GPIO10" (effects (font (size 1.016 1.016)) )) (number "16" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -22.86 -13.97 0) (length 5.08) (name "GPIO11" (effects (font (size 1.016 1.016)) )) (number "17" (effects (font (size 1.016 1.016)) )))
(pin output line (at -22.86 -16.51 0) (length 5.08) (name "GPIO12" (effects (font (size 1.016 1.016)) )) (number "18" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -19.05 0) (length 5.08) (name "GPIO13" (effects (font (size 1.016 1.016)) )) (number "19" (effects (font (size 1.016 1.016)) )))
(pin input line (at -22.86 -21.59 0) (length 5.08) (name "GPIO14" (effects (font (size 1.016 1.016)) )) (number "20" (effects (font (size 1.016 1.016)) )))
(pin passive line (at -22.86 -24.13 0) (length 5.08) (name "5V" (effects (font (size 1.016 1.016)) )) (number "21" (effects (font (size 1.016 1.016)) )))
(pin power_out line (at -22.86 -26.67 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "22" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 26.67 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "23" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 24.13 180) (length 5.08) (name "GPIO43" (effects (font (size 1.016 1.016)) )) (number "24" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 21.59 180) (length 5.08) (name "GPIO44" (effects (font (size 1.016 1.016)) )) (number "25" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 19.05 180) (length 5.08) (name "GPIO1" (effects (font (size 1.016 1.016)) )) (number "26" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 16.51 180) (length 5.08) (name "GPIO2" (effects (font (size 1.016 1.016)) )) (number "27" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 13.97 180) (length 5.08) (name "GPIO42" (effects (font (size 1.016 1.016)) )) (number "28" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 11.43 180) (length 5.08) (name "GPIO41" (effects (font (size 1.016 1.016)) )) (number "29" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 8.89 180) (length 5.08) (name "GPIO40" (effects (font (size 1.016 1.016)) )) (number "30" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 6.35 180) (length 5.08) (name "GPIO39" (effects (font (size 1.016 1.016)) )) (number "31" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 3.81 180) (length 5.08) (name "GPIO38" (effects (font (size 1.016 1.016)) )) (number "32" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 1.27 180) (length 5.08) (name "GPIO37" (effects (font (size 1.016 1.016)) )) (number "33" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -1.27 180) (length 5.08) (name "GPIO36" (effects (font (size 1.016 1.016)) )) (number "34" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -3.81 180) (length 5.08) (name "GPIO35" (effects (font (size 1.016 1.016)) )) (number "35" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -6.35 180) (length 5.08) (name "GPIO0" (effects (font (size 1.016 1.016)) )) (number "36" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -8.89 180) (length 5.08) (name "GPIO45" (effects (font (size 1.016 1.016)) )) (number "37" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -11.43 180) (length 5.08) (name "GPIO48" (effects (font (size 1.016 1.016)) )) (number "38" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -13.97 180) (length 5.08) (name "GPIO47" (effects (font (size 1.016 1.016)) )) (number "39" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -16.51 180) (length 5.08) (name "GPIO21" (effects (font (size 1.016 1.016)) )) (number "40" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -19.05 180) (length 5.08) (name "GPIO20" (effects (font (size 1.016 1.016)) )) (number "41" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at 22.86 -21.59 180) (length 5.08) (name "GPIO19" (effects (font (size 1.016 1.016)) )) (number "42" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 -24.13 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "43" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 22.86 -26.67 180) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "44" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "Carrier:MAX3243_Module_Reference" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "U" (at 0 22.86 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "MAX3243_Module_Reference" (at 0 20.32 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "MAX3243_Module_Reference_0_1" (rectangle (start -15.24 17.78) (end 15.24 -17.78) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "MAX3243_Module_Reference_1_1"
(pin power_in line (at -20.32 13.97 0) (length 5.08) (name "Vin" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin power_in line (at -20.32 11.43 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 8.89 0) (length 5.08) (name "DCD" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 6.35 0) (length 5.08) (name "RX" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 3.81 0) (length 5.08) (name "TX" (effects (font (size 1.016 1.016)) )) (number "5" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 1.27 0) (length 5.08) (name "DTR" (effects (font (size 1.016 1.016)) )) (number "6" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -1.27 0) (length 5.08) (name "DSR" (effects (font (size 1.016 1.016)) )) (number "7" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 -3.81 0) (length 5.08) (name "RTS" (effects (font (size 1.016 1.016)) )) (number "8" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -6.35 0) (length 5.08) (name "CTS" (effects (font (size 1.016 1.016)) )) (number "9" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -8.89 0) (length 5.08) (name "RI" (effects (font (size 1.016 1.016)) )) (number "10" (effects (font (size 1.016 1.016)) )))
(pin output line (at -20.32 -11.43 0) (length 5.08) (name "VLD" (effects (font (size 1.016 1.016)) )) (number "11" (effects (font (size 1.016 1.016)) )))
(pin input line (at -20.32 -13.97 0) (length 5.08) (name "OFF_N" (effects (font (size 1.016 1.016)) )) (number "12" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "Carrier:OLED_I2C_26mm" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "DS" (at 0 12.7 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "OLED_I2C_26mm" (at 0 10.16 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:OLED_26mm_I2C_Provisional" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "OLED_I2C_26mm_0_1" (rectangle (start -10.16 7.62) (end 10.16 -7.62) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "OLED_I2C_26mm_1_1"
(pin power_in line (at -15.24 3.81 0) (length 5.08) (name "GND" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin power_in line (at -15.24 1.27 0) (length 5.08) (name "VCC" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
(pin input line (at -15.24 -1.27 0) (length 5.08) (name "SCL" (effects (font (size 1.016 1.016)) )) (number "3" (effects (font (size 1.016 1.016)) )))
(pin bidirectional line (at -15.24 -3.81 0) (length 5.08) (name "SDA" (effects (font (size 1.016 1.016)) )) (number "4" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "Carrier:SW_NO" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "SW" (at 0 7.62 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "SW_NO" (at 0 5.08 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "SW_NO_0_1" (polyline (pts (xy -2.54 0) (xy 2.54 1.524)) (stroke (width 0.254) (type default)) (fill (type none)))(circle (center -2.54 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none)))(circle (center 2.54 0) (radius 0.35) (stroke (width 0.1524) (type default)) (fill (type none))))
(symbol "SW_NO_1_1"
(pin passive line (at -5.08 0 0) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 5.08 0 180) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
)
)
(symbol "Carrier:R" (pin_names (offset 0.762)) (in_bom yes) (on_board yes)
(property "Reference" "R" (at 0 6.096 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "R" (at 0 3.556 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Description" "Resistor, axial DIN0207 footprint; exact MPN pending" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
(symbol "R_0_1" (rectangle (start -2.54 1.016) (end 2.54 -1.016) (stroke (width 0.254) (type default)) (fill (type background))))
(symbol "R_1_1"
(pin passive line (at -5.08 0 0) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "1" (effects (font (size 1.016 1.016)) )))
(pin passive line (at 5.08 0 180) (length 2.54) (name "~" (effects (font (size 1.016 1.016)) )) (number "2" (effects (font (size 1.016 1.016)) )))
)
))
(symbol (lib_id "Carrier:HW678_N16R8") (at 95.25 109.22 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "3ab265d0-7fd3-5891-94eb-d39426c6a8a3")
(property "Reference" "U1" (at 95.25 74.93 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "HW678 / S3-N16R8" (at 95.25 77.47 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:HW678_2x22_Provisional" (at 95.25 109.22 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 95.25 109.22 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Status" "Header geometry nominal; outline/antenna clearance pending" (at 95.25 109.22 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Pin_numbering" "Left 1-22; right 23-44; both top-to-bottom, antenna up" (at 95.25 109.22 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "4a464e7d-e18b-557c-8fae-641c5fc5419f"))
(pin "2" (uuid "8224a9d5-21e3-5814-9dda-20f82d0dff57"))
(pin "3" (uuid "c8362eee-41d1-5268-ba40-ab780577836d"))
(pin "4" (uuid "c38294c1-5e07-56bc-8afd-b92e622ee4c8"))
(pin "5" (uuid "e9137085-cd7e-5481-8165-89ead64f6f0d"))
(pin "6" (uuid "7400e635-9d97-5e1b-8bdb-cef9cc1eaaf9"))
(pin "7" (uuid "21108d2d-b874-58a0-b220-e7ed30048e86"))
(pin "8" (uuid "2d607a2c-f0d7-53db-9f0f-ba99c277bebf"))
(pin "9" (uuid "f0feb47f-2080-575a-adaa-6086d2b111ce"))
(pin "10" (uuid "f7e3c60e-a23d-5f8f-a0b4-e2259b587906"))
(pin "11" (uuid "751f386b-e0e0-5fd7-b93b-5006c04bf57e"))
(pin "12" (uuid "35b6d88b-5907-5977-aadc-eeb22f38e80a"))
(pin "13" (uuid "351de5e5-c93c-5146-ab20-04e74cf929e8"))
(pin "14" (uuid "6ecd8a0c-5a4e-5e23-82e6-4591738b1e23"))
(pin "15" (uuid "6a21011c-83bd-5d62-8359-1c2d77b4b8d8"))
(pin "16" (uuid "11810c32-a16c-5508-9766-59b4bace344a"))
(pin "17" (uuid "44b4c7dd-10ab-59cb-bf10-6e996820493b"))
(pin "18" (uuid "6d2d2610-f346-5f8b-924a-7b7a7b5dec7d"))
(pin "19" (uuid "bde6865b-0c4c-51d5-b38d-38fd9c0b2ba2"))
(pin "20" (uuid "dd0e2dad-30a0-5212-b7d5-873b048010e4"))
(pin "21" (uuid "234394ae-14d9-5f7c-86fc-96d485769c7d"))
(pin "22" (uuid "bfbc25e0-b266-5930-9094-3930cb4640e3"))
(pin "23" (uuid "18243b22-b464-570f-8adf-2ccce1be2e2d"))
(pin "24" (uuid "7a33ec03-7756-557b-8890-fddda9d64305"))
(pin "25" (uuid "98b98ba2-2019-5fd3-b268-37af497dda99"))
(pin "26" (uuid "c7309c87-2402-506d-8b55-d67799b08001"))
(pin "27" (uuid "2744eff7-a534-579f-9cac-da2eb007b47e"))
(pin "28" (uuid "50f8137f-f265-5cdb-844e-c4025996014c"))
(pin "29" (uuid "03a45791-629b-5973-b58b-de8e7260c3d9"))
(pin "30" (uuid "51687701-e717-570b-858f-d0037fde62d1"))
(pin "31" (uuid "36822cf0-a680-5e1f-9105-38331f2639e6"))
(pin "32" (uuid "2b4d7757-c4ba-5eff-9b68-ef9fbff349de"))
(pin "33" (uuid "0864a47a-88c4-5564-be5c-54cede1eb4ff"))
(pin "34" (uuid "87ae74c4-9ef5-520f-b909-5eef264c51cb"))
(pin "35" (uuid "1359325a-5bab-59f5-97c1-0ef02ad26e24"))
(pin "36" (uuid "d2ffd2b2-a6ed-55dc-b6f6-e23ab2ee6823"))
(pin "37" (uuid "fdaaa89f-154c-565f-a9d1-6946c6b7a59a"))
(pin "38" (uuid "5db889a0-00f5-55dc-bd75-68665bc1d769"))
(pin "39" (uuid "9a24d8b0-bd02-5752-ac4d-5003fce71762"))
(pin "40" (uuid "06937bd2-e8de-5f19-81fb-5d225f94caa6"))
(pin "41" (uuid "7079189c-8336-52a4-a74b-10823730406d"))
(pin "42" (uuid "08e6fb4c-12a9-5cdd-a96e-5a66b544c321"))
(pin "43" (uuid "c0a0436c-8a72-5462-83b4-5aed704aaf5d"))
(pin "44" (uuid "cf42b784-faa2-51f5-8b6e-9cbe009fc3ac"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "U1") (unit 1)))))
(wire (pts (xy 72.39 82.55) (xy 59.69 82.55)) (stroke (width 0) (type default)) (uuid "6a5c3165-28b8-521e-b59d-937eb48ebc86"))
(label "+3V3" (at 59.69 82.55 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "8e9c5cf1-acd7-5bd0-b378-16feb2193986"))
(wire (pts (xy 72.39 85.09) (xy 59.69 85.09)) (stroke (width 0) (type default)) (uuid "eadb072f-1c0c-5867-bf06-c526efaa7ffa"))
(label "+3V3" (at 59.69 85.09 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "e35c02e0-4ded-52af-ae01-eebe07485e0a"))
(no_connect (at 72.39 87.63) (uuid "14e4ee36-cb86-5b36-8d06-52b3a1804732"))
(wire (pts (xy 72.39 90.17) (xy 59.69 90.17)) (stroke (width 0) (type default)) (uuid "d9ad45bb-26c2-586f-8ab9-30ddeba5fc31"))
(label "RS_DCD" (at 59.69 90.17 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "af29ef10-6f48-5a58-801f-c006fcf9b755"))
(wire (pts (xy 72.39 92.71) (xy 59.69 92.71)) (stroke (width 0) (type default)) (uuid "ca28ce58-fdf8-5a5f-89da-488eab1b4bfd"))
(label "RS_DSR" (at 59.69 92.71 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "ac3c4cd0-0cce-5cdb-a61d-0b588434404d"))
(wire (pts (xy 72.39 95.25) (xy 59.69 95.25)) (stroke (width 0) (type default)) (uuid "78f6e2c3-e080-56f6-9960-3033cc78e51a"))
(label "RS_RI" (at 59.69 95.25 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "8de0b0a5-4244-5078-a0da-3ee5745e5680"))
(wire (pts (xy 72.39 97.79) (xy 59.69 97.79)) (stroke (width 0) (type default)) (uuid "04298e7e-1e1a-5b86-92e8-c5785182688b"))
(label "RS_DTR" (at 59.69 97.79 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "d9d01c92-5403-5947-8a87-fae7e62d0d8e"))
(wire (pts (xy 72.39 100.33) (xy 59.69 100.33)) (stroke (width 0) (type default)) (uuid "9456c018-93ae-5ed8-9c1b-32a325d8b379"))
(label "RS_RTS" (at 59.69 100.33 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "4d8c3f06-455e-55db-8fff-396fa0b7e11a"))
(wire (pts (xy 72.39 102.87) (xy 59.69 102.87)) (stroke (width 0) (type default)) (uuid "d2ec21ba-7af9-5da0-9e08-1661dd30374a"))
(label "RS_CTS" (at 59.69 102.87 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "8c1a0ed8-16a1-5fc1-ba90-0548b73dc83b"))
(wire (pts (xy 72.39 105.41) (xy 59.69 105.41)) (stroke (width 0) (type default)) (uuid "037b448f-7b47-5741-87e1-1b6f49b9b9f0"))
(label "RS_TX" (at 59.69 105.41 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "072eac8c-bd61-5168-a15c-94d4da50a863"))
(wire (pts (xy 72.39 107.95) (xy 59.69 107.95)) (stroke (width 0) (type default)) (uuid "56d395e8-2d19-5606-9ca3-743ff07722be"))
(label "RS_RX" (at 59.69 107.95 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "e024b664-2aef-5efc-ba59-2a443f66599f"))
(wire (pts (xy 72.39 110.49) (xy 59.69 110.49)) (stroke (width 0) (type default)) (uuid "77cbac5d-1230-55d8-8e74-1e41c2cd8da4"))
(label "RS_VALID" (at 59.69 110.49 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "35c2e93d-3962-566b-8683-cac12c0fa27a"))
(no_connect (at 72.39 113.03) (uuid "4aefdb9e-0fca-5fcc-875b-f873870fdebe"))
(no_connect (at 72.39 115.57) (uuid "2948fac0-bacc-54c4-a568-6ee3d49ff31d"))
(wire (pts (xy 72.39 118.11) (xy 59.69 118.11)) (stroke (width 0) (type default)) (uuid "1aef75f4-ccce-555e-b023-5194df0779c2"))
(label "RS_OFF_N" (at 59.69 118.11 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "b709828f-6e83-5536-afa3-68fa465f6c7c"))
(wire (pts (xy 72.39 120.65) (xy 59.69 120.65)) (stroke (width 0) (type default)) (uuid "c7d046c1-c102-5045-a0d5-88b122a73c46"))
(label "BTN_PREVIOUS" (at 59.69 120.65 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "1482502c-6db5-5ee4-a35b-e191cbdf59c0"))
(wire (pts (xy 72.39 123.19) (xy 59.69 123.19)) (stroke (width 0) (type default)) (uuid "fda79afa-ecf6-5090-8178-a3d646b2a38b"))
(label "OLED_SDA" (at 59.69 123.19 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "ce3c03fe-db6d-5427-98cb-b65007bbe53a"))
(wire (pts (xy 72.39 125.73) (xy 59.69 125.73)) (stroke (width 0) (type default)) (uuid "9da4032e-2f54-5d2e-93f9-49b135df292b"))
(label "OLED_SCL" (at 59.69 125.73 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "eca1f64d-3fe1-5d99-b1b8-acb054f69bb6"))
(wire (pts (xy 72.39 128.27) (xy 59.69 128.27)) (stroke (width 0) (type default)) (uuid "c109b035-7762-5c19-b891-fa46ddfcdec5"))
(label "BTN_SELECT" (at 59.69 128.27 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "b5214ec2-aedd-52d0-8a85-45d5c7d79446"))
(wire (pts (xy 72.39 130.81) (xy 59.69 130.81)) (stroke (width 0) (type default)) (uuid "9d3c585c-1f7e-5a44-bcad-7339d79884d3"))
(label "BTN_NEXT" (at 59.69 130.81 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "0d34aeea-5456-5b2f-8971-ad47f4c23529"))
(no_connect (at 72.39 133.35) (uuid "bcd294b1-7855-539a-88ec-8de20fec460c"))
(wire (pts (xy 72.39 135.89) (xy 59.69 135.89)) (stroke (width 0) (type default)) (uuid "8294090c-9506-522a-b079-913fdf29a6ad"))
(label "GND" (at 59.69 135.89 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "8614b2df-be43-533a-b997-e83d9f4c10f9"))
(wire (pts (xy 118.11 82.55) (xy 130.81 82.55)) (stroke (width 0) (type default)) (uuid "cde93e84-4bb2-5344-b18d-5f88f9a1f470"))
(label "GND" (at 130.81 82.55 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "1d57f5aa-46b1-5519-ab0d-26076787e212"))
(no_connect (at 118.11 85.09) (uuid "b0127af0-5316-5fdd-a572-e50e49541d94"))
(no_connect (at 118.11 87.63) (uuid "8171ff7a-3067-5768-aba5-6df8c9bb0001"))
(no_connect (at 118.11 90.17) (uuid "4e3e062e-eb44-5b93-ad09-fb7e0c85f42f"))
(no_connect (at 118.11 92.71) (uuid "d3db7568-87aa-5f7a-809f-c72a30f1d126"))
(no_connect (at 118.11 95.25) (uuid "16df0850-16e0-5696-8909-523afebe0b32"))
(no_connect (at 118.11 97.79) (uuid "0a438c8b-add8-5a25-b26a-242898998c02"))
(no_connect (at 118.11 100.33) (uuid "13b2680e-cbda-5e35-87d8-7342c24bee65"))
(no_connect (at 118.11 102.87) (uuid "071dfd7b-7397-52ad-9228-900c74ee3cd6"))
(no_connect (at 118.11 105.41) (uuid "0f2099f9-3f16-5cdd-9e67-692d8c63d801"))
(no_connect (at 118.11 107.95) (uuid "1b7e29af-3042-5688-b8e3-b9072f2d45bc"))
(no_connect (at 118.11 110.49) (uuid "da9f9ee2-d65e-5e7c-b541-066369051c98"))
(no_connect (at 118.11 113.03) (uuid "c09c2fc0-4f3a-5a00-a922-dbf5240160d0"))
(no_connect (at 118.11 115.57) (uuid "45c970fe-f750-5241-b68f-a3210b95f132"))
(no_connect (at 118.11 118.11) (uuid "706fb938-caad-5342-90a4-6aa8d26a6983"))
(no_connect (at 118.11 120.65) (uuid "c9aaa7a4-9c4c-5e1e-b271-2c56582d90c1"))
(no_connect (at 118.11 123.19) (uuid "b205a9f5-8146-532d-ad52-36338d4e5a24"))
(no_connect (at 118.11 125.73) (uuid "30825573-28ae-50d6-8a7d-0b77a1808f1f"))
(no_connect (at 118.11 128.27) (uuid "9f2dc584-46f4-512b-90a1-38635223684a"))
(no_connect (at 118.11 130.81) (uuid "ab0f10b3-46d5-5781-a1e1-7d23622f3c22"))
(wire (pts (xy 118.11 133.35) (xy 130.81 133.35)) (stroke (width 0) (type default)) (uuid "bb4a0a06-459f-57bb-a130-ac9bfacba61e"))
(label "GND" (at 130.81 133.35 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "bb66c329-fbef-5925-99b1-cc08edbbe010"))
(wire (pts (xy 118.11 135.89) (xy 130.81 135.89)) (stroke (width 0) (type default)) (uuid "4571a4ff-b89e-51b0-adc4-45846a4d8d8c"))
(label "GND" (at 130.81 135.89 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "9c4d50e5-720c-5ef0-8c5e-a0a59725f0a8"))
(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 "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 "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"))
(pin "3" (uuid "5d477802-5ff5-5dda-b518-af959915061c"))
(pin "4" (uuid "86c4ca1b-d379-5b5c-93d3-383e356ea671"))
(pin "5" (uuid "280aecb0-4e1c-5a83-b678-4ef080970881"))
(pin "6" (uuid "339f561b-dd28-56b7-bd1b-1a8b222b3494"))
(pin "7" (uuid "1960dc0c-b0fa-5dd9-9623-258d944e85d7"))
(pin "8" (uuid "97cacb1f-982d-5831-b852-2ced708e5b19"))
(pin "9" (uuid "40aa2c26-8837-51c7-89f3-c91fdfa456de"))
(pin "10" (uuid "a890a4e8-65ab-5a8b-a491-6b1d4531d079"))
(pin "11" (uuid "ec2dfe5e-4072-560e-880e-ab8f8a85cbe9"))
(pin "12" (uuid "7e4f35fe-028a-5516-8170-04eadc2b1c28"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "U2") (unit 1)))))
(wire (pts (xy 208.28 82.55) (xy 195.58 82.55)) (stroke (width 0) (type default)) (uuid "e93d1edd-22c3-5eae-800d-3921e5221a7c"))
(label "+3V3" (at 195.58 82.55 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "ac84abc3-1072-5498-9c83-e40d54a3bea6"))
(wire (pts (xy 208.28 85.09) (xy 195.58 85.09)) (stroke (width 0) (type default)) (uuid "82783bca-950c-5c43-996b-9408064b2edf"))
(label "GND" (at 195.58 85.09 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "75ec2808-cc79-570e-9845-9fb0a98d3231"))
(wire (pts (xy 208.28 87.63) (xy 195.58 87.63)) (stroke (width 0) (type default)) (uuid "c0de1ff8-0c7f-5ae8-ba62-2b3302eadc93"))
(label "RS_DCD" (at 195.58 87.63 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "9904c95d-f8a0-5894-afa6-445f8a3cfa00"))
(wire (pts (xy 208.28 90.17) (xy 195.58 90.17)) (stroke (width 0) (type default)) (uuid "8d826d42-7bf4-55d8-b1f8-8346d7a75102"))
(label "RS_RX" (at 195.58 90.17 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "4edfb969-0023-5b12-842b-d010d64b6c9f"))
(wire (pts (xy 208.28 92.71) (xy 195.58 92.71)) (stroke (width 0) (type default)) (uuid "1de1a809-0c2b-5c66-93fa-b6572d03b40f"))
(label "RS_TX" (at 195.58 92.71 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "8a97d238-7df7-5201-a7c2-215b084d0edd"))
(wire (pts (xy 208.28 95.25) (xy 195.58 95.25)) (stroke (width 0) (type default)) (uuid "6df22e5c-3f66-557d-b3d0-d2ed031525c5"))
(label "RS_DTR" (at 195.58 95.25 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "7c80f558-16d4-5ba6-b673-c2b2cbff09e1"))
(wire (pts (xy 208.28 97.79) (xy 195.58 97.79)) (stroke (width 0) (type default)) (uuid "886620b9-42d6-52e1-8483-23facfd02f19"))
(label "RS_DSR" (at 195.58 97.79 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "a0a94ae8-b47e-5b05-846f-0efd8f397f55"))
(wire (pts (xy 208.28 100.33) (xy 195.58 100.33)) (stroke (width 0) (type default)) (uuid "70dbba76-347e-5f98-8da2-e037f8f592f9"))
(label "RS_RTS" (at 195.58 100.33 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "09b654e4-afd9-5168-9822-b62724f90a67"))
(wire (pts (xy 208.28 102.87) (xy 195.58 102.87)) (stroke (width 0) (type default)) (uuid "1894bdb0-a2a3-5854-a12b-6a69d76e862b"))
(label "RS_CTS" (at 195.58 102.87 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "cf747b92-81c2-5cca-8e92-d01f0789fe3f"))
(wire (pts (xy 208.28 105.41) (xy 195.58 105.41)) (stroke (width 0) (type default)) (uuid "3e973901-b924-5ee4-a21b-264b3911c50c"))
(label "RS_RI" (at 195.58 105.41 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "cda14f57-0d83-5d8d-a65d-3fd304c5cef9"))
(wire (pts (xy 208.28 107.95) (xy 195.58 107.95)) (stroke (width 0) (type default)) (uuid "1fa8fc2c-f25a-5d81-8a72-ab7b3168abd2"))
(label "RS_VALID" (at 195.58 107.95 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "7e7957b3-d8ff-51ef-996c-19b68b0254a0"))
(wire (pts (xy 208.28 110.49) (xy 195.58 110.49)) (stroke (width 0) (type default)) (uuid "ae100e76-5158-5372-b653-0e3d739e4d8d"))
(label "RS_OFF_N" (at 195.58 110.49 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "734376cf-c18c-512e-a0a4-10246dd2fa26"))
(symbol (lib_id "Carrier:OLED_I2C_26mm") (at 228.6 152.4 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "0ae411c1-94cc-5551-9037-1d59c01a8ee5")
(property "Reference" "DS1" (at 228.6 139.7 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "128x64 I2C OLED / 0x3C" (at 228.6 142.24 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Carrier:OLED_26mm_I2C_Provisional" (at 228.6 152.4 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 228.6 152.4 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Status" "Module outline/header/display user fit-verified; pad/drill fit provisional" (at 228.6 152.4 0) (effects (font (size 1.27 1.27)) (hide yes)))
(pin "1" (uuid "0d23b99f-a7aa-5862-b319-434604b281fc"))
(pin "2" (uuid "5697134b-5fea-552c-a972-f5008c22b049"))
(pin "3" (uuid "8b10ead6-0242-5709-97f0-2f4290180cdd"))
(pin "4" (uuid "3908fc82-c607-5bec-ab4c-90efe237abf6"))
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "DS1") (unit 1)))))
(wire (pts (xy 213.36 148.59) (xy 200.66 148.59)) (stroke (width 0) (type default)) (uuid "41bf2da6-78f1-52d1-9f16-4b8edb2221fe"))
(label "GND" (at 200.66 148.59 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "7942dcf6-75b1-533a-b9d5-c9a5c9032399"))
(wire (pts (xy 213.36 151.13) (xy 200.66 151.13)) (stroke (width 0) (type default)) (uuid "8da97684-8a90-54ff-b1ca-8bc93e05eacc"))
(label "+3V3" (at 200.66 151.13 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "5388601d-60c8-5d05-b5b4-c0abd390b4b2"))
(wire (pts (xy 213.36 153.67) (xy 200.66 153.67)) (stroke (width 0) (type default)) (uuid "459e4f2f-bf9d-5e2f-a3c6-35fbef8ce0cf"))
(label "OLED_SCL" (at 200.66 153.67 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "abe6b20c-25f7-555c-b995-c60fe96c0db5"))
(wire (pts (xy 213.36 156.21) (xy 200.66 156.21)) (stroke (width 0) (type default)) (uuid "9a563337-ee5e-5679-a957-fac4fe55933a"))
(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 "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)))
(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)))))
(wire (pts (xy 322.58 88.9) (xy 309.88 88.9)) (stroke (width 0) (type default)) (uuid "72f62c22-6c21-5ce4-95b5-97fe6afaea38"))
(label "+3V3" (at 309.88 88.9 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "da597fd4-3753-56ba-8a8c-b7ae3d6d6c20"))
(wire (pts (xy 332.74 88.9) (xy 345.44 88.9)) (stroke (width 0) (type default)) (uuid "a965fd28-156e-51a9-a567-a09ee3288b26"))
(label "BTN_PREVIOUS" (at 345.44 88.9 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "b0cdb46e-c123-5a1b-9450-8d6d573bf376"))
(symbol (lib_id "Carrier:SW_NO") (at 327.66 104.14 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "dd5f1f75-0093-55fc-9814-437ed26871eb")
(property "Reference" "SW1" (at 327.66 96.52 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "B3F-1000" (at 327.66 99.06 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
(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)))
(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)))))
(wire (pts (xy 322.58 104.14) (xy 309.88 104.14)) (stroke (width 0) (type default)) (uuid "5b028b05-8a36-55db-aaf9-9b382e614203"))
(label "BTN_PREVIOUS" (at 309.88 104.14 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "5e45c195-3cf8-5a7b-93e1-84b53b154824"))
(wire (pts (xy 332.74 104.14) (xy 345.44 104.14)) (stroke (width 0) (type default)) (uuid "4036fecd-dbe2-5786-a9b9-dce5b14fbf7f"))
(label "GND" (at 345.44 104.14 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "d3b7a7cc-4c33-5080-b795-7892d393bb53"))
(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 "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)))
(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)))))
(wire (pts (xy 322.58 124.46) (xy 309.88 124.46)) (stroke (width 0) (type default)) (uuid "eb68b8eb-dbe6-5133-8089-e9de01165396"))
(label "+3V3" (at 309.88 124.46 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "5bef1a07-0985-5bb8-9b36-ed0c9e2c82ab"))
(wire (pts (xy 332.74 124.46) (xy 345.44 124.46)) (stroke (width 0) (type default)) (uuid "b3b13ef4-4203-52d8-889b-70534a4a1b28"))
(label "BTN_SELECT" (at 345.44 124.46 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "e8d69aad-ffba-5ace-a414-b44ff7528289"))
(symbol (lib_id "Carrier:SW_NO") (at 327.66 139.7 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "c522a716-55a4-584d-a12e-ab135b4c08eb")
(property "Reference" "SW2" (at 327.66 132.08 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "B3F-1000" (at 327.66 134.62 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
(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)))
(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)))))
(wire (pts (xy 322.58 139.7) (xy 309.88 139.7)) (stroke (width 0) (type default)) (uuid "05f5a256-5e68-53ae-a051-81eab3a14d4d"))
(label "BTN_SELECT" (at 309.88 139.7 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "a90ee2dc-ed10-5bb9-9082-99cdbfeac4c9"))
(wire (pts (xy 332.74 139.7) (xy 345.44 139.7)) (stroke (width 0) (type default)) (uuid "04211708-439f-5d65-a5c6-21cf89318109"))
(label "GND" (at 345.44 139.7 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "7f92da60-87bd-5c50-afc0-4046d0c67767"))
(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 "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)))
(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)))))
(wire (pts (xy 322.58 160.02) (xy 309.88 160.02)) (stroke (width 0) (type default)) (uuid "ad7355d6-b196-5b55-b518-28095d4cd010"))
(label "+3V3" (at 309.88 160.02 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "08ba6112-d16a-5597-b3f3-8794ed1ea849"))
(wire (pts (xy 332.74 160.02) (xy 345.44 160.02)) (stroke (width 0) (type default)) (uuid "7a2af359-d23d-5777-9193-ff203ac538d8"))
(label "BTN_NEXT" (at 345.44 160.02 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "5011a121-e49f-5298-8cc1-2998dfa40f7d"))
(symbol (lib_id "Carrier:SW_NO") (at 327.66 175.26 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "35caa6bb-42a6-512c-848c-f1e4457aa97f")
(property "Reference" "SW3" (at 327.66 167.64 0) (effects (font (size 1.27 1.27)) ))
(property "Value" "B3F-1000" (at 327.66 170.18 0) (effects (font (size 1.27 1.27)) ))
(property "Footprint" "Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(property "Datasheet" "" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
(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)))
(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)))))
(wire (pts (xy 322.58 175.26) (xy 309.88 175.26)) (stroke (width 0) (type default)) (uuid "899495f9-e27d-5c7f-9961-bda42dd5b5b5"))
(label "BTN_NEXT" (at 309.88 175.26 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "4917cd5a-7d37-54f4-8139-7ae4739c44c4"))
(wire (pts (xy 332.74 175.26) (xy 345.44 175.26)) (stroke (width 0) (type default)) (uuid "f48c99dc-9974-5205-8a87-3b1278a4fdfd"))
(label "GND" (at 345.44 175.26 0) (effects (font (size 1.016 1.016)) (justify left bottom)) (uuid "3c7713ca-d8f2-53fd-8e98-6fd376e4570c"))
(text "Next" (at 302.26 181.61 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "0db41e9e-455f-5007-82ee-b485aacda4c4"))
(text "MODULE CARRIER - INITIAL SCHEMATIC" (at 15.24 15.24 0) (effects (font (size 2.54 2.54)) (justify left top)) (uuid "81374533-4a9c-521b-8834-9fa30bb907b5"))
(text "Native KiCad 10 schematic. Named wire labels join matching nets on this sheet.\nRead hardware/PCB/README.md before layout or fabrication." (at 15.24 24.13 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "d9d89d85-2a9c-5e76-bd60-48c81ac7f524"))
(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 "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 "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 "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"))
)
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
(sym_lib_table (version 7)
(lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.kicad_sym")(options "")(descr "Project-local module interfaces"))
)
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""Read-only regression checks for the two provisional module footprints.
Run with the system Python that provides KiCad's installed pcbnew binding:
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
Paths resolve relative to this script, independent of the working directory.
No board, reports, or footprint files are written. Passing checks do not establish
physical connector fit, electrical correctness, DRC, or fabrication readiness.
"""
from collections import Counter
from pathlib import Path
import sys
import unittest
try:
import pcbnew as k
except ImportError as exc:
raise SystemExit(
"KiCad pcbnew binding unavailable. Use the system Python associated with "
"your KiCad installation (on this machine: /usr/bin/python3)."
) from exc
LIBRARY = Path(__file__).resolve().parent / "Carrier.pretty"
def xy(vector):
"""Keep native integer coordinates so comparisons do not hide deviations."""
return vector.x, vector.y
def mm(x, y):
return k.FromMM(x), k.FromMM(y)
class FootprintRegression(unittest.TestCase):
def load(self, name):
self.assertTrue((LIBRARY / (name + ".kicad_mod")).is_file(), name)
footprint = k.FootprintLoad(str(LIBRARY), name)
self.assertIsNotNone(footprint, f"KiCad could not parse {name}")
self.assertEqual(footprint.GetValue(), name)
self.assertEqual(footprint.GetLayer(), k.F_Cu)
self.assertIn("PROVISIONAL connector fit", footprint.GetLibDescription())
self.assertEqual(len(list(footprint.Zones())), 0, "Unexpected zone/keepout")
self.assertEqual(len(list(footprint.Models())), 0, "Unexpected 3D envelope")
graphics = list(footprint.GraphicalItems())
for item in graphics:
self.assertNotIn(item.GetLayer(), (k.F_CrtYd, k.B_CrtYd, k.Edge_Cuts))
self.assertIsInstance(item, (k.PCB_TEXT, k.PCB_SHAPE),
"Unexpected graphic/envelope type")
self.assertTrue(any(isinstance(item, k.PCB_TEXT)
and item.GetText() == "PROVISIONAL CONNECTOR FIT"
for item in graphics), "Missing visible provisional label")
return footprint
def check_header(self, footprint, positions, hole_count=0):
pads = list(footprint.Pads())
self.assertEqual(len(pads), len(positions) + hole_count)
numbered = [pad for pad in pads if pad.GetNumber()]
# Compare multiplicities so duplicate numbers cannot hide missing pads.
self.assertEqual(Counter(p.GetNumber() for p in numbered),
Counter({str(n): 1 for n in positions}))
for pad in numbered:
number = int(pad.GetNumber())
with self.subTest(pad=number):
self.assertEqual(xy(pad.GetPosition()), mm(*positions[number]))
self.assertEqual(xy(pad.GetSize()), mm(1.7, 1.7))
self.assertEqual(xy(pad.GetDrillSize()), mm(1, 1))
self.assertEqual(pad.GetDrillShape(), k.PAD_DRILL_SHAPE_CIRCLE)
self.assertEqual(pad.GetAttribute(), k.PAD_ATTRIB_PTH)
self.assertEqual(pad.GetShape(),
k.PAD_SHAPE_RECT if number == 1 else k.PAD_SHAPE_CIRCLE)
self.assertEqual(pad.GetNetCode(), 0, "Unexpected assigned net")
for layer in (k.F_Cu, k.B_Cu, k.F_Mask, k.B_Mask):
self.assertTrue(pad.IsOnLayer(layer), "Missing copper/mask layer")
for layer in (k.F_Paste, k.B_Paste):
self.assertFalse(pad.IsOnLayer(layer), "Unexpected paste layer")
return [pad for pad in pads if not pad.GetNumber()]
def test_oled(self):
footprint = self.load("OLED_26mm_I2C_Provisional")
positions = {n: (x, 1.5) for n, x in
enumerate((9.19, 11.73, 14.27, 16.81), 1)}
holes = self.check_header(footprint, positions, hole_count=4)
self.assertEqual(Counter(xy(p.GetPosition()) for p in holes),
Counter(mm(x, y) for x, y in (
(1.75, 1.75), (24.25, 1.75),
(1.75, 24.25), (24.25, 24.25))))
for pad in holes:
with self.subTest(hole=xy(pad.GetPosition())):
self.assertEqual(pad.GetAttribute(), k.PAD_ATTRIB_NPTH)
self.assertEqual(pad.GetShape(), k.PAD_SHAPE_CIRCLE)
self.assertEqual(pad.GetDrillShape(), k.PAD_DRILL_SHAPE_CIRCLE)
self.assertEqual(xy(pad.GetSize()), mm(2, 2))
self.assertEqual(xy(pad.GetDrillSize()), mm(2, 2))
self.assertEqual(pad.GetNetCode(), 0)
shapes = [g for g in footprint.GraphicalItems() if isinstance(g, k.PCB_SHAPE)]
self.assertEqual(len(shapes), 2, "Only board/display rectangles are verified")
self.assertEqual(Counter((xy(g.GetStart()), xy(g.GetEnd())) for g in shapes),
Counter([(mm(0, 0), mm(26, 26)),
(mm(1, 4.5), mm(25.5, 21))]))
for shape in shapes:
self.assertEqual(shape.GetShape(), k.SHAPE_T_RECT)
self.assertEqual(shape.GetLayer(), k.F_Fab)
self.assertEqual(shape.GetWidth(), k.FromMM(0.1))
self.assertFalse(shape.IsAnyFill())
# Stroke styles are not checked: this binding returns opaque LINE_STYLE
# pointers, so comparing GetLineStyle() results would compare identities,
# not the solid/dash values, and also emits SWIG leak warnings.
def test_hw678(self):
footprint = self.load("HW678_2x22_Provisional")
positions = {
row * 22 + index + 1: (x, round(index * 2.54, 2))
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")
if __name__ == "__main__":
print(f"KiCad {k.GetBuildVersion()}; library: {LIBRARY}", flush=True)
suite = unittest.defaultTestLoader.loadTestsFromTestCase(FootprintRegression)
result = unittest.TextTestRunner(verbosity=2).run(suite)
sys.exit(0 if result.wasSuccessful() else 1)
+94
View File
@@ -0,0 +1,94 @@
#!/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.
"""
import json
from pathlib import Path
import subprocess
import xml.etree.ElementTree as ET
import pcbnew as p
ROOT=Path(__file__).resolve().parent
PCB=ROOT/"serial-carrier.kicad_pcb"
OUT=ROOT/"validation"
def run(*args):
subprocess.run(args,check=True,cwd=ROOT)
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))
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()}
assert set(footprints)==set(comps)|{"H1","H2","H3","H4"}
assert board.GetCopperLayerCount()==2
assert p.ToMM(board.GetDesignSettings().GetBoardThickness())==1.6
outline=[d for d in board.GetDrawings() if d.GetLayer()==p.Edge_Cuts]
assert len(outline)==6
points={(round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5)) for d in outline for v in (d.GetStart(),d.GetEnd())}
assert points=={(0,0),(82,0),(82,80),(36,80),(36,61),(0,61)}
assert max(x for x,y in points)-min(x for x,y in points)==82
assert max(y for x,y in points)-min(y for x,y in points)==80
padnets={}
for net in source.findall("nets/net"):
for node in net.findall("node"):
padnets[(node.get("ref"),node.get("pin"))]=net.get("name")
seen=set()
for ref,comp in comps.items():
fp=footprints[ref]
assert fp.GetValue()==comp.findtext("value"),ref
assert fp.GetPath().AsString().endswith("/"+comp.findtext("tstamps")),ref
wanted=comp.findtext("footprint")
if ref=="U2":
assert not wanted
assert fp.GetFPIDAsString()=="Carrier:MAX3243_Reference_Provisional"
else:
assert fp.GetFPIDAsString()==wanted,ref
for pad in fp.Pads():
if not pad.GetNumber():
continue
key=ref,pad.GetNumber()
assert pad.GetNetname()==padnets[key],(key,pad.GetNetname(),padnets[key])
seen.add(key)
assert seen==set(padnets),"Missing or extra electrical pads"
for ref in ("H1","H2","H3","H4"):
assert footprints[ref].IsBoardOnly()
pads=list(footprints[ref].Pads())
assert len(pads)==1 and pads[0].GetDrillSize().x==p.FromMM(3.2)
assert pads[0].GetAttribute()==p.PAD_ATTRIB_NPTH
tracks=list(board.GetTracks())
assert tracks,"Draft unexpectedly unrouted"
for track in tracks:
if isinstance(track,p.PCB_VIA):
assert track.GetDrillValue()==p.FromMM(0.3)
assert track.GetWidth(p.F_Cu)==p.FromMM(0.7)
else:
assert track.GetLayer() in (p.F_Cu,p.B_Cu)
expected=0.5 if track.GetNetname() in ("/+3V3","/GND") else 0.25
assert track.GetWidth()==p.FromMM(expected)
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()
summary={
"carrier_bounds_mm":[82,80],
"copper_layers":2,
"draft_thickness_mm":1.6,
"electrical_footprints":len(comps),
"proposed_M3_mounts":4,
"physical_DRC_violations":0,
"unconnected_items":0,
"schematic_pad_nets_match":True,
"known_parity_warning":"U2 provisional female-reference footprint assigned on PCB only; schematic footprint intentionally blank",
"ignored_DRC_checks":report["ignored_checks"],
"not_validated":["male module pin map/fit", "assembly envelope and heights", "header/socket fit", "RF performance and antenna clearance", "power/return-path integrity", "USB power path", "OLED pull-ups", "fabrication process", "hardware operation"],
}
(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.")
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Export and validate the checked-in schematic; never regenerates the design."""
import json
from pathlib import Path
import re
import subprocess
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parent
OUT = ROOT / "validation"
SCHEMATIC = ROOT / "serial-carrier.kicad_sch"
def run(*args):
subprocess.run(args, check=True, cwd=ROOT)
run("kicad-cli", "sch", "export", "netlist", "--format", "kicadxml", "-o", str(OUT / "serial-carrier.xml"), str(SCHEMATIC))
run("kicad-cli", "sch", "erc", "--format", "json", "--exit-code-violations", "-o", str(OUT / "erc.json"), str(SCHEMATIC))
root = ET.parse(OUT / "serial-carrier.xml").getroot()
nets = {net.get("name").removeprefix("/"): net.findall("node") for net in root.findall("nets/net")}
members = {name: sorted((node.get("ref"), node.get("pin")) for node in nodes) for name, nodes in nets.items()}
components = {comp.get("ref"): comp for comp in root.findall("components/comp")}
assert set(components) == {"U1", "U2", "DS1", "SW1", "SW2", "SW3", "R1", "R2", "R3"}
# Independent firmware-to-module contract, not imported from the schematic generator.
contracts = [
("RS_TX", "RS232_TX_GPIO", "TX", "5"),
("RS_RX", "RS232_RX_GPIO", "RX", "4"),
("RS_RTS", "RS232_RTS_GPIO", "RTS", "8"),
("RS_CTS", "RS232_CTS_GPIO", "CTS", "9"),
("RS_DTR", "RS232_DTR_GPIO", "DTR", "6"),
("RS_DSR", "RS232_DSR_GPIO", "DSR", "7"),
("RS_DCD", "RS232_DCD_GPIO", "DCD", "3"),
("RS_RI", "RS232_RI_GPIO", "RI", "10"),
("RS_VALID", "RS232_VALID_GPIO", "VLD", "11"),
("RS_OFF_N", "RS232_FORCE_OFF_N_GPIO", "OFF_N", "12"),
("OLED_SDA", "LOCAL_UI_DISPLAY_SDA_GPIO", "SDA", "4"),
("OLED_SCL", "LOCAL_UI_DISPLAY_SCL_GPIO", "SCL", "3"),
("BTN_PREVIOUS", "LOCAL_UI_BUTTON_PREVIOUS_GPIO", None, None),
("BTN_SELECT", "LOCAL_UI_BUTTON_SELECT_GPIO", None, None),
("BTN_NEXT", "LOCAL_UI_BUTTON_NEXT_GPIO", None, None),
]
firmware = ROOT.parents[1].joinpath("src/board_pins.h").read_text()
for net, macro, peripheral_name, peripheral_pin in contracts:
match = re.search(r"^#define\s+" + re.escape(macro) + r"\s+GPIO_NUM_(\d+)\s*$", firmware, re.MULTILINE)
assert match, macro
gpio = "GPIO" + match.group(1)
mcu = [node for node in nets[net] if node.get("ref") == "U1"]
assert len(mcu) == 1 and mcu[0].get("pinfunction") == gpio + "_" + mcu[0].get("pin"), (net, gpio)
if peripheral_name:
ref = "DS1" if net.startswith("OLED_") else "U2"
node = [node for node in nets[net] if node.get("ref") == ref]
assert len(node) == 1 and node[0].get("pin") == peripheral_pin
assert node[0].get("pinfunction") == peripheral_name + "_" + peripheral_pin
assert len(nets[net]) == 2, (net, members[net])
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 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-")}
assert active == {"+3V3", "GND"} | {row[0] for row in contracts}
for name, nodes in nets.items():
if name.startswith("unconnected-"):
assert len(nodes) == 1 and nodes[0].get("ref") == "U1"
# All 44 MCU header pins must occur exactly once, including deliberate no-connects.
assert sorted(int(node.get("pin")) for nodes in nets.values() for node in nodes if node.get("ref") == "U1") == list(range(1,45))
# 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")
expected = json.loads((OUT / "expected-nets.json").read_text())
assert set(expected) == active
for net, nodes in expected.items():
assert members[net] == sorted(tuple(node) for node in nodes), net
report = json.loads((OUT / "erc.json").read_text())
assert not any(sheet["violations"] for sheet in report["sheets"])
run("kicad-cli", "sch", "export", "pdf", "-o", str(ROOT / "serial-carrier.pdf"), str(SCHEMATIC))
run("kicad-cli", "sch", "export", "svg", "-o", str(OUT) + "/", str(SCHEMATIC))
summary = "\n".join([
"Native schematic validation passed.",
"KiCad ERC: zero reported errors/warnings; no project exclusions added.",
"17 connected nets, 9 components; exact net memberships verified.",
"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.",
]) + "\n"
(OUT / "summary.txt").write_text(summary)
print(summary)
+36
View File
@@ -0,0 +1,36 @@
{
"$schema": "https://schemas.kicad.org/erc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T20:14:47",
"ignored_checks": [
{
"description": "Global label only appears once in the schematic",
"key": "single_global_label"
},
{
"description": "Four connection points are joined together",
"key": "four_way_junction"
},
{
"description": "SPICE model issue",
"key": "simulation_model_issue"
},
{
"description": "Assigned footprint doesn't match footprint filters",
"key": "footprint_filter"
}
],
"included_severities": [
"error",
"warning"
],
"kicad_version": "10.0.6",
"sheets": [
{
"path": "/",
"uuid_path": "/c86e0d56-4290-5f2b-b790-14926c8cdeea",
"violations": []
}
],
"source": "serial-carrier.kicad_sch"
}
+232
View File
@@ -0,0 +1,232 @@
{
"+3V3": [
[
"U1",
"1"
],
[
"U1",
"2"
],
[
"U2",
"1"
],
[
"DS1",
"2"
],
[
"R1",
"1"
],
[
"R2",
"1"
],
[
"R3",
"1"
]
],
"BTN_NEXT": [
[
"U1",
"20"
],
[
"R3",
"2"
],
[
"SW3",
"1"
]
],
"BTN_PREVIOUS": [
[
"U1",
"16"
],
[
"R1",
"2"
],
[
"SW1",
"1"
]
],
"BTN_SELECT": [
[
"U1",
"19"
],
[
"R2",
"2"
],
[
"SW2",
"1"
]
],
"GND": [
[
"U1",
"22"
],
[
"U1",
"23"
],
[
"U1",
"43"
],
[
"U1",
"44"
],
[
"U2",
"2"
],
[
"DS1",
"1"
],
[
"SW1",
"2"
],
[
"SW2",
"2"
],
[
"SW3",
"2"
]
],
"OLED_SCL": [
[
"U1",
"18"
],
[
"DS1",
"3"
]
],
"OLED_SDA": [
[
"U1",
"17"
],
[
"DS1",
"4"
]
],
"RS_CTS": [
[
"U1",
"9"
],
[
"U2",
"9"
]
],
"RS_DCD": [
[
"U1",
"4"
],
[
"U2",
"3"
]
],
"RS_DSR": [
[
"U1",
"5"
],
[
"U2",
"7"
]
],
"RS_DTR": [
[
"U1",
"7"
],
[
"U2",
"6"
]
],
"RS_OFF_N": [
[
"U1",
"15"
],
[
"U2",
"12"
]
],
"RS_RI": [
[
"U1",
"6"
],
[
"U2",
"10"
]
],
"RS_RTS": [
[
"U1",
"8"
],
[
"U2",
"8"
]
],
"RS_RX": [
[
"U1",
"11"
],
[
"U2",
"4"
]
],
"RS_TX": [
[
"U1",
"10"
],
[
"U2",
"5"
]
],
"RS_VALID": [
[
"U1",
"12"
],
[
"U2",
"11"
]
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 152 KiB

@@ -0,0 +1,53 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T23:07:29",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning",
"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": 130.0
},
"uuid": "812387f6-6524-459e-8102-fe9c55122668"
}
],
"severity": "warning",
"type": "footprint_symbol_mismatch"
}
],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [],
"violations": []
}
@@ -0,0 +1,37 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T22:50:33",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning",
"exclusion"
],
"kicad_version": "10.0.6",
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [],
"violations": []
}
@@ -0,0 +1,911 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T22:39:18",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning"
],
"kicad_version": "10.0.6",
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 19 [/BTN_SELECT] von U1",
"pos": {
"x": 108.0,
"y": 149.72
},
"uuid": "f7b9badd-1db5-4575-9da8-979a7c8f3946"
},
{
"description": "Durchsteckpad 1 [/BTN_SELECT] von SW2",
"pos": {
"x": 155.0,
"y": 168.0
},
"uuid": "574d93e4-d3fc-4c28-b2d5-4e7d8682c22f"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_SELECT] von SW2",
"pos": {
"x": 155.0,
"y": 168.0
},
"uuid": "574d93e4-d3fc-4c28-b2d5-4e7d8682c22f"
},
{
"description": "Durchsteckpad 1 [/BTN_SELECT] von SW2",
"pos": {
"x": 161.5,
"y": 168.0
},
"uuid": "0398530b-79dc-45b1-886c-51343182e1d3"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_SELECT] von SW2",
"pos": {
"x": 161.5,
"y": 168.0
},
"uuid": "0398530b-79dc-45b1-886c-51343182e1d3"
},
{
"description": "Durchsteckpad 2 [/BTN_SELECT] von R2",
"pos": {
"x": 161.62,
"y": 134.0
},
"uuid": "c118f27f-8ad4-4abe-b9c0-a30a22fe60c9"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 22 [/GND] von U1",
"pos": {
"x": 108.0,
"y": 157.34
},
"uuid": "9d509ec2-7588-4c77-9146-8d03c8646e37"
},
{
"description": "Durchsteckpad 44 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 157.34
},
"uuid": "91b9e8e9-7e2c-43ab-b2ef-ec8ba16a25f1"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 23 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 104.0
},
"uuid": "b66490f6-5d60-415d-babb-215c70511b77"
},
{
"description": "Durchsteckpad 2 [/GND] von U2",
"pos": {
"x": 145.54,
"y": 130.0
},
"uuid": "7744910d-b37b-4fd5-a46a-95ee262d2a08"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 43 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 154.8
},
"uuid": "23b353d7-7ede-43a0-a655-57496d4f8aee"
},
{
"description": "Durchsteckpad 1 [/GND] von DS1",
"pos": {
"x": 152.19,
"y": 137.5
},
"uuid": "0d95ad6d-f278-4240-9618-08ad8fcb5e1c"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 44 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 157.34
},
"uuid": "91b9e8e9-7e2c-43ab-b2ef-ec8ba16a25f1"
},
{
"description": "Durchsteckpad 43 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 154.8
},
"uuid": "23b353d7-7ede-43a0-a655-57496d4f8aee"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW1",
"pos": {
"x": 143.0,
"y": 172.5
},
"uuid": "cc9a376c-9410-4f75-a7a6-3a849cc384b3"
},
{
"description": "Durchsteckpad 2 [/GND] von SW1",
"pos": {
"x": 149.5,
"y": 172.5
},
"uuid": "24c82a41-4f9f-4c94-aaaf-2a3df71bbd08"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW1",
"pos": {
"x": 143.0,
"y": 172.5
},
"uuid": "cc9a376c-9410-4f75-a7a6-3a849cc384b3"
},
{
"description": "Durchsteckpad 44 [/GND] von U1",
"pos": {
"x": 133.4,
"y": 157.34
},
"uuid": "91b9e8e9-7e2c-43ab-b2ef-ec8ba16a25f1"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW1",
"pos": {
"x": 149.5,
"y": 172.5
},
"uuid": "24c82a41-4f9f-4c94-aaaf-2a3df71bbd08"
},
{
"description": "Durchsteckpad 2 [/GND] von SW2",
"pos": {
"x": 155.0,
"y": 172.5
},
"uuid": "970e56ae-cc93-46cd-8807-ccc9cf082aa1"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/GND] von DS1",
"pos": {
"x": 152.19,
"y": 137.5
},
"uuid": "0d95ad6d-f278-4240-9618-08ad8fcb5e1c"
},
{
"description": "Durchsteckpad 2 [/GND] von U2",
"pos": {
"x": 145.54,
"y": 130.0
},
"uuid": "7744910d-b37b-4fd5-a46a-95ee262d2a08"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW2",
"pos": {
"x": 155.0,
"y": 172.5
},
"uuid": "970e56ae-cc93-46cd-8807-ccc9cf082aa1"
},
{
"description": "Durchsteckpad 2 [/GND] von SW2",
"pos": {
"x": 161.5,
"y": 172.5
},
"uuid": "2f44f580-c7e4-4f03-ba46-49605735875c"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW2",
"pos": {
"x": 161.5,
"y": 172.5
},
"uuid": "2f44f580-c7e4-4f03-ba46-49605735875c"
},
{
"description": "Durchsteckpad 2 [/GND] von SW3",
"pos": {
"x": 167.0,
"y": 172.5
},
"uuid": "3cdbd911-6e07-4f86-8887-5953fa3d2a01"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/GND] von SW3",
"pos": {
"x": 167.0,
"y": 172.5
},
"uuid": "3cdbd911-6e07-4f86-8887-5953fa3d2a01"
},
{
"description": "Durchsteckpad 2 [/GND] von SW3",
"pos": {
"x": 173.5,
"y": 172.5
},
"uuid": "8e780381-cbfb-4f62-ae3b-8b0b8fa2a6a8"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/+3V3] von U1",
"pos": {
"x": 108.0,
"y": 104.0
},
"uuid": "9aaad355-039f-439b-9cd7-308d07ace02e"
},
{
"description": "Durchsteckpad 2 [/+3V3] von U1",
"pos": {
"x": 108.0,
"y": 106.54
},
"uuid": "2c8cc7c5-7b73-4bc2-a384-b3a5ac459c92"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/+3V3] von U2",
"pos": {
"x": 143.0,
"y": 130.0
},
"uuid": "f8aafd85-e635-47cd-ba22-ddf4a036d1cd"
},
{
"description": "Durchsteckpad 1 [/+3V3] von R1",
"pos": {
"x": 143.0,
"y": 134.0
},
"uuid": "56ff90bc-b894-4f81-8274-7a34384229e7"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/+3V3] von U2",
"pos": {
"x": 143.0,
"y": 130.0
},
"uuid": "f8aafd85-e635-47cd-ba22-ddf4a036d1cd"
},
{
"description": "Durchsteckpad 2 [/+3V3] von U1",
"pos": {
"x": 108.0,
"y": 106.54
},
"uuid": "2c8cc7c5-7b73-4bc2-a384-b3a5ac459c92"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/+3V3] von R2",
"pos": {
"x": 154.0,
"y": 134.0
},
"uuid": "73a249ed-c3b1-46fb-afd5-f8bfe0e3fa65"
},
{
"description": "Durchsteckpad 1 [/+3V3] von R1",
"pos": {
"x": 143.0,
"y": 134.0
},
"uuid": "56ff90bc-b894-4f81-8274-7a34384229e7"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/+3V3] von DS1",
"pos": {
"x": 154.73,
"y": 137.5
},
"uuid": "62a98206-7a36-4b20-a201-d4ca10b404df"
},
{
"description": "Durchsteckpad 1 [/+3V3] von R2",
"pos": {
"x": 154.0,
"y": 134.0
},
"uuid": "73a249ed-c3b1-46fb-afd5-f8bfe0e3fa65"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/+3V3] von DS1",
"pos": {
"x": 154.73,
"y": 137.5
},
"uuid": "62a98206-7a36-4b20-a201-d4ca10b404df"
},
{
"description": "Durchsteckpad 1 [/+3V3] von R3",
"pos": {
"x": 165.0,
"y": 134.0
},
"uuid": "72b8a1ea-1fa7-480b-8122-aceb08f4bb4b"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 4 [/RS_DCD] von U1",
"pos": {
"x": 108.0,
"y": 111.62
},
"uuid": "d9f0d2af-a701-43e9-9d1e-58a6c7cb5250"
},
{
"description": "Durchsteckpad 3 [/RS_DCD] von U2",
"pos": {
"x": 148.08,
"y": 130.0
},
"uuid": "d589a6a4-0d53-4c2a-8f68-fb4407d9f10e"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 5 [/RS_DSR] von U1",
"pos": {
"x": 108.0,
"y": 114.16
},
"uuid": "0ceca603-53a8-4a53-85cc-f7f8e7be90e4"
},
{
"description": "Durchsteckpad 7 [/RS_DSR] von U2",
"pos": {
"x": 158.24,
"y": 130.0
},
"uuid": "e2d6b2cb-6025-490c-8371-d7cc34ac37f6"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 6 [/RS_RI] von U1",
"pos": {
"x": 108.0,
"y": 116.7
},
"uuid": "f35f1696-b778-437b-8cce-67b8c4d5f5d0"
},
{
"description": "Durchsteckpad 10 [/RS_RI] von U2",
"pos": {
"x": 165.86,
"y": 130.0
},
"uuid": "e28de6f2-fb85-4849-ba1a-e98e2a231492"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 7 [/RS_DTR] von U1",
"pos": {
"x": 108.0,
"y": 119.24
},
"uuid": "354ab122-85b1-4c9c-9385-d5091fe12303"
},
{
"description": "Durchsteckpad 6 [/RS_DTR] von U2",
"pos": {
"x": 155.7,
"y": 130.0
},
"uuid": "81e8ad67-4bdc-4f57-90b1-68cff6a95261"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 8 [/RS_RTS] von U1",
"pos": {
"x": 108.0,
"y": 121.78
},
"uuid": "55162730-46fd-465d-a1c5-20aed09ed534"
},
{
"description": "Durchsteckpad 8 [/RS_RTS] von U2",
"pos": {
"x": 160.78,
"y": 130.0
},
"uuid": "5c304724-2bc6-409d-9c59-2e40adc557b9"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 9 [/RS_CTS] von U1",
"pos": {
"x": 108.0,
"y": 124.32
},
"uuid": "f75c20f8-d3b5-4c3b-9919-cb044b2da5c3"
},
{
"description": "Durchsteckpad 9 [/RS_CTS] von U2",
"pos": {
"x": 163.32,
"y": 130.0
},
"uuid": "a8411036-6859-4041-84b8-2c80c326c58e"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 10 [/RS_TX] von U1",
"pos": {
"x": 108.0,
"y": 126.86
},
"uuid": "22c323db-7146-44af-951b-04a1d7dbb2fe"
},
{
"description": "Durchsteckpad 5 [/RS_TX] von U2",
"pos": {
"x": 153.16,
"y": 130.0
},
"uuid": "f919098b-4ad7-4600-9e8b-3375a1cff050"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 11 [/RS_RX] von U1",
"pos": {
"x": 108.0,
"y": 129.4
},
"uuid": "6573852d-5714-4c8d-8793-0be87f5a052a"
},
{
"description": "Durchsteckpad 4 [/RS_RX] von U2",
"pos": {
"x": 150.62,
"y": 130.0
},
"uuid": "3d90cce4-14d8-46a0-b560-09524f11698f"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 12 [/RS_VALID] von U1",
"pos": {
"x": 108.0,
"y": 131.94
},
"uuid": "2fbc596b-c88f-4fc6-aa94-5dad4fcac9cc"
},
{
"description": "Durchsteckpad 11 [/RS_VALID] von U2",
"pos": {
"x": 168.4,
"y": 130.0
},
"uuid": "9ad42f44-160f-4aa5-bcf0-a4466c090438"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 15 [/RS_OFF_N] von U1",
"pos": {
"x": 108.0,
"y": 139.56
},
"uuid": "34e1afe9-b3fc-4d3a-9e33-38b32244f19e"
},
{
"description": "Durchsteckpad 12 [/RS_OFF_N] von U2",
"pos": {
"x": 170.94,
"y": 130.0
},
"uuid": "a43ba2f2-477d-4850-87de-e1591119d5ae"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_PREVIOUS] von SW1",
"pos": {
"x": 143.0,
"y": 168.0
},
"uuid": "5214c011-545b-4ae7-9cdb-852ade815805"
},
{
"description": "Durchsteckpad 1 [/BTN_PREVIOUS] von SW1",
"pos": {
"x": 149.5,
"y": 168.0
},
"uuid": "84202d30-b2fe-4170-8146-69b68d3b82a1"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_PREVIOUS] von SW1",
"pos": {
"x": 149.5,
"y": 168.0
},
"uuid": "84202d30-b2fe-4170-8146-69b68d3b82a1"
},
{
"description": "Durchsteckpad 2 [/BTN_PREVIOUS] von R1",
"pos": {
"x": 150.62,
"y": 134.0
},
"uuid": "8237f507-f5c7-4eb6-9aa1-f46af2981e7c"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 2 [/BTN_PREVIOUS] von R1",
"pos": {
"x": 150.62,
"y": 134.0
},
"uuid": "8237f507-f5c7-4eb6-9aa1-f46af2981e7c"
},
{
"description": "Durchsteckpad 16 [/BTN_PREVIOUS] von U1",
"pos": {
"x": 108.0,
"y": 142.1
},
"uuid": "fc705960-92fa-4d8f-bb0e-ce2b82d1e7fc"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 17 [/OLED_SDA] von U1",
"pos": {
"x": 108.0,
"y": 144.64
},
"uuid": "1f91549c-5cb2-465f-86df-b2f1b0c16ed8"
},
{
"description": "Durchsteckpad 4 [/OLED_SDA] von DS1",
"pos": {
"x": 159.81,
"y": 137.5
},
"uuid": "91a4dcef-1b36-4e3d-a15c-c4578077c27b"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 18 [/OLED_SCL] von U1",
"pos": {
"x": 108.0,
"y": 147.18
},
"uuid": "1c55fd03-0b3e-494f-a37b-6e0b3496cfc9"
},
{
"description": "Durchsteckpad 3 [/OLED_SCL] von DS1",
"pos": {
"x": 157.27,
"y": 137.5
},
"uuid": "704668d0-88bf-4b51-840a-60a35804fafd"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 20 [/BTN_NEXT] von U1",
"pos": {
"x": 108.0,
"y": 152.26
},
"uuid": "e7159c90-4e8d-4b21-8e84-a7f59f793440"
},
{
"description": "Durchsteckpad 1 [/BTN_NEXT] von SW3",
"pos": {
"x": 167.0,
"y": 168.0
},
"uuid": "c67d0b07-c47f-48b3-9e79-7aed82edbbfc"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_NEXT] von SW3",
"pos": {
"x": 167.0,
"y": 168.0
},
"uuid": "c67d0b07-c47f-48b3-9e79-7aed82edbbfc"
},
{
"description": "Durchsteckpad 1 [/BTN_NEXT] von SW3",
"pos": {
"x": 173.5,
"y": 168.0
},
"uuid": "62f3da3b-8297-4232-92ee-88f3ce4a7d36"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 1 [/BTN_NEXT] von SW3",
"pos": {
"x": 173.5,
"y": 168.0
},
"uuid": "62f3da3b-8297-4232-92ee-88f3ce4a7d36"
},
{
"description": "Durchsteckpad 2 [/BTN_NEXT] von R3",
"pos": {
"x": 172.62,
"y": 134.0
},
"uuid": "9e9dfe49-4b68-472b-a86c-bfcee913a81b"
}
],
"severity": "error",
"type": "unconnected_items"
}
],
"violations": []
}
@@ -0,0 +1,85 @@
{
"immutable_geometry_nets_pads_linkage_preserved": true,
"board_sha256": "03b69c4084dba328b11e2373c219e8fe462bcb3add934cf9f527214b21bd9cb6",
"independent_min_different_net_or_npth_clearance_mm": 0.2749999999999886,
"closest_items": [
"R2.2:c118f27f-8ad4-4abe-b9c0-a30a22fe60c9",
"99fb402a-445b-4eba-aedf-76b7f0cad042"
],
"min_copper_or_npth_edge_gap_mm": 0.5250000000000057,
"min_rf_gap_mm": 0.025000000000005684,
"min_via_to_other_drilled_hole_gap_mm": 0.8401754250991242,
"track_width_counts": {
"0.25": 425,
"0.5": 144
},
"routed_length_mm_by_net": {
"/BTN_SELECT": 95.16152954766495,
"/GND": 152.2615364335217,
"/+3V3": 106.94219089103528,
"/RS_DCD": 47.76152984055992,
"/RS_DSR": 56.835284377834164,
"/RS_RI": 66.02000635033798,
"/RS_DTR": 52.162796381301995,
"/RS_RTS": 56.23655121147421,
"/RS_CTS": 58.073809511675655,
"/RS_TX": 47.51030692030297,
"/RS_RX": 43.90406233625865,
"/RS_VALID": 64.36573922778403,
"/RS_OFF_N": 67.05000417516253,
"/BTN_PREVIOUS": 86.95076506672746,
"/OLED_SDA": 54.781626385188304,
"/OLED_SCL": 53.30787155502609,
"/BTN_NEXT": 106.4266552277327
},
"tracks": 569,
"vias": 20,
"copper_zones": 0,
"drc_freshly_run": true,
"drc_violations": 0,
"unconnected_items": 0,
"drc_ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"overwrite_guard_tests": [
{
"args": [
"--route"
],
"exit_code": 2,
"reason": "route_pcb_draft.py: error: --route requires --overwrite; back up manual routing first"
},
{
"args": [
"--route",
"--overwrite"
],
"exit_code": 2,
"reason": "route_pcb_draft.py: error: existing routing protected; --replace-routes required even for preview"
},
{
"args": [],
"exit_code": 2,
"reason": "route_pcb_draft.py: error: existing routing protected; --replace-routes required even for preview"
}
]
}
@@ -0,0 +1,122 @@
#!/usr/bin/python3
"""Read-only draft audit; needs pcbnew, numpy, shapely and kicad-cli.
Writes fresh DRC and audit JSON reports, never modifies the board.
Independent continuous centreline/radius checks complement KiCad DRC. Restricted
here to the existing circle/axis-aligned rectangular THT pads and straight tracks.
"""
from collections import Counter
import hashlib
import importlib.util
import json
from pathlib import Path
import subprocess
import pcbnew as p
from shapely.geometry import Point, LineString, Polygon, box
ROOT = Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location('router', ROOT / 'route_pcb_draft.py')
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'
items, holes = [], []
for f in board.GetFootprints():
for pad in f.Pads():
xy = r.pos(pad)
assert pad.GetOrientationDegrees() % 90 == 0
assert pad.GetShape() in (p.PAD_SHAPE_CIRCLE, p.PAD_SHAPE_RECT)
if pad.GetShape() == p.PAD_SHAPE_CIRCLE:
assert pad.GetSize().x == pad.GetSize().y
geom, radius = Point(xy), p.ToMM(pad.GetSize().x) / 2
else:
bb = pad.GetBoundingBox()
geom = box(p.ToMM(bb.GetX()), p.ToMM(bb.GetY()),
p.ToMM(bb.GetRight()), p.ToMM(bb.GetBottom()))
radius = 0
label = f.GetReference() + '.' + pad.GetNumber() + ':' + pad.m_Uuid.AsString()
layers = {l for l in r.LAYERS if pad.IsOnLayer(l)}
items.append((label, pad.GetNetCode(), layers, geom, radius))
if pad.GetDrillSize().x:
assert pad.GetDrillSize().x == pad.GetDrillSize().y
holes.append((label, Point(xy), p.ToMM(pad.GetDrillSize().x) / 2, False))
track_lengths = Counter()
widths = Counter()
for t in board.GetTracks():
is_via = isinstance(t, p.PCB_VIA)
if is_via:
geom, radius, layers = Point(r.pos(t)), p.ToMM(t.GetWidth(p.F_Cu)) / 2, set(r.LAYERS)
assert abs(radius - 0.35) < 1e-8 and t.GetDrillValue() == r.mm(0.3)
holes.append((t.m_Uuid.AsString(), geom, 0.15, True))
else:
geom = LineString([(p.ToMM(t.GetStart().x), p.ToMM(t.GetStart().y)),
(p.ToMM(t.GetEnd().x), p.ToMM(t.GetEnd().y))])
radius, layers = p.ToMM(t.GetWidth()) / 2, {t.GetLayer()}
expected = 0.5 if t.GetNetname() in ('/+3V3', '/GND') else 0.25
assert abs(2 * radius - expected) < 1e-8
widths[str(2 * radius)] += 1
track_lengths[t.GetNetname()] += geom.length
items.append((t.m_Uuid.AsString(), t.GetNetCode(), layers, geom, radius))
min_clearance = float('inf')
closest = None
for i, a in enumerate(items):
for b in items[i + 1:]:
if not a[2].intersection(b[2]) or (a[1] and a[1] == b[1]):
continue
distance = a[3].distance(b[3]) - a[4] - b[4]
if distance < min_clearance:
min_clearance, closest = distance, (a[0], b[0])
assert min_clearance >= 0.25 - 1e-6, (min_clearance, closest)
outline = Polygon([(100,100),(182,100),(182,180),(136,180),(136,161),(100,161)])
rf = box(110, 100, 131, 113)
min_edge, min_rf = float('inf'), float('inf')
for label, net, layers, geom, radius in items:
assert outline.covers(geom), label
edge_gap = outline.boundary.distance(geom) - radius
rf_gap = rf.distance(geom) - radius
min_edge, min_rf = min(min_edge, edge_gap), min(min_rf, rf_gap)
assert edge_gap >= 0.5 - 1e-6, (label, edge_gap)
assert rf_gap > 0, (label, rf_gap)
min_via_hole_gap = float('inf')
for i, a in enumerate(holes):
for b in holes[i + 1:]:
if not (a[3] or b[3]):
continue
gap = a[1].distance(b[1]) - a[2] - b[2]
min_via_hole_gap = min(min_via_hole_gap, gap)
assert gap >= 0.25 - 1e-6, (a[0], b[0], gap)
# Refusal paths must leave the routed board byte-for-byte unchanged.
before = r.PCB.read_bytes()
guards = []
for args in (['--route'], ['--route', '--overwrite'], []):
result = subprocess.run(['/usr/bin/python3', '-B', str(ROOT/'route_pcb_draft.py'), *args],
capture_output=True, text=True, timeout=20)
assert result.returncode == 2, (args, result.stdout, result.stderr)
assert r.PCB.read_bytes() == before
guards.append({'args': args, 'exit_code': result.returncode,
'reason': result.stderr.splitlines()[-1]})
subprocess.run(['kicad-cli', 'pcb', 'drc', '--format', 'json', '--all-track-errors',
'--severity-all', '--exit-code-violations', '-o',
str(ROOT/'validation/pcb-routing-drc.json'), str(r.PCB)],
check=True, timeout=60)
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,
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,
min_rf_gap_mm=min_rf, min_via_to_other_drilled_hole_gap_mm=min_via_hole_gap,
track_width_counts=widths, routed_length_mm_by_net=track_lengths,
tracks=sum(not isinstance(t,p.PCB_VIA) for t in board.GetTracks()),
vias=sum(isinstance(t,p.PCB_VIA) for t in board.GetTracks()),
copper_zones=sum(not z.GetIsRuleArea() for z in board.Zones()),
drc_freshly_run=True,
drc_violations=len(drc['violations']), unconnected_items=len(drc['unconnected_items']),
drc_ignored_checks=drc['ignored_checks'], overwrite_guard_tests=guards)
ROOT.joinpath('validation/pcb-routing-check.json').write_text(json.dumps(report, indent=2)+'\n')
print(json.dumps(report, indent=2))
@@ -0,0 +1,37 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T23:07:33",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning",
"exclusion"
],
"kicad_version": "10.0.6",
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [],
"violations": []
}
@@ -0,0 +1,58 @@
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
{"net": "/+3V3", "start": "U1.1", "end": "U1.2", "start_mm": [108.0, 104.0], "end_mm": [108.0, 106.54], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 52}
{"net": "/+3V3", "start": "R2.1", "end": "DS1.2", "start_mm": [154.0, 134.0], "end_mm": [154.73, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 911}
{"net": "/+3V3", "start": "U2.1", "end": "R1.1", "start_mm": [143.0, 130.0], "end_mm": [143.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 81}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "SW3.1", "start_mm": [167.0, 168.0], "end_mm": [173.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/BTN_PREVIOUS", "start": "SW1.1", "end": "SW1.1", "start_mm": [149.5, 168.0], "end_mm": [143.0, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "SW2.1", "start_mm": [155.0, 168.0], "end_mm": [161.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/+3V3", "start": "DS1.2", "end": "R3.1", "start_mm": [154.73, 137.5], "end_mm": [165.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 5139}
{"net": "/+3V3", "start": "R1.1", "end": "R2.1", "start_mm": [143.0, 134.0], "end_mm": [154.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 14153}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "R2.2", "start_mm": [161.5, 168.0], "end_mm": [161.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 2039}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "R3.2", "start_mm": [173.5, 168.0], "end_mm": [172.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 12615}
{"net": "/BTN_PREVIOUS", "start": "R1.2", "end": "SW1.1", "start_mm": [150.62, 134.0], "end_mm": [149.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 15179}
{"net": "/+3V3", "start": "U1.2", "end": "U2.1", "start_mm": [108.0, 106.54], "end_mm": [143.0, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 894511}
{"net": "/RS_RX", "start": "U1.11", "end": "U2.4", "start_mm": [108.0, 129.4], "end_mm": [150.62, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 51516}
{"net": "/BTN_PREVIOUS", "start": "U1.16", "end": "R1.2", "start_mm": [108.0, 142.1], "end_mm": [150.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 94479}
{"net": "/RS_DCD", "start": "U1.4", "end": "U2.3", "start_mm": [108.0, 111.62], "end_mm": [148.08, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 266031}
{"net": "/RS_TX", "start": "U1.10", "end": "U2.5", "start_mm": [108.0, 126.86], "end_mm": [153.16, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 175689}
{"net": "/RS_DTR", "start": "U1.7", "end": "U2.6", "start_mm": [108.0, 119.24], "end_mm": [155.7, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 326823}
{"net": "/OLED_SCL", "start": "U1.18", "end": "DS1.3", "start_mm": [108.0, 147.18], "end_mm": [157.27, 137.5], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 82557}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "U1.19", "start_mm": [155.0, 168.0], "end_mm": [108.0, 149.72], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 147997}
{"net": "/OLED_SDA", "start": "U1.17", "end": "DS1.4", "start_mm": [108.0, 144.64], "end_mm": [159.81, 137.5], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 266428}
{"net": "/RS_DSR", "start": "U1.5", "end": "U2.7", "start_mm": [108.0, 114.16], "end_mm": [158.24, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 372014}
{"net": "/RS_RTS", "start": "U1.8", "end": "U2.8", "start_mm": [108.0, 121.78], "end_mm": [160.78, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 298514}
{"net": "/RS_CTS", "start": "U1.9", "end": "U2.9", "start_mm": [108.0, 124.32], "end_mm": [163.32, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 265272}
{"net": "/RS_RI", "start": "U1.6", "end": "U2.10", "start_mm": [108.0, 116.7], "end_mm": [165.86, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 332506}
{"net": "/RS_VALID", "start": "U1.12", "end": "U2.11", "start_mm": [108.0, 131.94], "end_mm": [168.4, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 348384}
{"net": "/BTN_NEXT", "start": "U1.20", "end": "SW3.1", "start_mm": [108.0, 152.26], "end_mm": [167.0, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 272795}
{"net": "/RS_OFF_N", "start": "U1.15", "end": "U2.12", "start_mm": [108.0, 139.56], "end_mm": [170.94, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 351630}
{"net": "/GND", "start": "U1.44", "end": "U1.43", "start_mm": [133.4, 157.34], "end_mm": [133.4, 154.8], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 52}
{"net": "/GND", "start": "SW2.2", "end": "SW1.2", "start_mm": [155.0, 172.5], "end_mm": [149.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 111}
{"net": "/GND", "start": "SW2.2", "end": "SW3.2", "start_mm": [161.5, 172.5], "end_mm": [167.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 111}
{"net": "/GND", "start": "SW2.2", "end": "SW2.2", "start_mm": [155.0, 172.5], "end_mm": [161.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "SW3.2", "end": "SW3.2", "start_mm": [167.0, 172.5], "end_mm": [173.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "SW1.2", "end": "SW1.2", "start_mm": [149.5, 172.5], "end_mm": [143.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "DS1.1", "end": "U2.2", "start_mm": [152.19, 137.5], "end_mm": [145.54, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 17468}
{"net": "/GND", "start": "SW1.2", "end": "U1.44", "start_mm": [143.0, 172.5], "end_mm": [133.4, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 27488}
{"net": "/GND", "start": "U1.44", "end": "U1.22", "start_mm": [133.4, 157.34], "end_mm": [108.0, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 73753}
{"net": "/GND", "start": "U1.43", "end": "DS1.1", "start_mm": [133.4, 154.8], "end_mm": [152.19, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 54021}
{"net": "/GND", "start": "U2.2", "end": "U1.23", "start_mm": [145.54, 130.0], "end_mm": [133.4, 104.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 98250}
{
"draft_only": true,
"saved": false,
"grid_mm": 0.05,
"clearance_mm": 0.25,
"safety_margin_mm": 0.02,
"via_diameter_mm": 0.7,
"via_drill_mm": 0.3,
"elapsed_seconds": 27.992941715005145,
"routed_tree_edges": 38,
"total_tree_edges": 38,
"tracks": 569,
"vias": 20,
"immutable_identity_sha256": "81def52e6c1d574b4d3ab1a5859a5266b893df0ea6c5b9aa7d7b688cc4a98519",
"input_sha256": "bf94b6c9b00c43a97f933054758a2c9d6541981ae09f2f285dcdb567bd73d9b1",
"validation": "KiCad DRC must be run separately; tree-edge counts are not DRC connectivity"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,244 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T22:43:07",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning"
],
"kicad_version": "10.0.6",
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 5 [/RS_DSR] von U1",
"pos": {
"x": 108.0,
"y": 114.16
},
"uuid": "0ceca603-53a8-4a53-85cc-f7f8e7be90e4"
},
{
"description": "Durchsteckpad 7 [/RS_DSR] von U2",
"pos": {
"x": 158.24,
"y": 130.0
},
"uuid": "e2d6b2cb-6025-490c-8371-d7cc34ac37f6"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 6 [/RS_RI] von U1",
"pos": {
"x": 108.0,
"y": 116.7
},
"uuid": "f35f1696-b778-437b-8cce-67b8c4d5f5d0"
},
{
"description": "Durchsteckpad 10 [/RS_RI] von U2",
"pos": {
"x": 165.86,
"y": 130.0
},
"uuid": "e28de6f2-fb85-4849-ba1a-e98e2a231492"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 7 [/RS_DTR] von U1",
"pos": {
"x": 108.0,
"y": 119.24
},
"uuid": "354ab122-85b1-4c9c-9385-d5091fe12303"
},
{
"description": "Durchsteckpad 6 [/RS_DTR] von U2",
"pos": {
"x": 155.7,
"y": 130.0
},
"uuid": "81e8ad67-4bdc-4f57-90b1-68cff6a95261"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 8 [/RS_RTS] von U1",
"pos": {
"x": 108.0,
"y": 121.78
},
"uuid": "55162730-46fd-465d-a1c5-20aed09ed534"
},
{
"description": "Durchsteckpad 8 [/RS_RTS] von U2",
"pos": {
"x": 160.78,
"y": 130.0
},
"uuid": "5c304724-2bc6-409d-9c59-2e40adc557b9"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 9 [/RS_CTS] von U1",
"pos": {
"x": 108.0,
"y": 124.32
},
"uuid": "f75c20f8-d3b5-4c3b-9919-cb044b2da5c3"
},
{
"description": "Durchsteckpad 9 [/RS_CTS] von U2",
"pos": {
"x": 163.32,
"y": 130.0
},
"uuid": "a8411036-6859-4041-84b8-2c80c326c58e"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 10 [/RS_TX] von U1",
"pos": {
"x": 108.0,
"y": 126.86
},
"uuid": "22c323db-7146-44af-951b-04a1d7dbb2fe"
},
{
"description": "Durchsteckpad 5 [/RS_TX] von U2",
"pos": {
"x": 153.16,
"y": 130.0
},
"uuid": "f919098b-4ad7-4600-9e8b-3375a1cff050"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 11 [/RS_RX] von U1",
"pos": {
"x": 108.0,
"y": 129.4
},
"uuid": "6573852d-5714-4c8d-8793-0be87f5a052a"
},
{
"description": "Durchsteckpad 4 [/RS_RX] von U2",
"pos": {
"x": 150.62,
"y": 130.0
},
"uuid": "3d90cce4-14d8-46a0-b560-09524f11698f"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 12 [/RS_VALID] von U1",
"pos": {
"x": 108.0,
"y": 131.94
},
"uuid": "2fbc596b-c88f-4fc6-aa94-5dad4fcac9cc"
},
{
"description": "Durchsteckpad 11 [/RS_VALID] von U2",
"pos": {
"x": 168.4,
"y": 130.0
},
"uuid": "9ad42f44-160f-4aa5-bcf0-a4466c090438"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 15 [/RS_OFF_N] von U1",
"pos": {
"x": 108.0,
"y": 139.56
},
"uuid": "34e1afe9-b3fc-4d3a-9e33-38b32244f19e"
},
{
"description": "Durchsteckpad 12 [/RS_OFF_N] von U2",
"pos": {
"x": 170.94,
"y": 130.0
},
"uuid": "a43ba2f2-477d-4850-87de-e1591119d5ae"
}
],
"severity": "error",
"type": "unconnected_items"
}
],
"violations": []
}
@@ -0,0 +1,221 @@
{
"$schema": "https://schemas.kicad.org/drc.v1.json",
"coordinate_units": "mm",
"date": "2026-09-20T22:46:03",
"ignored_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"included_severities": [
"error",
"warning"
],
"kicad_version": "10.0.6",
"schematic_parity": [],
"source": "serial-carrier.kicad_pcb",
"unconnected_items": [
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 5 [/RS_DSR] von U1",
"pos": {
"x": 108.0,
"y": 114.16
},
"uuid": "0ceca603-53a8-4a53-85cc-f7f8e7be90e4"
},
{
"description": "Durchsteckpad 7 [/RS_DSR] von U2",
"pos": {
"x": 158.24,
"y": 130.0
},
"uuid": "e2d6b2cb-6025-490c-8371-d7cc34ac37f6"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 6 [/RS_RI] von U1",
"pos": {
"x": 108.0,
"y": 116.7
},
"uuid": "f35f1696-b778-437b-8cce-67b8c4d5f5d0"
},
{
"description": "Durchsteckpad 10 [/RS_RI] von U2",
"pos": {
"x": 165.86,
"y": 130.0
},
"uuid": "e28de6f2-fb85-4849-ba1a-e98e2a231492"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 8 [/RS_RTS] von U1",
"pos": {
"x": 108.0,
"y": 121.78
},
"uuid": "55162730-46fd-465d-a1c5-20aed09ed534"
},
{
"description": "Durchsteckpad 8 [/RS_RTS] von U2",
"pos": {
"x": 160.78,
"y": 130.0
},
"uuid": "5c304724-2bc6-409d-9c59-2e40adc557b9"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 9 [/RS_CTS] von U1",
"pos": {
"x": 108.0,
"y": 124.32
},
"uuid": "f75c20f8-d3b5-4c3b-9919-cb044b2da5c3"
},
{
"description": "Durchsteckpad 9 [/RS_CTS] von U2",
"pos": {
"x": 163.32,
"y": 130.0
},
"uuid": "a8411036-6859-4041-84b8-2c80c326c58e"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 12 [/RS_VALID] von U1",
"pos": {
"x": 108.0,
"y": 131.94
},
"uuid": "2fbc596b-c88f-4fc6-aa94-5dad4fcac9cc"
},
{
"description": "Durchsteckpad 11 [/RS_VALID] von U2",
"pos": {
"x": 168.4,
"y": 130.0
},
"uuid": "9ad42f44-160f-4aa5-bcf0-a4466c090438"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 15 [/RS_OFF_N] von U1",
"pos": {
"x": 108.0,
"y": 139.56
},
"uuid": "34e1afe9-b3fc-4d3a-9e33-38b32244f19e"
},
{
"description": "Durchsteckpad 12 [/RS_OFF_N] von U2",
"pos": {
"x": 170.94,
"y": 130.0
},
"uuid": "a43ba2f2-477d-4850-87de-e1591119d5ae"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 17 [/OLED_SDA] von U1",
"pos": {
"x": 108.0,
"y": 144.64
},
"uuid": "1f91549c-5cb2-465f-86df-b2f1b0c16ed8"
},
{
"description": "Durchsteckpad 4 [/OLED_SDA] von DS1",
"pos": {
"x": 159.81,
"y": 137.5
},
"uuid": "91a4dcef-1b36-4e3d-a15c-c4578077c27b"
}
],
"severity": "error",
"type": "unconnected_items"
},
{
"description": "Missing connection between items",
"items": [
{
"description": "Durchsteckpad 20 [/BTN_NEXT] von U1",
"pos": {
"x": 108.0,
"y": 152.26
},
"uuid": "e7159c90-4e8d-4b21-8e84-a7f59f793440"
},
{
"description": "Leiterbahn [/BTN_NEXT] auf F.Cu, Länge 6,5000 mm",
"pos": {
"x": 167.0,
"y": 168.0
},
"uuid": "97cfdf4d-f068-4e4a-8684-aae638a809ad"
}
],
"severity": "error",
"type": "unconnected_items"
}
],
"violations": []
}
@@ -0,0 +1,70 @@
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
{"net": "/+3V3", "start": "U1.1", "end": "U1.2", "start_mm": [108.0, 104.0], "end_mm": [108.0, 106.54], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 26}
{"net": "/+3V3", "start": "R2.1", "end": "DS1.2", "start_mm": [154.0, 134.0], "end_mm": [154.73, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 239}
{"net": "/+3V3", "start": "U2.1", "end": "R1.1", "start_mm": [143.0, 130.0], "end_mm": [143.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 41}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "SW3.1", "start_mm": [167.0, 168.0], "end_mm": [173.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 66}
{"net": "/BTN_PREVIOUS", "start": "SW1.1", "end": "SW1.1", "start_mm": [149.5, 168.0], "end_mm": [143.0, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 66}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "SW2.1", "start_mm": [155.0, 168.0], "end_mm": [161.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 66}
{"net": "/+3V3", "start": "DS1.2", "end": "R3.1", "start_mm": [154.73, 137.5], "end_mm": [165.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 1334}
{"net": "/+3V3", "start": "R1.1", "end": "R2.1", "start_mm": [143.0, 134.0], "end_mm": [154.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 3711}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "R2.2", "start_mm": [161.5, 168.0], "end_mm": [161.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 681}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "R3.2", "start_mm": [173.5, 168.0], "end_mm": [172.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 3329}
{"net": "/BTN_PREVIOUS", "start": "R1.2", "end": "SW1.1", "start_mm": [150.62, 134.0], "end_mm": [149.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 3971}
{"net": "/+3V3", "start": "U1.2", "end": "U2.1", "start_mm": [108.0, 106.54], "end_mm": [143.0, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 224476}
{"net": "/RS_RX", "start": "U1.11", "end": "U2.4", "start_mm": [108.0, 129.4], "end_mm": [150.62, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 274662}
{"net": "/BTN_PREVIOUS", "start": "U1.16", "end": "R1.2", "start_mm": [108.0, 142.1], "end_mm": [150.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 227867}
{"net": "/RS_DCD", "start": "U1.4", "end": "U2.3", "start_mm": [108.0, 111.62], "end_mm": [148.08, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 201896}
{"net": "/RS_TX", "start": "U1.10", "end": "U2.5", "start_mm": [108.0, 126.86], "end_mm": [153.16, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 271117}
{"net": "/RS_DTR", "start": "U1.7", "end": "U2.6", "start_mm": [108.0, 119.24], "end_mm": [155.7, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 307013}
{"net": "/OLED_SCL", "start": "U1.18", "end": "DS1.3", "start_mm": [108.0, 147.18], "end_mm": [157.27, 137.5], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 127246}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "U1.19", "start_mm": [155.0, 168.0], "end_mm": [108.0, 149.72], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 73189}
{"net": "/OLED_SDA", "start": "U1.17", "end": "DS1.4", "start_mm": [108.0, 144.64], "end_mm": [159.81, 137.5], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272010}
{"net": "/RS_DSR", "start": "U1.5", "end": "U2.7", "start_mm": [108.0, 114.16], "end_mm": [158.24, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272046}
{"net": "/RS_RTS", "start": "U1.8", "end": "U2.8", "start_mm": [108.0, 121.78], "end_mm": [160.78, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272010}
{"net": "/RS_CTS", "start": "U1.9", "end": "U2.9", "start_mm": [108.0, 124.32], "end_mm": [163.32, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272000}
{"net": "/RS_RI", "start": "U1.6", "end": "U2.10", "start_mm": [108.0, 116.7], "end_mm": [165.86, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272008}
{"net": "/RS_VALID", "start": "U1.12", "end": "U2.11", "start_mm": [108.0, 131.94], "end_mm": [168.4, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272015}
{"net": "/BTN_NEXT", "start": "U1.20", "end": "SW3.1", "start_mm": [108.0, 152.26], "end_mm": [167.0, 168.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272010}
{"net": "/RS_OFF_N", "start": "U1.15", "end": "U2.12", "start_mm": [108.0, 139.56], "end_mm": [170.94, 130.0], "routed": false, "width_mm": 0.25, "power_fallback": false, "expansions": 272010}
{"net": "/GND", "start": "U1.44", "end": "U1.43", "start_mm": [133.4, 157.34], "end_mm": [133.4, 154.8], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 26}
{"net": "/GND", "start": "SW2.2", "end": "SW1.2", "start_mm": [155.0, 172.5], "end_mm": [149.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 56}
{"net": "/GND", "start": "SW2.2", "end": "SW3.2", "start_mm": [161.5, 172.5], "end_mm": [167.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 56}
{"net": "/GND", "start": "SW2.2", "end": "SW2.2", "start_mm": [155.0, 172.5], "end_mm": [161.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 66}
{"net": "/GND", "start": "SW3.2", "end": "SW3.2", "start_mm": [167.0, 172.5], "end_mm": [173.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 66}
{"net": "/GND", "start": "SW1.2", "end": "SW1.2", "start_mm": [149.5, 172.5], "end_mm": [143.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 66}
{"net": "/GND", "start": "DS1.1", "end": "U2.2", "start_mm": [152.19, 137.5], "end_mm": [145.54, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 3234}
{"net": "/GND", "start": "SW1.2", "end": "U1.44", "start_mm": [143.0, 172.5], "end_mm": [133.4, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 29110}
{"net": "/GND", "start": "U1.44", "end": "U1.22", "start_mm": [133.4, 157.34], "end_mm": [108.0, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 16915}
{"net": "/GND", "start": "U1.43", "end": "DS1.1", "start_mm": [133.4, 154.8], "end_mm": [152.19, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 57470}
{"net": "/GND", "start": "U2.2", "end": "U1.23", "start_mm": [145.54, 130.0], "end_mm": [133.4, 104.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 34158}
22:45:50: Debug: Adding duplicate image handler for 'PNG file'
22:45:50: Debug: Adding duplicate image handler for 'JPEG file'
22:45:50: Debug: Adding duplicate image handler for 'TIFF file'
22:45:50: Debug: Adding duplicate image handler for 'GIF file'
22:45:50: Debug: Adding duplicate image handler for 'PNM file'
22:45:50: Debug: Adding duplicate image handler for 'PCX file'
22:45:50: Debug: Adding duplicate image handler for 'IFF file'
22:45:50: Debug: Adding duplicate image handler for 'Windows icon file'
22:45:50: Debug: Adding duplicate image handler for 'Windows cursor file'
22:45:50: Debug: Adding duplicate image handler for 'Windows animated cursor file'
22:45:50: Debug: Adding duplicate image handler for 'TGA file'
22:45:50: Debug: Adding duplicate image handler for 'XPM file'
{
"draft_only": true,
"saved": true,
"grid_mm": 0.1,
"clearance_mm": 0.25,
"safety_margin_mm": 0.04,
"via_diameter_mm": 0.7,
"via_drill_mm": 0.3,
"elapsed_seconds": 20.799824999005068,
"routed_tree_edges": 30,
"total_tree_edges": 38,
"tracks": 248,
"vias": 13,
"immutable_identity_sha256": "81def52e6c1d574b4d3ab1a5859a5266b893df0ea6c5b9aa7d7b688cc4a98519",
"input_sha256": "ab3f0775dee575a11d06f80996169f5097dfff672b69152ad9b559707f70009b",
"validation": "KiCad DRC must be run separately; tree-edge counts are not DRC connectivity"
}
@@ -0,0 +1,70 @@
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
/builddir/build/BUILD/kicad-10.0.6-build/kicad-10.0.6/include/properties/property.h(607): assert "m_choices.GetCount() > 0" failed in PROPERTY_ENUM(): No enum choices defined
{"net": "/+3V3", "start": "U1.1", "end": "U1.2", "start_mm": [108.0, 104.0], "end_mm": [108.0, 106.54], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 52}
{"net": "/+3V3", "start": "R2.1", "end": "DS1.2", "start_mm": [154.0, 134.0], "end_mm": [154.73, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 911}
{"net": "/+3V3", "start": "U2.1", "end": "R1.1", "start_mm": [143.0, 130.0], "end_mm": [143.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 81}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "SW3.1", "start_mm": [167.0, 168.0], "end_mm": [173.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/BTN_PREVIOUS", "start": "SW1.1", "end": "SW1.1", "start_mm": [149.5, 168.0], "end_mm": [143.0, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "SW2.1", "start_mm": [155.0, 168.0], "end_mm": [161.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 131}
{"net": "/+3V3", "start": "DS1.2", "end": "R3.1", "start_mm": [154.73, 137.5], "end_mm": [165.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 5139}
{"net": "/+3V3", "start": "R1.1", "end": "R2.1", "start_mm": [143.0, 134.0], "end_mm": [154.0, 134.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 14153}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "R2.2", "start_mm": [161.5, 168.0], "end_mm": [161.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 2039}
{"net": "/BTN_NEXT", "start": "SW3.1", "end": "R3.2", "start_mm": [173.5, 168.0], "end_mm": [172.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 12615}
{"net": "/BTN_PREVIOUS", "start": "R1.2", "end": "SW1.1", "start_mm": [150.62, 134.0], "end_mm": [149.5, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 15179}
{"net": "/+3V3", "start": "U1.2", "end": "U2.1", "start_mm": [108.0, 106.54], "end_mm": [143.0, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 894511}
{"net": "/RS_RX", "start": "U1.11", "end": "U2.4", "start_mm": [108.0, 129.4], "end_mm": [150.62, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 51516}
{"net": "/BTN_PREVIOUS", "start": "U1.16", "end": "R1.2", "start_mm": [108.0, 142.1], "end_mm": [150.62, 134.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 94479}
{"net": "/RS_DCD", "start": "U1.4", "end": "U2.3", "start_mm": [108.0, 111.62], "end_mm": [148.08, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 266031}
{"net": "/RS_TX", "start": "U1.10", "end": "U2.5", "start_mm": [108.0, 126.86], "end_mm": [153.16, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 175689}
{"net": "/RS_DTR", "start": "U1.7", "end": "U2.6", "start_mm": [108.0, 119.24], "end_mm": [155.7, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 326823}
{"net": "/OLED_SCL", "start": "U1.18", "end": "DS1.3", "start_mm": [108.0, 147.18], "end_mm": [157.27, 137.5], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 82557}
{"net": "/BTN_SELECT", "start": "SW2.1", "end": "U1.19", "start_mm": [155.0, 168.0], "end_mm": [108.0, 149.72], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 147997}
{"net": "/OLED_SDA", "start": "U1.17", "end": "DS1.4", "start_mm": [108.0, 144.64], "end_mm": [159.81, 137.5], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 266428}
{"net": "/RS_DSR", "start": "U1.5", "end": "U2.7", "start_mm": [108.0, 114.16], "end_mm": [158.24, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 372014}
{"net": "/RS_RTS", "start": "U1.8", "end": "U2.8", "start_mm": [108.0, 121.78], "end_mm": [160.78, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 298514}
{"net": "/RS_CTS", "start": "U1.9", "end": "U2.9", "start_mm": [108.0, 124.32], "end_mm": [163.32, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 265272}
{"net": "/RS_RI", "start": "U1.6", "end": "U2.10", "start_mm": [108.0, 116.7], "end_mm": [165.86, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 332506}
{"net": "/RS_VALID", "start": "U1.12", "end": "U2.11", "start_mm": [108.0, 131.94], "end_mm": [168.4, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 348384}
{"net": "/BTN_NEXT", "start": "U1.20", "end": "SW3.1", "start_mm": [108.0, 152.26], "end_mm": [167.0, 168.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 272795}
{"net": "/RS_OFF_N", "start": "U1.15", "end": "U2.12", "start_mm": [108.0, 139.56], "end_mm": [170.94, 130.0], "routed": true, "width_mm": 0.25, "power_fallback": false, "expansions": 351630}
{"net": "/GND", "start": "U1.44", "end": "U1.43", "start_mm": [133.4, 157.34], "end_mm": [133.4, 154.8], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 52}
{"net": "/GND", "start": "SW2.2", "end": "SW1.2", "start_mm": [155.0, 172.5], "end_mm": [149.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 111}
{"net": "/GND", "start": "SW2.2", "end": "SW3.2", "start_mm": [161.5, 172.5], "end_mm": [167.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 111}
{"net": "/GND", "start": "SW2.2", "end": "SW2.2", "start_mm": [155.0, 172.5], "end_mm": [161.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "SW3.2", "end": "SW3.2", "start_mm": [167.0, 172.5], "end_mm": [173.5, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "SW1.2", "end": "SW1.2", "start_mm": [149.5, 172.5], "end_mm": [143.0, 172.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 131}
{"net": "/GND", "start": "DS1.1", "end": "U2.2", "start_mm": [152.19, 137.5], "end_mm": [145.54, 130.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 17468}
{"net": "/GND", "start": "SW1.2", "end": "U1.44", "start_mm": [143.0, 172.5], "end_mm": [133.4, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 27488}
{"net": "/GND", "start": "U1.44", "end": "U1.22", "start_mm": [133.4, 157.34], "end_mm": [108.0, 157.34], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 73753}
{"net": "/GND", "start": "U1.43", "end": "DS1.1", "start_mm": [133.4, 154.8], "end_mm": [152.19, 137.5], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 54021}
{"net": "/GND", "start": "U2.2", "end": "U1.23", "start_mm": [145.54, 130.0], "end_mm": [133.4, 104.0], "routed": true, "width_mm": 0.5, "power_fallback": false, "expansions": 98250}
22:47:09: Debug: Adding duplicate image handler for 'PNG file'
22:47:09: Debug: Adding duplicate image handler for 'JPEG file'
22:47:09: Debug: Adding duplicate image handler for 'TIFF file'
22:47:09: Debug: Adding duplicate image handler for 'GIF file'
22:47:09: Debug: Adding duplicate image handler for 'PNM file'
22:47:09: Debug: Adding duplicate image handler for 'PCX file'
22:47:09: Debug: Adding duplicate image handler for 'IFF file'
22:47:09: Debug: Adding duplicate image handler for 'Windows icon file'
22:47:09: Debug: Adding duplicate image handler for 'Windows cursor file'
22:47:09: Debug: Adding duplicate image handler for 'Windows animated cursor file'
22:47:09: Debug: Adding duplicate image handler for 'TGA file'
22:47:09: Debug: Adding duplicate image handler for 'XPM file'
{
"draft_only": true,
"saved": true,
"grid_mm": 0.05,
"clearance_mm": 0.25,
"safety_margin_mm": 0.02,
"via_diameter_mm": 0.7,
"via_drill_mm": 0.3,
"elapsed_seconds": 26.517815405000874,
"routed_tree_edges": 38,
"total_tree_edges": 38,
"tracks": 569,
"vias": 20,
"immutable_identity_sha256": "81def52e6c1d574b4d3ab1a5859a5266b893df0ea6c5b9aa7d7b688cc4a98519",
"input_sha256": "41643d6089dae5ef2eee1a2071a6d11ae4c00f3c922eddfeaafff619fc5fab19",
"validation": "KiCad DRC must be run separately; tree-edge counts are not DRC connectivity"
}
@@ -0,0 +1,665 @@
{
"draft_only": true,
"saved": true,
"grid_mm": 0.05,
"clearance_mm": 0.25,
"safety_margin_mm": 0.02,
"via_diameter_mm": 0.7,
"via_drill_mm": 0.3,
"elapsed_seconds": 26.517815405000874,
"connections": [
{
"net": "/+3V3",
"start": "U1.1",
"end": "U1.2",
"start_mm": [
108.0,
104.0
],
"end_mm": [
108.0,
106.54
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 52
},
{
"net": "/+3V3",
"start": "R2.1",
"end": "DS1.2",
"start_mm": [
154.0,
134.0
],
"end_mm": [
154.73,
137.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 911
},
{
"net": "/+3V3",
"start": "U2.1",
"end": "R1.1",
"start_mm": [
143.0,
130.0
],
"end_mm": [
143.0,
134.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 81
},
{
"net": "/BTN_NEXT",
"start": "SW3.1",
"end": "SW3.1",
"start_mm": [
167.0,
168.0
],
"end_mm": [
173.5,
168.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 131
},
{
"net": "/BTN_PREVIOUS",
"start": "SW1.1",
"end": "SW1.1",
"start_mm": [
149.5,
168.0
],
"end_mm": [
143.0,
168.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 131
},
{
"net": "/BTN_SELECT",
"start": "SW2.1",
"end": "SW2.1",
"start_mm": [
155.0,
168.0
],
"end_mm": [
161.5,
168.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 131
},
{
"net": "/+3V3",
"start": "DS1.2",
"end": "R3.1",
"start_mm": [
154.73,
137.5
],
"end_mm": [
165.0,
134.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 5139
},
{
"net": "/+3V3",
"start": "R1.1",
"end": "R2.1",
"start_mm": [
143.0,
134.0
],
"end_mm": [
154.0,
134.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 14153
},
{
"net": "/BTN_SELECT",
"start": "SW2.1",
"end": "R2.2",
"start_mm": [
161.5,
168.0
],
"end_mm": [
161.62,
134.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 2039
},
{
"net": "/BTN_NEXT",
"start": "SW3.1",
"end": "R3.2",
"start_mm": [
173.5,
168.0
],
"end_mm": [
172.62,
134.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 12615
},
{
"net": "/BTN_PREVIOUS",
"start": "R1.2",
"end": "SW1.1",
"start_mm": [
150.62,
134.0
],
"end_mm": [
149.5,
168.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 15179
},
{
"net": "/+3V3",
"start": "U1.2",
"end": "U2.1",
"start_mm": [
108.0,
106.54
],
"end_mm": [
143.0,
130.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 894511
},
{
"net": "/RS_RX",
"start": "U1.11",
"end": "U2.4",
"start_mm": [
108.0,
129.4
],
"end_mm": [
150.62,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 51516
},
{
"net": "/BTN_PREVIOUS",
"start": "U1.16",
"end": "R1.2",
"start_mm": [
108.0,
142.1
],
"end_mm": [
150.62,
134.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 94479
},
{
"net": "/RS_DCD",
"start": "U1.4",
"end": "U2.3",
"start_mm": [
108.0,
111.62
],
"end_mm": [
148.08,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 266031
},
{
"net": "/RS_TX",
"start": "U1.10",
"end": "U2.5",
"start_mm": [
108.0,
126.86
],
"end_mm": [
153.16,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 175689
},
{
"net": "/RS_DTR",
"start": "U1.7",
"end": "U2.6",
"start_mm": [
108.0,
119.24
],
"end_mm": [
155.7,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 326823
},
{
"net": "/OLED_SCL",
"start": "U1.18",
"end": "DS1.3",
"start_mm": [
108.0,
147.18
],
"end_mm": [
157.27,
137.5
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 82557
},
{
"net": "/BTN_SELECT",
"start": "SW2.1",
"end": "U1.19",
"start_mm": [
155.0,
168.0
],
"end_mm": [
108.0,
149.72
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 147997
},
{
"net": "/OLED_SDA",
"start": "U1.17",
"end": "DS1.4",
"start_mm": [
108.0,
144.64
],
"end_mm": [
159.81,
137.5
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 266428
},
{
"net": "/RS_DSR",
"start": "U1.5",
"end": "U2.7",
"start_mm": [
108.0,
114.16
],
"end_mm": [
158.24,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 372014
},
{
"net": "/RS_RTS",
"start": "U1.8",
"end": "U2.8",
"start_mm": [
108.0,
121.78
],
"end_mm": [
160.78,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 298514
},
{
"net": "/RS_CTS",
"start": "U1.9",
"end": "U2.9",
"start_mm": [
108.0,
124.32
],
"end_mm": [
163.32,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 265272
},
{
"net": "/RS_RI",
"start": "U1.6",
"end": "U2.10",
"start_mm": [
108.0,
116.7
],
"end_mm": [
165.86,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 332506
},
{
"net": "/RS_VALID",
"start": "U1.12",
"end": "U2.11",
"start_mm": [
108.0,
131.94
],
"end_mm": [
168.4,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 348384
},
{
"net": "/BTN_NEXT",
"start": "U1.20",
"end": "SW3.1",
"start_mm": [
108.0,
152.26
],
"end_mm": [
167.0,
168.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 272795
},
{
"net": "/RS_OFF_N",
"start": "U1.15",
"end": "U2.12",
"start_mm": [
108.0,
139.56
],
"end_mm": [
170.94,
130.0
],
"routed": true,
"width_mm": 0.25,
"power_fallback": false,
"expansions": 351630
},
{
"net": "/GND",
"start": "U1.44",
"end": "U1.43",
"start_mm": [
133.4,
157.34
],
"end_mm": [
133.4,
154.8
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 52
},
{
"net": "/GND",
"start": "SW2.2",
"end": "SW1.2",
"start_mm": [
155.0,
172.5
],
"end_mm": [
149.5,
172.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 111
},
{
"net": "/GND",
"start": "SW2.2",
"end": "SW3.2",
"start_mm": [
161.5,
172.5
],
"end_mm": [
167.0,
172.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 111
},
{
"net": "/GND",
"start": "SW2.2",
"end": "SW2.2",
"start_mm": [
155.0,
172.5
],
"end_mm": [
161.5,
172.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 131
},
{
"net": "/GND",
"start": "SW3.2",
"end": "SW3.2",
"start_mm": [
167.0,
172.5
],
"end_mm": [
173.5,
172.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 131
},
{
"net": "/GND",
"start": "SW1.2",
"end": "SW1.2",
"start_mm": [
149.5,
172.5
],
"end_mm": [
143.0,
172.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 131
},
{
"net": "/GND",
"start": "DS1.1",
"end": "U2.2",
"start_mm": [
152.19,
137.5
],
"end_mm": [
145.54,
130.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 17468
},
{
"net": "/GND",
"start": "SW1.2",
"end": "U1.44",
"start_mm": [
143.0,
172.5
],
"end_mm": [
133.4,
157.34
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 27488
},
{
"net": "/GND",
"start": "U1.44",
"end": "U1.22",
"start_mm": [
133.4,
157.34
],
"end_mm": [
108.0,
157.34
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 73753
},
{
"net": "/GND",
"start": "U1.43",
"end": "DS1.1",
"start_mm": [
133.4,
154.8
],
"end_mm": [
152.19,
137.5
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 54021
},
{
"net": "/GND",
"start": "U2.2",
"end": "U1.23",
"start_mm": [
145.54,
130.0
],
"end_mm": [
133.4,
104.0
],
"routed": true,
"width_mm": 0.5,
"power_fallback": false,
"expansions": 98250
}
],
"routed_tree_edges": 38,
"total_tree_edges": 38,
"tracks": 569,
"vias": 20,
"immutable_identity_sha256": "81def52e6c1d574b4d3ab1a5859a5266b893df0ea6c5b9aa7d7b688cc4a98519",
"input_sha256": "41643d6089dae5ef2eee1a2071a6d11ae4c00f3c922eddfeaafff619fc5fab19",
"validation": "KiCad DRC must be run separately; tree-edge counts are not DRC connectivity"
}
@@ -0,0 +1,139 @@
# Two-layer draft routing result
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
or an exported proposal. **38/38 required spanning-tree connections across 17
multi-pad nets routed; KiCad reports 0 unconnected items, 0 errors, 0 warnings.**
Intentional single-pad `unconnected-(...)` nets retain their original names and
are not connected to anything else.
- Two copper layers, F.Cu and B.Cu.
- 569 straight track segments: 425 at 0.25 mm for signals and 144 at 0.5 mm for
+3V3/GND. No narrow power-track fallback was necessary.
- 20 through vias, 0.7 mm copper diameter / 0.3 mm drill.
- No copper pours: all ground connections were routed explicitly.
- Original RF rule area, x110..131 / y100..113 mm on both copper layers, preserved.
No tracks, vias or fills enter the reservation. Minimum copper-to-reservation
gap measured independently is 0.025 mm; this is geometric compliance with the
specified provisional boundary, **not validation of an antenna's RF needs**.
- All 9 electrical footprints and 4 standalone mounting footprints preserved.
Pad positions, shapes, sizes, drills, types, nets, functions, pin types and UUIDs;
footprint placement, orientation, library IDs, values, UUIDs and schematic UUID
paths; net table, outline and RF rule-area geometry/flags match the input.
- Generator, README, schematic, project and libraries were not edited by routing.
No external network dependency or manufacturing exports were used.
## Validation performed
From `hardware/PCB`:
```sh
kicad-cli pcb drc --format json --refill-zones --save-board -o validation/pcb-routing-drc.json serial-carrier.kicad_pcb
kicad-cli pcb drc --format json --all-track-errors --severity-all --exit-code-violations -o validation/pcb-routing-all-drc.json serial-carrier.kicad_pcb
/usr/bin/python3 -B validation/pcb-routing-check.py
```
Both DRC runs: **0 violations and 0 unconnected items**. No DRC settings or
exclusions were added or changed. Existing project rules specify 0.25 mm minimum
clearance, 0.5 mm copper-edge clearance, 0.25 mm minimum track width and 0.7 mm
minimum via diameter.
Independent continuous-geometry audit (`pcb-routing-check.json`):
| Check | Measured minimum | Requirement |
|---|---:|---:|
| Different-net copper / copper-to-NPTH clearance | 0.275 mm | 0.25 mm |
| Copper or NPTH edge to board edge | 0.525 mm | 0.5 mm |
| Copper / NPTH edge to RF reserve | 0.025 mm | No intersection |
| Via drill edge to another drilled hole edge | 0.840175 mm | 0.25 mm |
The audit uses exact straight-line/point/rectangle distance calculations with
copper radii, rather than the router's raster masks. It also verifies dimensions,
source-preservation invariants, and three overwrite-refusal cases. Requires
locally installed `pcbnew`, NumPy and Shapely; the router itself only needs
`pcbnew` and NumPy.
DRC still honors the project's pre-existing ignored checks:
`missing_courtyard`, `track_not_centered_on_via`,
`tuning_profile_track_geometries`, `footprint_filters_mismatch`, and
`footprint_type_mismatch`. `--severity-all` does not enable ignored checks.
The routing subtask compared electrical UUID linkage and pad-net assignments
with the input board. Subsequent integration also ran fresh schematic-parity DRC
and added missing symbol metadata fields to the PCB without changing copper.
That check reports exactly one expected U2 footprint warning (provisional PCB
footprint versus intentionally blank schematic assignment); no net mismatch.
See `pcb-parity-drc.json` and `pcb-validation-summary.json`.
## Method and bounded passes
`../route_pcb_draft.py` uses a two-layer 8-direction raster A* search, an explicit
via cost, exact-pad-centre terminal stubs, conservative foreign-copper and hole
obstacles, and only collinear path compression. It preserves the L-shaped edge
and RF reservation. Per-net minimum spanning trees connect every physical pad,
including duplicate switch contacts; short links precede longer links and GND is
last. It does not optimize return-current paths, topology or total via count.
1. Initial 0.1 mm raster / 0.04 mm safety margin / 250,000 expansions per search:
29/38 connections; DRC 0 violations, 9 unconnected.
2. Increased search allowance to 1,200,000 expansions: 30/38; DRC 0 violations,
8 unconnected. Some raster header passages were blocked, not just expensive.
3. Final 0.05 mm raster / 0.02 mm additional safety margin, with 0.25 mm physical
clearance unchanged: 38/38 in approximately 26.5 seconds; DRC fully connected.
Each pass had a 240-second routing deadline. No unresolved routing remains.
A KiCad Python binding ownership problem in the first replacement attempt caused
an exception/segfault before any board save. Replacement now uses `board.Delete`
instead of `board.Remove`; the subsequent save and no-save rerun both passed.
The known startup PROPERTY_ENUM assertions do not prevent validation.
## Rerun safety
By default the script never saves. The existing routed board is protected even
from a preview unless `--replace-routes` is given. A preview can be run with:
```sh
/usr/bin/python3 -B route_pcb_draft.py --replace-routes --seconds 240
```
This exact no-save workflow was tested: it again found 38/38 connections, 569
segments and 20 vias, and left the board byte-for-byte unchanged.
**The following command discards all existing/manual tracks and vias**, then
replaces them with fresh draft routing. Back up manual work first:
```sh
/usr/bin/python3 -B route_pcb_draft.py --route --overwrite --replace-routes --seconds 240 --max-expansions 1200000
```
The script refuses a PCB-editor lock, unexpected outline/RF/board structure,
unsupported pad orientation/layers, unapproved existing routes, or detected board
changes before save. Candidate serialization must preserve immutable geometry
and linkage before replacing the board. It deliberately does not run DRC itself:
rerun the validation commands after any routing change. Runtime/expansion limits
can produce a partial board; inspect both the run report and actual DRC, not just
the script exit status.
## Evidence and remaining limitations
- `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.
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`;
geometry, pad-net linkage and copper are unchanged by the metadata copy.
**Still DRAFT / NOT FOR FABRICATION.** Provisional module/connector geometry,
antenna envelope, assembly/courtyard clearances, USB access, male RS-232 connector
fit, physical pin mapping, power integrity, EMI and hardware operation are not
qualified by routing or DRC. All previously documented mechanical/electrical
release gates remain. No physical-device or manufacturing validation performed.
@@ -0,0 +1,604 @@
<?xml version="1.0" encoding="UTF-8"?>
<export version="E">
<design>
<source>/home/mscholz/Repos/ESP32_serial_swiss_army_knife/hardware/PCB/serial-carrier.kicad_sch</source>
<date>2026-09-20T23:07:27</date>
<tool>Eeschema 10.0.6-1.fc44</tool>
<sheet number="1" name="/" tstamps="/">
<title_block>
<title>ESP32 Serial Swiss Army Knife - Module Carrier</title>
<company/>
<rev>A0 DRAFT</rev>
<date>2026-09-20</date>
<source>serial-carrier.kicad_sch</source>
<comment number="1" value="Not fabrication-ready: provisional module footprints and male RS-232 mapping"/>
<comment number="2" value=""/>
<comment number="3" value=""/>
<comment number="4" value=""/>
<comment number="5" value=""/>
<comment number="6" value=""/>
<comment number="7" value=""/>
<comment number="8" value=""/>
<comment number="9" value=""/>
</title_block>
</sheet>
</design>
<components>
<comp ref="DS1">
<value>128x64 I2C OLED / 0x3C</value>
<footprint>Carrier:OLED_26mm_I2C_Provisional</footprint>
<fields>
<field name="Status">Module outline/header/display user fit-verified; pad/drill fit provisional</field>
<field name="Footprint">Carrier:OLED_26mm_I2C_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="OLED_I2C_26mm" description="User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA"/>
<property name="Status" value="Module outline/header/display user fit-verified; pad/drill fit provisional"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>0ae411c1-94cc-5551-9037-1d59c01a8ee5</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R1">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>defb78eb-632f-5f9d-a59d-dffbb1d31dec</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R2">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>405055c0-35ab-5334-ad4d-30d45c5ef6f2</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R3">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>78f9a74f-5d91-5687-b50e-68176333df23</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW1">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>dd5f1f75-0093-55fc-9814-437ed26871eb</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW2">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>c522a716-55a4-584d-a12e-ab135b4c08eb</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW3">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>35caa6bb-42a6-512c-848c-f1e4457aa97f</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="U1">
<value>HW678 / S3-N16R8</value>
<footprint>Carrier:HW678_2x22_Provisional</footprint>
<fields>
<field name="Status">Header geometry nominal; outline/antenna clearance pending</field>
<field name="Pin_numbering">Left 1-22; right 23-44; both top-to-bottom, antenna up</field>
<field name="Footprint">Carrier:HW678_2x22_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="HW678_N16R8" description="HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained"/>
<property name="Status" value="Header geometry nominal; outline/antenna clearance pending"/>
<property name="Pin_numbering" value="Left 1-22; right 23-44; both top-to-bottom, antenna up"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>3ab265d0-7fd3-5891-94eb-d39426c6a8a3</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
<pin num="5"/>
<pin num="6"/>
<pin num="7"/>
<pin num="8"/>
<pin num="9"/>
<pin num="10"/>
<pin num="11"/>
<pin num="12"/>
<pin num="13"/>
<pin num="14"/>
<pin num="15"/>
<pin num="16"/>
<pin num="17"/>
<pin num="18"/>
<pin num="19"/>
<pin num="20"/>
<pin num="21"/>
<pin num="22"/>
<pin num="23"/>
<pin num="24"/>
<pin num="25"/>
<pin num="26"/>
<pin num="27"/>
<pin num="28"/>
<pin num="29"/>
<pin num="30"/>
<pin num="31"/>
<pin num="32"/>
<pin num="33"/>
<pin num="34"/>
<pin num="35"/>
<pin num="36"/>
<pin num="37"/>
<pin num="38"/>
<pin num="39"/>
<pin num="40"/>
<pin num="41"/>
<pin num="42"/>
<pin num="43"/>
<pin num="44"/>
</pins>
</unit>
</units>
</comp>
<comp ref="U2">
<value>Adafruit MAX3243 / male 6253</value>
<fields>
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
<field name="Footprint"/>
<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="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>1127195e-e393-5d1d-9119-b25d10556144</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
<pin num="5"/>
<pin num="6"/>
<pin num="7"/>
<pin num="8"/>
<pin num="9"/>
<pin num="10"/>
<pin num="11"/>
<pin num="12"/>
</pins>
</unit>
</units>
</comp>
</components>
<libparts>
<libpart lib="Carrier" part="HW678_N16R8">
<description>HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained</description>
<fields>
<field name="Reference">U</field>
<field name="Value">HW678_N16R8</field>
<field name="Footprint">Carrier:HW678_2x22_Provisional</field>
<field name="Datasheet"/>
<field name="Description">HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained</field>
</fields>
<pins>
<pin num="1" name="3V3" type="power_out"/>
<pin num="2" name="3V3" type="passive"/>
<pin num="3" name="EN" type="input"/>
<pin num="4" name="GPIO4" type="input"/>
<pin num="5" name="GPIO5" type="input"/>
<pin num="6" name="GPIO6" type="input"/>
<pin num="7" name="GPIO7" type="output"/>
<pin num="8" name="GPIO15" type="output"/>
<pin num="9" name="GPIO16" type="input"/>
<pin num="10" name="GPIO17" type="output"/>
<pin num="11" name="GPIO18" type="input"/>
<pin num="12" name="GPIO8" type="input"/>
<pin num="13" name="GPIO3" type="bidirectional"/>
<pin num="14" name="GPIO46" type="bidirectional"/>
<pin num="15" name="GPIO9" type="bidirectional"/>
<pin num="16" name="GPIO10" type="input"/>
<pin num="17" name="GPIO11" type="bidirectional"/>
<pin num="18" name="GPIO12" type="output"/>
<pin num="19" name="GPIO13" type="input"/>
<pin num="20" name="GPIO14" type="input"/>
<pin num="21" name="5V" type="passive"/>
<pin num="22" name="GND" type="power_out"/>
<pin num="23" name="GND" type="passive"/>
<pin num="24" name="GPIO43" type="bidirectional"/>
<pin num="25" name="GPIO44" type="bidirectional"/>
<pin num="26" name="GPIO1" type="bidirectional"/>
<pin num="27" name="GPIO2" type="bidirectional"/>
<pin num="28" name="GPIO42" type="bidirectional"/>
<pin num="29" name="GPIO41" type="bidirectional"/>
<pin num="30" name="GPIO40" type="bidirectional"/>
<pin num="31" name="GPIO39" type="bidirectional"/>
<pin num="32" name="GPIO38" type="bidirectional"/>
<pin num="33" name="GPIO37" type="bidirectional"/>
<pin num="34" name="GPIO36" type="bidirectional"/>
<pin num="35" name="GPIO35" type="bidirectional"/>
<pin num="36" name="GPIO0" type="bidirectional"/>
<pin num="37" name="GPIO45" type="bidirectional"/>
<pin num="38" name="GPIO48" type="bidirectional"/>
<pin num="39" name="GPIO47" type="bidirectional"/>
<pin num="40" name="GPIO21" type="bidirectional"/>
<pin num="41" name="GPIO20" type="bidirectional"/>
<pin num="42" name="GPIO19" type="bidirectional"/>
<pin num="43" name="GND" type="passive"/>
<pin num="44" name="GND" type="passive"/>
</pins>
</libpart>
<libpart lib="Carrier" part="MAX3243_Module_Reference">
<description>Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE</description>
<fields>
<field name="Reference">U</field>
<field name="Value">MAX3243_Module_Reference</field>
<field name="Footprint"/>
<field name="Datasheet"/>
<field name="Description">Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE</field>
</fields>
<pins>
<pin num="1" name="Vin" type="power_in"/>
<pin num="2" name="GND" type="power_in"/>
<pin num="3" name="DCD" type="output"/>
<pin num="4" name="RX" type="output"/>
<pin num="5" name="TX" type="input"/>
<pin num="6" name="DTR" type="input"/>
<pin num="7" name="DSR" type="output"/>
<pin num="8" name="RTS" type="input"/>
<pin num="9" name="CTS" type="output"/>
<pin num="10" name="RI" type="output"/>
<pin num="11" name="VLD" type="output"/>
<pin num="12" name="OFF_N" type="input"/>
</pins>
</libpart>
<libpart lib="Carrier" part="OLED_I2C_26mm">
<description>User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA</description>
<fields>
<field name="Reference">DS</field>
<field name="Value">OLED_I2C_26mm</field>
<field name="Footprint">Carrier:OLED_26mm_I2C_Provisional</field>
<field name="Datasheet"/>
<field name="Description">User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA</field>
</fields>
<pins>
<pin num="1" name="GND" type="power_in"/>
<pin num="2" name="VCC" type="power_in"/>
<pin num="3" name="SCL" type="input"/>
<pin num="4" name="SDA" type="bidirectional"/>
</pins>
</libpart>
<libpart lib="Carrier" part="R">
<description>Resistor, axial DIN0207 footprint; exact MPN pending</description>
<fields>
<field name="Reference">R</field>
<field name="Value">R</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description">Resistor, axial DIN0207 footprint; exact MPN pending</field>
</fields>
<pins>
<pin num="1" name="" type="passive"/>
<pin num="2" name="" type="passive"/>
</pins>
</libpart>
<libpart lib="Carrier" part="SW_NO">
<description>Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs</description>
<fields>
<field name="Reference">SW</field>
<field name="Value">SW_NO</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description">Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs</field>
</fields>
<pins>
<pin num="1" name="" type="passive"/>
<pin num="2" name="" type="passive"/>
</pins>
</libpart>
</libparts>
<libraries>
<library logical="Carrier">
<uri>${KIPRJMOD}/Carrier.kicad_sym</uri>
</library>
</libraries>
<nets>
<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"/>
<node ref="R3" pin="1" pintype="passive"/>
<node ref="U1" pin="1" pinfunction="3V3_1" pintype="power_out"/>
<node ref="U1" pin="2" pinfunction="3V3_2" pintype="passive"/>
<node ref="U2" pin="1" pinfunction="Vin_1" pintype="power_in"/>
</net>
<net code="2" name="/BTN_NEXT" class="Default">
<node ref="R3" pin="2" pintype="passive"/>
<node ref="SW3" pin="1" pintype="passive"/>
<node ref="U1" pin="20" pinfunction="GPIO14_20" pintype="input"/>
</net>
<net code="3" name="/BTN_PREVIOUS" class="Default">
<node ref="R1" pin="2" pintype="passive"/>
<node ref="SW1" pin="1" pintype="passive"/>
<node ref="U1" pin="16" pinfunction="GPIO10_16" pintype="input"/>
</net>
<net code="4" name="/BTN_SELECT" class="Default">
<node ref="R2" pin="2" pintype="passive"/>
<node ref="SW2" pin="1" pintype="passive"/>
<node ref="U1" pin="19" pinfunction="GPIO13_19" pintype="input"/>
</net>
<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"/>
<node ref="SW3" pin="2" pintype="passive"/>
<node ref="U1" pin="22" pinfunction="GND_22" pintype="power_out"/>
<node ref="U1" pin="23" pinfunction="GND_23" pintype="passive"/>
<node ref="U1" pin="43" pinfunction="GND_43" pintype="passive"/>
<node ref="U1" pin="44" pinfunction="GND_44" pintype="passive"/>
<node ref="U2" pin="2" pinfunction="GND_2" pintype="power_in"/>
</net>
<net code="6" name="/OLED_SCL" class="Default">
<node ref="DS1" pin="3" pinfunction="SCL_3" pintype="input"/>
<node ref="U1" pin="18" pinfunction="GPIO12_18" pintype="output"/>
</net>
<net code="7" name="/OLED_SDA" class="Default">
<node ref="DS1" pin="4" pinfunction="SDA_4" pintype="bidirectional"/>
<node ref="U1" pin="17" pinfunction="GPIO11_17" pintype="bidirectional"/>
</net>
<net code="8" name="/RS_CTS" class="Default">
<node ref="U1" pin="9" pinfunction="GPIO16_9" pintype="input"/>
<node ref="U2" pin="9" pinfunction="CTS_9" pintype="output"/>
</net>
<net code="9" name="/RS_DCD" class="Default">
<node ref="U1" pin="4" pinfunction="GPIO4_4" pintype="input"/>
<node ref="U2" pin="3" pinfunction="DCD_3" pintype="output"/>
</net>
<net code="10" name="/RS_DSR" class="Default">
<node ref="U1" pin="5" pinfunction="GPIO5_5" pintype="input"/>
<node ref="U2" pin="7" pinfunction="DSR_7" pintype="output"/>
</net>
<net code="11" name="/RS_DTR" class="Default">
<node ref="U1" pin="7" pinfunction="GPIO7_7" pintype="output"/>
<node ref="U2" pin="6" pinfunction="DTR_6" pintype="input"/>
</net>
<net code="12" name="/RS_OFF_N" class="Default">
<node ref="U1" pin="15" pinfunction="GPIO9_15" pintype="bidirectional"/>
<node ref="U2" pin="12" pinfunction="OFF_N_12" pintype="input"/>
</net>
<net code="13" name="/RS_RI" class="Default">
<node ref="U1" pin="6" pinfunction="GPIO6_6" pintype="input"/>
<node ref="U2" pin="10" pinfunction="RI_10" pintype="output"/>
</net>
<net code="14" name="/RS_RTS" class="Default">
<node ref="U1" pin="8" pinfunction="GPIO15_8" pintype="output"/>
<node ref="U2" pin="8" pinfunction="RTS_8" pintype="input"/>
</net>
<net code="15" name="/RS_RX" class="Default">
<node ref="U1" pin="11" pinfunction="GPIO18_11" pintype="input"/>
<node ref="U2" pin="4" pinfunction="RX_4" pintype="output"/>
</net>
<net code="16" name="/RS_TX" class="Default">
<node ref="U1" pin="10" pinfunction="GPIO17_10" pintype="output"/>
<node ref="U2" pin="5" pinfunction="TX_5" pintype="input"/>
</net>
<net code="17" name="/RS_VALID" class="Default">
<node ref="U1" pin="12" pinfunction="GPIO8_12" pintype="input"/>
<node ref="U2" pin="11" pinfunction="VLD_11" pintype="output"/>
</net>
<net code="18" name="unconnected-(U1-5V-Pad21)" class="Default">
<node ref="U1" pin="21" pinfunction="5V_21" pintype="passive+no_connect"/>
</net>
<net code="19" name="unconnected-(U1-EN-Pad3)" class="Default">
<node ref="U1" pin="3" pinfunction="EN_3" pintype="input+no_connect"/>
</net>
<net code="20" name="unconnected-(U1-GPIO0-Pad36)" class="Default">
<node ref="U1" pin="36" pinfunction="GPIO0_36" pintype="bidirectional+no_connect"/>
</net>
<net code="21" name="unconnected-(U1-GPIO1-Pad26)" class="Default">
<node ref="U1" pin="26" pinfunction="GPIO1_26" pintype="bidirectional+no_connect"/>
</net>
<net code="22" name="unconnected-(U1-GPIO2-Pad27)" class="Default">
<node ref="U1" pin="27" pinfunction="GPIO2_27" pintype="bidirectional+no_connect"/>
</net>
<net code="23" name="unconnected-(U1-GPIO3-Pad13)" class="Default">
<node ref="U1" pin="13" pinfunction="GPIO3_13" pintype="bidirectional+no_connect"/>
</net>
<net code="24" name="unconnected-(U1-GPIO19-Pad42)" class="Default">
<node ref="U1" pin="42" pinfunction="GPIO19_42" pintype="bidirectional+no_connect"/>
</net>
<net code="25" name="unconnected-(U1-GPIO20-Pad41)" class="Default">
<node ref="U1" pin="41" pinfunction="GPIO20_41" pintype="bidirectional+no_connect"/>
</net>
<net code="26" name="unconnected-(U1-GPIO21-Pad40)" class="Default">
<node ref="U1" pin="40" pinfunction="GPIO21_40" pintype="bidirectional+no_connect"/>
</net>
<net code="27" name="unconnected-(U1-GPIO35-Pad35)" class="Default">
<node ref="U1" pin="35" pinfunction="GPIO35_35" pintype="bidirectional+no_connect"/>
</net>
<net code="28" name="unconnected-(U1-GPIO36-Pad34)" class="Default">
<node ref="U1" pin="34" pinfunction="GPIO36_34" pintype="bidirectional+no_connect"/>
</net>
<net code="29" name="unconnected-(U1-GPIO37-Pad33)" class="Default">
<node ref="U1" pin="33" pinfunction="GPIO37_33" pintype="bidirectional+no_connect"/>
</net>
<net code="30" name="unconnected-(U1-GPIO38-Pad32)" class="Default">
<node ref="U1" pin="32" pinfunction="GPIO38_32" pintype="bidirectional+no_connect"/>
</net>
<net code="31" name="unconnected-(U1-GPIO39-Pad31)" class="Default">
<node ref="U1" pin="31" pinfunction="GPIO39_31" pintype="bidirectional+no_connect"/>
</net>
<net code="32" name="unconnected-(U1-GPIO40-Pad30)" class="Default">
<node ref="U1" pin="30" pinfunction="GPIO40_30" pintype="bidirectional+no_connect"/>
</net>
<net code="33" name="unconnected-(U1-GPIO41-Pad29)" class="Default">
<node ref="U1" pin="29" pinfunction="GPIO41_29" pintype="bidirectional+no_connect"/>
</net>
<net code="34" name="unconnected-(U1-GPIO42-Pad28)" class="Default">
<node ref="U1" pin="28" pinfunction="GPIO42_28" pintype="bidirectional+no_connect"/>
</net>
<net code="35" name="unconnected-(U1-GPIO43-Pad24)" class="Default">
<node ref="U1" pin="24" pinfunction="GPIO43_24" pintype="bidirectional+no_connect"/>
</net>
<net code="36" name="unconnected-(U1-GPIO44-Pad25)" class="Default">
<node ref="U1" pin="25" pinfunction="GPIO44_25" pintype="bidirectional+no_connect"/>
</net>
<net code="37" name="unconnected-(U1-GPIO45-Pad37)" class="Default">
<node ref="U1" pin="37" pinfunction="GPIO45_37" pintype="bidirectional+no_connect"/>
</net>
<net code="38" name="unconnected-(U1-GPIO46-Pad14)" class="Default">
<node ref="U1" pin="14" pinfunction="GPIO46_14" pintype="bidirectional+no_connect"/>
</net>
<net code="39" name="unconnected-(U1-GPIO47-Pad39)" class="Default">
<node ref="U1" pin="39" pinfunction="GPIO47_39" pintype="bidirectional+no_connect"/>
</net>
<net code="40" name="unconnected-(U1-GPIO48-Pad38)" class="Default">
<node ref="U1" pin="38" pinfunction="GPIO48_38" pintype="bidirectional+no_connect"/>
</net>
</nets>
</export>
@@ -0,0 +1,47 @@
{
"carrier_bounds_mm": [
82,
80
],
"copper_layers": 2,
"draft_thickness_mm": 1.6,
"electrical_footprints": 9,
"proposed_M3_mounts": 4,
"physical_DRC_violations": 0,
"unconnected_items": 0,
"schematic_pad_nets_match": true,
"known_parity_warning": "U2 provisional female-reference footprint assigned on PCB only; schematic footprint intentionally blank",
"ignored_DRC_checks": [
{
"description": "Footprint has no courtyard defined",
"key": "missing_courtyard"
},
{
"description": "Track endpoint not centered on via",
"key": "track_not_centered_on_via"
},
{
"description": "Tuning profile track geometries",
"key": "tuning_profile_track_geometries"
},
{
"description": "Footprint doesn't match symbol's footprint filters",
"key": "footprint_filters_mismatch"
},
{
"description": "Footprint component type doesn't match footprint pads",
"key": "footprint_type_mismatch"
}
],
"not_validated": [
"male module pin map/fit",
"assembly envelope and heights",
"header/socket fit",
"RF performance and antenna clearance",
"power/return-path integrity",
"USB power path",
"OLED pull-ups",
"fabrication process",
"hardware operation"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.3 MiB

+604
View File
@@ -0,0 +1,604 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<tool>Eeschema 10.0.6-1.fc44</tool>
<sheet number="1" name="/" tstamps="/">
<title_block>
<title>ESP32 Serial Swiss Army Knife - Module Carrier</title>
<company/>
<rev>A0 DRAFT</rev>
<date>2026-09-20</date>
<source>serial-carrier.kicad_sch</source>
<comment number="1" value="Not fabrication-ready: provisional module footprints and male RS-232 mapping"/>
<comment number="2" value=""/>
<comment number="3" value=""/>
<comment number="4" value=""/>
<comment number="5" value=""/>
<comment number="6" value=""/>
<comment number="7" value=""/>
<comment number="8" value=""/>
<comment number="9" value=""/>
</title_block>
</sheet>
</design>
<components>
<comp ref="DS1">
<value>128x64 I2C OLED / 0x3C</value>
<footprint>Carrier:OLED_26mm_I2C_Provisional</footprint>
<fields>
<field name="Status">Module outline/header/display user fit-verified; pad/drill fit provisional</field>
<field name="Footprint">Carrier:OLED_26mm_I2C_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="OLED_I2C_26mm" description="User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA"/>
<property name="Status" value="Module outline/header/display user fit-verified; pad/drill fit provisional"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>0ae411c1-94cc-5551-9037-1d59c01a8ee5</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R1">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>defb78eb-632f-5f9d-a59d-dffbb1d31dec</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R2">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>405055c0-35ab-5334-ad4d-30d45c5ef6f2</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="R3">
<value>2.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="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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>78f9a74f-5d91-5687-b50e-68176333df23</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW1">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>dd5f1f75-0093-55fc-9814-437ed26871eb</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW2">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>c522a716-55a4-584d-a12e-ab135b4c08eb</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="SW3">
<value>B3F-1000</value>
<footprint>Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</footprint>
<fields>
<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="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="SW_NO" description="Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs"/>
<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="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>35caa6bb-42a6-512c-848c-f1e4457aa97f</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
</pins>
</unit>
</units>
</comp>
<comp ref="U1">
<value>HW678 / S3-N16R8</value>
<footprint>Carrier:HW678_2x22_Provisional</footprint>
<fields>
<field name="Status">Header geometry nominal; outline/antenna clearance pending</field>
<field name="Pin_numbering">Left 1-22; right 23-44; both top-to-bottom, antenna up</field>
<field name="Footprint">Carrier:HW678_2x22_Provisional</field>
<field name="Datasheet"/>
<field name="Description"/>
</fields>
<libsource lib="Carrier" part="HW678_N16R8" description="HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained"/>
<property name="Status" value="Header geometry nominal; outline/antenna clearance pending"/>
<property name="Pin_numbering" value="Left 1-22; right 23-44; both top-to-bottom, antenna up"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>3ab265d0-7fd3-5891-94eb-d39426c6a8a3</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
<pin num="5"/>
<pin num="6"/>
<pin num="7"/>
<pin num="8"/>
<pin num="9"/>
<pin num="10"/>
<pin num="11"/>
<pin num="12"/>
<pin num="13"/>
<pin num="14"/>
<pin num="15"/>
<pin num="16"/>
<pin num="17"/>
<pin num="18"/>
<pin num="19"/>
<pin num="20"/>
<pin num="21"/>
<pin num="22"/>
<pin num="23"/>
<pin num="24"/>
<pin num="25"/>
<pin num="26"/>
<pin num="27"/>
<pin num="28"/>
<pin num="29"/>
<pin num="30"/>
<pin num="31"/>
<pin num="32"/>
<pin num="33"/>
<pin num="34"/>
<pin num="35"/>
<pin num="36"/>
<pin num="37"/>
<pin num="38"/>
<pin num="39"/>
<pin num="40"/>
<pin num="41"/>
<pin num="42"/>
<pin num="43"/>
<pin num="44"/>
</pins>
</unit>
</units>
</comp>
<comp ref="U2">
<value>Adafruit MAX3243 / male 6253</value>
<fields>
<field name="Status">VERIFY male header numbering; footprint intentionally unassigned</field>
<field name="Datasheet_source">adafruit-research.md; reference female 5988 CAD</field>
<field name="Footprint"/>
<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="Datasheet_source" value="adafruit-research.md; reference female 5988 CAD"/>
<property name="Sheetname" value="serial-carrier"/>
<property name="Sheetfile" value="serial-carrier.kicad_sch"/>
<sheetpath names="/" tstamps="/"/>
<tstamps>1127195e-e393-5d1d-9119-b25d10556144</tstamps>
<units>
<unit name="A">
<pins>
<pin num="1"/>
<pin num="2"/>
<pin num="3"/>
<pin num="4"/>
<pin num="5"/>
<pin num="6"/>
<pin num="7"/>
<pin num="8"/>
<pin num="9"/>
<pin num="10"/>
<pin num="11"/>
<pin num="12"/>
</pins>
</unit>
</units>
</comp>
</components>
<libparts>
<libpart lib="Carrier" part="HW678_N16R8">
<description>HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained</description>
<fields>
<field name="Reference">U</field>
<field name="Value">HW678_N16R8</field>
<field name="Footprint">Carrier:HW678_2x22_Provisional</field>
<field name="Datasheet"/>
<field name="Description">HW-678 V0.0.0 / N16R8 carrier interface; project numbering; onboard USB/regulator retained</field>
</fields>
<pins>
<pin num="1" name="3V3" type="power_out"/>
<pin num="2" name="3V3" type="passive"/>
<pin num="3" name="EN" type="input"/>
<pin num="4" name="GPIO4" type="input"/>
<pin num="5" name="GPIO5" type="input"/>
<pin num="6" name="GPIO6" type="input"/>
<pin num="7" name="GPIO7" type="output"/>
<pin num="8" name="GPIO15" type="output"/>
<pin num="9" name="GPIO16" type="input"/>
<pin num="10" name="GPIO17" type="output"/>
<pin num="11" name="GPIO18" type="input"/>
<pin num="12" name="GPIO8" type="input"/>
<pin num="13" name="GPIO3" type="bidirectional"/>
<pin num="14" name="GPIO46" type="bidirectional"/>
<pin num="15" name="GPIO9" type="bidirectional"/>
<pin num="16" name="GPIO10" type="input"/>
<pin num="17" name="GPIO11" type="bidirectional"/>
<pin num="18" name="GPIO12" type="output"/>
<pin num="19" name="GPIO13" type="input"/>
<pin num="20" name="GPIO14" type="input"/>
<pin num="21" name="5V" type="passive"/>
<pin num="22" name="GND" type="power_out"/>
<pin num="23" name="GND" type="passive"/>
<pin num="24" name="GPIO43" type="bidirectional"/>
<pin num="25" name="GPIO44" type="bidirectional"/>
<pin num="26" name="GPIO1" type="bidirectional"/>
<pin num="27" name="GPIO2" type="bidirectional"/>
<pin num="28" name="GPIO42" type="bidirectional"/>
<pin num="29" name="GPIO41" type="bidirectional"/>
<pin num="30" name="GPIO40" type="bidirectional"/>
<pin num="31" name="GPIO39" type="bidirectional"/>
<pin num="32" name="GPIO38" type="bidirectional"/>
<pin num="33" name="GPIO37" type="bidirectional"/>
<pin num="34" name="GPIO36" type="bidirectional"/>
<pin num="35" name="GPIO35" type="bidirectional"/>
<pin num="36" name="GPIO0" type="bidirectional"/>
<pin num="37" name="GPIO45" type="bidirectional"/>
<pin num="38" name="GPIO48" type="bidirectional"/>
<pin num="39" name="GPIO47" type="bidirectional"/>
<pin num="40" name="GPIO21" type="bidirectional"/>
<pin num="41" name="GPIO20" type="bidirectional"/>
<pin num="42" name="GPIO19" type="bidirectional"/>
<pin num="43" name="GND" type="passive"/>
<pin num="44" name="GND" type="passive"/>
</pins>
</libpart>
<libpart lib="Carrier" part="MAX3243_Module_Reference">
<description>Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE</description>
<fields>
<field name="Reference">U</field>
<field name="Value">MAX3243_Module_Reference</field>
<field name="Footprint"/>
<field name="Datasheet"/>
<field name="Description">Adafruit male 6253 intended; physical numbering from published female 5988 CAD, VERIFY MALE</field>
</fields>
<pins>
<pin num="1" name="Vin" type="power_in"/>
<pin num="2" name="GND" type="power_in"/>
<pin num="3" name="DCD" type="output"/>
<pin num="4" name="RX" type="output"/>
<pin num="5" name="TX" type="input"/>
<pin num="6" name="DTR" type="input"/>
<pin num="7" name="DSR" type="output"/>
<pin num="8" name="RTS" type="input"/>
<pin num="9" name="CTS" type="output"/>
<pin num="10" name="RI" type="output"/>
<pin num="11" name="VLD" type="output"/>
<pin num="12" name="OFF_N" type="input"/>
</pins>
</libpart>
<libpart lib="Carrier" part="OLED_I2C_26mm">
<description>User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA</description>
<fields>
<field name="Reference">DS</field>
<field name="Value">OLED_I2C_26mm</field>
<field name="Footprint">Carrier:OLED_26mm_I2C_Provisional</field>
<field name="Datasheet"/>
<field name="Description">User fit-verified 26 mm OLED; front/top header GND VCC SCL SDA</field>
</fields>
<pins>
<pin num="1" name="GND" type="power_in"/>
<pin num="2" name="VCC" type="power_in"/>
<pin num="3" name="SCL" type="input"/>
<pin num="4" name="SDA" type="bidirectional"/>
</pins>
</libpart>
<libpart lib="Carrier" part="R">
<description>Resistor, axial DIN0207 footprint; exact MPN pending</description>
<fields>
<field name="Reference">R</field>
<field name="Value">R</field>
<field name="Footprint">Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal</field>
<field name="Datasheet"/>
<field name="Description">Resistor, axial DIN0207 footprint; exact MPN pending</field>
</fields>
<pins>
<pin num="1" name="" type="passive"/>
<pin num="2" name="" type="passive"/>
</pins>
</libpart>
<libpart lib="Carrier" part="SW_NO">
<description>Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs</description>
<fields>
<field name="Reference">SW</field>
<field name="Value">SW_NO</field>
<field name="Footprint">Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x</field>
<field name="Datasheet"/>
<field name="Description">Normally open contact; footprint duplicates pads 1 and 2 for internally common terminal pairs</field>
</fields>
<pins>
<pin num="1" name="" type="passive"/>
<pin num="2" name="" type="passive"/>
</pins>
</libpart>
</libparts>
<libraries>
<library logical="Carrier">
<uri>${KIPRJMOD}/Carrier.kicad_sym</uri>
</library>
</libraries>
<nets>
<net code="1" name="/+3V3" class="Default">
<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"/>
<node ref="R3" pin="1" pintype="passive"/>
<node ref="U1" pin="1" pinfunction="3V3_1" pintype="power_out"/>
<node ref="U1" pin="2" pinfunction="3V3_2" pintype="passive"/>
<node ref="U2" pin="1" pinfunction="Vin_1" pintype="power_in"/>
</net>
<net code="2" name="/BTN_NEXT" class="Default">
<node ref="R3" pin="2" pintype="passive"/>
<node ref="SW3" pin="1" pintype="passive"/>
<node ref="U1" pin="20" pinfunction="GPIO14_20" pintype="input"/>
</net>
<net code="3" name="/BTN_PREVIOUS" class="Default">
<node ref="R1" pin="2" pintype="passive"/>
<node ref="SW1" pin="1" pintype="passive"/>
<node ref="U1" pin="16" pinfunction="GPIO10_16" pintype="input"/>
</net>
<net code="4" name="/BTN_SELECT" class="Default">
<node ref="R2" pin="2" pintype="passive"/>
<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">
<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"/>
<node ref="SW3" pin="2" pintype="passive"/>
<node ref="U1" pin="22" pinfunction="GND_22" pintype="power_out"/>
<node ref="U1" pin="23" pinfunction="GND_23" pintype="passive"/>
<node ref="U1" pin="43" pinfunction="GND_43" pintype="passive"/>
<node ref="U1" pin="44" pinfunction="GND_44" pintype="passive"/>
<node ref="U2" pin="2" pinfunction="GND_2" pintype="power_in"/>
</net>
<net code="6" name="/OLED_SCL" class="Default">
<node ref="DS1" pin="3" pinfunction="SCL_3" pintype="input"/>
<node ref="U1" pin="18" pinfunction="GPIO12_18" pintype="output"/>
</net>
<net code="7" name="/OLED_SDA" class="Default">
<node ref="DS1" pin="4" pinfunction="SDA_4" pintype="bidirectional"/>
<node ref="U1" pin="17" pinfunction="GPIO11_17" pintype="bidirectional"/>
</net>
<net code="8" name="/RS_CTS" class="Default">
<node ref="U1" pin="9" pinfunction="GPIO16_9" pintype="input"/>
<node ref="U2" pin="9" pinfunction="CTS_9" pintype="output"/>
</net>
<net code="9" name="/RS_DCD" class="Default">
<node ref="U1" pin="4" pinfunction="GPIO4_4" pintype="input"/>
<node ref="U2" pin="3" pinfunction="DCD_3" pintype="output"/>
</net>
<net code="10" name="/RS_DSR" class="Default">
<node ref="U1" pin="5" pinfunction="GPIO5_5" pintype="input"/>
<node ref="U2" pin="7" pinfunction="DSR_7" pintype="output"/>
</net>
<net code="11" name="/RS_DTR" class="Default">
<node ref="U1" pin="7" pinfunction="GPIO7_7" pintype="output"/>
<node ref="U2" pin="6" pinfunction="DTR_6" pintype="input"/>
</net>
<net code="12" name="/RS_OFF_N" class="Default">
<node ref="U1" pin="15" pinfunction="GPIO9_15" pintype="bidirectional"/>
<node ref="U2" pin="12" pinfunction="OFF_N_12" pintype="input"/>
</net>
<net code="13" name="/RS_RI" class="Default">
<node ref="U1" pin="6" pinfunction="GPIO6_6" pintype="input"/>
<node ref="U2" pin="10" pinfunction="RI_10" pintype="output"/>
</net>
<net code="14" name="/RS_RTS" class="Default">
<node ref="U1" pin="8" pinfunction="GPIO15_8" pintype="output"/>
<node ref="U2" pin="8" pinfunction="RTS_8" pintype="input"/>
</net>
<net code="15" name="/RS_RX" class="Default">
<node ref="U1" pin="11" pinfunction="GPIO18_11" pintype="input"/>
<node ref="U2" pin="4" pinfunction="RX_4" pintype="output"/>
</net>
<net code="16" name="/RS_TX" class="Default">
<node ref="U1" pin="10" pinfunction="GPIO17_10" pintype="output"/>
<node ref="U2" pin="5" pinfunction="TX_5" pintype="input"/>
</net>
<net code="17" name="/RS_VALID" class="Default">
<node ref="U1" pin="12" pinfunction="GPIO8_12" pintype="input"/>
<node ref="U2" pin="11" pinfunction="VLD_11" pintype="output"/>
</net>
<net code="18" name="unconnected-(U1-5V-Pad21)" class="Default">
<node ref="U1" pin="21" pinfunction="5V_21" pintype="passive+no_connect"/>
</net>
<net code="19" name="unconnected-(U1-EN-Pad3)" class="Default">
<node ref="U1" pin="3" pinfunction="EN_3" pintype="input+no_connect"/>
</net>
<net code="20" name="unconnected-(U1-GPIO0-Pad36)" class="Default">
<node ref="U1" pin="36" pinfunction="GPIO0_36" pintype="bidirectional+no_connect"/>
</net>
<net code="21" name="unconnected-(U1-GPIO1-Pad26)" class="Default">
<node ref="U1" pin="26" pinfunction="GPIO1_26" pintype="bidirectional+no_connect"/>
</net>
<net code="22" name="unconnected-(U1-GPIO2-Pad27)" class="Default">
<node ref="U1" pin="27" pinfunction="GPIO2_27" pintype="bidirectional+no_connect"/>
</net>
<net code="23" name="unconnected-(U1-GPIO3-Pad13)" class="Default">
<node ref="U1" pin="13" pinfunction="GPIO3_13" pintype="bidirectional+no_connect"/>
</net>
<net code="24" name="unconnected-(U1-GPIO19-Pad42)" class="Default">
<node ref="U1" pin="42" pinfunction="GPIO19_42" pintype="bidirectional+no_connect"/>
</net>
<net code="25" name="unconnected-(U1-GPIO20-Pad41)" class="Default">
<node ref="U1" pin="41" pinfunction="GPIO20_41" pintype="bidirectional+no_connect"/>
</net>
<net code="26" name="unconnected-(U1-GPIO21-Pad40)" class="Default">
<node ref="U1" pin="40" pinfunction="GPIO21_40" pintype="bidirectional+no_connect"/>
</net>
<net code="27" name="unconnected-(U1-GPIO35-Pad35)" class="Default">
<node ref="U1" pin="35" pinfunction="GPIO35_35" pintype="bidirectional+no_connect"/>
</net>
<net code="28" name="unconnected-(U1-GPIO36-Pad34)" class="Default">
<node ref="U1" pin="34" pinfunction="GPIO36_34" pintype="bidirectional+no_connect"/>
</net>
<net code="29" name="unconnected-(U1-GPIO37-Pad33)" class="Default">
<node ref="U1" pin="33" pinfunction="GPIO37_33" pintype="bidirectional+no_connect"/>
</net>
<net code="30" name="unconnected-(U1-GPIO38-Pad32)" class="Default">
<node ref="U1" pin="32" pinfunction="GPIO38_32" pintype="bidirectional+no_connect"/>
</net>
<net code="31" name="unconnected-(U1-GPIO39-Pad31)" class="Default">
<node ref="U1" pin="31" pinfunction="GPIO39_31" pintype="bidirectional+no_connect"/>
</net>
<net code="32" name="unconnected-(U1-GPIO40-Pad30)" class="Default">
<node ref="U1" pin="30" pinfunction="GPIO40_30" pintype="bidirectional+no_connect"/>
</net>
<net code="33" name="unconnected-(U1-GPIO41-Pad29)" class="Default">
<node ref="U1" pin="29" pinfunction="GPIO41_29" pintype="bidirectional+no_connect"/>
</net>
<net code="34" name="unconnected-(U1-GPIO42-Pad28)" class="Default">
<node ref="U1" pin="28" pinfunction="GPIO42_28" pintype="bidirectional+no_connect"/>
</net>
<net code="35" name="unconnected-(U1-GPIO43-Pad24)" class="Default">
<node ref="U1" pin="24" pinfunction="GPIO43_24" pintype="bidirectional+no_connect"/>
</net>
<net code="36" name="unconnected-(U1-GPIO44-Pad25)" class="Default">
<node ref="U1" pin="25" pinfunction="GPIO44_25" pintype="bidirectional+no_connect"/>
</net>
<net code="37" name="unconnected-(U1-GPIO45-Pad37)" class="Default">
<node ref="U1" pin="37" pinfunction="GPIO45_37" pintype="bidirectional+no_connect"/>
</net>
<net code="38" name="unconnected-(U1-GPIO46-Pad14)" class="Default">
<node ref="U1" pin="14" pinfunction="GPIO46_14" pintype="bidirectional+no_connect"/>
</net>
<net code="39" name="unconnected-(U1-GPIO47-Pad39)" class="Default">
<node ref="U1" pin="39" pinfunction="GPIO47_39" pintype="bidirectional+no_connect"/>
</net>
<net code="40" name="unconnected-(U1-GPIO48-Pad38)" class="Default">
<node ref="U1" pin="38" pinfunction="GPIO48_38" pintype="bidirectional+no_connect"/>
</net>
</nets>
</export>
+8
View File
@@ -0,0 +1,8 @@
Native schematic validation passed.
KiCad ERC: zero reported errors/warnings; no project exclusions added.
17 connected nets, 9 components; exact net memberships verified.
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.
+2
View File
@@ -13,3 +13,5 @@ board_build.partitions = partitions.csv
monitor_speed = 115200 monitor_speed = 115200
monitor_filters = esp32_exception_decoder monitor_filters = esp32_exception_decoder
extra_scripts = pre:extra_script.py
+25 -1
View File
@@ -1,4 +1,6 @@
# ESP32-S3-WROOM-1-N16R8 hardware configuration # ESP32-S3-WROOM-1-N16R8 hardware configuration
# Single-variable web throughput experiment; target validation pending.
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_160=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_MODE_OCT=y
@@ -14,7 +16,7 @@ CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4 CONFIG_LWIP_TCP_OOSEQ_MAX_PBUFS=4
# Keep concurrent HTTPS handshakes from exhausting scarce internal DRAM. # Keep concurrent HTTPS handshakes from exhausting scarce internal DRAM.
# Active TLS material remains unencrypted in PSRAM until the hardening phase. # Active TLS material remains unencrypted in PSRAM; physical extraction is outside scope.
CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y
# CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set
# Hardware AES can hang in the PSRAM DMA path and cannot be shared safely with # Hardware AES can hang in the PSRAM DMA path and cannot be shared safely with
@@ -37,6 +39,9 @@ CONFIG_ESP_HTTPS_SERVER_ENABLE=y
CONFIG_HTTPD_WS_SUPPORT=y CONFIG_HTTPD_WS_SUPPORT=y
# Reserve capacity for HTTPS/WebSocket clients plus two bounded SSH sessions. # Reserve capacity for HTTPS/WebSocket clients plus two bounded SSH sessions.
CONFIG_LWIP_MAX_SOCKETS=16 CONFIG_LWIP_MAX_SOCKETS=16
# One dual-stack listener per service; SLAAC is enabled on STA by its owner.
CONFIG_LWIP_IPV4=y
CONFIG_LWIP_IPV6=y
# Keep work submission bounded; one-second socket timeouts limit shared-task stalls. # Keep work submission bounded; one-second socket timeouts limit shared-task stalls.
# CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set # CONFIG_HTTPD_QUEUE_WORK_BLOCKING is not set
@@ -49,6 +54,20 @@ CONFIG_ESP_ENABLE_WOLFSSH=y
# The managed component emits a generic RSA stack warning although this target disables RSA. # The managed component emits a generic RSA stack warning although this target disables RSA.
CONFIG_ESP_WOLFSSL_NO_STACK_SIZE_BUILD_WARNING=y CONFIG_ESP_WOLFSSL_NO_STACK_SIZE_BUILD_WARNING=y
# Keep configurable hostname discovery STA-only and bounded. The responder task
# remains internal; general mDNS metadata prefers PSRAM to protect internal heap.
CONFIG_MDNS_MAX_INTERFACES=1
CONFIG_MDNS_MAX_SERVICES=2
CONFIG_MDNS_PREDEF_NETIF_STA=y
# CONFIG_MDNS_PREDEF_NETIF_AP is not set
# CONFIG_MDNS_PREDEF_NETIF_ETH is not set
# CONFIG_MDNS_ENABLE_CONSOLE_CLI is not set
# CONFIG_MDNS_ENABLE_BROWSE is not set
# CONFIG_MDNS_MULTIPLE_INSTANCE is not set
CONFIG_MDNS_TASK_CREATE_FROM_INTERNAL=y
CONFIG_MDNS_MEMORY_ALLOC_SPIRAM=y
# CONFIG_MDNS_MEMORY_ALLOC_INTERNAL is not set
# Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP. # Support WPA3-SAE for station profiles and the WPA2/WPA3 fallback AP.
CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y CONFIG_ESP_WIFI_ENABLE_WPA3_SAE=y
CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y CONFIG_ESP_WIFI_ENABLE_SAE_H2E=y
@@ -56,3 +75,8 @@ CONFIG_ESP_WIFI_SOFTAP_SAE_SUPPORT=y
# Keep diagnostic and interactive-console logging concise but useful. # Keep diagnostic and interactive-console logging concise but useful.
CONFIG_LOG_DEFAULT_LEVEL_INFO=y CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# Intermittent-use baseline: avoid secret-bearing crash dumps/register output.
# Ordinary logging remains enabled; this is not physical-memory protection.
CONFIG_ESP_COREDUMP_ENABLE_TO_NONE=y
CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y
+44
View File
@@ -2,10 +2,17 @@ idf_component_register(
SRCS SRCS
"main.c" "main.c"
"console_completion.c" "console_completion.c"
"console_input.c"
"network_console.c" "network_console.c"
"system_console.c" "system_console.c"
"secure_random.c" "secure_random.c"
"status_led.c" "status_led.c"
"local_display.c"
"local_boot_animation.c"
"local_status_ui.c"
"local_ui_config.c"
"local_ui_console.c"
"local_ui_hw_test.c"
"rs232_hw_test.c" "rs232_hw_test.c"
"rs232_port_owner.c" "rs232_port_owner.c"
"serial_config.c" "serial_config.c"
@@ -14,24 +21,50 @@ idf_component_register(
"session_broker.c" "session_broker.c"
"session_console.c" "session_console.c"
"ssh_security.c" "ssh_security.c"
"admin_command_gate.c"
"admin_ssh_console.c"
"ssh_transport.c" "ssh_transport.c"
"ssh_console.c" "ssh_console.c"
"usb_cdc_transport.c" "usb_cdc_transport.c"
"usb_console.c" "usb_console.c"
"user_database.c"
"user_console.c"
"web_security.c" "web_security.c"
"web_serial_transport.c" "web_serial_transport.c"
"web_serial_settings.c"
"web_account_settings.c"
"web_network_settings.c"
"web_display_settings.c"
"web_broker_settings.c"
"web_ssh_settings.c"
"web_lifecycle_settings.c"
"web_firmware_update.c"
"web_admin_tickets.c"
"web_admin_transport.c"
"web_assets_data.c" "web_assets_data.c"
"web_ui.c" "web_ui.c"
"web_server.c" "web_server.c"
"web_diagnostics.c"
"web_session_store.c"
"web_auth_parse.c"
"web_httpd_adapter.c"
"web_httpd_idle.c"
"web_cookie_auth.c"
"web_login_ui.c"
"web_console.c" "web_console.c"
"wifi_config.c" "wifi_config.c"
"wifi_manager.c" "wifi_manager.c"
"wifi_console.c" "wifi_console.c"
"mdns_config.c"
"mdns_service.c"
"mdns_console.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
REQUIRES REQUIRES
app_update
bootloader_support bootloader_support
console console
esp_driver_gpio esp_driver_gpio
esp_driver_i2c
esp_driver_uart esp_driver_uart
esp_event esp_event
esp_http_server esp_http_server
@@ -46,11 +79,22 @@ idf_component_register(
led_strip led_strip
lwip lwip
mbedtls mbedtls
mdns
nvs_flash nvs_flash
wolfssl__wolfssh wolfssl__wolfssh
wolfssl__wolfssl wolfssl__wolfssl
) )
# Only web_httpd_adapter.c uses this private, version-checked boundary.
target_include_directories(${COMPONENT_LIB} PRIVATE
"$ENV{IDF_PATH}/components/esp_http_server/src"
"$ENV{IDF_PATH}/components/esp_http_server/src/port/esp32")
# HTTPD debug logs include header values; URI warnings include ticket queries.
# Compile those out, independently of runtime log-level changes.
idf_component_get_property(httpd_lib esp_http_server COMPONENT_LIB)
target_compile_definitions(${httpd_lib} PRIVATE LOG_LOCAL_LEVEL=ESP_LOG_ERROR)
# Public wolfSSH headers include wolfCrypt configuration from user_settings.h. # Public wolfSSH headers include wolfCrypt configuration from user_settings.h.
target_compile_definitions(${COMPONENT_LIB} PRIVATE target_compile_definitions(${COMPONENT_LIB} PRIVATE
WOLFSSL_USER_SETTINGS WOLFSSL_USER_SETTINGS
+44
View File
@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Shared recursive gate for administrative command execution origins. */
#include "admin_command_gate.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED;
static SemaphoreHandle_t s_gate;
esp_err_t admin_command_gate_take(void)
{
taskENTER_CRITICAL(&s_lock);
SemaphoreHandle_t gate = s_gate;
taskEXIT_CRITICAL(&s_lock);
if (gate == NULL) {
SemaphoreHandle_t candidate = xSemaphoreCreateRecursiveMutex();
if (candidate == NULL) {
return ESP_ERR_NO_MEM;
}
taskENTER_CRITICAL(&s_lock);
if (s_gate == NULL) {
s_gate = candidate;
candidate = NULL;
}
gate = s_gate;
taskEXIT_CRITICAL(&s_lock);
if (candidate != NULL) {
vSemaphoreDelete(candidate);
}
}
return xSemaphoreTakeRecursive(gate, portMAX_DELAY) == pdTRUE ? ESP_OK : ESP_FAIL;
}
void admin_command_gate_give(void)
{
taskENTER_CRITICAL(&s_lock);
SemaphoreHandle_t gate = s_gate;
taskEXIT_CRITICAL(&s_lock);
if (gate != NULL) {
(void)xSemaphoreGiveRecursive(gate);
}
}
+17
View File
@@ -0,0 +1,17 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Serializes trusted UART0 and authenticated SSH administrative mutations. */
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
esp_err_t admin_command_gate_take(void);
void admin_command_gate_give(void);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded administrative dispatcher with a small remote-owner boundary. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "user_database.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Nonblocking typed settings admission to the canonical dispatcher. */
esp_err_t admin_ssh_console_submit_serial_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_account_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_network_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_display_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_broker_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_ssh_settings(uint32_t id);
esp_err_t admin_ssh_console_submit_lifecycle_settings(uint32_t id);
/* Fits the longest supported ECDSA P-256 OpenSSH key import command. */
#define ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY 256U
#define ADMIN_CONSOLE_TRANSPORT_SSH 0U
#define ADMIN_CONSOLE_TRANSPORT_WEB 1U
typedef struct {
uint8_t slot_index;
uint32_t session_id;
uint32_t slot_generation;
uint8_t transport; /* Zero is SSH, including legacy designated initializers. */
} admin_ssh_console_token_t;
typedef enum {
ADMIN_SSH_DEFER_NONE = 0,
ADMIN_SSH_DEFER_REBOOT,
ADMIN_SSH_DEFER_STOP,
ADMIN_SSH_DEFER_DISCONNECT,
ADMIN_SSH_DEFER_HOST_KEY_ROTATE,
ADMIN_SSH_DEFER_HOST_KEY_RESET,
ADMIN_CONSOLE_DEFER_SELF_CLOSE,
ADMIN_CONSOLE_DEFER_WEB_STOP,
ADMIN_CONSOLE_DEFER_WEB_CERTIFICATE_ROTATE,
} admin_ssh_deferred_action_type_t;
/* Small owner boundary; module/API names are retained for existing SSH callers.
* Exactly two shared console slots, not two per transport. slot_index addresses
* this pool; open_available atomically selects a free slot. Owners must not reuse an identity while
* old work can exist. transport is a firmware-assigned namespace (0 = SSH).
* An occupied or still-executing slot cannot be replaced by open_owned().
*
* The immutable adapter lives for firmware lifetime. Callbacks run on the
* control task OUTSIDE console locks for drained/perform, except perform actions
* selected by dispatcher_actions run serialized on the existing 12KiB dispatcher
* after drain/delay and queued identity/principal revalidation (no command replay).
* Zero dispatcher_actions preserves legacy control-task execution. Required is_current
* runs on the dispatcher outside console locks; it must be bounded and validate
* full transport identity, originating-session liveness and principal binding,
* without calling socket libraries or handlers. Core separately checks accounts.
* drained must be nonblocking, validate the full identity and include pending
* owner output. perform must revalidate identity and marshal lifecycle work to
* its owner, never call socket libraries here. Neither callback may call console
* handlers. supported_actions is a bitmask (1U << action); reject unsupported
* actions before side effects. Legacy STOP/DISCONNECT/key actions mean SSH;
* SELF_CLOSE means this frontend; WEB_STOP means HTTPS, not SSH.
* WEB_CERTIFICATE_ROTATE replaces the HTTPS identity and restarts HTTPS.
* These WEB actions and SELF_CLOSE ignore argument.
*
* One owner serializes feed calls per session; different owners may feed in
* parallel. Shared completion scratch is nonblocking/serialized by the core.
* The owner alone consumes output, maintains authentication/session liveness,
* and calls close on disconnect/revocation. Core copies/rechecks principals at
* admission and dispatch. Dispatch and prompts also check owner currentness;
* blocked prompts recheck every 250ms (plus check/scheduling latency). This does
* not cancel or roll back arbitrary executing handlers. Admission remains the
* owner's responsibility; is_current need not accept unpublished admission.
* Close wakes prompts; executing state is retained until the handler returns.
* Output remains bounded (5s write backpressure); deferred work waits at most
* 10s for application drain plus 200ms, NOT peer-delivery confirmation.
* Dispatcher actions then wait behind queued commands/prompts, with input gated
* until completion or cancellation; the drain bound is not an execution deadline.
* No new tasks, queues, slots, or browser endpoint are provided by this API.
*/
typedef struct {
uint32_t supported_actions;
uint32_t dispatcher_actions; /* Subset of supported_actions; immutable. */
bool (*is_current)(const admin_ssh_console_token_t *token,
const user_principal_t *principal);
bool (*drained)(const admin_ssh_console_token_t *token);
esp_err_t (*perform)(const admin_ssh_console_token_t *token,
admin_ssh_deferred_action_type_t action, uint32_t argument);
} admin_console_owner_t;
/* Selects any inactive, nonexecuting slot from the shared two-slot pool.
* Input slot_index is ignored; only slot_index changes, and only on success.
* Caller supplies transport/session_id/slot_generation and must retain the
* returned token. Full pool returns ESP_ERR_INVALID_STATE, like open_owned.
*/
esp_err_t admin_ssh_console_open_available(admin_ssh_console_token_t *token,
const user_principal_t *principal,
const admin_console_owner_t *owner);
esp_err_t admin_ssh_console_open_owned(const admin_ssh_console_token_t *token,
const user_principal_t *principal,
const admin_console_owner_t *owner);
typedef struct {
bool active;
bool command_pending;
bool input_pending;
bool output_pending;
bool deferred_action_pending;
size_t input_length;
size_t output_length;
} admin_ssh_console_session_snapshot_t;
/* Starts the single command worker. It is the sole esp_console_run() caller. */
esp_err_t admin_ssh_console_init(void);
/* Register administration-shell-only root commands after esp_console initialization. */
esp_err_t admin_ssh_console_register_commands(void);
/* Called after all ESP-IDF commands are registered; starts the UART0 frontend. */
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);
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. */
bool admin_ssh_console_dispatch_is_current(void);
/* Shared parsed browser account policy: dispatcher admission + handler defense. */
bool admin_ssh_console_web_user_command_allowed(
size_t argc, char **argv, const user_principal_t *principal);
esp_err_t admin_ssh_console_dispatch_read_input(
const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length);
esp_err_t admin_ssh_console_dispatch_defer(
admin_ssh_deferred_action_type_t action, uint32_t argument);
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);
bool admin_ssh_console_feed_input(const admin_ssh_console_token_t *token,
const uint8_t *data, size_t length,
size_t *consumed);
/* Called by the session owner; copies already-produced output without blocking. */
esp_err_t admin_ssh_console_read_output(const admin_ssh_console_token_t *token,
uint8_t *data, size_t capacity,
size_t *received);
esp_err_t admin_ssh_console_get_session_snapshot(
const admin_ssh_console_token_t *token,
admin_ssh_console_session_snapshot_t *snapshot);
#ifdef __cplusplus
}
#endif
+9
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "driver/gpio.h" #include "driver/gpio.h"
#include "driver/i2c_types.h"
#include "driver/uart.h" #include "driver/uart.h"
/* /*
@@ -12,6 +13,14 @@
*/ */
#define BOARD_RGB_LED_GPIO GPIO_NUM_48 #define BOARD_RGB_LED_GPIO GPIO_NUM_48
/* Phase 7 local OLED and active-low navigation buttons. */
#define LOCAL_UI_I2C_PORT I2C_NUM_0
#define LOCAL_UI_DISPLAY_SDA_GPIO GPIO_NUM_11
#define LOCAL_UI_DISPLAY_SCL_GPIO GPIO_NUM_12
#define LOCAL_UI_BUTTON_PREVIOUS_GPIO GPIO_NUM_10
#define LOCAL_UI_BUTTON_SELECT_GPIO GPIO_NUM_13
#define LOCAL_UI_BUTTON_NEXT_GPIO GPIO_NUM_14
#define RS232_UART_PORT UART_NUM_1 #define RS232_UART_PORT UART_NUM_1
/* UART2 is used only as an internal traffic generator during flow-control tests. */ /* UART2 is used only as an internal traffic generator during flow-control tests. */
+206 -20
View File
@@ -4,15 +4,21 @@
#include "console_completion.h" #include "console_completion.h"
#include <stddef.h> #include <stddef.h>
#include <stdio.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include "esp_console.h"
#include "linenoise/linenoise.h" #include "linenoise/linenoise.h"
static const char *const s_root_candidates[] = {
"help", "exit", "debug", "display", "serial", "broker", "usb", "user",
"wifi", "mdns", "web", "ssh", "ping", "nslookup", "traceroute", "reboot", "memory",
};
/* Keep full-line candidate strings grouped by their registered root command. */ /* Keep full-line candidate strings grouped by their registered root command. */
static const char *const s_completion_candidates[] = { static const char *const s_completion_candidates[] = {
/* Hardware debug commands and safe fixed arguments. */ /* Hardware debug commands and safe fixed arguments. */
"debug status",
"debug transceiver", "debug transceiver",
"debug transceiver enable", "debug transceiver enable",
"debug transceiver disable", "debug transceiver disable",
@@ -24,6 +30,44 @@ static const char *const s_completion_candidates[] = {
"debug uart-suite", "debug uart-suite",
"debug cts-flow-test", "debug cts-flow-test",
"debug rts-flow-test", "debug rts-flow-test",
"debug display",
"debug display status",
"debug display probe",
"debug display scan",
"debug display scan --force",
"debug display init",
"debug display init 0x3c",
"debug display init 0x78",
"debug display init 0x79",
"debug display init 0x3d",
"debug display init 0x7a",
"debug display init 0x7b",
"debug display off",
"debug display pattern",
"debug display pattern clear",
"debug display pattern fill",
"debug display pattern checker",
"debug display pattern grid",
"debug display pattern corners",
"debug display pattern layout",
"debug display row",
"debug display contrast",
"debug display invert",
"debug display invert on",
"debug display invert off",
"debug buttons",
"debug buttons status",
"debug buttons test",
/* Persistent local OLED aging settings. */
"display status",
"display set",
"display set dim-seconds",
"display set off-seconds",
"display save",
"display load",
"display defaults",
"display reset",
/* Serial service lifecycle, persistence, counters, and settings. */ /* Serial service lifecycle, persistence, counters, and settings. */
"serial status", "serial status",
@@ -78,6 +122,19 @@ static const char *const s_completion_candidates[] = {
"usb request-writer", "usb request-writer",
"usb release-writer", "usb release-writer",
/* Physical role-based user, password, and SSH-key administration. */
"user status",
"user list",
"user show",
"user recover --force",
"user add",
"user delete",
"user role",
"user password",
"user key add",
"user key delete",
"user key clear",
/* Wi-Fi lifecycle, persistence, profiles, AP policy, and diagnostics. */ /* Wi-Fi lifecycle, persistence, profiles, AP policy, and diagnostics. */
"wifi status", "wifi status",
"wifi profiles", "wifi profiles",
@@ -86,6 +143,7 @@ static const char *const s_completion_candidates[] = {
"wifi start", "wifi start",
"wifi stop", "wifi stop",
"wifi reconnect", "wifi reconnect",
"wifi next-profile",
"wifi save", "wifi save",
"wifi load", "wifi load",
"wifi defaults", "wifi defaults",
@@ -109,6 +167,14 @@ static const char *const s_completion_candidates[] = {
"wifi nslookup", "wifi nslookup",
"wifi traceroute", "wifi traceroute",
/* Station mDNS hostname configuration. */
"mdns status",
"mdns suffix",
"mdns save",
"mdns load",
"mdns defaults",
"mdns reset",
/* Authenticated HTTPS lifecycle and physical-admin recovery operations. */ /* Authenticated HTTPS lifecycle and physical-admin recovery operations. */
"web help", "web help",
"web status", "web status",
@@ -116,10 +182,14 @@ static const char *const s_completion_candidates[] = {
"web stop", "web stop",
"web counters", "web counters",
"web clear-counters", "web clear-counters",
"web credentials", "web diagnostics enable",
"web credentials show", "web diagnostics disable",
"web credentials rotate", "web diagnostics show",
"web credentials rotate --force", "web diagnostics clear",
"web performance enable",
"web performance disable",
"web performance show",
"web performance clear",
"web certificate", "web certificate",
"web certificate info", "web certificate info",
"web certificate rotate", "web certificate rotate",
@@ -144,6 +214,115 @@ static const char *const s_completion_candidates[] = {
"ssh reset --force", "ssh reset --force",
}; };
void console_completion_visit(const char *line,
console_completion_visitor_t visitor,
void *context)
{
if (line == NULL || visitor == NULL) {
return;
}
size_t line_length = strlen(line);
const char *const *candidates = strchr(line, ' ') == NULL
? s_root_candidates
: s_completion_candidates;
size_t candidate_count = strchr(line, ' ') == NULL
? sizeof(s_root_candidates) / sizeof(s_root_candidates[0])
: sizeof(s_completion_candidates) /
sizeof(s_completion_candidates[0]);
for (size_t index = 0U; index < candidate_count; ++index) {
const char *candidate = candidates[index];
if (strlen(candidate) > line_length &&
strncmp(candidate, line, line_length) == 0 &&
!visitor(candidate, context)) {
return;
}
}
}
typedef struct {
const char *line;
const char *first;
size_t common_length;
} completion_expand_context_t;
static bool collect_common_prefix(const char *candidate, void *context)
{
completion_expand_context_t *result = context;
if (result->first == NULL) {
result->first = candidate;
result->common_length = strlen(candidate);
return true;
}
size_t candidate_length = strlen(candidate);
if (result->common_length > candidate_length) {
result->common_length = candidate_length;
}
size_t offset = strlen(result->line);
while (offset < result->common_length && result->first[offset] == candidate[offset]) {
++offset;
}
result->common_length = offset;
return true;
}
bool console_completion_expand(const char *line, char *completed, size_t capacity)
{
if (line == NULL || completed == NULL || capacity == 0U) {
return false;
}
completion_expand_context_t result = {.line = line};
console_completion_visit(line, collect_common_prefix, &result);
size_t line_length = strlen(line);
if (result.first == NULL || result.common_length <= line_length ||
result.common_length >= capacity) {
return false;
}
memcpy(completed, result.first, result.common_length);
completed[result.common_length] = '\0';
return true;
}
typedef struct {
char *output;
size_t capacity;
size_t length;
bool complete;
} completion_format_context_t;
static bool format_completion_candidate(const char *candidate, void *context)
{
completion_format_context_t *result = context;
size_t candidate_length = strlen(candidate);
if (candidate_length + 2U > result->capacity - result->length) {
result->complete = false;
return false;
}
memcpy(result->output + result->length, candidate, candidate_length);
result->length += candidate_length;
result->output[result->length++] = '\r';
result->output[result->length++] = '\n';
return true;
}
bool console_completion_format_matches(const char *line, char *output, size_t capacity,
size_t *output_length)
{
if (line == NULL || output == NULL || output_length == NULL || capacity == 0U) {
return false;
}
completion_format_context_t result = {
.output = output,
.capacity = capacity,
.complete = true,
};
console_completion_visit(line, format_completion_candidate, &result);
if (!result.complete) {
return false;
}
*output_length = result.length;
return true;
}
static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor, static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
void *buffer, void *buffer,
size_t size) size_t size)
@@ -160,27 +339,34 @@ static ssize_t console_read_with_late_terminal_upgrade(int file_descriptor,
return received; return received;
} }
/* The UART frontend is the sole caller of linenoise's completion callback. */
static char s_uart_completion_output[CONSOLE_COMPLETION_OUTPUT_CAPACITY];
static void console_completion_callback(const char *buffer, linenoiseCompletions *completions) static void console_completion_callback(const char *buffer, linenoiseCompletions *completions)
{ {
/* Preserve ESP-IDF completion for registered root command names. */ char completed[257U] = {0};
if (strchr(buffer, ' ') == NULL) { if (console_completion_expand(buffer, completed, sizeof(completed))) {
esp_console_get_completion(buffer, completions); linenoiseAddCompletion(completions, completed);
return; return;
} }
const size_t buffer_length = strlen(buffer); size_t output_length = 0U;
for (size_t index = 0; if (!console_completion_format_matches(buffer, s_uart_completion_output,
index < sizeof(s_completion_candidates) / sizeof(s_completion_candidates[0]); sizeof(s_uart_completion_output),
++index) { &output_length) ||
const char *const candidate = s_completion_candidates[index]; output_length == 0U) {
const size_t candidate_length = strlen(candidate); return;
}
/* linenoise expects the complete replacement line, not only its suffix. */ /*
if (candidate_length > buffer_length && * Linenoise cycles every completion it receives. Print the shared list
strncmp(candidate, buffer, buffer_length) == 0) { * ourselves, then return the unchanged line as its one completion so its
linenoiseAddCompletion(completions, candidate); * normal refresh restores the prompt without selecting a candidate.
} */
} fputs("\r\n", stdout);
(void)fwrite(s_uart_completion_output, 1U, output_length, stdout);
fflush(stdout);
linenoiseAddCompletion(completions, buffer);
} }
void console_completion_install(void) void console_completion_install(void)
+25
View File
@@ -2,13 +2,38 @@
#pragma once #pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
/* Current longest formatted match list is below this; overflow fails closed. */
#define CONSOLE_COMPLETION_OUTPUT_CAPACITY 1024U
/* Install late-terminal upgrade handling and project-specific completion. */ /* Install late-terminal upgrade handling and project-specific completion. */
void console_completion_install(void); void console_completion_install(void);
typedef bool (*console_completion_visitor_t)(const char *candidate, void *context);
/* Visit the same matching hint candidates used by both UART0 and admin SSH. */
void console_completion_visit(const char *line,
console_completion_visitor_t visitor,
void *context);
/* Bounded longest-prefix completion shared by the UART and admin SSH frontends. */
bool console_completion_expand(const char *line, char *completed, size_t capacity);
/*
* Format the matching candidates as CRLF-terminated lines for a frontend that
* cannot use linenoise's native completion display. A successful empty result
* means no candidate matched; false means the supplied output buffer was too
* small or an argument was invalid.
*/
bool console_completion_format_matches(const char *line, char *output, size_t capacity,
size_t *output_length);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif
+113
View File
@@ -0,0 +1,113 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded UART0 input helpers for physical-administration prompts. */
#include "console_input.h"
#include <stdio.h>
#include <string.h>
#include "admin_ssh_console.h"
#include "driver/uart.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "secure_random.h"
#define CONSOLE_INPUT_UART UART_NUM_0
static esp_err_t prepare_prompt(const char *prompt)
{
vTaskDelay(1U);
esp_err_t error = uart_flush_input(CONSOLE_INPUT_UART);
if (error != ESP_OK) {
return error;
}
printf("%s", prompt);
fflush(stdout);
return ESP_OK;
}
static esp_err_t read_input(const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length)
{
if (prompt == NULL || output == NULL || output_length == NULL || capacity == 0U) {
return ESP_ERR_INVALID_ARG;
}
if (admin_ssh_console_dispatch_is_remote()) {
return admin_ssh_console_dispatch_read_input(
prompt, output, capacity, hidden, output_length);
}
*output_length = 0U;
memset(output, 0, capacity);
esp_err_t error = prepare_prompt(prompt);
if (error != ESP_OK) {
return error;
}
for (;;) {
uint8_t byte = 0U;
if (uart_read_bytes(CONSOLE_INPUT_UART, &byte, 1U, portMAX_DELAY) != 1) {
secure_wipe(output, capacity);
*output_length = 0U;
printf("\nInput failed.\n");
return ESP_FAIL;
}
if (byte == 0x03U) {
secure_wipe(output, capacity);
*output_length = 0U;
printf("\nCancelled.\n");
return ESP_ERR_INVALID_STATE;
}
if (byte == '\r' || byte == '\n') {
break;
}
if (byte == 0x08U || byte == 0x7fU) {
if (*output_length > 0U) {
output[--*output_length] = 0U;
if (!hidden) {
printf("\b \b");
fflush(stdout);
}
}
continue;
}
if (byte < 0x20U || byte > 0x7eU || *output_length >= capacity - 1U) {
putchar('\a');
fflush(stdout);
continue;
}
output[(*output_length)++] = byte;
if (!hidden) {
putchar((int)byte);
fflush(stdout);
}
}
putchar('\n');
return ESP_OK;
}
esp_err_t console_input_read_hidden(const char *prompt,
uint8_t *output, size_t capacity,
size_t minimum_length, size_t maximum_length,
size_t *output_length)
{
if (minimum_length > maximum_length || maximum_length >= capacity) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t error = read_input(prompt, output, capacity, true, output_length);
if (error != ESP_OK) {
return error;
}
if (*output_length < minimum_length || *output_length > maximum_length) {
secure_wipe(output, capacity);
*output_length = 0U;
return ESP_ERR_INVALID_SIZE;
}
return ESP_OK;
}
esp_err_t console_input_read_line(const char *prompt,
uint8_t *output, size_t capacity,
size_t *output_length)
{
return read_input(prompt, output, capacity, false, output_length);
}
+17
View File
@@ -0,0 +1,17 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded UART0 input helpers for physical-administration prompts. */
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
esp_err_t console_input_read_hidden(const char *prompt,
uint8_t *output, size_t capacity,
size_t minimum_length, size_t maximum_length,
size_t *output_length);
esp_err_t console_input_read_line(const char *prompt,
uint8_t *output, size_t capacity,
size_t *output_length);
+1
View File
@@ -4,6 +4,7 @@ dependencies:
idf: ">=5.3.0" idf: ">=5.3.0"
espressif/led_strip: "^3.0.3" espressif/led_strip: "^3.0.3"
espressif/esp_tinyusb: "^2.2.1" espressif/esp_tinyusb: "^2.2.1"
espressif/mdns: "^1.8.2"
# Exact official registry versions form the reviewed Phase 6 integration baseline. # Exact official registry versions form the reviewed Phase 6 integration baseline.
wolfssl/wolfssl: "5.8.2~1" wolfssl/wolfssl: "5.8.2~1"
wolfssl/wolfssh: "1.4.20" wolfssl/wolfssh: "1.4.20"
+174
View File
@@ -0,0 +1,174 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Compact monochrome derivative of images/logo.png for the 128x48 content panel. */
#include "local_boot_animation.h"
#include <stdbool.h>
#include <stdint.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "local_display.h"
#define BOOT_ANIMATION_FRAMES 20U
#define BOOT_ANIMATION_FRAME_MS 250U
#define BOOT_ANIMATION_NAME_SPEED 16U
static const char s_device_name[] = "ESP32 SERIAL SWISS ARMY KNIFE";
static void pixel(int x, int y)
{
if (x >= 0 && x < (int)LOCAL_DISPLAY_WIDTH &&
y >= 0 && y < (int)LOCAL_DISPLAY_CONTENT_HEIGHT) {
local_display_frame_set_pixel(LOCAL_DISPLAY_PANEL_CONTENT, (uint8_t)x,
(uint8_t)y, true);
}
}
static void line(int x0, int y0, int x1, int y1)
{
int dx = x1 >= x0 ? x1 - x0 : x0 - x1;
int sx = x0 < x1 ? 1 : -1;
int dy = y1 >= y0 ? y0 - y1 : y1 - y0;
int sy = y0 < y1 ? 1 : -1;
int error = dx + dy;
for (;;) {
pixel(x0, y0);
if (x0 == x1 && y0 == y1) {
return;
}
int twice_error = 2 * error;
if (twice_error >= dy) {
error += dy;
x0 += sx;
}
if (twice_error <= dx) {
error += dx;
y0 += sy;
}
}
}
static void rectangle(int x, int y, int width, int height)
{
line(x, y, x + width - 1, y);
line(x, y, x, y + height - 1);
line(x + width - 1, y, x + width - 1, y + height - 1);
line(x, y + height - 1, x + width - 1, y + height - 1);
}
static void draw_serial_connector(void)
{
rectangle(2, 8, 16, 10);
rectangle(4, 10, 12, 6);
for (int column = 0; column < 5; ++column) {
pixel(6 + column * 2, 12);
pixel(6 + column * 2, 14);
}
line(18, 13, 30, 22);
line(18, 16, 28, 25);
}
static void draw_usb_connector(void)
{
rectangle(2, 31, 14, 11);
rectangle(4, 33, 10, 7);
line(16, 35, 29, 30);
line(16, 39, 30, 34);
pixel(7, 35);
pixel(10, 38);
}
static void draw_terminal(bool cursor_on)
{
rectangle(28, 21, 42, 22);
rectangle(30, 23, 38, 18);
/* Keep the prompt upright even though the source logo is reoriented. */
line(38, 28, 45, 33);
line(45, 33, 38, 38);
if (cursor_on) {
line(51, 37, 59, 37);
}
line(31, 27, 31, 39);
line(67, 27, 67, 39);
line(34, 22, 34, 20);
line(63, 22, 63, 20);
}
static void draw_board(void)
{
rectangle(72, 7, 28, 36);
rectangle(78, 14, 16, 17);
for (int row = 0; row < 5; ++row) {
pixel(74, 11 + row * 6);
pixel(76, 11 + row * 6);
pixel(96, 11 + row * 6);
pixel(98, 11 + row * 6);
}
for (int column = 0; column < 6; ++column) {
pixel(79 + column * 3, 35);
pixel(79 + column * 3, 38);
}
line(70, 28, 72, 28);
line(70, 34, 72, 34);
}
static void draw_wifi(uint8_t frame)
{
bool outer = (frame % 2U) == 0U;
if (outer) {
line(103, 8, 109, 3);
line(109, 3, 115, 8);
}
line(105, 12, 109, 8);
line(109, 8, 113, 12);
line(107, 16, 109, 14);
line(109, 14, 111, 16);
pixel(109, 19);
}
static void draw_logo(uint8_t frame)
{
draw_serial_connector();
draw_usb_connector();
draw_terminal((frame % 2U) == 0U);
draw_board();
draw_wifi(frame);
}
static void draw_scrolling_name(uint8_t frame)
{
int text_width = ((int)sizeof(s_device_name) - 1) * 6;
int x = (int)LOCAL_DISPLAY_WIDTH - (int)frame * BOOT_ANIMATION_NAME_SPEED;
if (x < -text_width) {
x += text_width + (int)LOCAL_DISPLAY_WIDTH;
}
const char *text = s_device_name;
while (x < 0 && *text != '\0') {
x += 6;
++text;
}
if (x < (int)LOCAL_DISPLAY_WIDTH && *text != '\0') {
local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, (uint8_t)x, 4U, text);
}
}
esp_err_t local_boot_animation_play(void)
{
for (uint8_t frame = 0U; frame < BOOT_ANIMATION_FRAMES; ++frame) {
esp_err_t error = local_display_frame_begin();
if (error != ESP_OK) {
return error;
}
local_display_frame_clear_all();
draw_scrolling_name(frame);
draw_logo(frame);
error = local_display_frame_end();
if (error != ESP_OK) {
return error;
}
vTaskDelay(pdMS_TO_TICKS(BOOT_ANIMATION_FRAME_MS));
}
return ESP_OK;
}
+9
View File
@@ -0,0 +1,9 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded boot animation for the optional local OLED. */
#pragma once
#include "esp_err.h"
/* Plays the OLED-only startup identity animation; a missing display is nonfatal. */
esp_err_t local_boot_animation_play(void);
+656
View File
@@ -0,0 +1,656 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded SSD1315-compatible OLED service with separate physical panels. */
#include "local_display.h"
#include <string.h>
#include "board_pins.h"
#include "driver/i2c_master.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#define LOCAL_DISPLAY_PAGE_COUNT (LOCAL_DISPLAY_HEIGHT / 8U)
#define LOCAL_DISPLAY_FRAMEBUFFER_SIZE (LOCAL_DISPLAY_WIDTH * LOCAL_DISPLAY_PAGE_COUNT)
#define LOCAL_DISPLAY_I2C_SPEED_HZ 100000U
#define LOCAL_DISPLAY_I2C_TIMEOUT_MS 250U
#define LOCAL_DISPLAY_PROBE_TIMEOUT_MS 50U
#define LOCAL_DISPLAY_SCAN_TIMEOUT_MS 10U
#define LOCAL_DISPLAY_LOCK_TIMEOUT_MS 3000U
#define LOCAL_DISPLAY_DEFAULT_CONTRAST 127U
#define LOCAL_DISPLAY_COMMAND_CAPACITY 32U
#define LOCAL_DISPLAY_DATA_CHUNK_SIZE 128U
#define LOCAL_DISPLAY_FLUSH_BUDGET_US (500LL * 1000LL)
static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU};
typedef struct {
char character;
uint8_t columns[5];
} glyph_t;
/* Compact project-owned 5x7 ASCII subset with distinct upper/lower-case glyphs. */
static const glyph_t s_glyphs[] = {
{' ', {0x00, 0x00, 0x00, 0x00, 0x00}},
{'!', {0x00, 0x00, 0x5f, 0x00, 0x00}},
{'-', {0x08, 0x08, 0x08, 0x08, 0x08}},
{'.', {0x00, 0x60, 0x60, 0x00, 0x00}},
{'/', {0x20, 0x10, 0x08, 0x04, 0x02}},
{':', {0x00, 0x36, 0x36, 0x00, 0x00}},
{'>', {0x00, 0x41, 0x22, 0x14, 0x08}},
{'?', {0x02, 0x01, 0x51, 0x09, 0x06}},
{'_', {0x40, 0x40, 0x40, 0x40, 0x40}},
{'0', {0x3e, 0x51, 0x49, 0x45, 0x3e}},
{'1', {0x00, 0x42, 0x7f, 0x40, 0x00}},
{'2', {0x42, 0x61, 0x51, 0x49, 0x46}},
{'3', {0x21, 0x41, 0x45, 0x4b, 0x31}},
{'4', {0x18, 0x14, 0x12, 0x7f, 0x10}},
{'5', {0x27, 0x45, 0x45, 0x45, 0x39}},
{'6', {0x3c, 0x4a, 0x49, 0x49, 0x30}},
{'7', {0x01, 0x71, 0x09, 0x05, 0x03}},
{'8', {0x36, 0x49, 0x49, 0x49, 0x36}},
{'9', {0x06, 0x49, 0x49, 0x29, 0x1e}},
{'A', {0x7e, 0x11, 0x11, 0x11, 0x7e}},
{'B', {0x7f, 0x49, 0x49, 0x49, 0x36}},
{'C', {0x3e, 0x41, 0x41, 0x41, 0x22}},
{'D', {0x7f, 0x41, 0x41, 0x22, 0x1c}},
{'E', {0x7f, 0x49, 0x49, 0x49, 0x41}},
{'F', {0x7f, 0x09, 0x09, 0x09, 0x01}},
{'G', {0x3e, 0x41, 0x49, 0x49, 0x7a}},
{'H', {0x7f, 0x08, 0x08, 0x08, 0x7f}},
{'I', {0x00, 0x41, 0x7f, 0x41, 0x00}},
{'J', {0x20, 0x40, 0x41, 0x3f, 0x01}},
{'K', {0x7f, 0x08, 0x14, 0x22, 0x41}},
{'L', {0x7f, 0x40, 0x40, 0x40, 0x40}},
{'M', {0x7f, 0x02, 0x0c, 0x02, 0x7f}},
{'N', {0x7f, 0x04, 0x08, 0x10, 0x7f}},
{'O', {0x3e, 0x41, 0x41, 0x41, 0x3e}},
{'P', {0x7f, 0x09, 0x09, 0x09, 0x06}},
{'Q', {0x3e, 0x41, 0x51, 0x21, 0x5e}},
{'R', {0x7f, 0x09, 0x19, 0x29, 0x46}},
{'S', {0x46, 0x49, 0x49, 0x49, 0x31}},
{'T', {0x01, 0x01, 0x7f, 0x01, 0x01}},
{'U', {0x3f, 0x40, 0x40, 0x40, 0x3f}},
{'V', {0x1f, 0x20, 0x40, 0x20, 0x1f}},
{'W', {0x7f, 0x20, 0x18, 0x20, 0x7f}},
{'X', {0x63, 0x14, 0x08, 0x14, 0x63}},
{'Y', {0x03, 0x04, 0x78, 0x04, 0x03}},
{'Z', {0x61, 0x51, 0x49, 0x45, 0x43}},
{'a', {0x20, 0x54, 0x54, 0x54, 0x78}},
{'b', {0x7f, 0x48, 0x44, 0x44, 0x38}},
{'c', {0x38, 0x44, 0x44, 0x44, 0x20}},
{'d', {0x38, 0x44, 0x44, 0x48, 0x7f}},
{'e', {0x38, 0x54, 0x54, 0x54, 0x18}},
{'f', {0x08, 0x7e, 0x09, 0x01, 0x02}},
{'g', {0x0c, 0x52, 0x52, 0x52, 0x3e}},
{'h', {0x7f, 0x08, 0x04, 0x04, 0x78}},
{'i', {0x00, 0x44, 0x7d, 0x40, 0x00}},
{'j', {0x20, 0x40, 0x44, 0x3d, 0x00}},
{'k', {0x7f, 0x10, 0x28, 0x44, 0x00}},
{'l', {0x00, 0x41, 0x7f, 0x40, 0x00}},
{'m', {0x7c, 0x04, 0x18, 0x04, 0x78}},
{'n', {0x7c, 0x08, 0x04, 0x04, 0x78}},
{'o', {0x38, 0x44, 0x44, 0x44, 0x38}},
{'p', {0x7c, 0x14, 0x14, 0x14, 0x08}},
{'q', {0x08, 0x14, 0x14, 0x18, 0x7c}},
{'r', {0x7c, 0x08, 0x04, 0x04, 0x08}},
{'s', {0x48, 0x54, 0x54, 0x54, 0x20}},
{'t', {0x04, 0x3f, 0x44, 0x40, 0x20}},
{'u', {0x3c, 0x40, 0x40, 0x20, 0x7c}},
{'v', {0x1c, 0x20, 0x40, 0x20, 0x1c}},
{'w', {0x3c, 0x40, 0x30, 0x40, 0x3c}},
{'x', {0x44, 0x28, 0x10, 0x28, 0x44}},
{'y', {0x0c, 0x50, 0x50, 0x50, 0x3c}},
{'z', {0x44, 0x64, 0x54, 0x4c, 0x44}},
};
static i2c_master_bus_handle_t s_bus;
static i2c_master_dev_handle_t s_device;
static StaticSemaphore_t s_mutex_storage;
static SemaphoreHandle_t s_mutex;
static uint8_t s_framebuffer[LOCAL_DISPLAY_FRAMEBUFFER_SIZE];
static bool s_bus_ready;
static bool s_initialized;
static bool s_frame_active;
static TaskHandle_t s_frame_owner;
static uint8_t s_address;
static uint8_t s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST;
static bool s_inverted;
static uint8_t s_dirty_pages;
static esp_err_t s_last_error = ESP_ERR_INVALID_STATE;
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
{
TickType_t ticks = pdMS_TO_TICKS(milliseconds);
return (milliseconds > 0U && ticks == 0U) ? 1U : ticks;
}
static esp_err_t take_lock(void)
{
if (s_mutex == NULL) {
return ESP_ERR_INVALID_STATE;
}
return xSemaphoreTake(s_mutex, milliseconds_to_ticks(LOCAL_DISPLAY_LOCK_TIMEOUT_MS)) == pdTRUE
? ESP_OK
: ESP_ERR_TIMEOUT;
}
static void give_lock(void)
{
if (s_mutex != NULL) {
(void)xSemaphoreGive(s_mutex);
}
}
static void set_last_error(esp_err_t error)
{
s_last_error = error;
}
static esp_err_t send_commands_locked(const uint8_t *commands, size_t count)
{
if (!s_initialized || s_device == NULL || commands == NULL || count == 0U ||
count > LOCAL_DISPLAY_COMMAND_CAPACITY) {
return ESP_ERR_INVALID_STATE;
}
uint8_t transfer[LOCAL_DISPLAY_COMMAND_CAPACITY + 1U];
transfer[0] = 0x00U;
memcpy(&transfer[1], commands, count);
return i2c_master_transmit(s_device, transfer, count + 1U,
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
}
static esp_err_t send_command_locked(uint8_t command)
{
return send_commands_locked(&command, 1U);
}
static esp_err_t flush_dirty_locked(void)
{
if (!s_initialized || s_device == NULL) {
return ESP_ERR_INVALID_STATE;
}
int64_t started = esp_timer_get_time();
uint8_t transfer[LOCAL_DISPLAY_DATA_CHUNK_SIZE + 1U];
transfer[0] = 0x40U;
for (uint8_t page = 0U; page < LOCAL_DISPLAY_PAGE_COUNT; ++page) {
uint8_t page_mask = (uint8_t)(1U << page);
if ((s_dirty_pages & page_mask) == 0U) {
continue;
}
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
return ESP_ERR_TIMEOUT;
}
const uint8_t commands[] = {
0x21U, 0x00U, (uint8_t)(LOCAL_DISPLAY_WIDTH - 1U),
0x22U, page, page,
};
esp_err_t error = send_commands_locked(commands, sizeof(commands));
if (error != ESP_OK) {
return error;
}
if ((esp_timer_get_time() - started) >= LOCAL_DISPLAY_FLUSH_BUDGET_US) {
return ESP_ERR_TIMEOUT;
}
memcpy(&transfer[1], &s_framebuffer[(size_t)page * LOCAL_DISPLAY_WIDTH],
LOCAL_DISPLAY_WIDTH);
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
if (error != ESP_OK) {
return error;
}
s_dirty_pages &= (uint8_t)~page_mask;
}
return ESP_OK;
}
static void set_pixel_raw(uint8_t x, uint8_t y, bool on)
{
if (x >= LOCAL_DISPLAY_WIDTH || y >= LOCAL_DISPLAY_HEIGHT) {
return;
}
size_t index = (size_t)(y / 8U) * LOCAL_DISPLAY_WIDTH + x;
uint8_t mask = (uint8_t)(1U << (y & 7U));
uint8_t before = s_framebuffer[index];
if (on) {
s_framebuffer[index] |= mask;
} else {
s_framebuffer[index] &= (uint8_t)~mask;
}
if (before != s_framebuffer[index]) {
s_dirty_pages |= (uint8_t)(1U << (y / 8U));
}
}
static bool panel_geometry(local_display_panel_t panel, uint8_t *origin_y, uint8_t *height)
{
if (origin_y == NULL || height == NULL) {
return false;
}
switch (panel) {
case LOCAL_DISPLAY_PANEL_STATUS:
*origin_y = 0U;
*height = LOCAL_DISPLAY_STATUS_HEIGHT;
return true;
case LOCAL_DISPLAY_PANEL_CONTENT:
*origin_y = LOCAL_DISPLAY_STATUS_HEIGHT;
*height = LOCAL_DISPLAY_CONTENT_HEIGHT;
return true;
default:
return false;
}
}
static const glyph_t *find_glyph(char character)
{
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
if (s_glyphs[index].character == character) {
return &s_glyphs[index];
}
}
for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) {
if (s_glyphs[index].character == '?') {
return &s_glyphs[index];
}
}
return NULL;
}
static esp_err_t select_device_locked(uint8_t address)
{
if (address != 0x3cU && address != 0x3dU) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t error = i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS);
if (error != ESP_OK) {
return error;
}
if (s_device != NULL && s_address == address) {
return ESP_OK;
}
if (s_device != NULL) {
error = i2c_master_bus_rm_device(s_device);
if (error != ESP_OK) {
return error;
}
s_device = NULL;
s_initialized = false;
}
const i2c_device_config_t config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = address,
.scl_speed_hz = LOCAL_DISPLAY_I2C_SPEED_HZ,
};
error = i2c_master_bus_add_device(s_bus, &config, &s_device);
if (error == ESP_OK) {
s_address = address;
}
return error;
}
static esp_err_t initialize_locked(uint8_t address)
{
s_initialized = false;
esp_err_t error = select_device_locked(address);
if (error != ESP_OK) {
return error;
}
const uint8_t commands[] = {
0xaeU, 0xd5U, 0x80U, 0xa8U, 0x3fU, 0xd3U, 0x00U, 0x40U,
0x8dU, 0x14U, 0x20U, 0x00U, 0xa1U, 0xc8U, 0xdaU, 0x12U,
0x81U, LOCAL_DISPLAY_DEFAULT_CONTRAST, 0xd9U, 0xf1U, 0xdbU,
0x40U, 0xa4U, 0xa6U, 0x2eU,
};
/* send_commands_locked requires initialized, so issue the bootstrap directly. */
uint8_t transfer[sizeof(commands) + 1U];
transfer[0] = 0x00U;
memcpy(&transfer[1], commands, sizeof(commands));
error = i2c_master_transmit(s_device, transfer, sizeof(transfer),
LOCAL_DISPLAY_I2C_TIMEOUT_MS);
if (error != ESP_OK) {
return error;
}
s_initialized = true;
s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST;
s_inverted = false;
memset(s_framebuffer, 0, sizeof(s_framebuffer));
s_dirty_pages = (uint8_t)((1U << LOCAL_DISPLAY_PAGE_COUNT) - 1U);
error = flush_dirty_locked();
if (error == ESP_OK) {
error = send_command_locked(0xafU);
}
if (error != ESP_OK) {
s_initialized = false;
}
return error;
}
esp_err_t local_display_init(void)
{
if (s_bus_ready || s_mutex != NULL) {
return ESP_ERR_INVALID_STATE;
}
s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_storage);
if (s_mutex == NULL) {
return ESP_ERR_NO_MEM;
}
const i2c_master_bus_config_t config = {
.i2c_port = LOCAL_UI_I2C_PORT,
.sda_io_num = LOCAL_UI_DISPLAY_SDA_GPIO,
.scl_io_num = LOCAL_UI_DISPLAY_SCL_GPIO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.flags.enable_internal_pullup = false,
};
esp_err_t error = i2c_new_master_bus(&config, &s_bus);
if (error != ESP_OK) {
vSemaphoreDelete(s_mutex);
s_mutex = NULL;
set_last_error(error);
return error;
}
s_bus_ready = true;
set_last_error(ESP_OK);
return ESP_OK;
}
esp_err_t local_display_probe_expected(uint8_t *address_7bit)
{
if (address_7bit == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (!s_bus_ready) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
uint8_t found = 0U;
for (size_t index = 0U; index < sizeof(s_expected_addresses) / sizeof(s_expected_addresses[0]); ++index) {
uint8_t address = s_expected_addresses[index];
if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS) == ESP_OK) {
if (found == 0U) {
*address_7bit = address;
}
++found;
}
}
error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK;
set_last_error(error);
give_lock();
return error;
}
esp_err_t local_display_start_at(uint8_t address_7bit)
{
if (!s_bus_ready) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
error = initialize_locked(address_7bit);
set_last_error(error);
give_lock();
return error;
}
esp_err_t local_display_start(void)
{
uint8_t address = 0U;
esp_err_t error = local_display_probe_expected(&address);
if (error != ESP_OK) {
return error;
}
return local_display_start_at(address);
}
esp_err_t local_display_stop(void)
{
if (!s_bus_ready) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
if (!s_initialized) {
error = ESP_ERR_INVALID_STATE;
} else {
error = send_command_locked(0xaeU);
/* A failed command leaves panel state unknown; force a clean reinit. */
s_initialized = false;
}
set_last_error(error);
give_lock();
return error;
}
esp_err_t local_display_get_snapshot(local_display_snapshot_t *snapshot)
{
if (snapshot == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
*snapshot = (local_display_snapshot_t){
.bus_ready = s_bus_ready,
.initialized = s_initialized,
.address_7bit = s_address,
.contrast = s_contrast,
.inverted = s_inverted,
.dirty_page_mask = s_dirty_pages,
.last_error = s_last_error,
};
give_lock();
return ESP_OK;
}
esp_err_t local_display_scan(local_display_scan_callback_t callback,
void *context,
size_t *responding_count)
{
if (!s_bus_ready) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
uint8_t responses[0x78U - 0x08U];
size_t found = 0U;
for (uint16_t address = 0x08U; address <= 0x77U; ++address) {
if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_SCAN_TIMEOUT_MS) == ESP_OK) {
responses[found++] = (uint8_t)address;
}
vTaskDelay(1U);
}
if (responding_count != NULL) {
*responding_count = found;
}
error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK;
set_last_error(error);
give_lock();
/* Callers may safely use the display service from the callback. */
if (callback != NULL) {
for (size_t index = 0U; index < found; ++index) {
callback(responses[index], context);
}
}
return error;
}
esp_err_t local_display_set_contrast(uint8_t contrast)
{
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
const uint8_t commands[] = {0x81U, contrast};
error = send_commands_locked(commands, sizeof(commands));
if (error == ESP_OK) {
s_contrast = contrast;
} else {
s_initialized = false;
}
set_last_error(error);
give_lock();
return error;
}
esp_err_t local_display_set_inverted(bool inverted)
{
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
error = send_command_locked(inverted ? 0xa7U : 0xa6U);
if (error == ESP_OK) {
s_inverted = inverted;
} else {
s_initialized = false;
}
set_last_error(error);
give_lock();
return error;
}
esp_err_t local_display_frame_begin(void)
{
esp_err_t error = take_lock();
if (error != ESP_OK) {
return error;
}
if (!s_initialized || s_frame_active) {
give_lock();
return ESP_ERR_INVALID_STATE;
}
s_frame_active = true;
s_frame_owner = xTaskGetCurrentTaskHandle();
return ESP_OK;
}
esp_err_t local_display_frame_end(void)
{
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t error = flush_dirty_locked();
if (error != ESP_OK) {
s_initialized = false;
}
set_last_error(error);
s_frame_active = false;
s_frame_owner = NULL;
give_lock();
return error;
}
void local_display_frame_cancel(void)
{
if (s_frame_active && s_frame_owner == xTaskGetCurrentTaskHandle()) {
s_frame_active = false;
s_frame_owner = NULL;
give_lock();
}
}
void local_display_frame_clear(local_display_panel_t panel)
{
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
return;
}
uint8_t origin_y = 0U;
uint8_t height = 0U;
if (!panel_geometry(panel, &origin_y, &height)) {
return;
}
for (uint8_t y = origin_y; y < origin_y + height; ++y) {
for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) {
set_pixel_raw(x, y, false);
}
}
}
void local_display_frame_clear_all(void)
{
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
return;
}
for (size_t index = 0U; index < sizeof(s_framebuffer); ++index) {
if (s_framebuffer[index] != 0U) {
s_framebuffer[index] = 0U;
s_dirty_pages |= (uint8_t)(1U << (index / LOCAL_DISPLAY_WIDTH));
}
}
}
void local_display_frame_set_pixel(local_display_panel_t panel,
uint8_t x,
uint8_t y,
bool on)
{
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) {
return;
}
uint8_t origin_y = 0U;
uint8_t height = 0U;
if (!panel_geometry(panel, &origin_y, &height) || x >= LOCAL_DISPLAY_WIDTH || y >= height) {
return;
}
set_pixel_raw(x, (uint8_t)(origin_y + y), on);
}
void local_display_frame_draw_text(local_display_panel_t panel,
uint8_t x,
uint8_t y,
const char *text)
{
if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle() || text == NULL) {
return;
}
uint8_t origin_y = 0U;
uint8_t height = 0U;
if (!panel_geometry(panel, &origin_y, &height) || y >= height) {
return;
}
uint16_t cursor_x = x;
for (const char *character = text; *character != '\0'; ++character) {
const glyph_t *glyph = find_glyph(*character);
if (glyph == NULL || cursor_x + 5U > LOCAL_DISPLAY_WIDTH) {
break;
}
for (uint8_t column = 0U; column < 5U; ++column) {
for (uint8_t row = 0U; row < 7U; ++row) {
if ((glyph->columns[column] & (uint8_t)(1U << row)) != 0U &&
y + row < height) {
set_pixel_raw((uint8_t)(cursor_x + column),
(uint8_t)(origin_y + y + row), true);
}
}
}
cursor_x += 6U;
if (cursor_x >= LOCAL_DISPLAY_WIDTH) {
break;
}
}
}
+86
View File
@@ -0,0 +1,86 @@
/* SPDX-License-Identifier: GPL-3.0-only */
/* Bounded SSD1315-compatible local OLED service. */
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
#define LOCAL_DISPLAY_WIDTH 128U
#define LOCAL_DISPLAY_HEIGHT 64U
#define LOCAL_DISPLAY_STATUS_HEIGHT 16U
#define LOCAL_DISPLAY_CONTENT_HEIGHT 48U
/* The physical black divider lies between status row 15 and content row 16. */
typedef enum {
LOCAL_DISPLAY_PANEL_STATUS = 0,
LOCAL_DISPLAY_PANEL_CONTENT,
} local_display_panel_t;
typedef struct {
bool bus_ready;
bool initialized;
uint8_t address_7bit;
uint8_t contrast;
bool inverted;
uint8_t dirty_page_mask;
esp_err_t last_error;
} local_display_snapshot_t;
typedef void (*local_display_scan_callback_t)(uint8_t address_7bit, void *context);
/* Set up I2C0 on the board-profile pins. No display probe occurs here. */
esp_err_t local_display_init(void);
/* Probe standard OLED addresses and initialize the first responding display. */
esp_err_t local_display_start(void);
/* Select and initialize one supported 7-bit address (0x3c or 0x3d). */
esp_err_t local_display_start_at(uint8_t address_7bit);
/* Turn off the panel while preserving the I2C bus for later diagnostics/restart. */
esp_err_t local_display_stop(void);
esp_err_t local_display_get_snapshot(local_display_snapshot_t *snapshot);
esp_err_t local_display_probe_expected(uint8_t *address_7bit);
/* Bounded scan of usable 7-bit addresses 0x08 through 0x77. */
esp_err_t local_display_scan(local_display_scan_callback_t callback,
void *context,
size_t *responding_count);
esp_err_t local_display_set_contrast(uint8_t contrast);
esp_err_t local_display_set_inverted(bool inverted);
/*
* A frame holds only the display's own mutex and is owned by the task that
* begins it. Callers must never retain a service/broker mutex while beginning
* or ending a frame. Only the owning task may end or cancel it; end sends only
* modified 8-pixel pages and releases the display mutex on all outcomes.
*/
esp_err_t local_display_frame_begin(void);
esp_err_t local_display_frame_end(void);
void local_display_frame_cancel(void);
/* Drawing coordinates are panel-local and are clipped to the selected panel. */
void local_display_frame_clear(local_display_panel_t panel);
void local_display_frame_clear_all(void);
void local_display_frame_set_pixel(local_display_panel_t panel,
uint8_t x,
uint8_t y,
bool on);
void local_display_frame_draw_text(local_display_panel_t panel,
uint8_t x,
uint8_t y,
const char *text);
#ifdef __cplusplus
}
#endif

Some files were not shown because too many files have changed in this diff Show More