Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e17df0ac5 |
@@ -1,10 +0,0 @@
|
||||
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
|
||||
@@ -1,17 +0,0 @@
|
||||
// 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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,4 @@ pio device monitor -b 115200
|
||||
|
||||
The first migration from the former default/factory layout to the custom partition table requires `pio run --target erase`. Erasing destroys persisted configuration and credentials; never run it without explicit user approval.
|
||||
|
||||
No automated host test command is defined in the repository. Hardware validation procedures live in `docs/electrical_tests.md` and `docs/devel/user_administration_tests.md`; do not claim they passed unless actually performed.
|
||||
No automated host test command is defined in the repository. Hardware validation procedures live in `docs/electrical_tests.md` and `docs/user_administration_tests.md`; do not claim they passed unless actually performed.
|
||||
|
||||
@@ -13,6 +13,3 @@ add_compile_definitions(
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
set(PROJECT_VER "0.1.0")
|
||||
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)
|
||||
|
||||
@@ -4,65 +4,80 @@
|
||||
|
||||
ESP32-S3 firmware for a secure, multi-transport RS-232 adapter. It operates one MAX3243-backed UART1 serial port and safely shares it between native USB CDC-ACM, an HTTPS/WebSocket browser terminal, and SSH: one connected client can write while others observe. The firmware also provides persistent serial and Wi-Fi configuration, a UART0 recovery console, and hardware diagnostics; it is not a general-purpose router, captive portal, or unauthenticated TCP serial server.
|
||||
|
||||
## Supported hardware
|
||||
## Initial hardware target
|
||||
|
||||
- ESP32-S3-DevKitC-1-compatible development board
|
||||
- ESP32-S3-WROOM-1-N16R8 module
|
||||
- 16 MB flash
|
||||
- 8 MB octal PSRAM
|
||||
- Adafruit MAX3243 full-pinout RS-232 breakout; the male connector version is preferred (see [Hardware wiring](docs/wiring.md) for supported connector variants)
|
||||
- Optional 128×64 I²C OLED and three buttons, as described in the wiring guide
|
||||
|
||||
## Development status
|
||||
|
||||
Hardware characterization, the serial core, USB CDC-ACM, Wi-Fi, HTTPS/WebSocket, SSH serial transport, and the local display/control interface are implemented and Phase 7 target-hardware validated. Phase 8A's bounded role-based user database and UART0 administration, Phase 8B's role-aware HTTPS/SSH authentication and revocation, and Phase 8C's shared UART0/admin-SSH command shell are target-hardware validated. Phase 8D remains in progress: explicit browser sessions, exact-session serial/admin WebSockets, the canonical browser admin shell, typed serial controls, guided user/password/role/authorized-key management, generation-safe Wi-Fi profile/AP/secret editing and saving, display-aging controls, broker-client popovers, and atomic writer transfer are implemented. Switching terminal modes preserves the browser serial client and writer lease. Service/session administration, network diagnostics, security/danger operations, and unusual hardware/debug commands remain in the Admin shell rather than guided forms. All current Phase 8D target-hardware validation remains pending. Configurable STA-only mDNS naming as `sak-<suffix>.local` is implemented with independent NVS persistence; target-hardware validation is pending. See the [Roadmap](docs/roadmap.md) for phase status and validation details.
|
||||
|
||||
## Documentation
|
||||
|
||||
Start with [Getting started](docs/getting_started.md) for installation, first-time configuration, and USB or network access.
|
||||
- [Hardware wiring](docs/wiring.md): hardware profile, GPIO assignments, connector guidance, and safety notes.
|
||||
- [Electrical tests](docs/electrical_tests.md): OLED/buttons, MAX3243, UART loopback, and session-broker verification procedures.
|
||||
- [Role-based user database and UART0 administration](docs/user_administration_tests.md): user migration and administration, HTTPS/SSH authentication, session revocation, and the pending integrated web-administration validation matrix.
|
||||
- [Command reference](docs/command_reference.md): UART0/admin-SSH administration, serial, broker, USB, Wi-Fi, mDNS, web, SSH, and diagnostic commands.
|
||||
|
||||
- [Hardware wiring](docs/wiring.md): GPIO assignments, power safety, cables, USB connectors, OLED, and buttons.
|
||||
- [Electrical tests](docs/electrical_tests.md): safe hardware bring-up, loopback, flow control, and shared-serial checks.
|
||||
- [Command reference](docs/command_reference.md): administration, serial, USB, Wi-Fi, discovery, SSH, and diagnostics.
|
||||
- [Web administration](docs/web_administration.md): browser Serial, Admin, Settings, writer control, and reconnecting after changes.
|
||||
- [User administration](docs/user_administration.md): accounts, passwords, SSH client keys, and local recovery.
|
||||
- [Firmware update](docs/firmware_update.md): application upload through the browser, update precautions, and wired recovery.
|
||||
- [Security operations](docs/security_operations.md): trusted-network use, identity verification, credentials, shutdown, and recovery.
|
||||
## Flash partition layout
|
||||
|
||||
The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the custom [`partitions.csv`](partitions.csv) layout:
|
||||
|
||||
| Partition | Offset | Size | Purpose |
|
||||
|---|---:|---:|---|
|
||||
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, mDNS hostname, local-display, role-based user, legacy recovery credential, HTTPS identity, and SSH host-key data |
|
||||
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
|
||||
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
|
||||
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
|
||||
| `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps |
|
||||
| `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot |
|
||||
| `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot |
|
||||
| `storage` | `0x8B0000` | 7488 KiB | Reserved for future LittleFS web assets, logs, and files |
|
||||
|
||||
Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
|
||||
|
||||
The table reserves OTA and storage space; it does not implement OTA downloads, rollback confirmation, core-dump handling, NVS encryption, or filesystem mounting.
|
||||
|
||||
### One-time migration from the default partition table
|
||||
|
||||
The previous 1 MiB factory application began at `0x10000`, which now lies inside the enlarged NVS range. A normal upload does not erase stale data in that range. When first switching to this layout, erase the flash completely:
|
||||
|
||||
```sh
|
||||
pio run --target erase
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
This removes saved serial configuration and all other flash contents. The firmware recreates NVS with safe defaults. Subsequent ordinary uploads do not need a full erase.
|
||||
|
||||
## Build
|
||||
|
||||
Install PlatformIO Core and run from the project root:
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
The [getting-started guide](docs/getting_started.md#2-install-firmware-by-wire) covers prerequisites and first installation. The configured target is `esp32-s3-devkitc-1-n16r8`.
|
||||
|
||||
## Upload and monitor
|
||||
|
||||
Connect the board's **USB-to-UART** port for firmware upload and the UART0 recovery console:
|
||||
Connect the board's **USB-to-UART** port for firmware upload and the UART0 development console, then run:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
If necessary, add `--upload-port` with your actual bridge device path. The bridge normally appears as `/dev/ttyUSB*`; it is separate from native USB CDC (`/dev/ttyACM*`), which carries UART1 data. At `serial-tool>`, press Enter and run `help`.
|
||||
The firmware provides an interactive UART0 console at `serial-tool>`. Run `help` for available commands. The USB-to-UART bridge normally appears as `/dev/ttyUSB*`; it is separate from the native USB CDC serial transport, which normally appears as `/dev/ttyACM*`.
|
||||
|
||||
**Do not erase for routine updates.** Erasing destroys all persisted configuration and credentials. Migration from the former default/factory layout to the custom OTA layout is a separate destructive step; see [partition migration](docs/getting_started.md#partition-migration-and-erasing). Subsequent application-only updates can use the [web updater](docs/firmware_update.md).
|
||||
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.
|
||||
|
||||
## Before using network access
|
||||
Serial, Wi-Fi, display-aging, and mDNS hostname edits remain in RAM until explicitly saved with `serial save`, `wifi save`, `display save`, or `mdns save`. Authenticated admin SSH sessions and the admin-only browser shell expose the shared operational administration registry, including interactive secrets, recovery-material management, network diagnostics, and deferred self-affecting lifecycle commands. Initial administrator bootstrap and explicit recovery of an unavailable user database remain UART0-only. A remotely connected administrator also cannot generate a replacement password for its own account, preventing the one-time value from being lost when that mutation revokes the session. `web credentials show` exposes only the legacy migration/recovery credential, not an active Phase 8B network login.
|
||||
|
||||
Create the first administrator on trusted UART0 with `user add <username> admin`; there is no default shared login. Use a separate `user` account for SSH serial access: SSH `admin` opens device administration instead. Verify HTTPS and SSH fingerprints locally before trusting a new or changed identity.
|
||||
## Security notes
|
||||
|
||||
Serial, Wi-Fi, mDNS hostname, and display edits need their explicit Save commands to survive reboot. Account and device-identity changes persist immediately. USB host baud/framing choices do not change physical UART1 settings.
|
||||
|
||||
Use a trusted LAN or the password-protected device AP, not Internet exposure. HTTPS uses a device-specific self-signed certificate and password login; SSH accepts passwords and supported public keys. Browser logins expire after one hour, even during active use. **Disconnect** is not sign-out: use **Sign out** when finished.
|
||||
|
||||
The firmware is **not resistant to physical flash or RAM extraction**. Passwords are stored as salted PBKDF2-HMAC-SHA256 verifiers, but Wi-Fi secrets and private device keys remain recoverable from unencrypted NVS; offline password guessing and old flash copies also remain possible. Keep local ports physically controlled. Read [Security operations](docs/security_operations.md) before deployment.
|
||||
|
||||
## Development documentation
|
||||
|
||||
[Developer index](docs/devel/README.md) · [Roadmap and acceptance history](docs/devel/roadmap.md) · [Current development state](docs/agent/current-state.md)
|
||||
|
||||
Implementation contracts, regression procedures, PCB research, and the flash layout live under `docs/devel/`. Agent working memory lives under `docs/agent/`. Historical acceptance records are not claims that every fault or hardware test has passed; the carrier PCB remains a draft, not fabrication-ready.
|
||||
The HTTPS interface uses a device-specific self-signed certificate and an explicit same-origin login/logout flow over TLS; opaque eight-hour browser sessions are held in a bounded RAM table, while raw session tokens are sent only in a host-only `Secure`, `HttpOnly`, `SameSite=Strict` cookie. State-changing web requests require a session-bound CSRF token and strict same-origin validation; 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 legacy recovery password, 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. Do not treat this firmware as resistant to physical flash or RAM extraction until the planned hardening work is complete.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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()
|
||||
@@ -1,62 +0,0 @@
|
||||
#!/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")
|
||||
@@ -23,10 +23,6 @@ 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](../devel/web_administration.md#network).
|
||||
|
||||
## Startup and initialization
|
||||
|
||||
`app_main()` in `src/main.c` is the composition root. The implemented order matters:
|
||||
@@ -36,14 +32,14 @@ SSH role=admin ------> shared administration dispatcher <------ UART0
|
||||
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.
|
||||
6. Load/generate HTTPS material, then initialize the user database using the legacy web credential for first migration when available. 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.
|
||||
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. Both gates also require HTTPS security readiness, and SSH additionally requires its own security/runtime readiness. The HTTPS-security gate on SSH is an implemented dependency even though SSH has a separate host key.
|
||||
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.
|
||||
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. Network services start before command registration, so role-`user` serial access can be admitted in that interval while remote admin-console admission is rejected until the administration frontend is ready.
|
||||
|
||||
## Serial service and physical ownership
|
||||
|
||||
@@ -78,10 +74,6 @@ UART RX -> serial-service RX stream -> broker task
|
||||
|
||||
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](../devel/web_throughput_diagnostics.md).
|
||||
|
||||
### Clients to RS-232
|
||||
|
||||
```text
|
||||
@@ -104,35 +96,25 @@ TinyUSB callbacks enqueue/copy data and state; the transport task owns broker li
|
||||
|
||||
### 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.
|
||||
`web_server` runs HTTPS only on port 443 using the device-specific self-signed P-256 certificate from `web_security`. Current routes provide same-origin login/logout, the UI and static assets, status, typed admin operations, and separate serial/admin WebSocket ticket and upgrade paths.
|
||||
|
||||
Cookie login/logout replaces Basic/cache. Digest-only records carry copied principals, CSRF state, absolute expiry and nonreused originating-session IDs. Strict same-origin/CSRF mutations and session/principal checks gate admission; logout invalidates its session before transport cleanup, account mutations invalidate only the affected account, and ongoing currentness is authoritative. Authentication initialization failure gates HTTPS; failed start/accepted stop wipes records. RNG/SHA/database calls run outside short spinlocks with post-call epoch/identity revalidation. [Authentication contract](../devel/web_administration.md#authentication-and-admission).
|
||||
Browser passwords authenticate through `user_database`; the legacy web credential remains migration/recovery material and is not an active browser login after bootstrap. Successful login creates one of eight fixed eight-hour RAM sessions, with at most two retained per account. The raw 192-bit token is sent only in the host-only `__Host-sak-session` cookie (`Secure`, `HttpOnly`, `SameSite=Strict`, `Path=/`) while storage retains its SHA-256 digest, copied principal, generation-safe slot identity, and monotonic expiry. A boot-local key derives a deterministic session-bound CSRF token. Logout and every state-changing endpoint require strict `Origin == https://<Host>` and CSRF validation; a four-entry source-address limiter provides bounded, deliberately modest login throttling.
|
||||
|
||||
`web_httpd_adapter` is the sole private IDF 5.5.0 boundary for duplicate headers, admission-before-101, consumed-scratch wiping, staged optional URI registration, combined binary sends and owner-only idle sweeps. Re-audit its version guard on SDK upgrades. HTTPD debug logging must not expose headers/tickets. `web_diagnostics` independently observes public post-TLS callbacks using six metadata records and a default-disabled 32-event ring; it cannot see preaccept/in-progress/failed TLS. [Admission diagnostics](../devel/web_admission_diagnostics.md).
|
||||
The serial WebSocket requires a one-time exact-browser-session-bound ticket with a maximum 30-second lifetime. Tickets are stored as digests, consumed before currentness validation, and never persisted. Admission and active input revalidate both the copied principal and exact browser-session reference. An admitted serial connection starts the service if necessary, creates a broker client, and opportunistically requests writer ownership. The serial transport has two fixed slots and four ticket slots. Binary frames carry serial data; small text messages request or release writer ownership. HTTPD owns socket send/close operations, while the permanent transport task mediates broker work through bounded scheduling.
|
||||
|
||||
`web_httpd_idle` uses one one-second timer, six rows and at most one queued owner probe. Current-owner shutdown follows 15 seconds of observed ordinary idle, exempting actual WebSockets/async/pending input. Owner delays prevent hard timeout guarantees; accepted-but-lost work stays reserved until successful destruction, failed stop retains ownership. [Idle lifecycle contract](../devel/https_idle_cleanup.md).
|
||||
Administrators can separately mint a one-time ticket for one fixed browser admin-console WebSocket. That transport has its own permanent task, bounded input/output storage, copied admin principal, exact browser-session reference, and a single canonical-console slot; it never starts serial service or joins the broker. Terminal-mode switching changes only DOM visibility/focus and lazily opens the admin route, leaving the serial socket and writer lease alive. The combined Connect/Disconnect serial control explicitly manages only the serial route.
|
||||
|
||||
`web_ui`/`web_login_ui` own authored documents/scripts and hash-bound CSP loaders; authentication documents/app are no-store. Checked-in generated xterm/logo assets are compiled, not regenerated by ordinary builds. Browser Serial/Admin/Settings navigation changes view/input only, preserving serial client/lease and hidden output draining. Session-identity changes require a clean document; pagehide/restore revalidates before exposing buffers. One shared quick-settings host/controller preserves drafts, stale selections and pending uncertainty. [Terminal and console contract](../devel/web_administration.md#terminal-and-console-ownership).
|
||||
Typed admin endpoints cover full serial framing/lifecycle/persistence operations; Wi-Fi lifecycle/profile rotation; guided user CRUD, roles, entered/generated passwords, and authorized Ed25519/P-256 key add/remove; generation-safe station-profile/AP/secret editing and exact-generation save; display-aging apply/save/load/defaults/reset; a bounded secret-free broker-client list; and atomic expected-writer-to-target transfer. All authenticate the admin role server-side; mutators revalidate the exact browser session after their bounded body is parsed and immediately before the typed side effect. The common URL-form parser decodes in its body buffer, accepts at most 512 bytes and 10 unique fields, and rejects duplicate fields. User edits compare the database generation plus stable user ID; Wi-Fi edits compare the working-config generation, and reads disclose only `secret_set` flags. On stale conflicts the browser reloads user/Wi-Fi state without replaying the request and clears entered/generated secret fields on failure or Settings close.
|
||||
|
||||
### Browser administration and HTTPS lifecycle
|
||||
The Admin shell remains the web route for broader service/session controls, network diagnostics, security/danger operations, and unusual hardware/debug commands; those operations do not have guided forms.
|
||||
|
||||
`web_admin_transport`/`web_admin_tickets` add one optional admin socket, two session-bound tickets and admission to the same two remote-console slots shared with SSH, never a broker client. HTTPD owns a 1,552-byte PSRAM-only payload and IO; a 20 ms timer queues at most one poll. Current-owner shutdown avoids queued reusable socket pointers. Detach fences submissions and only successful HTTPD stop retires old work. Console/owner checks enforce currentness before sensitive work; unsupported parsed shell commands reject before effects. [Browser-shell policy](../devel/web_administration.md#browser-shell-policy).
|
||||
Serial and admin WebSocket initialization are failure-isolated from base HTTPS. An unavailable transport leaves login and non-WebSocket routes available. A lifecycle mutex serializes start, stop, and TLS refresh; a generation records explicit desired-running intent so a post-material refresh cannot override a newer request. Certificate rotation and full material reset require that refresh after persistence. HTTPS stop first disables further transport-owned HTTPD calls and tracks calls already in progress. Failed HTTPD destruction retains the handle for a retry, while a timed-out or failed admin detach is finalized only after successful HTTPD destruction and remains pending for retry before a later start.
|
||||
|
||||
`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](../devel/web_administration.md#https-and-reboot).
|
||||
|
||||
### Admin firmware upload
|
||||
|
||||
`web_firmware_update` streams a raw admin-only application image through HTTPD into the inactive OTA slot using standard ESP-IDF APIs. Existing Origin/CSRF/session checks precede body IO; raw length is bounded before HTTPD's narrowed length is trusted, SDK image validation and final principal currentness precede boot selection. One 4KiB buffer and a transient reboot task bound application storage; NVS and partition layout are untouched. HTTPD is occupied during upload, so normal web traffic can stall; deadlines bound receive progress, not flash-operation latency.
|
||||
|
||||
The server transition and identity reservations exclude competing lifecycle work. A separate atomic gate excludes ordinary reboot paths without depending on HTTPS initialization, preserving UART0 recovery. Successful response schedules delayed reset while retaining reservations; failed response after selection retains the selected-image latch but releases resources for manual reboot. A subsequent upload is refused until reset. No automatic retry, rollback, signing infrastructure or dependency patching. [Contract and validation limits](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload).
|
||||
`web_ui.c` contains authored index/application strings and response policy. Its restrictive CSP contains a hard-coded hash of the inline loader, so those two must change atomically; preserve same-origin connections, no-referrer behavior, frame denial, and the existing cache policy. `web_assets_data.c` contains checked-in generated arrays for vendored compressed xterm assets and the logo. Normal builds compile these arrays directly; they do not regenerate assets.
|
||||
|
||||
### SSH
|
||||
|
||||
Typed SSH settings use the existing ID dispatcher and original-login result slot, never HTTPD wolfSSH calls or owner waits. Conditional lifecycle/session controls compare a saturated service generation and exact nonreused session ID under canonical locks. `ssh_transport_replace_identity` reserves service then identity before stop, retaining the command mutex across stop → commit → conditional restart. Failed stop skips mutation/start; failed persistence may follow disconnection; committed identity is never rolled back after restart failure. Only the SSH owner frees context after all slots retire, and start rejects orphan handles. Direct security/CLI/deferred SSH callers share task-bound identity reservations; crypto/NVS run outside security locks. HTTPS remains available, so no self-cutting HTTP ACK gate is needed. [SSH contracts](../devel/web_administration.md#ssh).
|
||||
|
||||
`ssh_transport` uses wolfSSH on port 22 with one explicitly dual-stack IPv6 wildcard listener and two fixed session/handshake slots. Peer formatting preserves IPv6 interface scope; IPv4 shares the listener, not a second socket. Initialization calls `wolfSSH_Init()` in the caller before task creation; after that, one owner task pinned to core 1 exclusively owns runtime contexts/sessions and wolfSSH calls. It enforces bounded handshakes, authentication attempts, receive work, and session buffers.
|
||||
`ssh_transport` uses wolfSSH on port 22 with two fixed session/handshake slots. 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`.
|
||||
|
||||
@@ -151,57 +133,41 @@ Network code holds copied, secret-free principals rather than pointers into data
|
||||
|
||||
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;
|
||||
1. `user_admin_service` serializes typed web and console mutations with `admin_command_gate`; after a database mutation commits, it makes best-effort targeted WebSocket/SSH revocation calls, and 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.
|
||||
The web user editor supplies an expected database generation for every mutation and the stable target user ID for existing-account operations. A mismatch rejects stale state, including delete/recreate of the same username. The final administrator remains protected, and remote self-generated password replacement is rejected so its one-time result cannot be lost during revocation.
|
||||
|
||||
NVS is not encrypted. Password verifiers improve password storage, but Wi-Fi credentials and TLS/SSH private keys remain recoverable under physical flash extraction.
|
||||
The final administrator cannot be deleted or demoted. UART0 is trusted for initial administrator bootstrap and explicit unavailable-database recovery. Authenticated admin SSH and the browser Admin shell can run the operational registry but are denied those two recovery operations; other secret-bearing commands are remotely available unless their handlers deny them.
|
||||
|
||||
## 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](../devel/web_administration.md#accounts-and-authorized-keys).
|
||||
|
||||
`web_serial_settings` queues bounded typed operations to the same dispatcher, retaining one original-login result and a 30-second dequeue check. Apply/Defaults are RAM-only; Save persists device working state. Reconfiguration can discard serial-service pending bytes while broker clients/lease/output remain. Snapshot reads are zero-wait and consistent; `/api/status` reports unavailable running state as null. Settings navigation preserves both terminals; bounded completion checks and manual uncertainty recovery never replay mutations. [Typed API/lifetime](../devel/web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
NVS is not encrypted. Password verifiers improve password storage, but Wi-Fi credentials, legacy recovery credentials, and TLS/SSH private keys remain recoverable under physical flash extraction.
|
||||
|
||||
## Console architecture
|
||||
|
||||
UART0 and admin SSH share canonical command implementations:
|
||||
UART0, admin SSH, and the browser admin shell share canonical command implementations:
|
||||
|
||||
```text
|
||||
UART0 linenoise frontend --\
|
||||
> fixed request queue -> one dispatcher -> esp_console_run()
|
||||
admin SSH line editor ----/ |
|
||||
+-> registered *_console handlers
|
||||
UART0 linenoise frontend -----\
|
||||
admin SSH line editor ---------> fixed request queue -> one dispatcher -> esp_console_run()
|
||||
browser admin 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.
|
||||
`admin_ssh_console` creates the transport-neutral dispatcher before network services but marks command dispatch ready only after ESP-IDF console registration and successful UART frontend task creation. Remote admin admission 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 dispatcher is the sole caller of `esp_console_run()`, serializing UART0, admin SSH, and browser-admin 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 remote frontends, 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 remote session. SSH and browser admin use the same bounded editor, four-entry history, completion formatter, overlong-line discard state, prompt handling, and generation-safe console tokens. Exact frontend callbacks revalidate the transport/session binding immediately before dispatch. Only the SSH owner task moves SSH ring output through wolfSSH; only the web-admin task queues browser output to HTTPD.
|
||||
|
||||
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](../devel/web_administration.md#browser-shell-policy).
|
||||
|
||||
### Shared IPv4/IPv6 diagnostics
|
||||
|
||||
`network_console` owns root and `wifi`-alias ping/traceroute/nslookup for UART0, admin SSH and browser Admin. Family flags are parsed once; explicit IPv6 never silently falls back to IPv4. Hostname probes use IPv4-first resolution fallback only; nslookup queries A and AAAA separately and shows unscoped link-local answers. Probes require a device-local link-local zone (`%sta`, `%ap`, valid index/name). Resolver policy/DNS provisioning and serial transport ownership are unchanged.
|
||||
|
||||
Traceroute owns one raw socket and a 1280-byte transient receive buffer on the shared dispatcher stack, one eight-byte probe per hop (max30), and one-second absolute receive deadlines. It rejects incomplete/corrupt/unrelated packets and unsupported IPv6 extension headers/fragments. IDF5.5.0-specific framing, automatic ICMPv6 checksum, common `IP_TTL` hop-limit behavior and millisecond timeout flooring are pinned/retested; <1ms remaining returns timeout rather than configuring an infinite wait. DNS and send/scheduler latency are not covered by the receive deadline.
|
||||
|
||||
Ping keeps the 4200-byte PSRAM-only lazy queue payload and SDK transient task. Permanent callback context plus a pending reservation survives the count-based console deadline; later pings cannot reset a live producer's queue. SDK receive can outlast its timeout under unrelated ICMP, so a timed-out session stays reserved until END. END means callback production ended, not synchronous task/socket destruction; retiring resources can briefly overlap a new session. These diagnostics serialize administration but never execute on HTTPD or enter the serial hot path. Tests and exact limits: `tests/network_diagnostics/README.md`.
|
||||
Remote `exit`, reboot, SSH stop/session disconnect/host-key changes, and web-origin HTTPS stop/restart 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.
|
||||
|
||||
## 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.
|
||||
`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 deadlines, profile failover, AP policy, retries/backoff, next-profile requests, and the mDNS announcement lifecycle. `mdns_service` initializes the responder at most once after a validated STA `GOT_IP`; the managed component's own event handlers withdraw and restore the STA announcement across transient connectivity changes, while the project tracks whether announcement is currently expected. Initialization failure is latched rather than retried because partial upstream low-memory initialization is not safely recoverable; mDNS failure is nonfatal. It also reconciles against authoritative driver/netif state so dropped events do not permanently wedge policy. 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.
|
||||
|
||||
`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.
|
||||
A dedicated config-writer mutex serializes complete working-config writers. The typed browser editor copies the credential-bearing config with its exact nonzero generation, modifies and validates the copy, and commits only through compare-and-swap; Save holds the same writer serialization through NVS and persists only the expected generation. Generation mismatch or exhaustion fails closed. Credential-bearing copies are securely wiped, browser reads return only per-profile/AP `secret_set` booleans, and routine status/local UI use secret-free snapshots.
|
||||
|
||||
Persistent namespaces/blobs include:
|
||||
|
||||
@@ -215,19 +181,13 @@ Persistent namespaces/blobs include:
|
||||
|
||||
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](../devel/web_administration.md#serial-and-display).
|
||||
|
||||
`web_broker_settings` exposes compact zero-wait rows and confirmed writer assignment through one login-isolated slot. The broker atomically snapshots clients/writer/lease version and compares selected target/version inside the force-writer lock before effects. Three-bit slot/29-bit client generations retire rather than wrap; a separate saturated 32-bit lease generation advances before advisory event delivery and survives counter clear. Saturation blocks conditional assignment, not ordinary request/release/disconnect/recovery force. Contextual refresh never silently rebases explicit selections or clears sticky stale/absence latches; deliberate reselection is required. [Broker/context contracts](../devel/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.
|
||||
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 and typed `/api/admin/display` aging operations depend on the UI task; web Apply/Save/Load/Defaults/Reset reuse the local-UI validation and persistence contracts and share `admin_command_gate` with console display writers so each complete RAM/NVS operation is serialized. 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.
|
||||
|
||||
@@ -235,16 +195,9 @@ Hardware diagnostics are synchronous console commands. RS-232 tests own the phys
|
||||
|
||||
- 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.
|
||||
- Transport slot generations, account authentication generations/stable IDs, Wi-Fi working-config generations, and HTTPS lifecycle generations solve different stale-reference problems; preserve each domain's checks.
|
||||
- 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.
|
||||
- Password authentication performs PBKDF2 outside the user-database mutex and revalidates afterward. Typed user mutations are serialized by `user_admin_service` plus `admin_command_gate`; typed display Apply/Save/Load/Defaults/Reset also use that gate with console display writers. Some password mutation paths derive verifiers while holding the mutation lock, so 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](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
|
||||
@@ -12,12 +12,6 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
- 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](../devel/roadmap.md#phase-9--security-and-production-hardening): intermittent trusted-network use, unchanged authentication/recovery, no physical-extraction protection. Keep upgrades separate and do not restore the abandoned extensive patchset.
|
||||
|
||||
## Secure randomness
|
||||
|
||||
**Responsibility:** provide the sole project-owned, mutex-serialized application DRBG, seeded before Wi-Fi/radio use.
|
||||
@@ -43,13 +37,12 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
**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`.
|
||||
- Interfaces: connect/disconnect, request/release/force writer, nonblocking read/write/event APIs, snapshots and counters
|
||||
- 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/devel/web_throughput_diagnostics.md`.
|
||||
|
||||
## Native USB CDC
|
||||
|
||||
@@ -67,61 +60,25 @@ This is a semantic map, not a complete file inventory. Start here, then read the
|
||||
|
||||
**Responsibility:** serve authenticated HTTPS UI/API, issue WebSocket tickets, and adapt browser serial sessions to broker clients.
|
||||
|
||||
- Files: `src/web_server.{h,c}`, `src/web_serial_transport.{h,c}`, `src/web_ui.{h,c}`, `src/web_console.{h,c}`
|
||||
- Ordinary HTTPS idle cleanup: `src/web_httpd_idle.{c,h}`, owner sweep in `web_httpd_adapter.{c,h}`, lifecycle/TLS composition in `web_server.c`; `tests/web_httpd_idle/run.py`. Independent of diagnostics/optional transports: 15-second observed idle, one-second timer/one queued probe, six rows, actual WS/async/pending exemptions, safe current-owner shutdown and stop/restart fencing. No LRU/socket/timeout/stack increase. SDK queue/owner-delay and regression contract: `docs/devel/https_idle_cleanup.md`.
|
||||
- Independent throughput diagnostics: `web_serial_transport.{c,h}` owns two fixed per-slot binary-TX aggregates and epoch fences; `web_console.c` exposes default-disabled `web performance enable|disable|show|clear`. Queue-entry/callback-entry, synchronous-send and completion/drain-return estimates, not peer receipt or scheduler-only latency. `tests/web_serial_performance/run.py`; resource/evidence limits and UART0 paired capture: `docs/devel/web_throughput_diagnostics.md`.
|
||||
- Storage compatibility: `user_database` persists missing storage empty and preserves valid v1 user bytes; private derived `v1_admin_marker`, no public bootstrap/migration/sync APIs. `web_security` privately migrates v1 1392-byte material to TLS-only v2 1340-byte material, exact identity/generation retained, commit before publish, fail closed without fallback overwrite. Credential commands removed; user generated passwords and TLS rotation remain. Contracts, downgrade and evidence limits: [legacy compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
- Security files: `src/web_security.{h,c}`, `src/web_cookie_auth.{h,c}`, `src/web_session_store.{h,c}`, `src/web_auth_parse.{h,c}`. Private IDF boundary: `src/web_httpd_adapter.{h,c}`.
|
||||
- HTTP policy/UI: `web_cookie_auth` + `web_auth_parse` enforce bounded cookie/Origin/CSRF/admin admission; `web_login_ui.{c,h}` serves login, `web_ui.c` owns session-fenced Serial/Admin/Settings and shared quick controllers. Tests: `tests/web_cookie_auth/run.py` (domain variants), `tests/web_auth_parse/run.py`, `tests/web_login_ui/run.py`, `tests/web_ui_session/run.py`.
|
||||
- Admission diagnostics: `web_diagnostics.{c,h}`, `tests/web_diagnostics/run.py`; six post-TLS records/32-event opt-in ring, no HTTPD off-owner inspection. [Contract](../devel/web_admission_diagnostics.md).
|
||||
- Identity/lifecycle: `web_server_replace_identity()` + `web_security` reserve service before identity; commit before reserved stop/start, no rollback after commit. `web_lifecycle_settings.{c,h}` owns original-login ID/ACK handoff. Tests: `tests/web_security/run.py`, `tests/web_admin_transport/server_lifecycle.py`.
|
||||
- Files: `src/web_server.{h,c}`, `src/web_session.{h,c}`, `src/web_serial_transport.{h,c}`, `src/web_admin_transport.{h,c}`, `src/web_ui.{h,c}`, `src/web_console.{h,c}`
|
||||
- Security files: `src/web_security.{h,c}`
|
||||
- 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
|
||||
- Interfaces: generation-tagged web init/start/stop/TLS refresh and snapshots; HTTP handlers including `/api/admin/users`, `/api/admin/wifi-config`, and `/api/admin/display`; ticket mint/consume; attach/detach/finalize; 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.
|
||||
- Flow: `browser -> HTTPS login session -> exact-session ticket -> serial WebSocket -> broker`; admin sessions may separately use `admin ticket -> admin WebSocket -> canonical dispatcher` without joining the broker.
|
||||
- Ownership: HTTPD owns socket send/close work; separate permanent web tasks own serial broker mediation and browser-admin console I/O; there are two serial slots/four serial tickets and one admin slot/two admin tickets. HTTPS lifecycle transitions are serialized separately from state snapshots, carry a lifecycle generation, and retain failed-stop/finalizer ownership for retry. The admin transport disables new HTTPD calls during detach and tracks calls already in progress.
|
||||
- Security constraints: eight opaque browser sessions retain digest-only tokens and copied current principals, with at most two sessions per account. Mutations require strict same-origin and session-bound CSRF checks. Typed URL-form bodies decode in their own storage and are limited to 512 bytes/10 unique fields. User and Wi-Fi editors use optimistic generations; user edits also bind stable user IDs, while Wi-Fi reads expose only `secret_set` flags. Terminal-mode switching never closes the serial socket or releases its writer lease; the combined control explicitly connects/disconnects only serial. Changes to the authored inline loader must update its hard-coded CSP hash in the same change.
|
||||
- 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](../devel/web_administration.md#browser-shell-policy). Tests: `tests/admin_console_boundary/{run,accounts,lifecycle}.py`, `tests/admin_ssh_policy/run.py`, `tests/web_admin_transport/run.py --tickets`, `tests/web_cookie_auth/run.py --admin`.
|
||||
- Ownership: 20 ms ESP timer queues at most one HTTPD poll, no new task; HTTPD owns 1,552 B PSRAM-only payload and IO. Closure uses HTTPD-owned `shutdown`, not IDF's reusable-pointer queued close. Detach fences submitters; only successful HTTPD stop retires queued state before restart. Session/principal currentness and generation checks protect all sensitive boundaries.
|
||||
|
||||
## Firmware upload
|
||||
|
||||
- `src/web_firmware_update.{c,h}`: admin raw `POST /api/firmware`, cookie/Origin/CSRF admission via `web_cookie_auth`, registration in `web_server`, UI in `web_ui`. Standard SDK OTA into inactive app only; bounded4KiB buffer, validated raw length/header/final image and principal before boot selection. No NVS/layout writes or vendor patches.
|
||||
- HTTPD handles streaming synchronously; upload is deliberately disruptive to web traffic. Server/identity reservations fence lifecycle changes; atomic ordinary-reboot gate also covers console/SSH/browser/button reset paths. Success-response schedules delayed reset; response failure after selection latches uploads until deliberate manual reboot. No blind retries.
|
||||
- Tests: `tests/web_firmware_update/run.py` (active-build SDK5.5.0 headers and actual begin/abort contract), `tests/web_ui_session/run.py`, existing auth/lifecycle tests. [Usage, acceptance limits and reusable regression checks](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload). Phase 10 complete by explicit user acceptance on 2026-09-18: upload works and normal operation verified; no specific fault/NVS-comparison/power-loss/recovery pass implied. Initial install by wire; subsequent upload is application `firmware.bin`, not full-flash image.
|
||||
|
||||
## Typed settings source and regression map
|
||||
|
||||
HTTPD reads zero-wait projections and queues only IDs to the existing dispatcher. One original-login slot per domain; canonical owners compare/reserve at execution. [API/lifetime and failure contracts](../devel/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`.
|
||||
- Interfaces: init/start/stop, session snapshots/disconnect/revocation, host-key replacement, counters
|
||||
- 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).
|
||||
- Dependencies: user database, broker, admin SSH console, secure random, wolfSSH/wolfSSL; current boot start gate also depends on `web_security` readiness
|
||||
- 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.
|
||||
@@ -130,51 +87,49 @@ Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtur
|
||||
|
||||
**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`.
|
||||
- Files: `src/user_database.{h,c}`, `src/user_admin_service.{h,c}`, `src/user_console.{h,c}`, `src/admin_command_gate.{h,c}`
|
||||
- Interfaces: init/migration/recovery, authenticate, principal-currentness, account/password/role/key mutations, optimistic mutation results, snapshots
|
||||
- Called by: web and SSH authentication/currentness checks, `/api/admin/users`, and console administration
|
||||
- Dependencies: NVS, secure random, mbedTLS cryptography, web/SSH targeted revocation
|
||||
- Ownership: database mutex protects the internal live record and PSRAM-preferred transactional candidate; password authentication runs PBKDF2 outside the mutex and revalidates afterward. `user_admin_service` is the shared typed mutation boundary for web and console paths: its recursive `admin_command_gate` region serializes snapshot expectation checks plus commit, then performs best-effort web and SSH revocation after a committed change.
|
||||
- Authorization: UART0 exclusively owns initial administrator bootstrap and unavailable-database recovery. Both roles may use browser serial; only current admins may use guided admin APIs, the browser Admin shell, or admin SSH, subject to handler policy.
|
||||
- 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.
|
||||
**Responsibility:** provide one canonical command registry and serialized execution for UART0, admin SSH, and the browser Admin shell.
|
||||
|
||||
- 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
|
||||
- Files: `src/admin_ssh_console.{h,c}`, `src/console_input.{h,c}`, `src/console_completion.{h,c}`, `src/system_console.{h,c}` and all `*_console.{h,c}` modules
|
||||
- Entry points: `admin_ssh_console_init()`, `admin_ssh_console_start_uart_frontend()`, `admin_ssh_console_open()`, 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.
|
||||
- Dual-stack diagnostics: `network_console.*` is the shared root/`wifi` handler on all three admin surfaces. `-4`/`-6`, device-local `%sta`/`%ap`/index/name; default probes IPv4-first resolution fallback only, nslookup explicit A+AAAA and unscoped link-local display. One-second/hop raw IPv4/IPv6 traceroute validates full outer packet/quoted echo and rejects IPv6 extensions/fragments. IDF5.5.0 raw framing/hoplimit/checksum/timeout contract is version-guarded. Ping uses existing PSRAM queue and SDK transient task with permanent callback context/deadline/pending reservation; END does not imply completed SDK teardown. Tests: `tests/network_diagnostics` (actual handlers/packet and SDK contracts), `tests/network_diagnostics_surfaces` (routing/policy/completion). No serial hot-path changes.
|
||||
- Lifecycle: remote session tokens include frontend identity and slot generation; fixed output/history/prompt state is wiped immediately on idle close or after an executing handler returns. Remote `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. Remote admin-console admission 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
|
||||
- Interfaces: config defaults/validate/load/save; manager init/start/stop/apply/reconnect/next-profile/snapshot; versioned working-config copy, compare-and-swap, and exact-generation save
|
||||
- Called by: startup, console, local UI, typed web handlers, ESP event callbacks
|
||||
- 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.
|
||||
- Lifecycle: permanent manager task and bounded queue; callbacks enqueue compact events only.
|
||||
- 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. Browser edits compare a nonzero working-config generation, and browser Save persists exactly that generation; stale or exhausted generations fail closed. Working-config copies contain PSKs and must be tightly scoped and wiped; web reads expose only `secret_set` flags, and 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
|
||||
- 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}`; shared writer serialization uses `src/admin_command_gate.{h,c}`
|
||||
- Interfaces: display init/frame/draw/commit/snapshot; UI start/activity/config; versioned NVS settings
|
||||
- Called by: startup, local UI task, diagnostics, display console, and typed `/api/admin/display` handlers
|
||||
- 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.
|
||||
- Ownership: `local_display` solely owns I2C0 and framebuffer mutex; a frame belongs to its initiating task. Typed web Apply/Save/Load/Defaults/Reset and console display writers share `admin_command_gate`, serializing each complete working-config or persistence operation.
|
||||
- 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.
|
||||
- Constraint: collect service snapshots before I2C; local UI never joins broker or handles secrets.
|
||||
|
||||
## Hardware and diagnostics
|
||||
|
||||
@@ -200,8 +155,10 @@ Shared UI regression: `tests/web_ui_session/run.py` and its domain `.cjs` fixtur
|
||||
| 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 account roles/passwords/keys | `user_admin_service.*`, `user_database.*`, `user_console.c`, `/api/admin/users` handlers, transport revocation APIs |
|
||||
| Change Wi-Fi policy or profile persistence | `wifi_manager.*`, `wifi_config.*`, `wifi_console.c`, `/api/admin/wifi-config` handlers |
|
||||
| Change guided display aging | `local_ui_config.*`, `local_status_ui.*`, `/api/admin/display` handlers, `web_ui.c` |
|
||||
| Change HTTPS stop/restart or TLS-material refresh | `web_server.*`, `web_console.c`, `web_admin_transport.*`, deferred control in `admin_ssh_console.*` |
|
||||
| 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` |
|
||||
|
||||
@@ -1,134 +1,64 @@
|
||||
# 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).
|
||||
This file is working memory. Update it during active work and before handoff; do not treat it as a permanent design record.
|
||||
|
||||
## Documentation reorganization handoff
|
||||
## Development state
|
||||
|
||||
- Human-facing guides live in `docs/`; implementation contracts and development evidence live in `docs/devel/`; durable agent memory remains in `docs/agent/`. Agent/test web-contract references target `docs/devel/web_administration.md`, not the top-level operator guide.
|
||||
- README was refined from the user's already-thinned version, not restored from Git. New `docs/getting_started.md`, `docs/user_administration.md`, and `docs/firmware_update.md` provide operating procedures; `docs/web_administration.md` is now a user guide and its original contract is preserved under `docs/devel/`. Flash layout and PCB research moved into the developer archive; contextual test/hardware/provenance READMEs remain beside artifacts.
|
||||
- Documentation relocation adds no new device evidence and does not change historical acceptance limits. Firmware, generated assets, CAD and historical hash JSON are untouched. Hardware scripts have only relocated-note reference/protection adjustments; Python syntax was checked without executing generators. Documentation links/anchors and `git diff --check` were checked; no firmware build, upload, erase, or hardware test was performed.
|
||||
Based on the current uncommitted worktree plus `README.md` and `docs/roadmap.md`:
|
||||
|
||||
## Parametric enclosure — initial concept, physical validation pending
|
||||
- Hardware characterization, serial service, session broker, USB CDC, Wi-Fi, HTTPS/WebSocket, SSH serial transport, and local display/control are implemented and documented as target-hardware validated.
|
||||
- Phase 8A role-based user storage/UART0 administration and Phase 8B role-aware HTTPS/SSH authentication and targeted revocation are documented as target-hardware validated.
|
||||
- Phase 8C admin SSH is implemented in source, uses the shared `esp_console` registry, and has passed target-hardware validation.
|
||||
- Phase 8D is in progress. Explicit browser sessions, exact-session serial/admin WebSocket binding, one browser frontend for the canonical admin dispatcher, typed serial controls, guided user CRUD/roles/passwords/authorized keys, generation-safe Wi-Fi profile/AP/secret editing and save, display-aging operations serialized with console writers through `admin_command_gate`, contextual client/status popovers, atomic writer transfer, and serialized retry-safe HTTPS lifecycle handling are implemented in the current worktree. All current Phase 8D target-hardware validation remains pending.
|
||||
- Security/production hardening, OTA, BLE evaluation, advanced networking, and optional filesystem features remain future roadmap work.
|
||||
- Reserved OTA, coredump, NVS-key, and storage partitions do not imply those runtime features are implemented.
|
||||
|
||||
- Label-only follow-up: removed DISPLAY, grouped `USB: UART0 / CDC` (local wiring/measurement evidence does not establish physical left/right roles), and raised `ESP32 / Serial Swiss / Army Knife` on the blank roof left of the display. Shared labels export retained; unlabeled lid CSG exactly matches the pre-change source. Lid STL and three previews refreshed; base/plungers and PCB unchanged. CGAL simple lid/labels exports, source/datum/mesh checks and actual-glyph roof clearance >=1 mm passed; no physical print/test. Lid detail now views readable branding from the USB side.
|
||||
## Recent memory audit
|
||||
|
||||
- `hardware/case/` corrected per user: 5 mm base insert bosses directly support carrier underside Z7.4/top Z9; two M3 RS-232 and four M2 OLED brass male/female bodies ABOVE carrier span 10 mm to module underside Z19. Studs cross actual 1.6 mm carrier holes into respective inserts; four separate carrier holes take direct M3 screws. ESP32 has no supplied mounting holes and remains header-supported. No under-carrier brass or printable spacers remain. Shell now **106 × 86.8 × 37.4 mm**, roof underside Z35 (10 mm lower); ports/display/DB9 panel/hangers/buttons follow the corrected datums. Red RF overlay/enclosure reserve restriction removed; physical antenna retained, no Wi-Fi guarantee and no PCB/schematic/copper/reserve/layout/export changes.
|
||||
- Superseding DB9 change: separate plate/export/mode and M3 insert hangers removed. Cover now has integral nominal 48 mm-wide × 2.4 mm-thick connector panel at Y−0.47…1.93, Z21.07…35, continuous roof connection and two nominal 6 mm forward ribs outside flange bounds, extended into the wall to close former 1 mm side slits. Bottom-open 18.078 mm shell slot and closed Ø3.6 post holes at real 25 mm spacing permit straight vertical lowering **after removing both screw posts and all cables**; front-wall recess also opens to seam. Panel clears PCB by 0.5 mm and anchors by 3.10 mm in Y. No horizontal-slide claim. Insert count now 10 M3 + 4 M2, all base. Added forward-only base closure Y−3.4…−0.5: shoulders Z20.82, 17.578038-wide tongue Z21.69762024, 0.25 mm seams, 0.6 below STEP shell, 0.5 forward of module PCB. PCB files untouched.
|
||||
- Three default CGAL exports, native registration (including flange plane, hole diameters/no ESP32 NPTH), evaluated stack/connector/stud/web fixtures, edge-manifold meshes, three parameter CGAL smoke tests and continuous 60 mm vertical envelope sweeps passed; restored-post negative control detects blockage. Thicker-panel sweep caught and fixed residual inner-wall strip; default and raised/thicker variant now pass. Closure follow-up regenerated base/lid and all three previews (unchanged plungers revalidated); default plus two variant sweeps include per-step upstand/lid travel, base/lid seating and electronics insertion. Four parameter mesh tests pass; closure guards and STEP shell-minimum registration added. Three regenerated previews visually inspected, including bare-lid detail. This is selected-envelope CAD clearance, not complete hardware or strength qualification.
|
||||
- DB9 hardware remains a gate: no inferred M3/4-40 thread; removability, stud length, engagement and cable reach unmeasured. Panel consumes 2.4 mm of stud reach; verified longer studs/extensions may be needed. Reseated 4.8 mm bodies move mouths to Y−5.27, **1.4 mm ahead of nose**: full mating may require compatible shorter-body hardware and must be physically verified. Nose remains 0.47 mm beyond outer wall / 3.4 mm beyond panel. Do not force an incompatible post/cable combination or claim stock threads sufficient. Actual underside solder/prong clearance within 5 mm, brass lands/studs/inserts, header height, USB/glass/buttons and load strength also remain gates. See `hardware/case/README.md` and `checks.md`; no print, physical assembly or hardware test performed.
|
||||
- Fixed failed-initialization ownership leaks for wolfSSH, partial HTTPS startup, and TinyUSB teardown. Failed teardown now retains ownership and blocks unsafe duplicate initialization.
|
||||
- Serial-service RX/TX stream payloads (16 KiB and 8 KiB effective capacity) now prefer PSRAM with internal fallback; FreeRTOS controls and UART driver buffers remain internal.
|
||||
- The 5,360-byte transactional user-database candidate now prefers PSRAM with internal fallback while the live database remains internal. Candidate contents are wiped after each transaction and wiped/freed on initialization or recovery failure.
|
||||
- UART and admin-SSH completion formatter buffers were reduced from 2 KiB to 1 KiB each; current worst-case output is 890 bytes and overflow remains fail-closed.
|
||||
- Linked RAM fell from 99,508 to 92,188 bytes (7,320 bytes). PSRAM placement of serial payloads additionally removes about 24 KiB of normal internal-heap pressure on the target.
|
||||
- The reviewed mDNS-enabled build uses 94,532 bytes of linked static RAM, 2,344 bytes above the earlier 92,188-byte baseline, and 1,599,765 bytes of flash. Minimizing the managed component saved 112 bytes of linked RAM and about 5.9 KiB flash versus the first mDNS build. Its 4 KiB task stack remains internal, while checked-in settings move general mDNS allocations to PSRAM and disable unused browse, component CLI, AP/ETH, and multiple-instance features. Runtime heap impact still requires target measurement.
|
||||
- The final current Phase 8D build footprint is 119,860 bytes linked static RAM and 1,715,753 bytes flash. Runtime heap/stack/socket headroom requires target measurement.
|
||||
|
||||
## PCB accepted-fit labels and worksheet — complete
|
||||
## Clearly incomplete or transitional areas
|
||||
|
||||
- Removed visible `USB ACCESS - VERIFY` and title comment `USB / antenna geometry pending; verify module pinout / assembly`; replaced the duplicated pending-clearance caption with `Nominal RS-232 metal verified; RF qualification not established`. Specific RF and connector cautions remain; this is user acceptance, not certification.
|
||||
- Changed only the native worksheet from A4 to A3 landscape (420 × 297 mm), keeping all absolute board coordinates (carrier X=100…182, Y=100…180 mm). No physical translation/reroute/net change. `update_presentation.py` guards locks/concurrent changes, uses `SaveBoard(..., True)` and exact parsed-delta/idempotence checks. Generator and PDF exporter no longer emit the stale pending captions; original historical baselines remain unchanged.
|
||||
- Placement PDF now explicitly includes the border/title block at 1:1; PCB SVG/PNG and carrier-only 3D refreshed. Full-page render is `hardware/PCB/validation/pcb-placement-page.png`. Print the A3 PDF at actual size, not fit-to-A4. Schematic/OLED fit template are unaffected.
|
||||
- PASS: cleanup twice, full group/retained-copper/settings audit, U1/silk/stale-geometry regression, fresh PCB DRC/parity (all zero), independent routing checks, two footprint tests, legacy cleanup compatibility, Python syntax and `git diff --check` (after stripping KiCad SVG trailing whitespace). Rendered full A3 PDF visually checked: PCB and bottom-right title block are well separated; copper preview and 3D also inspected. KiCad emits known enum-property assertions but completes. No firmware/device tests, fabrication outputs or orders.
|
||||
- Phase 8C hardware validation passed, including route separation, shared command serialization, history/completion, prompts, output backpressure, revocation during queued work, deferred SSH lifecycle/reboot actions, and full concurrent transport operation. At 460800 baud with SSH and WebSocket clients in parallel, substantial packet drops and slow display controls were observed under load, without memory exhaustion; no baud-rate reduction is planned.
|
||||
- The browser Settings dialog covers complete serial framing/lifecycle/persistence controls, Wi-Fi lifecycle/profile rotation, guided user/password/role/authorized-key management, generation-safe Wi-Fi profile/AP/secret editing and exact-generation save, and display-aging apply/save/load/defaults/reset. Broader service/session controls, network diagnostics, security/danger operations, and unusual hardware/debug commands remain shell-only; do not describe them as guided forms.
|
||||
- The four-entry source-address login limiter is deliberately bounded and modest: oldest-entry replacement prevents limiter-induced global denial of service, but distributed/rotating-source attacks remain Phase 9 hardening work.
|
||||
- NVS encryption, secure boot/flash encryption review, comprehensive authentication lockout/rate limiting beyond the bounded browser-login limiter, production certificate/provisioning policy, and OTA are not implemented.
|
||||
|
||||
## PCB group alignment and user acceptance — complete
|
||||
## Known inconsistencies
|
||||
|
||||
- Current record: `docs/devel/hardware/PCB/user-acceptance.md`. This supersedes older pending-user-fit/USB/male-map/hardware-selection statuses below without upgrading their original evidence. User reports visual component fit and USB clearance correct, male pinout/header/footprint equivalence and pin-by-pin review; purchased Omron B3F-1000 and 2 kΩ 1% metal-film resistors. M3 carrier/daughterboard hardware, 10 mm brass standoffs into heat inserts and M2 display hardware approved. Intended/reported assembled module elevation is 10 mm; 8.5 mm female plus described 2.5 mm male is not an independently verified mating-height calculation. RS-232 prongs clear per user.
|
||||
- Retained U2 body centre X=156.970 mm (local 56.970); OLED +0.970 X, resistor group −0.840 X / −1.3315 Y, button group −1.280 X. Horizontal groups retained. Resistor body Y=131.4185…133.9185 leaves 2.0815 mm nominal clearance to both RS-232 and OLED PCBs; OLED/button body gap 5.25 mm. U1/measured USB, U2, outline, carrier holes and RF area unchanged.
|
||||
- Native board mutated with `SaveBoard(..., True)`, no whole-board regeneration/reset. 476/587 original copper items retained exactly, 111 removed and 503 added during incremental clearance/connectivity repair; current 938 segments / 41 vias. Snapshot `validation/pcb-before-group-alignment.kicad_pcb`, migration manifest and full parsed-content/retained-copper/body-geometry audit added. Project/protected preference hashes unchanged; original A0/U1 evidence preserved. U1 validator verifies original integration against the pre-alignment snapshot, then the exact authorized delta; no blanket exclusions/rebaselining.
|
||||
- R1–R3 now `2k 1%` in PCB/schematic/BOM/generator/validator. Purchased-part and U2 status fields synchronized; previously completed U2 footprint assignment and strict zero-parity validators preserved. User accepts nonisolated USB and reports successful 3.3 V operation, onboard OLED pull-ups and existing RF/power/ground routing. Backfeed safety, regulator margin, pull-up resistance/rail and manufacturer RF/EMC qualification remain unverified; no invented values or new hardware-test claims.
|
||||
- PASS: schematic ERC/exact nets/firmware pin contract, PCB physical DRC and parity (all zero), continuous routing clearance/edge/RF checks, group/preservation audit, U1/silk idempotence and stale-geometry rejection, both footprint tests, DB9 source/CAD validator. Minimum independent copper/NPTH gap 0.252762 mm; edge gap 1.900 mm. Silkscreen (94 items), DB9 projection, 1:1 placement PDF, PCB SVG/PNG/3D and schematic PDF/SVG/PNG refreshed. Missing module courtyards and absence of module assembly 3D models remain explicit limits. No Gerbers/order/firmware changes or hardware tests.
|
||||
These observations should be checked when touching the relevant area; they are not automatically bugs requiring unrelated cleanup.
|
||||
|
||||
## U2 schematic footprint — assigned
|
||||
- Some source comments still call shared commands UART0-only or call the current local status/control task read-only.
|
||||
- `USER_DATABASE_LOAD_EMPTY` is only an initialization/failure sentinel at the checked-in revision: every successful `user_database_init()` path returns `STORED` or `MIGRATED_LEGACY`, so `main.c`'s successful "new empty" log branch is unreachable.
|
||||
- SSH startup is currently gated on successful `web_security` initialization even though SSH uses separate host-key material. **Needs verification:** whether this coupling is intentional recovery policy or an accidental startup dependency.
|
||||
|
||||
- User confirmed the male Adafruit module has the reference pinout/header mapping/footprint. Assigned only schematic U2's `Footprint` field to the existing PCB identifier `Carrier:MAX3243_Reference_Provisional`; PCB, placement, routing, resistors and BOM unchanged. Legacy hidden status metadata remains historical, not current assignment status.
|
||||
- Both validators now require the assignment and clean parity. Fresh PCB validator and schematic ERC pass: zero physical DRC violations, unrouted connections, parity issues and ERC violations. KiCad emitted enum-property assertions but completed successfully. Review exports were not regenerated; no fabrication or hardware qualification is implied. Earlier expected-U2-warning records below are historical.
|
||||
## Items to verify in future work
|
||||
|
||||
## PCB presentation labels — removed and exports refreshed
|
||||
- Confirm task-local Newlib standard-stream behavior if ESP-IDF/Newlib configuration changes; admin SSH command output relies on dispatcher-task stream redirection.
|
||||
- If HTTPD changes from one handler task to concurrent request execution, protect or eliminate the shared 4 KiB typed-response scratch and audit all handler-local single-owner assumptions.
|
||||
|
||||
- Removed PCB title suffix `draft`, revision suffix `DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and title-comment prefix `NOT FOR FABRICATION -` (also in the PDF exporter). Generator no longer emits them. Specific technical cautions, hidden metadata, footprint identifiers, filenames and schematic remain unchanged; this is not fabrication approval.
|
||||
- `remove_draft_markers.py` guards editor locks/concurrent edits, saves with `SaveBoard(..., True)`, and audits the exact parsed-content delta and unchanged project settings. Two separate cleanup runs passed; same-process repeated board access hit a KiCad SWIG binding failure before any source overwrite.
|
||||
- 1:1 placement PDF and SVG/PNG/3D previews regenerated and visually reviewed. U1 preservation audit permits only the exact label delta in addition to prior measured-U1/device-silk changes; original baseline retained. U1/silk regression, two footprint tests and fresh PCB checks pass: 565 segments, 22 vias, zero physical DRC/unrouted, expected U2 parity warning. Existing mechanical/electrical release gates and historical A0 settings-hash limitation remain; no firmware/device tests or fabrication outputs.
|
||||
## Active Task
|
||||
|
||||
## ESP32 mechanical measurements — board and review exports integrated
|
||||
- **Objective:** Finish documentation and target-hardware validation for the current Phase 8D integrated web-administration implementation without weakening broker, recovery, command-dispatch, concurrency, or secret-handling invariants.
|
||||
- **Relevant files:** `src/user_admin_service.{c,h}`, `src/web_session.{c,h}`, `src/web_server.{c,h}`, `src/web_serial_transport.{c,h}`, `src/web_admin_transport.{c,h}`, `src/web_ui.{c,h}`, `src/admin_ssh_console.{c,h}`, `src/wifi_manager.{c,h}`, `src/local_ui_config.{c,h}`, `src/local_ui_console.c`, `src/local_status_ui.{c,h}`, `src/ssh_transport.c`, `src/session_broker.{c,h}`, and Phase 8 documentation.
|
||||
- **Findings:** User web mutations need both database generation and stable user ID, while Wi-Fi edits/save need one exact working-config generation. Revocation is post-commit best effort, with principal currentness authoritative. HTTPD stop/finalizer failures require retained ownership and retry rather than unsafe reuse. Browser stale-conflict handling must clear secrets and reload without replay.
|
||||
- **Decisions made:** Route typed user mutations through `user_admin_service` and serialize them with `admin_command_gate`. Serialize typed display Apply/Save/Load/Defaults/Reset with console display writers through the same gate. Use full-config Wi-Fi compare-and-swap and exact-generation save, expose only `secret_set`, and fail closed on generation conflict/exhaustion. Serialize HTTPS lifecycle with generation-tagged intent and require post-material TLS refresh. Keep service/session controls, network diagnostics, security/danger operations, and unusual hardware/debug commands in the Admin shell rather than adding guided forms.
|
||||
- **Changes completed:** In addition to the browser-session/admin-shell/serial/client foundation, implemented guided user CRUD/roles/entered and one-time generated passwords/authorized Ed25519 and P-256 key add/remove, typed Wi-Fi profile/AP/secret editing and save, typed display aging serialized with console writers, secret clearing/stale reload behavior, centralized post-commit revocation, bounded 512-byte/10-field in-place form parsing, and serialized retry-safe HTTPS teardown/TLS refresh. The final current build footprint is 119,860 bytes linked static RAM and 1,715,753 bytes flash.
|
||||
- **Remaining work:** Run every current Phase 8D target-hardware procedure in `docs/user_administration_tests.md`; none is yet recorded as passed.
|
||||
- **Risks / things to remember:** Measure internal free/minimum/largest blocks and web-admin task stack margin under maximum HTTPS/SSH/serial concurrency. Exercise revocation-hook failure, Wi-Fi/user stale editors, self-revocation/final-admin protection, duplicate-key scope, secret clearing, display validation, failed HTTPD stop, and retryable post-stop finalization. The login limiter is bounded defense-in-depth, not strong distributed attack protection. Do not move browser admin through the broker or auto-release its preserved serial writer lease.
|
||||
|
||||
- User supplied main PCB 56.07 × 28.09 mm excluding 6.89 mm antenna projection; antenna left/right insets 5.41/5.05 mm. Overall length 62.96 mm. Both USB shells 8.87 mm wide, outer-edge offsets 4.16 mm left / 3.66 mm right, mouth faces flush with main PCB bottom.
|
||||
- Preserve 2.54 mm pitch / 25.40 mm row spacing. User accepted approximate 1.365 mm top/bottom pin-centre offset and same left offset; derived right margin 1.325 mm. Library HW678 F.Fab now has main PCB/antenna rectangles and USB mouth spans; no guessed USB depth, RF keepout, courtyard or heights. Footprint regression updated.
|
||||
- Embedded U1 now has the four measured F.Fab shapes and two measurement-limit annotations. `update_u1_graphics.py` adds only owned graphics; pads/nets/existing UUIDs/placement/routing/project settings and existing footprint fields remain unchanged. Old hidden status/description fields are historical, not current measurement evidence. `add_device_silkscreen.py` replaces header guides with carrier/mask-clipped measured outlines and 0.30 mm width-only USB mouth markers. PDF now verifies saved U1 instead of overlaying library geometry; PDF/SVG/PNG/3D exports refreshed.
|
||||
- New `validate_u1_update.py` audits exact pre-update preservation, library geometry, silk idempotence/clipping/mouth positions and stale-geometry rejection; snapshot/report retained in hardware/PCB/validation. Fresh PCB, independent routing, footprint and DB9 CAD checks pass: 565 segments, 22 vias, no physical DRC/unrouted, one expected U2 parity warning. Historical A0 preservation script fails its old `serial-carrier.kicad_prl` hash gate; not weakened or rebaselined. New A1-specific preservation passes.
|
||||
- At unchanged A1 placement USB faces sit 2.295 mm inside carrier bottom edge; plug clearance and physical 1:1 fit remain pending. Antenna geometry is not RF guidance; no keepout, USB depth, courtyard or height was invented. No firmware/assets changes, rerouting, hardware tests or fabrication outputs.
|
||||
### Handoff template
|
||||
|
||||
## Carrier PCB A1 — revised placement and CAD integration
|
||||
|
||||
- `hardware/PCB/serial-carrier.kicad_pcb`: routed 82 × 80 mm two-layer draft; 36 × 19 mm top-left notch, ESP32 shifted down for bottom USB access, RS-232 bare PCB front flush with carrier top. Measured ESP32 planar body/antenna and USB mouths are integrated; full connector/assembly envelopes and RF qualification remain pending.
|
||||
- Adafruit STEP metal geometry extracted and validated; provenance/dimensions in `docs/devel/hardware/PCB/db9-mechanical-notes.md`. Flange front sits 1.930 mm inside carrier edge, screw-lock mouths 2.870 mm outside, shell nose 3.870 mm outside. Case wall plane is not yet fixed. Seven board-local Dwgs.User projections added; rerun `add_db9_envelope.py` and `add_device_silkscreen.py` after moving modules.
|
||||
- Placement PDF and SVG/PNG/3D previews refreshed. Fresh PCB, independent routing, preservation and CAD validators pass: 38/38 connections, 565 segments, 22 vias, 0 physical DRC violations/unrouted, one expected provisional U2 footprint parity warning. Not fabrication-ready; no hardware qualification or Gerbers. Existing project/schematic/library work preserved; board saves must use `SaveBoard(..., True)` and require checking editor lock. Prior A0 preserved in validation backup.
|
||||
|
||||
## Dual-stack diagnostics follow-up — implemented, device validation pending
|
||||
|
||||
- User explicitly requested IPv6 ping/traceroute/nslookup on all surfaces after accepting Phase12. Shared `network_console.*` now supports one `-4`/`-6` anywhere after command, preserves count/hop bounds, root and `wifi` aliases. Default probes try IPv4 then IPv6 on resolution failure only; nslookup performs A+AAAA queries separately (one selected address/family), displays unscoped link-local records. Link-local probes require ESP-local `%sta`/`%ap`/index/name; no client-interface guess or IPv4-mapped fallback. Help/completion updated; permissions unchanged.
|
||||
- Actual IDF5.5.0 audit: raw ICMPv6 includes outer IPv6 header, automatic TX checksum, common `IPPROTO_IP/IP_TTL` hoplimit; replies validate checksums/target/id/seq and reject extensions/fragments. Traceroute one socket/max30 probes/1s receive deadline, 1280B receive buffer. Review caught sub-ms timeout flooring to infinite SDK wait; <1000us now returns timeout, actual SDK conversion/mailbox tests verify. No dependency/config/partition/serial path change.
|
||||
- Ping retains4200B PSRAM queue, max20 probes and SDK transient task; permanent callback context plus pending reservation protects delayed END after count*2+2 second console deadline. SDK unrelated-ICMP receive loop may outlive timeout; later pings fail busy until END, which does not prove synchronous resource destruction. Read exact limits in command reference/tests README. IPv6-only unicast DNS still requires a configured resolver; no RDNSS/DHCPv6 provisioning added.
|
||||
- `pio run` PASS **94,452 B linked RAM / 1,857,673 B flash**, +8/+3,188 versus accepted prior build. Traceroute receive-wait object frame measured240→1408B (not runtime stack high-water). No new permanent task/socket/queue capacity. Hardware/ changes in working tree belong to other agent and were not read/edited.
|
||||
- Parent PASS: production diagnostics8027 checks+10k malformed packet inputs, seven installedSDK hashes+actualtimeout fixtures, 432 SSH/WEB root+alias routing cases/UART parity/ordinary-role and revocation guards/completion, existing admin policy and browser Wi-Fi secret fixture. Independent review fixes verified no remaining actionable findings in scoped paths. ASan/UBSan host runtimes absent. Broader console-boundary pre-existing missing rebootfake remains (not changed). No upload/device/network test; Phase12 acceptance remains valid only for its recorded baseline, not this follow-up.
|
||||
|
||||
## 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](../devel/roadmap.md#phase-12--advanced-network-integration). No unreported DHCP capture, DNS-zone update, exhaustive address/service transition, fault/soak or byte-integrity pass is implied.
|
||||
- User validated IPv6 ping/HTTPS and Avahi AAAA; Fedora authselect dual-family mDNS resolved the normal hostname lookup issue. Fresh-boot and full-mix captures show healthy reported service lifecycle. At 230400 baud 8N1 RTS/CTS: two SSH sessions with IPv6 ULA peers (user observer/admin console), two serial WebSockets (writer/observer), active browser admin, USB observer; four broker clients, one writer, zero pending/event counts at capture.
|
||||
- Memory table preserved in roadmap. Fresh internal8/DMA/PSRAM free: 65,880 / 58,124 / 8,196,732 B. Full-mix free: 35,408 / 27,652 / 8,111,952 B; lifetime minima: 8,528 / 772 / 8,072,612 B; largest blocks: 22,528 / 22,528 / 7,995,392 B. Capability pools overlap; minima are conservative per-region lifetime sums, not simultaneous reserves. DMA772 remains a watch item, not proof of OOM or an acceptance blocker. SSH stack20,480B minimum-free18,476→16,284B.
|
||||
- SSH two successful handshakes, no auth/handshake/timeouts/I/O failures; RX68 accepted65 rejected3, TX161,209B, broker revocations2. Web serial RX27B accepted, TX318,230B across782 frames; send/queue/protocol failures0. Browser admin RX106/TX3,721B with no reported send/queue failures. Do not reinterpret nonzero rejected bytes/writer denials as proven transport loss or claim zero UART/observer drops without their counters.
|
||||
- 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](../devel/roadmap.md#phase-12--advanced-network-integration).
|
||||
|
||||
## Phase 10 COMPLETE — explicit user acceptance, 2026-09-18
|
||||
|
||||
- User confirmed after firmware upload implementation and the concise-UI fix: “That works perfectly. And the usual operation is also verified.” Acceptance establishes that upload works and normal operation is verified. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or recovery passes. The roadmap's compact regression guidance is reusable, not an acceptance blocker.
|
||||
- Latest concise-UI change was copy-only; reported UI regression **169 groups PASS**. No rebuild after that text change. The integration build below is historical, not validation of a newly rebuilt UI or this documentation update.
|
||||
|
||||
- User authorized simple OTA implementation; initial Git clean. New web_firmware_update module with standard SDK APIs, raw POST /api/firmware, admin cookie/Origin/CSRF and final principal currentness. Settings → HTTPS / Reboot has File/XHR upload progress, confirmation, session fencing and no automatic retries. Only inactive app/otadata writes; NVS/layout untouched. No signatures/antirollback/automaticrollback, dependencies or generated assets changed. Consolidated procedure, contract and regression guidance: [Phase 10](../devel/roadmap.md#phase-10--simple-admin-web-firmware-upload); standalone guide removed.
|
||||
- 4KiB internal buffer + transient2048B-stack reboot owner allocated before erase; 10s stall/120s receive-loop budget, not totalflashdeadline. HTTPD synchronously blocks other web work during upload; networkserial maystall/drop, reboot disruptsall. No task/request/socket lifetime capture after handler. Service/identity reservation and atomic ordinary-reboot gate cover UI/UART0/SSH/browser/localbutton paths. Failed response after bootselect schedules noautomaticreset; selected latch rejects further uploads409, manual reboot available. Successful response schedules500ms reboot retaining reservations.
|
||||
- Review fixed two actualSDK5.5.0 edge cases: failed esp_ota_begin maypublishlivehandle beforeeraseerror (abortthat handle); rawContentLength64 canwrapHTTPDsize_t32 (overflow-safe actualslotbound/strictdecimal/equality check beforebody/erase). End consumes handle evenerror. SDKvalidation followed by exactparsedimage length includingSHA; basic header requiresS3appdescriptor/hash. Unrelated old/privateSDK code unpatched.
|
||||
- Parent final pio PASS **94,220 B RAM / 1,847,645 B flash**, +24RAM/+19,080flash vsPhase9, not runtimeheadroom. Parent newbackend88cases+actualSDKbeginfailurecontract, UI169groups+CSP, serverlifecycle44, admin25, consolelifecycle, SSHruntime, cookielifecyclePASS. Additionalbase/admin/display/lifecyclecookie, idle18, SSHmanagement/runtime/security agentPASS after adding missing rebootfake to adminfixture (no productionchange). Independent review final noactionablefindings; realbuilt firmware parsed with SDKmetadata bothOTAoffsets (notdeviceflashproof).
|
||||
- **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:** 8A–C target validated; explicit 8D.22 user acceptance on 2026-09-13. Completion/telemetry and legacy compatibility now live in [roadmap acceptance](../devel/roadmap.md#phase8-acceptance-evidence) and [storage compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility). Separate acceptance/legacy history documents removed; active [web contracts](../devel/web_administration.md) and [regression procedures](../devel/user_administration_tests.md) retained.
|
||||
- Post-acceptance baseline includes PSRAM-only ping payload/user snapshot allocations and refined web quick panels. Prior focused host/geometry/build checks passed; those are not additional hardware claims. Preserve lazy-allocation failure isolation and unchanged serial hot path.
|
||||
|
||||
## Evidence limits and follow-ups
|
||||
|
||||
- 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.
|
||||
- **Objective:**
|
||||
- **Relevant files:**
|
||||
- **Findings:**
|
||||
- **Decisions made:**
|
||||
- **Changes completed:**
|
||||
- **Remaining work:**
|
||||
- **Risks / things to remember:**
|
||||
|
||||
@@ -2,18 +2,6 @@
|
||||
|
||||
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](../devel/web_administration.md#serial-and-display).
|
||||
|
||||
## Typed Network edits preserve manager ownership and current secret bytes
|
||||
|
||||
**Decision:** HTTPD uses zero-wait secret-free projections and an ID-only dispatcher. Wi-Fi compare/merge/whole-candidate validation and queue-before-publication preserve omitted secrets and reject stale changes; its manager alone owns radio/reannouncement. mDNS generation/persistence is independent. Save stabilizes selected RAM; Wi-Fi Load is stored-only, not fallback-secret generation.
|
||||
|
||||
**Consequence:** Keep SSIDs reversible bytes, omitted/Replace/disabled-STA Clear distinct, AP clear denied, and Next profile separate from editor selection. mDNS may report RAM applied but reannouncement not queued, without rollback. A one-second queued-secret timer is not hard cancellation; `accepted` is not online/DNS. Recovery and no automatic replay are correctness requirements. [Network contract](../devel/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.
|
||||
@@ -32,9 +20,7 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence for future changes:** Do not replace fan-out with a blocking shared queue. Any added transport must tolerate partial/no-progress reads and expose drop/backpressure counters.
|
||||
|
||||
**Throughput observation and controlled experiments:** The initial diagnostic baseline used CPU160MHz; a CPU240MHz-only experiment reduced but did not eliminate browser queue overflow. Combining binary WebSocket header/payload into one bounded session-override send eliminated reported drops, and the user signed off230400-baud full-client-mix operation after returning to160MHz. Retain the combined send, not the frequency increase; evidence and limits are in `current-state.md`. Preserve scheduling/priorities and 4096/512-byte broker/web buffers while gathering per-client HWM/drop attribution and independent opt-in web binary-TX timing. Fixed-slot epoch/generation-fenced aggregates avoid stale attribution; no new runtime allocations. Clear preserves queued data and seeds broker HWM; disconnected rows disappear while global discard counts remain. Callback timestamps precede the transport lock; synchronous send return is not peer receipt. Completion-to-read intervals include broker/mutex/control work and possible idle, even when the first read is nonempty; never label them pure scheduling latency or proof of backlog at completion. Compare enabled/disabled target captures before drawing overhead conclusions. Contracts and reproduction: [throughput diagnostics](../devel/web_throughput_diagnostics.md).
|
||||
|
||||
**Relevant files:** `src/session_broker.c`, `src/session_broker.h`, `docs/devel/roadmap.md`
|
||||
**Relevant files:** `src/session_broker.c`, `src/session_broker.h`, `docs/roadmap.md`
|
||||
|
||||
## Physical UART ownership and logical writer ownership remain separate
|
||||
|
||||
@@ -48,25 +34,23 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
## 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.
|
||||
**Decision:** Broker clients, SSH/WebSocket slots, queued admin work, user principals/accounts, Wi-Fi working configuration, and HTTPS lifecycle intent carry domain-specific generations or random stable IDs to reject stale references, slot reuse, and lost updates.
|
||||
|
||||
**Consequence:** Never turn selected transport IDs into arbitrary broker IDs/fds, rebase a stale confirmation or wrap a published token. Exhausted broker/SSH slots retire; operation/reservation IDs do not reuse; saturated service/lease generations fence ABA without disabling canonical recovery. Reboot invalidates old logins. Validate/reserve at the owner immediately before effects, not snapshot-check/unlock/unconditional mutation. [Broker](../devel/web_administration.md#broker-and-contextual-controls) and [service](../devel/web_administration.md#service-lifecycle-and-identity-rotation) contracts.
|
||||
**Rationale/evidence:** Broker IDs encode slot generation; transports track slot generations; admin tokens include session/slot generation; user principal currentness includes account ID and authentication generation; user/Wi-Fi web editors carry optimistic generations; HTTPS snapshots expose lifecycle generation.
|
||||
|
||||
## Confirmed writer transfer compares a lease version inside the broker lock
|
||||
**Consequence for future changes:** Preserve transport-slot, account-authentication/database, Wi-Fi working-config, and HTTPS lifecycle generations as distinct concepts. Validate tokens immediately before side effects and discard late work after disconnect/reuse/revocation. Existing-account web mutations must also compare stable user ID so deletion/recreation of the same username cannot retarget stale work.
|
||||
|
||||
**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](../devel/web_administration.md#broker-and-contextual-controls).
|
||||
**Relevant files:** `src/session_broker.{h,c}`, `src/ssh_transport.c`, `src/web_serial_transport.c`, `src/admin_ssh_console.c`, `src/user_database.{h,c}`, `src/user_admin_service.{h,c}`, `src/wifi_manager.{h,c}`, `src/web_server.{h,c}`
|
||||
|
||||
## 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.
|
||||
**Decision:** UART0 remains independent of UART1 and networking. Initial administrator bootstrap and explicit unavailable-user-database recovery are restricted to UART0.
|
||||
|
||||
**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.
|
||||
**Consequence for future changes:** Network failures or credential corruption must not remove UART0 recovery. Do not expose bootstrap/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/devel/roadmap.md`
|
||||
**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
|
||||
|
||||
@@ -90,37 +74,93 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
## 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.
|
||||
**Decision:** Admin SSH `exit`, remote reboot, SSH stop/disconnect, and host-key rotate/reset are deferred until command state and administration/transport application buffers appear drained, with a ten-second limit and short final delay.
|
||||
|
||||
**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.
|
||||
**Rationale/evidence:** `admin_ssh_console` has a separate bounded control task and pending-action state. The check is a best-effort application-buffer heuristic, not peer-delivery confirmation. User account mutations and their immediate revocation calls do not use this path.
|
||||
|
||||
**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 for future changes:** Actions that would invalidate their own SSH transport should integrate with deferred control when acknowledgement preservation matters. Prevent new input while an action is pending, keep the wait bounded, and do not describe it as guaranteed delivery.
|
||||
|
||||
**Consequence:** Explicitly communicate partial effects, trusted UART0 fingerprint verification and fresh HTTPS login after restart. Preserve CLI recovery/reset but do not add browser Reset/export as if it were ordinary rotation. Native USB is independent UART1, not administration or uninterrupted reboot. [Lifecycle/identity contract](../devel/web_administration.md#service-lifecycle-and-identity-rotation).
|
||||
**Relevant files:** `src/admin_ssh_console.c`, `src/system_console.c`, `src/ssh_console.c`, `src/ssh_transport.c`
|
||||
|
||||
## 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.
|
||||
**Decision:** Network sessions retain secret-free copied principals. Account mutations invalidate generations/IDs; after commit, the command layer requests best-effort targeted transport revocation, while ongoing currentness checks are authoritative.
|
||||
|
||||
**Consequence:** Shared remote-console slots require transport-qualified tokens and immutable owner adapters. Validate owner currentness outside console locks, then recheck identity. Owner-side HTTPD/SSH IO and generation-safe cleanup remain mandatory; session liveness checks do not cancel executing handlers. Browser-shell permissions are parsed and narrower than typed Settings. [Authentication](../devel/web_administration.md#authentication-and-admission), [console policy](../devel/web_administration.md#browser-shell-policy).
|
||||
**Rationale/evidence:** `user_database` issues principals without secrets; web/SSH check currentness during admission and active sessions. Mutating console paths call transport revocation hooks.
|
||||
|
||||
## Typed serial mutations share the administration dispatcher
|
||||
**Consequence for future changes:** Do not retain pointers to database records or treat login as permanently authoritative. New authenticated sessions/transports must revalidate at admission, before sensitive input, and periodically or on relevant events. Database mutation APIs alone do not perform transport notification, and notification failure must not roll back an already committed mutation.
|
||||
|
||||
**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.
|
||||
**Relevant files:** `src/user_database.{h,c}`, `src/user_console.c`, `src/web_server.c`, `src/web_serial_transport.c`, `src/ssh_transport.c`
|
||||
|
||||
**Consequence:** No automatic mutation replay, including after navigation, timeout or logout. Ordinary deadlines limit dequeue admission, not execution. Accounts/Network queued-secret timers wipe only non-executing inputs; locals wipe after admitted work returns. Explicit RAM/NVS/reset semantics and partial-effect uncertainty must match each canonical owner. [API bounds and lifetime](../devel/web_administration.md#typed-settings-api-and-operation-lifetime).
|
||||
## Browser authentication uses bounded explicit sessions
|
||||
|
||||
## Typed account selection is checked inside the database mutation lock
|
||||
**Decision:** Browser access uses a same-origin login/logout flow and a fixed RAM session table rather than HTTP Basic. Raw opaque tokens exist only in host-only secure cookies; firmware storage retains token digests, copied principals, monotonic expiry, and generation-safe slot identity. State-changing requests require a session-bound CSRF token and exact Origin validation.
|
||||
|
||||
**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.
|
||||
**Rationale/evidence:** Explicit logout and account switching cannot reliably invalidate browser-managed HTTP Basic credentials. Exact browser-session references also allow logout of one session without revoking another session for the same account.
|
||||
|
||||
**Consequence:** Separate generated-value delivery from mutation and retain no retrieval history; context-bound saved acknowledgement is UX, not receipt proof. Self-revocation can deny results, so 401/disconnect cannot mean success or cancellation. Key slots are stable and sparse, fingerprint-only on output; import shares canonical validation. Final-admin invariants and UART0 provisioning/recovery remain. [Accounts contract](../devel/web_administration.md#accounts-and-authorized-keys).
|
||||
**Consequence for future changes:** Preserve digest-only storage, the two-per-account/eight-global capacity, exact-session WebSocket binding, and current-principal checks. Do not expose tokens, CSRF values, ticket values, or internal references in logs/snapshots. New mutation endpoints must use the body-backed in-place URL-form parser's 512-byte/10-unique-field bounds, parse closed schemas, and revalidate the exact admin session immediately before typed side effects. Browser failure/close paths must clear entered and generated secret material rather than replaying it after a reload.
|
||||
|
||||
## Browser authentication has a narrow version-pinned HTTPD boundary
|
||||
**Relevant files:** `src/web_session.{h,c}`, `src/web_server.c`, `src/web_serial_transport.c`, `src/web_admin_transport.c`
|
||||
|
||||
**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.
|
||||
## Browser admin shell is separate from browser serial
|
||||
|
||||
**Consequence:** Re-audit SDK assumptions on upgrade; never patch around Origin `null` by weakening same-origin policy. Browser authentication POST uses CORS mode with fixed same-origin URLs/credentials because no-referrer non-CORS POST can serialize Origin as null. Digest-only cookie/challenge sessions replace Basic without fallback or live-record eviction. CSP loader hashes and authored scripts change atomically. Navigation preserves terminals/lease, while session-identity changes require a clean document before showing retained buffers. [Authentication and terminal contracts](../devel/web_administration.md#authentication-and-admission).
|
||||
**Decision:** An administrator may keep the browser serial WebSocket alive while independently opening one admin-only WebSocket frontend for the canonical command dispatcher. Terminal switching changes only browser visibility and focus; the admin route never becomes a broker client.
|
||||
|
||||
**Rationale/evidence:** This preserves a browser-held writer lease while giving full canonical administrative command parity without a generic HTTP command endpoint.
|
||||
|
||||
**Consequence for future changes:** Do not multiplex admin command bytes into `/ws/serial`, and do not close or release the serial route as a side effect of mode switching, settings navigation, or popover display. HTTPD remains the owner of WebSocket send/close calls; the web-admin task only queues bounded work. Self-affecting HTTPS commands must use deferred drain control.
|
||||
|
||||
**Relevant files:** `src/web_admin_transport.{h,c}`, `src/admin_ssh_console.{h,c}`, `src/web_ui.c`, `src/web_server.c`
|
||||
|
||||
## Browser writer transfer is atomic and generation-safe
|
||||
|
||||
**Decision:** Guided writer assignment compares the expected current writer and validates the exact generation-safe target under the broker mutex before making one atomic ownership change.
|
||||
|
||||
**Rationale/evidence:** A browser dialog can become stale while open. Unconditional force assignment could overwrite a newer legitimate lease or release ownership when its target disconnected.
|
||||
|
||||
**Consequence for future changes:** Use `session_broker_compare_exchange_writer()` for stale UI/API transfers. Opening or hovering a writer control must never mutate ownership, and target/current conflicts must leave the current lease unchanged.
|
||||
|
||||
**Relevant files:** `src/session_broker.{h,c}`, `src/web_server.c`, `src/web_ui.c`
|
||||
|
||||
## User mutations have one serialized typed boundary
|
||||
|
||||
**Decision:** `user_admin_service` owns typed account/password/role/key mutations for console and web callers. Its recursive `admin_command_gate` critical region includes the optimistic snapshot check and database commit; a committed mutation is followed by independent best-effort web and SSH revocation attempts.
|
||||
|
||||
**Rationale/evidence:** Browser requests can race one another and canonical shell commands. Database generation plus stable user ID reject stale editors and username delete/recreate races, while the shared gate prevents caller-specific check-then-mutate interleaving. Revocation cannot be made atomic with the NVS commit, so principal currentness remains authoritative.
|
||||
|
||||
**Consequence for future changes:** Route new ordinary user mutations through this service instead of calling `user_database` directly. Do not roll back or report a committed mutation as failed solely because a transport notification failed. Preserve final-admin checks, remote self-generated-password restrictions, secret wiping, and generation/user-ID conflict reporting.
|
||||
|
||||
**Relevant files:** `src/user_admin_service.{h,c}`, `src/user_database.{h,c}`, `src/user_console.c`, `src/admin_command_gate.{h,c}`, `src/web_server.c`, `src/ssh_transport.c`
|
||||
|
||||
## Display configuration writers share the administration gate
|
||||
|
||||
**Decision:** Typed browser display Apply/Save/Load/Defaults/Reset operations and console display-writer commands serialize through the recursive `admin_command_gate` for the complete RAM and persistence operation.
|
||||
|
||||
**Rationale/evidence:** Browser handlers and canonical console frontends can mutate the same local-UI working configuration and NVS record concurrently. Serializing only individual lower-level calls could allow interleaved apply/save/load/reset sequences and inconsistent final state.
|
||||
|
||||
**Consequence for future changes:** Keep all new display configuration writers under the same gate, including any read-modify-write and rollback sequence. Do not hold the gate for read-only status or rendering work, and do not conflate it with the display framebuffer/I2C mutex.
|
||||
|
||||
**Relevant files:** `src/web_server.c`, `src/local_ui_console.c`, `src/local_ui_config.{h,c}`, `src/local_status_ui.{h,c}`, `src/admin_command_gate.{h,c}`
|
||||
|
||||
## Guided Wi-Fi editing uses exact-generation compare-and-swap
|
||||
|
||||
**Decision:** Browser Wi-Fi reads return configuration metadata plus `secret_set` flags, never PSKs. Each typed edit applies a complete validated working-config copy only if its expected nonzero generation remains current, and Save persists exactly the expected generation under the same writer serialization.
|
||||
|
||||
**Rationale/evidence:** Multiple browser editors, console changes, and lifecycle controls can update RAM configuration concurrently. A conventional read/modify/write or copy-then-save sequence could overwrite a newer secret or persist a generation the user never reviewed.
|
||||
|
||||
**Consequence for future changes:** Keep credential-bearing copies tightly scoped and wiped. Add config writers under the writer mutex and advance generation without wraparound; mismatch or exhaustion must fail closed. Stale browser forms must reload without replay and clear entered secrets.
|
||||
|
||||
**Relevant files:** `src/wifi_manager.{h,c}`, `src/wifi_config.{h,c}`, `src/wifi_console.c`, `src/web_server.c`, `src/web_ui.c`
|
||||
|
||||
## HTTPS lifecycle and post-material refresh are serialized
|
||||
|
||||
**Decision:** HTTPS start, stop, and TLS refresh share a lifecycle mutex and generation-tagged desired-running intent. Certificate/material replacement always proceeds to a TLS refresh. Teardown disables new admin-transport HTTPD calls, tracks calls already in progress, retains a server whose stop failed, and keeps incomplete post-stop finalization pending for retry before another start.
|
||||
|
||||
**Rationale/evidence:** Browser-shell commands can tear down their own transport while console commands race a restart or replace persisted TLS material. HTTPD-owned queued work must finish before its handle or the admin transport's static state can be reused.
|
||||
|
||||
**Consequence for future changes:** Do not start a second server around a retained handle, bypass lifecycle serialization, or make post-material refresh optional after persistence. A newer explicit lifecycle intent must win over an older refresh. Finalize timed-out detach state only after HTTPD destruction, and retry a failed finalizer before attaching a replacement server.
|
||||
|
||||
**Relevant files:** `src/web_server.{h,c}`, `src/web_console.c`, `src/web_admin_transport.{h,c}`, `src/admin_ssh_console.{h,c}`
|
||||
|
||||
## Security material and configuration use bounded, versioned NVS records
|
||||
|
||||
@@ -128,29 +168,29 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Rationale/evidence:** Serial, Wi-Fi, local UI, web security, users, and SSH security each validate schema/size and own their namespace. User/security mutations build and validate candidate state before committing it; security modules avoid silently replacing an established identity. The live user database remains internal while its 5,360-byte candidate is a persistent PSRAM-preferred allocation with internal fallback and is wiped after every transaction.
|
||||
|
||||
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Legacy credential synchronization and reconciliation are removed. Missing user storage commits empty; valid user v1 bytes remain compatible, with private `v1_admin_marker` derived from admin count, not a public bootstrap contract. HTTPS v1 (1,392 bytes) migrates through a private validated reader to TLS-only v2 (1,340 bytes), preserving exact DER/fingerprint/generation and committing before publication. Failures fail closed without fallback regeneration or overwriting rejected records. See [legacy compatibility](../devel/roadmap.md#phase8-legacy-credential-compatibility).
|
||||
**Consequence for future changes:** Add schema versions and transactional candidate validation. Do not overwrite unknown records automatically; provide explicit migration/reset behavior. Preserve the distinct persistence contracts: explicit save/load/default/reset for working configuration and per-blob commit-before-live-install for user and identity mutation. Keep candidate ownership mutex-local and wipe/free it on initialization or recovery failure. Recheck external-buffer staging in the flash/NVS implementation when upgrading from the pinned ESP-IDF 5.5 baseline. Pre-bootstrap legacy credential rotation spans `web_sec/material` and `user_db/database`, is not cross-namespace atomic, and relies on boot reconciliation after interruption.
|
||||
|
||||
**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.
|
||||
**Decision:** The current firmware stores Wi-Fi credentials, recovery 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.
|
||||
**Consequence for future changes:** Do not claim resistance to flash extraction. 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.
|
||||
**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 while its component handlers withdraw/re-enable the STA interface, and treats failure as nonfatal.
|
||||
|
||||
**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.
|
||||
**Relevant files:** `src/wifi_manager.{h,c}`, `src/wifi_config.{h,c}`, `src/mdns_service.{h,c}`, `src/mdns_config.{h,c}`
|
||||
|
||||
## Optional local UI cannot become a core dependency
|
||||
|
||||
@@ -180,7 +220,7 @@ Only constraints supported by implementation or current project documentation be
|
||||
|
||||
**Consequence for future changes:** Do not remove these definitions as a performance cleanup. Any re-enablement needs target-hardware concurrency testing with simultaneous USB, WebSocket, SSH, and serial traffic plus watchdog/stack telemetry.
|
||||
|
||||
**Relevant files:** `CMakeLists.txt`, `src/CMakeLists.txt`, `docs/devel/roadmap.md`, relevant `sdkconfig.defaults` crypto settings
|
||||
**Relevant files:** `CMakeLists.txt`, `src/CMakeLists.txt`, `docs/roadmap.md`, relevant `sdkconfig.defaults` crypto settings
|
||||
|
||||
## Embedded web assets are checked-in generated artifacts
|
||||
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
# Command reference
|
||||
|
||||
UART0 and authenticated `admin` SSH sessions use the same registered command implementations through one serialized dispatcher. Admin SSH exposes the full operational registry, including interactive prompts, network diagnostics, reboot, and HTTPS/SSH material mutation. Create the first administrator through normal `user add <username> admin` on physical UART0; explicit recovery of an unavailable database is UART0-only. Admin SSH also rejects generating a replacement password for its own account so the one-time value cannot be lost when the session is revoked. Run `help` for root commands and `<group> help` for a group summary. For serial, Wi-Fi, mDNS, and display settings, use the documented save/reset commands to persist working changes. Do not assume account or certificate/key changes are RAM-only.
|
||||
|
||||
## Choosing an administration connection
|
||||
|
||||
Keep physical UART0 available for recovery when changing network access, accounts, or service identities. Browser Admin uses a restricted command set, separate from the permissions of the Settings dialogs. It supports `reboot`, `web stop`, exact `web certificate rotate --force`, and `exit`. These actions briefly wait for queued output before disconnecting; this is not proof that your browser received the result, and pending input is discarded.
|
||||
|
||||
Certificate replacement is saved before HTTPS restarts. A restart failure does not restore the old identity. Verify the fingerprint through UART0 `web certificate info`; recover with UART0/admin SSH `web stop` / `web start`, then trust the new certificate and sign in again. HTTPS-only actions leave SSH, native USB, and UART0 independent. Reboot affects every transport and loses unsaved RAM settings.
|
||||
|
||||
Browser `web` allows only status/stop/exact forced certificate rotation; `mdns` allows status only. Browser `wifi` supports the canonical commands, including bare `wifi` status, settings, hidden secret prompts and explicit `wifi ap show-secret`. Secret input is not echoed, retained in history or completed. Wi-Fi changes can disconnect the browser before a result arrives, without deferred drain or cancellation; reconnect and inspect before retrying, or recover through UART0. Browser `user` allows status/list/show and interactive add/password plus forced role/delete for **other accounts**, not self/generated/key/recovery commands. Restricted SSH stop/disconnect/reset/host-key mutation remains unavailable in the browser shell. The Accounts, Network, and SSH Settings dialogs offer additional actions; an action available in Settings is not necessarily available in the browser shell. See the [browser user guide](web_administration.md).
|
||||
UART0, authenticated `admin` SSH sessions, and the browser Admin shell use the same registered command implementations through one serialized dispatcher. The remote shells expose the operational registry, including interactive prompts, recovery-secret display, network diagnostics, reboot, and HTTPS/SSH material mutation. Initial administrator bootstrap and explicit recovery of an unavailable user database remain physically bound to UART0. A remote administrator cannot generate 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.
|
||||
|
||||
## System
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `memory` | Show free memory, minimum free memory, and largest blocks for internal RAM, DMA-capable RAM, and PSRAM. |
|
||||
| `reboot` | Drain console output briefly and restart the ESP32; refused while a firmware upload or another reboot owns exclusion. |
|
||||
| `exit` | Close the current administrative SSH or browser session after its acknowledgement drains; unavailable on UART0. Browser `exit` leaves serial connected. Ctrl+D on an empty administrative command line does the same. |
|
||||
|
||||
Firmware upload is an admin **Settings → HTTPS / Reboot** action, not a shell command. Reboot requests are refused while an upload is active. If the new image was selected for boot but the upload response failed, no automatic restart is scheduled. The selected image remains, and further upload attempts return 409 until reset; a deliberate manual `reboot` is permitted once the upload has released the service. Inspect first; a lost response is not cancellation. See the [browser user guide](web_administration.md) for firmware upload and recovery. Do not erase for an ordinary update; native USB is UART1 access, not recovery administration.
|
||||
| `reboot` | Drain console output briefly and restart the ESP32. |
|
||||
| `exit` | Close the current administrative SSH session after its acknowledgement drains; unavailable on UART0. Ctrl+D on an empty admin SSH command line does the same. |
|
||||
|
||||
## Role-based users
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `user status` / `user list` | Show database generation, capacity, administrator count, and all secret-free account summaries. |
|
||||
| `user status` / `user list` | Show database generation, capacity, administrator/bootstrap state, and all secret-free account summaries. |
|
||||
| `user show <username>` | Show one account's role, ID, authentication generation, and SSH-key fingerprints. |
|
||||
| `user bootstrap` | Set and confirm the `admin` password without echo, then promote the migrated account to `admin`. |
|
||||
| `user bootstrap --generate` | Bootstrap `admin` with a generated 24-character password displayed once. |
|
||||
| `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 delete <username> --force` | Delete an account; the pre-bootstrap migrated `admin` and final administrator are 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. |
|
||||
@@ -36,13 +28,15 @@ Firmware upload is an admin **Settings → HTTPS / Reboot** action, not a shell
|
||||
| `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. |
|
||||
| `user recover --force` | When normal user-database initialization failed, explicitly replace its blob from the current legacy network credential. |
|
||||
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; supported key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
Usernames must match `[a-z][a-z0-9_-]{0,15}`. Passwords contain 12–64 printable ASCII characters. The fixed database supports eight users and three SSH keys per user; initial key types are `ssh-ed25519` and `ecdsa-sha2-nistp256`. A key may be assigned to multiple accounts but cannot be duplicated within one account. Password verifiers, salts, raw key blobs, and passwords are absent from ordinary status output. `Ctrl-C` cancels a password or key prompt, and generated passwords are shown once.
|
||||
|
||||
A device with no stored user database starts with an empty database. On UART0 run `user add <username> admin`, optionally with `--generate`, to create the first administrator. There is no default shared login. Existing valid user databases retain their accounts and roles; no account is silently promoted.
|
||||
On the first Phase 8A boot, the old shared `admin` credential is imported as a role-`user` account, not silently granted administrator rights. Run `user bootstrap` from physical UART0 to establish the administrator. Phase 8B now authenticates HTTPS and SSH passwords through this database and enables stored SSH public keys. Before bootstrap, `web credentials rotate --force` and `web reset --force` synchronize the migrated verifier; after bootstrap, that legacy credential is recovery-only and does not authenticate or alter role-based users.
|
||||
|
||||
`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.
|
||||
`user recover --force` is a destructive physical recovery operation and succeeds only while the database is unavailable. It replaces the user blob with one role-`user` account derived from the current legacy credential; run `user bootstrap` afterward. It does not erase unrelated NVS data. Successful password, role, key, bootstrap, and delete operations invalidate only that username's outstanding WebSocket tickets and active WebSocket/SSH sessions; unrelated users remain connected.
|
||||
|
||||
The administrator-only Settings dialog uses `/api/admin/users` for guided account create/list/edit/delete, role changes, entered or one-time generated passwords, and authorized Ed25519/P-256 key add/remove. Mutations include the displayed database generation and stable user ID, so a stale editor is rejected and reloaded instead of targeting a deleted/recreated account. CLI and browser mutations share `user_admin_service`; it serializes mutations with `admin_command_gate`, commits first, and then requests best-effort web and SSH revocation. Transport currentness checks remain authoritative if notification is incomplete. Entered keys/passwords and generated-password output are cleared from the dialog on close or failure.
|
||||
|
||||
## Local display
|
||||
|
||||
@@ -52,11 +46,9 @@ A device with no stored user database starts with an empty database. On UART0 ru
|
||||
| `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. |
|
||||
| `display defaults` / `display reset` | Apply 300/600-second defaults in RAM, or apply and persist 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. With the local UI task running and the I²C bus initialized, one new button press requests a bounded reprobe and is consumed without navigating. If `debug display status` reports `bus=unavailable`, correct the wiring with power off and reboot instead.
|
||||
|
||||
Display settings require a running local UI task, not an attached panel. Concurrent console/browser changes can report busy or reject an outdated browser edit. Save persists the device's working settings, not unsent browser drafts. Load selects defaults when saved storage is absent or incompatible without rewriting it. If Reset cannot save defaults, the working settings remain unchanged. A browser timeout or navigation does not cancel an accepted action; use **Check Result** and **Refresh** before retrying.
|
||||
When both transitions are enabled, `off-seconds` must be greater than `dim-seconds`. Applying settings counts as local UI activity. The administrator-only Settings dialog exposes the same typed Apply/Save/Load/Defaults/Reset behavior through `/api/admin/display`; all five operations are serialized with console display writers through `admin_command_gate`, and invalid aging combinations are rejected without applying them. 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.
|
||||
|
||||
## Serial service
|
||||
|
||||
@@ -76,7 +68,7 @@ Defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control,
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `broker status` / `broker clients` | Show broker state or connected clients. |
|
||||
| `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 counters` / `broker clear-counters` | Show or clear broker counters. |
|
||||
| `broker connect <name>` / `broker disconnect <client-id>` | Create or remove a console test client. |
|
||||
| `broker request-writer <client-id>` / `broker release-writer <client-id>` | Request or relinquish the single writer lease. |
|
||||
| `broker force-writer <client-id|none>` | Administratively assign or clear the writer lease. |
|
||||
@@ -84,7 +76,7 @@ Defaults are 115200 baud, 8 data bits, no parity, one stop bit, no flow control,
|
||||
| `broker read <client-id> [maximum-bytes]` | Read queued serial output for a client. |
|
||||
| `broker events <client-id>` | Show ownership and connection events for a client. |
|
||||
|
||||
Each client has a generation-safe ID. There can be one writer and multiple observers; a slow observer loses only its own queued output. HWM is bounded by 4,096 bytes; read counts transport handoff, not peer receipt. Disconnected client rows disappear, while global totals retain traffic and unread-output discards until cleared. Capture counters before disconnect. `broker read` consumes queued data and must not be used as a throughput diagnostic probe. See [Web throughput diagnostics](devel/web_throughput_diagnostics.md).
|
||||
Each client has a generation-safe ID. There can be one writer and multiple observers; a slow observer loses only its own queued output.
|
||||
|
||||
## Native USB CDC-ACM
|
||||
|
||||
@@ -112,35 +104,13 @@ Opening `/dev/ttyACM*` with DTR asserted creates the `usb-cdc` broker client, st
|
||||
| `wifi ap secret` / `wifi ap show-secret` | Set or reveal the AP password. |
|
||||
| `wifi save|load|defaults|reset` | Persist, restore, reset in RAM, or reset and persist configuration. |
|
||||
| `wifi counters|clear-counters` | Show or clear Wi-Fi counters. |
|
||||
| `wifi ping [-4\|-6] <host> [count]` | IPv4/IPv6 ICMP echo; 1–20 probes, default 4. |
|
||||
| `wifi nslookup [-4\|-6] <host>` | Explicit A/AAAA lookup; default queries both families. |
|
||||
| `wifi traceroute [-4\|-6] <host> [max-hops]` | IPv4/IPv6 ICMP echo traceroute; 1–30 hops, default 16. |
|
||||
| `wifi ping <host> [count]` | Send 1–20 IPv4 or IPv6 ICMP probes. |
|
||||
| `wifi nslookup <host>` | Resolve and display unique IPv4/IPv6 addresses. |
|
||||
| `wifi traceroute <host> [max-hops]` | Run IPv4 ICMP traceroute with up to 30 hops. |
|
||||
|
||||
`ping`, `nslookup`, and `traceroute` are root aliases. The four station-profile slots use lower priority values first. 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.
|
||||
`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.
|
||||
|
||||
### Dual-stack network diagnostics
|
||||
|
||||
Root `ping`, `traceroute` and `nslookup`, and their `wifi` aliases, share one implementation on **UART0, admin SSH and the browser Admin console**. They execute on the ESP32, not the client computer or the attached UART1 equipment. Ordinary serial-role users do not gain administration access. `-4` and `-6` completion is available for all six command prefixes.
|
||||
|
||||
```text
|
||||
ping -6 example.com 4
|
||||
ping example.com 4 -4
|
||||
wifi ping -6 fe80::1%sta 3
|
||||
traceroute -6 example.com 16
|
||||
wifi traceroute -4 example.com 16
|
||||
nslookup example.com
|
||||
nslookup -6 peer.local
|
||||
```
|
||||
|
||||
One optional family selector can appear before or after the host/count; duplicate/conflicting selectors and unknown options are rejected. Probe counts/hop limits are positional, not Linux `-c`/`-m` options. Without a selector, numeric addresses retain their family and probe hostnames try IPv4 resolution, then IPv6 if no address was obtained. A failed probe does **not** retry the other family; this is not Happy Eyeballs. Explicit `-6` never silently uses IPv4.
|
||||
|
||||
Link-local **probe destinations** require a scope on the ESP32: `%sta`, `%ap`, an existing numeric lwIP interface index, or an existing lwIP interface name. These are not the browser/SSH computer's interface names. If name resolution returns an unscoped link-local address, `nslookup` displays it, but ping/traceroute request an explicit scoped literal rather than guessing an interface. Numeric `nslookup` input is displayed without DNS/reverse lookup.
|
||||
|
||||
`nslookup` without a selector performs separate A and AAAA queries. The installed resolver returns one selected address per family (`CONFIG_LWIP_DNS_MAX_HOST_IP=1`), not a complete DNS RRset. Missing records, DNS timeouts and server failures can share the same SDK error; partial success is displayed rather than hidden. Unicast names require a usable configured DNS server; SLAAC alone does not provision one in this firmware. `.local` uses the existing lwIP mDNS query support. No DNS server or RA/DHCP policy is changed by these commands.
|
||||
|
||||
Traceroute sends one eight-byte echo probe per hop and waits up to approximately one second for a reply. Hop addresses are numeric. IPv6 extension headers/fragments in replies or quoted packets are unsupported and ignored. Filtering/rate limiting can produce `*` even when ordinary connectivity works. DNS, scheduler and send latency are outside the receive deadline.
|
||||
|
||||
Ping waits up to `count * 2 + 2` seconds in the console after starting. After a console timeout, further pings may be refused until the previous probe session finishes. IPv6 replies do not include a received TTL/hop-limit value in the output. Network diagnostics occupy the shared administration dispatcher while executing, so other administrative commands wait; serial traffic uses a separate path.
|
||||
The administrator-only Settings dialog uses `/api/admin/wifi-config` for typed station-profile, AP policy/SSID/channel, and write-only secret edits. Reads return only `secret_set` flags. Every mutation compares the exact working-configuration generation, and Save persists only that same generation; stale or exhausted generations fail closed without applying or saving another editor's state. On a conflict, the browser clears entered secrets and reloads current values without replaying the request.
|
||||
|
||||
## mDNS
|
||||
|
||||
@@ -151,31 +121,29 @@ Ping waits up to `count * 2 + 2` seconds in the console after starting. After a
|
||||
| `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. On-device diagnostics support explicit family selection and scoped IPv6 as described above; Linux client syntax and interface scope are separate. mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access. Discovery is best-effort and may not update immediately after network or service changes.
|
||||
When the Wi-Fi station receives an IPv4 address, the Wi-Fi manager announces `sak-<suffix>.local`. 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 while online causes a best-effort reannouncement; mDNS failures do not stop Wi-Fi, UART0, UART1, or native USB access.
|
||||
|
||||
## HTTPS web terminal
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `web` / `web help` | Show web-service command usage. |
|
||||
| `web status` | Show HTTPS and WebSocket state. |
|
||||
| `web status` | Show HTTPS, browser-session, serial-WebSocket, and admin-WebSocket state. |
|
||||
| `web start` / `web stop` | Start or stop HTTPS service. |
|
||||
| `web counters` / `web clear-counters` | Show or clear ordinary HTTPS/serial WebSocket counters, independently of performance capture. |
|
||||
| `web diagnostics enable\|disable\|show\|clear` | Independent opt-in admission tracing and post-TLS occupancy; not serial throughput timing. |
|
||||
| `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 counters` / `web clear-counters` | Show or clear web counters. |
|
||||
| `web credentials show` | Display the legacy migration/recovery credential on UART0 or an authenticated remote admin shell; it is not a role-based network login. |
|
||||
| `web credentials rotate --force` | Replace the legacy recovery credential and synchronize the migrated pre-bootstrap account only. |
|
||||
| `web certificate info` | Display certificate identity and fingerprint. |
|
||||
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. Browser admin defers commit and HTTPS restart; both browser routes close and new certificate trust/relogin is required. UART0/admin SSH behavior is unchanged. |
|
||||
| `web reset --force` | Explicitly replace HTTPS certificate/private key only, including missing, incompatible or damaged material; never changes users. |
|
||||
| `web certificate rotate --force` | Replace the HTTPS certificate and private key. |
|
||||
| `web reset --force` | Explicitly replace missing, incompatible, or damaged legacy credentials and web material. |
|
||||
|
||||
Use UART0 for quiet throughput captures (authenticated admin SSH also exposes these commands). Performance output includes current-epoch pending age and count/sum/estimated-average/max timings for queue entry to callback entry, synchronous send, and completion to drain return. These are not peer acknowledgements or scheduler-only measurements; nonempty intervals may include idle. No secrets or serial payloads are printed. See [Web throughput diagnostics](devel/web_throughput_diagnostics.md) for exact semantics, saturation limits and the reset/burst/drain/disable/capture-before-disconnect recipe.
|
||||
HTTPS listens on port 443 only. Sign in with any current user-database username/password through the same-origin login page; explicit logout permits account switching without relying on a browser HTTP-authentication cache. The device stores at most eight opaque eight-hour browser sessions, with at most two retained per account, and sends the raw token only in a host-only secure cookie. State-changing requests require strict Origin and session-bound CSRF validation.
|
||||
|
||||
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.
|
||||
Both roles receive the offline browser serial terminal. Its one-time ticket and active WebSocket are bound to the exact browser session and obey the broker's one-writer rule. The combined **Connect serial**/**Disconnect serial** control closes only the serial WebSocket and pauses automatic reconnect when active. Account mutations revoke that account's browser sessions, serial/admin tickets, and WebSockets without disturbing unrelated accounts.
|
||||
|
||||
### Older firmware compatibility
|
||||
An administrator additionally receives a **Serial terminal**/**Admin shell** selector, typed Serial controls, guided user/password/role/authorized-key management, generation-safe Wi-Fi profile/AP/secret editing and saving, display-aging controls, and contextual Serial, Wi-Fi, broker-client, and writer-transfer popovers. The browser admin shell uses the same bounded editor, history, completion, prompts, serialized dispatcher, and registered command handlers as admin SSH and UART0; it never joins the serial broker. Switching terminal modes only changes visibility and focus: it does not close the serial WebSocket or release its writer lease. Writer transfer requires an explicit confirmation and atomically checks both the expected current writer and generation-safe target ID. Service/session controls beyond the guided serial/Wi-Fi actions, network diagnostics, security/danger operations, and unusual hardware/debug commands remain shell-only. Initial `user bootstrap` and `user recover --force` remain physical-UART0-only.
|
||||
|
||||
HTTPS storage upgrades preserve the certificate identity, but older firmware that understands only the v1 storage format cannot read the newer v2 format. Logical replacement of stored credentials or keys is not secure flash erasure. A factory erase is not required for the storage upgrade. See [storage compatibility notes](devel/roadmap.md#phase8-legacy-credential-compatibility) before downgrading firmware.
|
||||
Typed mutation endpoints accept only body-backed URL-encoded forms bounded to 512 bytes and 10 unique fields; duplicate, oversized, malformed, stale-session, wrong-origin, and wrong-CSRF requests fail without a side effect. HTTPS start/stop and TLS refresh are serialized and carry a lifecycle generation. Certificate rotation and full material reset always trigger a post-material TLS refresh; a newer explicit lifecycle request takes precedence. Teardown disables transport-owned HTTPD calls, tracks any already in progress, retains an HTTPD handle after stop failure, and retries pending post-stop admin-transport finalization before a later start.
|
||||
|
||||
## SSH serial transport
|
||||
|
||||
@@ -191,13 +159,13 @@ HTTPS storage upgrades preserve the certificate identity, but older firmware tha
|
||||
| `ssh host-key rotate --force` | Replace the persistent SSH host key. |
|
||||
| `ssh reset --force` | Explicitly replace invalid or missing SSH material. |
|
||||
|
||||
SSH listens on port 22 and accepts user-database passwords plus stored `ssh-ed25519` and `ecdsa-sha2-nistp256` public keys. wolfSSH verifies key possession after the database authorizes the username/key pair; unsigned key probes do not complete authentication. A `user` receives the broker-backed UART1 serial stream and automatically requests writer ownership on connection. If denied, the session remains an observer and its input is discarded. Releasing another writer does not automatically promote it: deliberately assign control through administration or reconnect when the lease is free. An `admin` receives the administration shell instead, does not become a broker client, and cannot acquire a UART1 writer lease.
|
||||
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, admin SSH, and browser Admin share one command dispatcher. A long-running command or interactive prompt can therefore delay commands from another administrator.
|
||||
UART0, admin SSH, and the browser admin shell submit to one bounded queue, and one dispatcher task is the sole caller of `esp_console_run()`. Consequently, remote commands execute the canonical UART0 handlers rather than using separate command implementations. Each remote frontend has generation-safe session identity, exact authorization checks, bounded editor/history/output state, and transport-owned network I/O; only the SSH transport task accesses wolfSSH and only HTTPD performs browser WebSocket sends/closes.
|
||||
|
||||
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.
|
||||
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` is UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||
`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. Web recovery credentials/certificates, Wi-Fi secrets, and interactive user passwords/keys are available to authenticated administrators and must therefore be treated as remotely accessible administrative material. `user bootstrap` and `user recover --force` remain UART0-only. A connected administrator also cannot generate its own replacement password remotely, preventing the one-time password from being lost during self-revocation. SSH does not provide `exec`, SFTP, SCP, forwarding, or subsystems.
|
||||
|
||||
## Hardware diagnostics
|
||||
|
||||
@@ -223,4 +191,4 @@ UART0 and admin SSH use shared whole-line Tab completion. A unique/common prefix
|
||||
| `debug buttons status` | Show the current active-low state of previous/back GPIO10, select/confirm GPIO13, and next GPIO14. |
|
||||
| `debug buttons test [seconds]` | Run the bounded button event test for 1–30 seconds; the default is 10 seconds. |
|
||||
|
||||
Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. The OLED must be powered from 3.3 V because module I²C pull-ups may connect to `VCC`; verify that all external pull-ups also terminate at 3.3 V. Display diagnostics probe the standard SSD1315-compatible 7-bit `0x3c`/`0x3d` addresses. The currently tested module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78`/`0x79`; an explicit `scan --force` is available only for the dedicated local-UI bus. Diagnostics initially run at 100 kHz and treat an absent display as nonfatal. All RS-232 `debug` commands, including `debug status`, require `serial stop` to release the port. If diagnostic cleanup reports a port fault, reboot before continuing. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed.
|
||||
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.
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Developer documentation
|
||||
|
||||
This directory collects implementation contracts, research, design decisions,
|
||||
regression procedures, and historical acceptance evidence. It is not the operator
|
||||
manual: start with the [project README](../../README.md) for user documentation,
|
||||
the [command reference](../command_reference.md) for commands, and the
|
||||
[security operations checklist](../security_operations.md) for safe operation.
|
||||
[Wiring](../wiring.md) and [electrical tests](../electrical_tests.md) remain beside
|
||||
the user documentation because hardware safety and bring-up need that context.
|
||||
|
||||
## Firmware contracts and evidence
|
||||
|
||||
- [Flash partition layout](flash_layout.md): storage allocation, reserved partitions, and update boundaries.
|
||||
|
||||
- [Roadmap and acceptance record](roadmap.md): completed phases, retained scope,
|
||||
historical evidence, and limits of acceptance.
|
||||
- [Web administration contracts](web_administration.md): contributor-facing API,
|
||||
ownership, lifecycle, and recovery details.
|
||||
- [User/administration regression procedures](user_administration_tests.md):
|
||||
reusable checks, including historical baselines explicitly marked as superseded.
|
||||
- [HTTPS idle cleanup](https_idle_cleanup.md): HTTPD owner, SDK, and lifecycle contract.
|
||||
- [Web admission diagnostics](web_admission_diagnostics.md): instrumentation and interpretation limits.
|
||||
- [Web throughput diagnostics](web_throughput_diagnostics.md): counters, capture procedures, and evidence limits.
|
||||
- Display design references: [Phase 7 display mockup](phase7_display_mockup.svg)
|
||||
and [Phase 7C icon mockup](phase7c_icon_mockup.svg), not promises of current UI appearance.
|
||||
|
||||
Source code is authoritative for current behavior. A checklist is not a test result;
|
||||
host doubles do not prove target timing, flash durability, power-loss recovery, or
|
||||
physical electrical safety. Preserve dates, revisions, workload details, and explicit
|
||||
limitations when recording new evidence. Overall user acceptance does not establish
|
||||
that every proposed regression procedure was executed.
|
||||
|
||||
Markdown links are document-relative. Unless explicitly stated otherwise, source,
|
||||
build, and test paths in code spans and commands are repository-root-relative.
|
||||
Hardware notes identify retained artifact paths and any different command directory.
|
||||
|
||||
## Tests and asset provenance stay with their artifacts
|
||||
|
||||
Test fixtures, scripts, and contextual READMEs remain under [tests](../../tests/).
|
||||
Examples include the [HTTPD idle suite](../../tests/web_httpd_idle/README.md),
|
||||
[admission diagnostics suite](../../tests/web_diagnostics/README.md),
|
||||
[web administration suite](../../tests/web_admin_transport/README.md), and
|
||||
[browser UI suite](../../tests/web_ui_session/README.md). Read each suite's scope,
|
||||
prerequisites, and execution instructions rather than assuming a single host-test command.
|
||||
These links are navigation, not claims that tests ran during documentation cleanup.
|
||||
|
||||
[Web asset provenance and generation policy](../../web_assets/SOURCES.md) remains
|
||||
with the web assets. Do not hand-edit generated embedded assets or regenerate them
|
||||
as a side effect of documentation changes; preserve upstream licenses and attribution.
|
||||
|
||||
## Hardware research and design notes
|
||||
|
||||
The [contextual PCB README](../../hardware/PCB/README.md) stays with the editable
|
||||
KiCad project, CAD, libraries, exports, and scripts. The board remains a draft,
|
||||
not fabrication-ready. Development notes preserve the hardware-relative substructure:
|
||||
|
||||
- [Adafruit MAX3243 research](hardware/PCB/adafruit-research.md)
|
||||
- [Button selection and evidence](hardware/PCB/button-notes.md)
|
||||
- [DE-9 mechanical research](hardware/PCB/db9-mechanical-notes.md)
|
||||
- [Module footprint notes](hardware/PCB/footprint-notes.md)
|
||||
- [RS-232 reference footprint notes](hardware/PCB/rs232-footprint-notes.md)
|
||||
- [PCB draft design and release gates](hardware/PCB/pcb-draft-notes.md)
|
||||
- [Routing validation and historical evidence](hardware/PCB/validation/pcb-routing-summary.md)
|
||||
|
||||
Original reference/provenance READMEs remain beside their sources:
|
||||
[Adafruit Eagle reference](../../hardware/PCB/reference/README.md) and
|
||||
[DE-9 CAD reference](../../hardware/PCB/reference/db9-cad/README.md).
|
||||
Relocation does not change licenses, validate provisional male-module assumptions,
|
||||
or qualify the board for manufacture. Do not casually rerun destructive generators;
|
||||
use the retained hardware README and each script's documented safeguards.
|
||||
|
||||
## Security maintenance and evidence
|
||||
|
||||
Contributor maintenance includes reviewing relevant ESP-IDF, wolfSSL/wolfSSH, and
|
||||
bundled web-library upstream release/security notes, especially before an update.
|
||||
A relevant issue needs a separate upgrade/mitigation decision and scoped validation;
|
||||
this is not an ongoing CVE audit or a claim that the firmware is vulnerability-free.
|
||||
Dependency patches, crypto upgrades, secure boot, encrypted NVS, and eFuse changes
|
||||
are outside the accepted lightweight operating-checklist scope, not implicit work
|
||||
items or properties of a deployed device. See the [Phase 9 record](roadmap.md#phase-9--security-and-production-hardening).
|
||||
|
||||
The original security checklist's commands and limits were source-reviewed against
|
||||
application sources and the command reference at `f40c09c`. That provenance is not
|
||||
a new build, device test, security audit, or verification of flash/eFuse settings;
|
||||
verify current implementation when behavior changes. Keep the operator checklist
|
||||
focused on actionable use and recovery rather than contributor dependency work.
|
||||
Never put passwords, cookies, tickets, Wi-Fi secrets, private keys, or verifier
|
||||
material in routine logs, documentation captures, or status output.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Flash partition layout
|
||||
|
||||
The N16R8 target has 16 MiB flash and 8 MiB octal PSRAM. PlatformIO uses the custom [`partitions.csv`](../../partitions.csv) layout:
|
||||
|
||||
| Partition | Offset | Size | Purpose |
|
||||
|---|---:|---:|---|
|
||||
| `nvs` | `0x009000` | 512 KiB | Serial, Wi-Fi, mDNS hostname, local-display, role-based user, HTTPS identity, and SSH host-key data |
|
||||
| `otadata` | `0x089000` | 8 KiB | Active OTA-slot selection metadata |
|
||||
| `phy_init` | `0x08B000` | 4 KiB | Optional PHY initialization data |
|
||||
| `nvs_key` | `0x08C000` | 4 KiB | Reserved for future encrypted-NVS keys |
|
||||
| `coredump` | `0x08D000` | 128 KiB | Reserved for flash core dumps |
|
||||
| `ota_0` | `0x0B0000` | 4 MiB | Primary application/OTA slot |
|
||||
| `ota_1` | `0x4B0000` | 4 MiB | Alternate application/OTA slot |
|
||||
| `storage` | `0x8B0000` | 7488 KiB | Reserved; not mounted or used for session recording |
|
||||
|
||||
Application offsets are aligned to the ESP32-S3's required 64 KiB boundary. The final storage partition ends at `0x1000000`, exactly the end of the 16 MiB flash chip.
|
||||
|
||||
Admin HTTPS application upload uses the inactive OTA slot and `otadata`; NVS and other data partitions are untouched by the updater. It does not provide remote downloads, automatic rollback/health confirmation, core-dump handling, NVS encryption or filesystem mounting. Image validation does not guarantee a working application; compatible storage schemas and wired recovery remain the owner's responsibility.
|
||||
|
||||
Partition reservations do not enable their named features. In particular, `nvs_key` does not enable encryption, and the default firmware does not capture core dumps. Existing generated SDK configurations can override `sdkconfig.defaults`; inspect the effective configuration when verifying a build's security properties.
|
||||
|
||||
See [first installation and destructive migration](../getting_started.md#partition-migration-and-erasing) and the [firmware update guide](../firmware_update.md) for operator procedures. Do not change partition offsets as part of an ordinary application update.
|
||||
@@ -1,145 +0,0 @@
|
||||
# Adafruit MAX3243 breakout: authoritative CAD header research
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Research date: 2026-09-20. Scope: source retrieval and pad/geometry extraction only; no project schematic or layout modified.
|
||||
|
||||
## Critical product distinction
|
||||
|
||||
**Product 5988 is the DE-9 female version. The male version is product 6253.** The product-5988 page's male variant link goes to `/product/6253`; each product description explicitly identifies its connector gender. Both link to the same Learn guide. Do not describe the downloaded CAD as verified male-specific CAD: its connector is `X2`, value `DE9-F`, package `F09HP`, and the repository README identifies product 5988.
|
||||
|
||||
The exact header map below is proven for the published 5988 board. It is useful reference for the family, but identical male-board geometry/pad numbering has **not** been established by a male-specific CAD file or physical measurement. Confirm the actual 6253 module before releasing a male carrier footprint. No inference from connector gender or a generic DE-9 footprint is substituted for that check.
|
||||
|
||||
## Provenance and downloaded originals
|
||||
|
||||
Authoritative chain:
|
||||
|
||||
1. [Adafruit Learn downloads](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads) links to the Eagle repository below, a TI datasheet, fab print and 3D models.
|
||||
2. [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB).
|
||||
3. Retrieved `main` commit **`63c6200bb3ef17d491089cfa6441b1810d79bb20`**, committed 2024-08-12T22:42:27Z. All four local originals were downloaded from this immutable revision, not a moving branch.
|
||||
4. [Product 5988, female](https://www.adafruit.com/product/5988), [product 6253, male](https://www.adafruit.com/product/6253), and [Learn pinouts](https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/pinouts) corroborate identity and signal names.
|
||||
|
||||
Local files in `../../../../hardware/PCB/reference`, unchanged upstream contents:
|
||||
|
||||
| File | SHA-256 |
|
||||
|---|---|
|
||||
| `Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd` | `6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870` |
|
||||
| `Adafruit RS-232 Full Pinout Level-Shifter Breakout.sch` | `4efbbe0ad14194e4e4563dd5f7dac007dfd52eba636f7390ffb822f3e81cd896` |
|
||||
| `../../../../hardware/PCB/reference/README.md` | `2825ba741a8ce83abdaf2c7cb9170641473357630ccbee40a0016dcdb5cd80bb` |
|
||||
| `license.txt` | `075dad5e5fc96c27014fabc269f4f5732909cffd178a486f546d982b6cf86b74` |
|
||||
|
||||
Raw URL prefix for each filename (URL-encode spaces):
|
||||
|
||||
`https://raw.githubusercontent.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB/63c6200bb3ef17d491089cfa6441b1810d79bb20/`
|
||||
|
||||
**License:** upstream `license.txt` is **Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0)**. README attribution: “Designed by Limor Fried/Ladyada for Adafruit Industries.” README explicitly requires all its preceding text in redistribution; the full original README and license are therefore retained. Preserve these notices when redistributing CAD and comply with applicable attribution/share-alike obligations for adaptations. The CAD-derived content of this report is attributed to that design and provided under CC BY-SA 3.0.
|
||||
|
||||
The guide also links [5988 3D CAD](https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter). Its directory listing contains `.f3d`, `.step`, `.stl`, and `.jpg`; these were **not downloaded or used for measurements**, and their licensing was not independently assessed. No male-specific 3D model was verified in this research.
|
||||
|
||||
## Exact header numbering and net map
|
||||
|
||||
Source: Eagle XML `drawing/board/elements/element[@name='JP2']`, embedded `microbuilder` package `1X12_ROUND_76MIL`, and `drawing/board/signals/signal/contactref`. Cross-checked against schematic `JP2`, deviceset `HEADER-1X12`, selected device `76MIL`: symbol pin N maps directly to physical pad N.
|
||||
|
||||
**Viewing convention:** component/top view, DE-9 pointing toward the top of the page (+Y), logic header along the bottom. X increases rightward, Y upward. The origin is the lower-left bounding-box corner of the bare PCB (the corner itself is rounded). These are Eagle board coordinates in **millimetres**, not a mating-face view and not a bottom view.
|
||||
|
||||
```text
|
||||
DE-9 / +Y
|
||||
+-----------------------+
|
||||
| |
|
||||
| o o | module mounting holes
|
||||
| |
|
||||
| 1 2 3 ... 12 | JP2, y = 2.667 mm
|
||||
+-----------------------+
|
||||
Vin OFF
|
||||
+X ->
|
||||
```
|
||||
|
||||
| JP2 physical pad | Label / function | CAD net | X (mm) | Y (mm) | MAX3243 IC1 pad | DE-9 X2 pad via level shifter |
|
||||
|---:|---|---|---:|---:|---:|---:|
|
||||
| 1 | Vin | VCC | 1.905 | 2.667 | 26 | — |
|
||||
| 2 | GND | GND | 4.445 | 2.667 | 25 | 5 (direct ground) |
|
||||
| 3 | DCD | DCD | 6.985 | 2.667 | 15 | 1 |
|
||||
| 4 | RX | RXD | 9.525 | 2.667 | 16 | 2 |
|
||||
| 5 | TX | TXD | 12.065 | 2.667 | 13 | 3 |
|
||||
| 6 | DTR | DTR | 14.605 | 2.667 | 14 | 4 |
|
||||
| 7 | DSR | DSR | 17.145 | 2.667 | 17 | 6 |
|
||||
| 8 | RTS | RTS | 19.685 | 2.667 | 12 | 7 |
|
||||
| 9 | CTS | CTS | 22.225 | 2.667 | 19 | 8 |
|
||||
| 10 | RI | RI | 24.765 | 2.667 | 18 | 9 |
|
||||
| 11 | VLD | !INVALID | 27.305 | 2.667 | 21 | — |
|
||||
| 12 | OFF | !FORCEOFF | 29.845 | 2.667 | 22 | — |
|
||||
|
||||
**Left-to-right order: `Vin GND DCD RX TX DTR DSR RTS CTS RI VLD OFF`.** This is physical pad order, not the order of rows in the project's wiring table.
|
||||
|
||||
At the logic header, TX/DTR/RTS are inputs to the MAX3243; RX/DCD/DSR/CTS/RI are outputs from it. Schematic gates explicitly map TX to T2IN, DTR to T1IN, RTS to T3IN, RX to R4OUT, DCD to R5OUT, DSR to R3OUT, CTS to R1OUT and RI to R2OUT. RS-232 and logic signal nets are separated by the chip; matching signal names do not mean direct continuity.
|
||||
|
||||
OFF is **active low** (`!FORCEOFF`, IC1 pad 22) with 10 kΩ pull-up R1 to VCC. The Learn pinout page agrees. The upstream README and shop prose incorrectly say high shuts it down; do not propagate that prose error. VLD is `!INVALID`, high on valid input detection. Use 3.3 V Vin for this project's ESP32 interface, as already required by `docs/wiring.md`.
|
||||
|
||||
## Exact published PCB geometry
|
||||
|
||||
All figures below are nominal CAD dimensions, not measured tolerances.
|
||||
|
||||
### Logic header JP2
|
||||
|
||||
- Single row, **12 plated through-hole pads at 2.54 mm (0.100 inch) pitch**.
|
||||
- Element origin `(15.875, 2.667)`; no element rotation or mirroring (R0).
|
||||
- Package local pad centres: `x = -13.970 + 2.540 × (N - 1)`, `y = 0`, N = 1…12.
|
||||
- Board coordinates: `x = 1.905 + 2.540 × (N - 1)`, `y = 2.667`.
|
||||
- First-to-last centre span **27.940 mm (1.100 inch)**.
|
||||
- All holes: **1.000 mm drill**, explicit **1.9304 mm (76 mil) round copper diameter**. Each pad has `rot="R90"`; that does not rotate the header row or change a round pad's outline.
|
||||
- Both end pad centres are **1.905 mm** from the corresponding vertical board edge.
|
||||
- Row is **2.667 mm (0.105 inch)** above the lower bounding-box edge.
|
||||
- No square pad-1 marker is defined by this package: identify pad 1 using Vin and the stated view, not assumed square copper.
|
||||
|
||||
### Bare-board outline and mounting holes
|
||||
|
||||
Source: `drawing/board/plain/wire[@layer='20']` and mounting-hole element/package definitions.
|
||||
|
||||
- Bare PCB bounding box: **31.750 mm X × 29.337 mm Y** (1.250 × 1.155 inches), X = 0…31.750, Y = 0…29.337.
|
||||
- Straight top edge `(0,29.337)` to `(31.750,29.337)`; square top corners.
|
||||
- Bottom corners: **2.540 mm radius**, defined by 90° arcs `(0,2.540)` → `(2.540,0)` and `(29.210,0)` → `(31.750,2.540)`.
|
||||
- Bottom straight edge: X = 2.540…29.210 at Y = 0.
|
||||
- Module mounting holes: `U$2` at **(3.810, 11.430)** and `U$3` at **(27.940, 11.430)**.
|
||||
- Both use `MOUNTINGHOLE_3.0_PLATEDTHIN`, pad `P$1`, **3.000 mm drill / 3.600 mm copper diameter**. They are plated pads, not anonymous NPTH holes, and neither appears in the board's signal contactrefs.
|
||||
- Hole centre spacing **24.130 mm (0.950 inch)**; hole row is **8.763 mm** above the header row.
|
||||
|
||||
For a carrier footprint using header pad 1 as `(0,0)` with Y still upward:
|
||||
|
||||
- Header pads: `(2.540 × (N - 1), 0)`.
|
||||
- Mounting hole centres: **(1.905, 8.763)** and **(26.035, 8.763)**.
|
||||
- Bare-board bounding box: X = **−1.905…29.845**, Y = **−2.667…26.670**; retain corner arcs described above.
|
||||
|
||||
If the destination CAD uses screen coordinates with Y downward, transform Y deliberately; do not silently mirror physical pad numbering. The placement side and whether the module faces up or down must be fixed before generating a carrier footprint.
|
||||
|
||||
### DE-9 on the downloaded female CAD (not male qualification)
|
||||
|
||||
`X2` origin `(15.875,17.907)`, R0, package `F09HP`, value `DE9-F`.
|
||||
|
||||
| X2 pad | Board X (mm) | Board Y (mm) | CAD net |
|
||||
|---|---:|---:|---|
|
||||
| 1 | 10.3886 | 16.637 | RS_DCD |
|
||||
| 2 | 13.1318 | 16.637 | RS_RXD |
|
||||
| 3 | 15.8750 | 16.637 | RS_TXD |
|
||||
| 4 | 18.6182 | 16.637 | RS_DTR |
|
||||
| 5 | 21.3614 | 16.637 | GND |
|
||||
| 6 | 11.7602 | 19.177 | RS_DSR |
|
||||
| 7 | 14.5034 | 19.177 | RS_RTS |
|
||||
| 8 | 17.2466 | 19.177 | RS_CTS |
|
||||
| 9 | 19.9898 | 19.177 | RS_RI |
|
||||
| G1 | 3.3528 | 17.907 | no signal contactref |
|
||||
| G2 | 28.3972 | 17.907 | no signal contactref |
|
||||
|
||||
Signal holes have 1.016 mm drill and 1.778 mm octagonal copper; shell/mechanical pads G1/G2 have 3.302 mm drill and 5.080 mm copper, spaced 25.0444 mm. Shell pads are not shown tied to GND by signal contactrefs; DE-9 pin 5 is grounded. These are **PCB solder-tail coordinates**, not front-face pin coordinates.
|
||||
|
||||
The connector package's layer-21 drawing reaches local Y = 17.907, hence board Y = 35.814, beyond the board's Y = 29.337. This is library drawing evidence, **not a validated mechanical envelope**. Do not size an enclosure from silkscreen. Product pages list assembled dimensions of 33.6 × 32.0 × 14.1 mm (5988) and 33.7 × 31.8 × 14.2 mm (6253), with no coordinate/tolerance definition; these do not replace a male-specific mechanical drawing, and differ from the female library drawing extent.
|
||||
|
||||
## Validation and next-use boundary
|
||||
|
||||
- Parsed the actual Eagle board and schematic XML successfully using Python's standard-library XML parser.
|
||||
- Joined package pad names to board signal contactrefs, applied JP2's unrotated element translation, and cross-checked schematic header numbering/net assignments.
|
||||
- Extracted outline arcs, mounting-hole package dimensions, and connector pad data directly; no dimensions inferred from product photos.
|
||||
- Recorded SHA-256 hashes of the downloaded originals.
|
||||
- No firmware build or hardware tests performed: this task changed research/reference files only.
|
||||
- **Ready:** exact published 5988 header map, pad centres, board outline and hole coordinates for CAD reference.
|
||||
- **Still required for a male-module carrier release:** establish that the purchased module is 6253, verify its header order and mounting geometry against male-specific CAD or the physical module, and verify connector overhang, mating clearance, module height and mounting orientation. The shared guide alone is insufficient proof of identical male geometry.
|
||||
@@ -1,112 +0,0 @@
|
||||
# Button selection: Omron B3F-1000
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Verified 2026-09-20 against the Omron datasheet and installed KiCad libraries. This research covers three active-low GPIO-to-GND buttons.
|
||||
|
||||
**Integration decision:** the A0 schematic now implements B3F-1000 switches with one **2 kΩ, 1%, metal-film external pull-up (design rating at least 0.125 W)** per input. The lower-current/internal-pull-up-only discussion below records the selection rationale, not an unselected schematic option. Firmware is unchanged. User reports B3F-1000 and resistors purchased. Exact resistor MPN/wattage and measured supply margin remain unspecified.
|
||||
|
||||
## Schematic-ready recommendation
|
||||
|
||||
**B3F-1000 fits the installed footprint mechanically and its normally-open contact topology matches the circuit. Low-current operation with internal pull-ups needs the qualification below.**
|
||||
|
||||
For each of the three buttons:
|
||||
|
||||
- Value / manufacturer part number: **B3F-1000**, Omron; standard silver-plated contacts, ivory flat plunger, no separate ground terminal.
|
||||
- Symbol: **`Switch:SW_Push`** (normally open, two passive pins).
|
||||
- Explicitly assign footprint: **`Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x`**. The installed symbol's default footprint field is empty.
|
||||
- Symbol pin **1 → GPIO**, symbol pin **2 → GND**. The switch is nonpolar; swapping these two nets is electrically equivalent.
|
||||
|
||||
| Function | Pin 1 net | Pin 2 net |
|
||||
|---|---|---|
|
||||
| Previous/back | GPIO10 | GND |
|
||||
| Select/confirm | GPIO13 | GND |
|
||||
| Next | GPIO14 | GND |
|
||||
|
||||
GPIO assignments verified in `src/board_pins.h`. The PCB README specifies normally-open buttons to GND with internal pull-ups. Released = high; pressed = low. No voltage source connects directly through the switch: any external pull-up goes through a resistor to 3.3 V.
|
||||
|
||||
## Exact terminal and pad mapping
|
||||
|
||||
The installed footprint has **four physical holes but only two distinct electrical pad numbers: `1,1,2,2`**, not four uniquely numbered pads. The installed `SW_Push` symbol has pin 1 at the left and pin 2 at the right in its default orientation; pin numbers are hidden by the symbol.
|
||||
|
||||
Unrotated footprint, front/top view; local coordinates in mm, positive Y downward:
|
||||
|
||||
```text
|
||||
6.5 mm
|
||||
KiCad pad 1 (0,0) ----------- KiCad pad 1 (6.5,0)
|
||||
Omron terminal 4 Omron terminal 3
|
||||
NO
|
||||
contact 4.5 mm
|
||||
Omron terminal 2 Omron terminal 1
|
||||
KiCad pad 2 (0,4.5) --------- KiCad pad 2 (6.5,4.5)
|
||||
```
|
||||
|
||||
The horizontal connections above are permanent internal connections; pressing joins the two rows. Omron datasheet **page 4**, upper-left **“Terminal Arrangement/Internal Connections (Top View)”**, explicitly shows terminals **4–3 common** and **2–1 common**. This maps to KiCad's upper pair numbered 1 and lower pair numbered 2 when aligned as shown. **Omron terminal 1 is not KiCad pad 1 in this orientation.** The manufacturer's separate bottom-view numbering sketch must not be mistaken for a top view. Omron states terminal numbers are not marked on the switches. A 180° insertion swaps the two electrical sides without affecting this nonpolar circuit.
|
||||
|
||||
Both pads numbered 1 receive the symbol pin-1 net; both pads numbered 2 receive pin-2's net. Do not attach a four-unique-pin symbol to this footprint without an explicit remapping. Connecting GPIO and GND to two terminals within the same common pair would permanently ground the GPIO.
|
||||
|
||||
The footprint sets `duplicate_pad_numbers_are_jumpers no`; do not assume PCB routing software will treat the internal metal as a routing jumper. Route/check the duplicate-pad nets normally.
|
||||
|
||||
## Manufacturer dimensions and ratings
|
||||
|
||||
Source: Omron B3F datasheet, ordering table p. 1, ratings/operating characteristics p. 3, B3F-1000 drawing p. 4. Dimensions below are mm.
|
||||
|
||||
| Item | Datasheet specification |
|
||||
|---|---|
|
||||
| Body | 6 ±0.2 × 6 ±0.2 |
|
||||
| Overall height above seating plane | 4.3 ±0.2 |
|
||||
| Plunger diameter | 3.5 |
|
||||
| Terminal row spacing | 4.5 ±0.2 |
|
||||
| Formed terminal span at hole-entry region | 6.5 ±0.5 |
|
||||
| Maximum-width dimension shown across formed leads | 7.7 ±0.5 |
|
||||
| Lead projection below seating plane | 3.5 |
|
||||
| Lead thickness / width callouts | 0.3 / 0.7 |
|
||||
| Recommended PCB hole-centre grid | 6.5 ±0.1 × 4.5 ±0.1 |
|
||||
| Recommended PCB holes | Four, diameter 1 ±0.1; reference drawing for PCB thickness 1.6 |
|
||||
| General dimensional tolerance | ±0.4 unless otherwise specified |
|
||||
| Contact form | SPST-NO |
|
||||
| Rated resistive load, standard silver version | **1–50 mA at 3–24 VDC** |
|
||||
| Minimum applicable load | **10 µA at 1 VDC, reference value** |
|
||||
| Initial contact resistance | 100 mΩ maximum |
|
||||
| Bounce | 5 ms maximum |
|
||||
| Operating force | 0.98 ±0.29 N (100 ±30 gf) |
|
||||
| Release force | 0.2 N minimum |
|
||||
| Pretravel | 0.25 +0.2/−0.1 |
|
||||
| Durability for B3F-1000, 0.98 N | 1,000,000 operations minimum |
|
||||
| Protection / washing | IP00 / washing not possible |
|
||||
|
||||
### Installed footprint comparison
|
||||
|
||||
Inspected `/usr/share/kicad/footprints/Button_Switch_THT.pretty/SW_TH_Tactile_Omron_B3F-100x.kicad_mod` (file format version `20260206`, generator version `10.0`) and `/usr/share/kicad/symbols/Switch.kicad_sym`.
|
||||
|
||||
- Hole centres: `(0,0)`, `(6.5,0)`, `(0,4.5)`, `(6.5,4.5)` — match Omron's nominal PCB grid.
|
||||
- All four plated through-hole pads: circular copper diameter **1.7**, drill **1.0**, front/back copper and mask — nominal drill matches Omron's reference hole diameter. Confirm the fabricator's finished-hole tolerance when ordering.
|
||||
- Fab body: `(0.25,-0.75)` to `(6.25,5.25)`, **6 × 6**, matching nominal body dimensions.
|
||||
- Courtyard: `(-1.1,-1.1)` to `(7.6,5.6)`, **8.7 × 6.7**. This is a library placement boundary, not an enclosure or finger-access clearance specification.
|
||||
- Footprint description specifies H4.3 mm. No physical sample fit, enclosure-height check, or independent 3D-model measurement was performed.
|
||||
|
||||
## Low-current qualification / selection decision
|
||||
|
||||
**Do not claim internal-pull-up operation meets the standard part's full rated-load range.** At 3.3 V, pull-up resistance greater than 3.3 kΩ gives less than 1 mA contact current. For illustration only, 45 kΩ would give about 73 µA; that is not a verified resistance specification for this board's GPIOs.
|
||||
|
||||
Omron's 10 µA / 1 V minimum is explicitly a **reference value**, not an unconditional low-current reliability guarantee. B3F-1000 is a reasonable prototype choice, but internal-pull-up-only contact reliability should be qualified in the intended environment and over life.
|
||||
|
||||
The original A0 recommendation was 2.2 kΩ (about 1.5 mA); the purchased-part update uses **one external 2 kΩ 1% pull-up to 3.3 V per button** (nominal 1.65 mA while pressed); verify supply/resistor tolerances and GPIO limits before adopting. This recommendation was subsequently adopted in the A0 carrier schematic; see the integration decision above. If low current is important, the datasheet also lists **B3F-1002-G** with gold contacts in the same p. 4 dimensional group, rated 100 µA–50 mA at 3–24 VDC, but with a different operating force (1.76 ±0.49 N); weak internal pull-ups are not automatically within that rated range either. It has not been selected or stock-checked here.
|
||||
|
||||
Retain firmware debouncing; a mechanical switch is not bounce-free. The device is unsealed (IP00), and the datasheet prohibits washing.
|
||||
|
||||
## Supply evidence
|
||||
|
||||
**Live stock not verified; no supply assurance.** On 2026-09-20 a request to the [DigiKey B3F-1000 product URL](https://www.digikey.com/en/products/detail/omron-electronics-inc-emc-div/B3F-1000/33150) returned HTTP 403 / an anti-bot challenge. No quantity, price, lead time, or lifecycle status could be established from that response. Confirm the exact MPN and available quantity with a distributor before BOM release; datasheet availability is not stock evidence.
|
||||
|
||||
## Evidence and remaining checks
|
||||
|
||||
- [Authoritative Omron datasheet](https://omronfs.omron.com/en_US/ecb/products/pdf/en-b3f.pdf), retrieved 2026-09-20.
|
||||
- [Local PDF](../../../../hardware/PCB/reference/button-omron-b3f.pdf).
|
||||
- [Extracted text](../../../../hardware/PCB/reference/button-omron-b3f.txt) for searching; use the PDF for drawing interpretation.
|
||||
- [Rendered page 4](../../../../hardware/PCB/reference/button-omron-b3f-page4.png), visually checked for the internal connections and dimensions.
|
||||
|
||||
Before manufacturing: validate the selected pull-up strategy against supply tolerances/budget; check ERC/net assignment and PCB DRC in the actual design; confirm finished holes, enclosure/actuator access and a sample's fit/continuity; confirm procurement. This review verifies datasheet-to-library compatibility, not a completed-board electrical or mechanical test.
|
||||
@@ -1,138 +0,0 @@
|
||||
# DE-9 mechanical reference — pinned Adafruit 5988 STEP
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Research: 2026-09-21. **Nominal CAD geometry, not a toleranced connector drawing or physical fit qualification.** No board, schematic, footprint, application dependency or main README changes.
|
||||
|
||||
**User-confirmed assumption:** male and female modules have identical external dimensions; use the external geometry below for mechanical planning of the male module on that basis. This is not independent male-CAD verification and **does not verify the male pin map**. The source explicitly identifies `DE9-F` / `F09HP` (5988). Do not infer contact numbering from gender, the model, or the front view.
|
||||
|
||||
## Sources, attribution and licensing
|
||||
|
||||
- User's link: https://github.com/adafruit/Adafruit_CAD_Parts/tree/main/5988%20RS-232%20Level%20Shifter
|
||||
- Pinned repository revision: **`5b8a21fb5e2e48478faf8901077be812f5224066`**, 2024-09-05, Noe Ruiz, “Adding 5988 RS-232 Level Shifter”. GitHub path history returned this as the latest change to that directory at retrieval.
|
||||
- Immutable directory: https://github.com/adafruit/Adafruit_CAD_Parts/tree/5b8a21fb5e2e48478faf8901077be812f5224066/5988%20RS-232%20Level%20Shifter
|
||||
- Unmodified originals retained in `../../../../hardware/PCB/reference/db9-cad`: STEP, upstream repository-root `../../../../hardware/PCB/reference/db9-cad/README.md`, upstream repository-root `../../../../hardware/PCB/reference/db9-cad/LICENSE`. `../../../../hardware/PCB/reference/db9-cad/sources.json` records **full immutable raw URLs and SHA-256 hashes**.
|
||||
|
||||
| Original | SHA-256 |
|
||||
|---|---|
|
||||
| `../../../../hardware/PCB/reference/db9-cad/5988 RS-232 Level Shifter.step` | `8eab9dc591a985fc987ceec2d7534e5e259263c73bebc64c0c3cd847e135906f` |
|
||||
| `../../../../hardware/PCB/reference/db9-cad/LICENSE` | `f60436644e066301d4c2e8f1f0674a157b1f60e4fd5586717b99dde255cc18cd` |
|
||||
| `../../../../hardware/PCB/reference/db9-cad/README.md` | `9ed9beac7600623b7b9bd0d02e1fdcd86610ce22966723179e6c6b618e399dce` |
|
||||
|
||||
The CAD repository root license is **MIT, Copyright (c) 2016 Adafruit Industries**; root README merely describes STEP/Fusion/STL parts. No part-specific license file is present in the four-file directory. Retain the original MIT notice and warranty disclaimer with redistribution. This assessment follows the repository's stated license, not an independent audit of any imported vendor geometry. F3D, STL and JPG were listed but not downloaded or used; no image-derived dimensions or mesh fallback were needed.
|
||||
|
||||
The separate Eagle board reference is from `Adafruit-RS-232-Level-Shifter-Breakout-PCB`, commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`; its local `.brd` hash was rechecked (`6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`). Eagle-derived registration/hole information is attributed to **Limor Fried/Ladyada for Adafruit Industries**, under **CC BY-SA 3.0 Unported**; retain `../../../../hardware/PCB/reference/README.md` and `../../../../hardware/PCB/reference/license.txt`. The Eagle-derived portions of these notes use that license. Do not silently substitute the CAD-parts repository's MIT license for the Eagle design license.
|
||||
|
||||
## Extraction method and confidence
|
||||
|
||||
Used Python/OpenCascade (`cadquery-ocp==7.9.3.1.1`) in the isolated project `.cache/cad-tools/venv`; no application dependencies changed. The initially attempted older OCP wheel was unavailable for this Python; the pinned installed version succeeded.
|
||||
|
||||
- `../../../../hardware/PCB/reference/db9-cad/extract.py`: STEPControl import with assembly placements applied; `BRepBndLib.AddOptimal` with triangulation and shape-tolerance enlargement disabled; analytic planes/cylinders via `BRepAdaptor_Surface`. No tessellation used for measurement.
|
||||
- `../../../../hardware/PCB/reference/db9-cad/geometry.json`: all 15 solids and their faces, bounds, validity, analytic cylinder radii/axes, plane normals. All 15 pass `BRepCheck_Analyzer`.
|
||||
- `../../../../hardware/PCB/reference/db9-cad/validate.py`: checks original hashes, all 12 header centres against actual Eagle XML, board bounds, mounting holes and selected front-face geometry; writes `../../../../hardware/PCB/reference/db9-cad/measurements.json` containing explicit face selections and their bounds.
|
||||
- For unique PCB/connector bodies, STEP `CLOSED_SHELL` face order is associated with import face order, checking face counts, every surface type and every cylinder radius. Appearance assignments corroborate feature identification; **colour is not a material certification**.
|
||||
- Imported solid 12 = STEP `#101` / `Body2`, the main connector. It fuses housing/insert, contacts, flange, shell and front hex posts; its whole-solid bounds are **not metal-only dimensions**. Solids 13/14 = `#102`/`#103` (`Body3`/`Body4`), separate anchor brackets.
|
||||
- Front shell/flange/hex-post faces have the shared `Opaque(245,245,246)` appearance and the expected geometric continuity. The report measures those **face subsets**, excluding plastic, contacts, PCB and ICs. There is no separate hollow metal shell solid from which to certify wall thickness.
|
||||
|
||||
**High confidence:** numerical nominal positions/bounds/radii of identified modeled features and STEP-to-Eagle board registration. **Moderate confidence:** physical fidelity of the simplified connector model and material boundaries; the contact/tail geometry differs from Eagle. **Unverified:** manufacturing tolerances, actual purchased-part fit, threads, male electrical mapping, cable/backshell envelope and final enclosure cutout. Extra decimal places below support reproducibility, not manufacturing accuracy.
|
||||
|
||||
## Coordinate system and orientation
|
||||
|
||||
All dimensions are **mm**. Use `(X,Y,Z)` in the imported STEP assembly:
|
||||
|
||||
- X rightward in component/top view; Y toward the connector/cable, Z above the PCB.
|
||||
- Bare breakout PCB: **X 0…31.750, Y 0…29.337, Z 0…1.570**. PCB front edge is **Y = 29.337**; underside is Z = 0, component-side surface Z = 1.570.
|
||||
- Logic header hole row: **Y = 2.667**, X = `1.905 + 2.540 × (N−1)`, N = 1…12. No installed header/socket body is modeled; this is the hole-axis datum, not a plastic-header edge.
|
||||
- STEP XY is already Eagle board XY: **identity transform**, no scale, rotation or translation. Verified by all 12 header holes, both mounting holes and PCB bounds. STEP connector symmetry alone would not establish this registration.
|
||||
- Existing carrier footprint coordinates (header pad 1 origin, Y downward): **`x = X − 1.905; y = 2.667 − Y`**. Height above module PCB top: **`h = Z − 1.570`**. Add the chosen module-top elevation later to get case/carrier height; socket/standoff stack is not yet selected.
|
||||
- Connector front/mating approach is along Y, **parallel to the PCB**, not down into it. A cable approaches from +Y and inserts toward −Y. A front view looks along −Y with +Z up; +X then appears to the viewer's **left**, unlike component/top view. Do not reuse top-view left/right pin numbering as front-view numbering.
|
||||
|
||||
## Metal feature dimensions — selected subsets, not assembly bounds
|
||||
|
||||
The Z centre of the D profile and front screw-lock axes is **7.84762024**, or **6.27762024 above the PCB top**. Connector centre X = **15.875** (carrier-local x = **13.970**).
|
||||
|
||||
| Feature | X bounds | Y bounds | Z bounds | Nominal dimensions / evidence |
|
||||
|---|---|---|---|---|
|
||||
| Front metal flange, exposed faces | 0.475…31.275 | 27.007…27.407 | 1.572620…14.122620 | **30.800 wide × 12.550 high × 0.400 axial thickness**; solid 12 faces 8, 11–14 |
|
||||
| D-shell straight outer wall region | 8.035981…23.714019 | 28.007…32.607 | 3.897620…11.797620 | **15.678038 wide × 7.900 high**, straight axial length **4.600**; faces 18–25 |
|
||||
| D-shell outer surface including root blend and rounded front lip | 7.435981…24.314019 | 27.407…33.207 | 3.297620…12.397620 | **16.878038 wide × 9.100 high × 5.800 axial extent**; faces 18–25, 81–88, 103–110 |
|
||||
| Left front hex screw-lock post | 0.719189…6.030811 | 27.407…32.207 | 5.547620…10.147620 | **4.600 across flats**, **5.311622 across corners**, **4.800 long**; faces 53–61, 89 |
|
||||
| Right front hex screw-lock post | 25.719189…31.030811 | 27.407…32.207 | 5.547620…10.147620 | Same dimensions; faces 62–70, 90 |
|
||||
|
||||
The flange is centred laterally on the PCB, inset **0.475** from each side. Its lower edge is only **0.002620** above the modeled PCB top, upper edge **12.552620** above it. Preserve this tiny source offset in registration; do not interpret it as a designed assembly clearance.
|
||||
|
||||
The D-shell has sloping side planes at **10° from vertical** in the front cross-section (`|normal X| = cos 10°`, `|normal Z| = sin 10°`), corner-cylinder radii **1.700**, root/lip rounding radius **0.600**. The wider side is upper (+Z). The maximum 16.878038 × 9.100 envelope occurs at the **root blend**, not uniformly along the projecting nose. These are bounding dimensions of a D shape, not a rectangular cutout prescription. Shell wall thickness / actual hollow cavity geometry is **not verified** by this fused representation.
|
||||
|
||||
### Front cable screw-lock holes: actual modeled cylinders, not thread specifications
|
||||
|
||||
- Axes **parallel to Y**, at `(X,Z) = (3.375,7.84762024)` and `(28.375,7.84762024)`.
|
||||
- **Axis spacing = 25.000**, symmetric ±12.500 about X = 15.875.
|
||||
- Carrier-local axis x = **1.470 and 26.470**; post front plane y = **−29.540**.
|
||||
- Solid 12 faces **60 / 69**, raw STEP faces **#13822 / #13831**, are inward-facing cylindrical hole surfaces: **radius 1.500, diameter 3.000**, Y = **27.407…32.007** (straight cylindrical length **4.600**).
|
||||
- Mouth cones (faces **89 / 90**) run Y = **32.007…32.207**, growing to **diameter 3.400** at the front: **0.200 axial × 0.200 radial** chamfer. Front mouth plane **Y = 32.207**.
|
||||
- Flat modeled bottoms at Y = **27.407** (faces 61/70): total modeled mouth-to-bottom depth **4.800** including chamfer.
|
||||
- **No helical threads modeled. Do not call this M3, 4-40, a tap drill, minor/major thread diameter, or usable engagement depth.** Actual screw standard, pitch, engagement and tolerances require a connector drawing or physical check.
|
||||
|
||||
## Front planes, PCB overhang and header offsets
|
||||
|
||||
There is no single interchangeable “front plane”. The flange, hex-post mouth and shell lip differ. The shell's foremost geometric plane is a useful **mating-face reference**, but the source does not establish a manufacturer's functional fully-mated datum or panel seating specification.
|
||||
|
||||
| Reference plane | STEP/Eagle Y | Forward of PCB front edge Y=29.337 | Forward of header row Y=2.667 | Carrier-local y |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Flange rear face | 27.007 | −2.330 | 24.340 | −24.340 |
|
||||
| Flange front face / shell root | **27.407** | **−1.930** | **24.740** | **−24.740** |
|
||||
| Straight shell begins | 28.007 | −1.330 | 25.340 | −25.340 |
|
||||
| Breakout PCB front edge | **29.337** | 0 | **26.670** | **−26.670** |
|
||||
| Front hex-post mouth faces | **32.207** | **+2.870** | **29.540** | **−29.540** |
|
||||
| Straight shell ends / rounded lip begins | 32.607 | +3.270 | 29.940 | −29.940 |
|
||||
| Foremost shell lip / modeled insert face | **33.207** | **+3.870** | **30.540** | **−30.540** |
|
||||
|
||||
Thus the modeled shell nose projects **3.870 beyond the breakout PCB**; the hex posts project **2.870**, ending **1.000 behind the shell nose**. The flange itself is **1.930 behind the PCB front edge**. Do not position a case wall at the flange while ignoring that the PCB extends 1.930 beyond it.
|
||||
|
||||
For later unrotated, component-up carrier placement with header pad 1 at `(x0,y0)`:
|
||||
|
||||
- Breakout front edge: `y0 − 26.670`.
|
||||
- Flange front: `y0 − 24.740`.
|
||||
- Hex mouths: `y0 − 29.540`.
|
||||
- Shell nose: `y0 − 30.540`.
|
||||
- If the **breakout PCB front edge** is aligned with carrier top edge `ytop`, choose `y0 = ytop + 26.670`; shell nose is then at `ytop − 3.870`, hex mouths at `ytop − 2.870`.
|
||||
- If instead the **shell nose** is aligned with that edge, choose `y0 = ytop + 30.540`; breakout front edge is then 3.870 inward. These are different choices, not a recommended final layout.
|
||||
|
||||
## Three distinct mechanical systems — do not interchange
|
||||
|
||||
| System | Axes / coordinates | Spacing | Diameter or geometry | Purpose |
|
||||
|---|---|---:|---|---|
|
||||
| Front cable screw-lock holes | Y axes; X 3.375 / 28.375, Z 7.847620 | **25.000** | Modeled bore Ø3.000, mouth Ø3.400; no thread qualification | Retain mating cable at connector front |
|
||||
| Breakout PCB mounting holes | Z axes; `(3.810,11.430)` / `(27.940,11.430)` | **24.130** | Eagle drill Ø3.000 / plated copper Ø3.600; STEP hole cylinders Ø3.000 | Mount module to carrier/standoffs |
|
||||
| Connector shell/anchor PCB holes G1/G2 | Z axes; Eagle `(3.3528,17.907)` / `(28.3972,17.907)` | **25.0444** | Eagle drill Ø3.302 / copper Ø5.080; STEP rounds X to 3.353 / 28.397 | Receive connector anchor legs, **not** carrier mounting or cable screws |
|
||||
|
||||
### Separate metal anchor brackets and underside clearance
|
||||
|
||||
Solids **13/14** represent the bent anchor brackets/legs, not the front hex posts. Per bracket whole-solid bounds (leg-only components, not whole assembly):
|
||||
|
||||
- Right: **X 25.875…30.875, Y 17.907…24.307, Z −2.230…10.122620**.
|
||||
- Left: **X 0.875…5.875**, same Y and Z.
|
||||
- Each bracket envelope: **5.000 wide × 6.400 deep × 12.352620 high**. Bent sheet regions are **0.500 thick**; the lower two fingers occupy Y = **17.907…18.407**, not a circular screw shank.
|
||||
- Tips reach **2.230 below the module underside**, i.e. **3.800 below the module top**. This needs carrier/case underside clearance; do not turn these bounds into a mounting-hole drill recommendation.
|
||||
- Bracket symmetry centres X = **3.375 / 28.375**, lower sheet midplane Y = **18.157**. Relative to Eagle G1/G2, centre offsets are respectively **+0.0222 / −0.0222 in X** and **+0.250 in Y**. These are source-model differences, not a reason to move the Eagle pads.
|
||||
- Main fused body's signal tails separately reach Z = **−1.607380**. Their modeled rows are Y = **16.467 / 19.307**, versus Eagle **16.637 / 19.177**; horizontal pitch also differs (model 2.770 versus Eagle 2.7432). The 3D model is **not suitable as electrical pin/drill authority**.
|
||||
|
||||
No complete all-metal rear housing reconstruction is claimed: only the explicitly selected front surfaces and separate anchor brackets above. Main solid 12's 30.800 × 18.200 × 15.730 bounds include plastic/contact tails and must not be relabeled as a shell-only envelope.
|
||||
|
||||
## Validation, reproduction and release boundary
|
||||
|
||||
From project root:
|
||||
|
||||
```sh
|
||||
.cache/cad-tools/venv/bin/python -B hardware/PCB/reference/db9-cad/extract.py
|
||||
python3 -B hardware/PCB/reference/db9-cad/validate.py
|
||||
```
|
||||
|
||||
Both successfully ran; validation reported PASS for original hashes, 15 valid solids, all 12 Eagle header centres, PCB bounds, mounting holes, metal-appearance subsets and front lock cylinders. Extraction artifacts retain unrounded floating-point values. No firmware build, board DRC, hardware measurement or physical mating test was performed (research-only task).
|
||||
|
||||
**Ready for nominal placement planning:** metal flange dimensions, shell outer envelope, front hex-post dimensions and bore geometry, their distinct front planes, 25.000 front-axis spacing, PCB/header registration and overhang, anchor-leg underside extent. The A1 PCB now aligns the breakout front edge with the carrier top edge. Nominal metal bounding projections and screw-lock axes appear on Dwgs.User; companion layout/footprint notes have been updated. Neither projection nor nominal bound is an enclosure cutout or clearance qualification.
|
||||
|
||||
**Before fabrication / final case design:** verify actual male pin map; check physical connector profile and overhang against these nominal values; select header/socket and standoff heights; establish manufacturing/assembly clearances, cable backshell/tool access, wall thickness and cutout shape; verify thread standard and retention; perform a 1:1 fit/mating check. In particular, do not use exact-fit nominal bounds as an enclosure clearance or assume the CAD's Ø3.000 lock bore specifies a real screw thread.
|
||||
@@ -1,140 +0,0 @@
|
||||
# Provisional carrier module footprints
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
These project-local footprints are in `../../../../hardware/PCB/Carrier.pretty`. They are mechanical
|
||||
starting points, **not fabrication-ready connector selections**. Both are now
|
||||
assigned to their matching symbols in the native schematic through the project
|
||||
library tables and used in the routed PCB draft. The assembled module 3D models
|
||||
are not provided. See `pcb-draft-notes.md` for placement/routing and
|
||||
`rs232-footprint-notes.md` for the additional provisional RS-232 reference footprint.
|
||||
|
||||
## Shared pad/drill provenance and limits
|
||||
|
||||
Reference inspected locally: installed KiCad library footprint
|
||||
`Connector_PinHeader_2.54mm:PinHeader_1x04_P2.54mm_Vertical`, file
|
||||
`/usr/share/kicad/footprints/Connector_PinHeader_2.54mm.pretty/PinHeader_1x04_P2.54mm_Vertical.kicad_mod`
|
||||
(format version `20260206`; local KiCad `10.0.6-1.fc44`).
|
||||
|
||||
Both footprints use that reference's **1.70 × 1.70 mm plated through-hole pads,
|
||||
1.00 mm drills, rectangular pad 1, and circular remaining pads**, on
|
||||
`*.Cu` and `*.Mask`, with unused copper layers retained. Nominal pitch is 2.54 mm.
|
||||
Only the reference's pad/drill convention is reused, not its connector body,
|
||||
courtyard, or 3D model. HW678 pad 23 is circular: only overall pad 1 is rectangular.
|
||||
|
||||
**PROVISIONAL connector fit — awaiting the selected actual pin/socket datasheet.**
|
||||
Breadboard fit supports centre spacing, not drill size, finished-hole tolerance,
|
||||
annular-ring suitability, socket body clearance, mating height, or solderability.
|
||||
Validate those against the actual final headers and beta-test sockets and the PCB
|
||||
fabricator's process before release. No electrical nets or ESP32 GPIO assignments
|
||||
are inferred. No courtyards are supplied: module/connector assembly clearances
|
||||
remain incomplete, so automated courtyard checks cannot establish fit.
|
||||
|
||||
Coordinates below are millimetres, unrotated on the carrier front side, X right
|
||||
and Y down. Views are not mirrored. Placement on the back side or viewing the
|
||||
module from its solder side requires the appropriate KiCad transform; do not
|
||||
silently reverse the numbering.
|
||||
|
||||
## `Carrier:OLED_26mm_I2C_Provisional`
|
||||
|
||||
Origin: top-left corner of the module PCB, viewed from the front/display side,
|
||||
header at the top. User-verified geometry from the current request and the later
|
||||
mechanical-evidence section of `../../../../hardware/PCB/README.md`:
|
||||
|
||||
| Pad | Signal | X | Y |
|
||||
|---|---|---:|---:|
|
||||
| 1 | GND | 9.19 | 1.50 |
|
||||
| 2 | VCC | 11.73 | 1.50 |
|
||||
| 3 | SCL | 14.27 | 1.50 |
|
||||
| 4 | SDA | 16.81 | 1.50 |
|
||||
|
||||
- PCB boundary: (0, 0) to (26, 26), solid rectangle on `F.Fab`.
|
||||
This depicts the module, not the carrier's routed edge; no `Edge.Cuts` is used.
|
||||
- Four unnumbered, non-plated through holes, diameter 2.00 mm, at
|
||||
(1.75, 1.75), (24.25, 1.75), (1.75, 24.25), (24.25, 24.25).
|
||||
Hole centres are 1.75 mm from adjacent edges, with 22.50 mm spacing in both axes.
|
||||
These transfer the verified module hole geometry to the carrier; they do not
|
||||
select screws, inserts, bosses, or hardware clearance envelopes.
|
||||
- Display boundary: (1, 4.5) to (25.5, 21), 24.5 × 16.5 mm, dashed on `F.Fab`.
|
||||
This is the supplied display boundary, not a verified active-pixel area or case cutout.
|
||||
- Pad-to-signal mapping is documented here and in the footprint description;
|
||||
footprint pads themselves have numbers, not assigned nets.
|
||||
|
||||
The approved geometry supersedes the earlier 22.16 mm hole-spacing measurement:
|
||||
22.50 mm hole spacing, Y=1.50 mm header centres, and the centred header coordinates
|
||||
above apply. See README for the user-reported successful template fit check.
|
||||
|
||||
## `Carrier:HW678_2x22_Provisional`
|
||||
|
||||
Component/top view: antenna up, USB connectors down. Origin is the top-left header
|
||||
centre (pad 1), **not a PCB corner**.
|
||||
|
||||
- Left row: pads 1 through 22, top to bottom, X=0.
|
||||
- Right row: pads 23 through 44, top to bottom, X=25.40.
|
||||
- Y=0 through 53.34 in 2.54 mm increments for each row (21 intervals).
|
||||
- User breadboard fit verifies nominal 2.54 mm pitch and 25.40 mm row spacing;
|
||||
these supersede the earlier raw caliper dimensions in README.
|
||||
- Numbering follows the project's explicit convention, not a generic dual-row
|
||||
connector's alternating or counterclockwise numbering. Physical numbers do
|
||||
**not** establish GPIO, supply, or ground assignments.
|
||||
- User-measured planar geometry is now drawn on `F.Fab`, relative to pad 1:
|
||||
|
||||
| Feature | Start (X, Y), mm | End (X, Y), mm |
|
||||
|---|---|---|
|
||||
| Main PCB rectangle | (-1.365, -1.365) | (26.725, 54.705) |
|
||||
| Projecting antenna rectangle | (4.045, -8.255) | (21.675, -1.365) |
|
||||
| Left USB-C shell mouth span | (2.795, 54.705) | (11.665, 54.705) |
|
||||
| Right USB-C shell mouth span | (14.195, 54.705) | (23.065, 54.705) |
|
||||
|
||||
Main PCB is 28.09 mm wide × 56.07 mm long; antenna projection adds 6.89 mm,
|
||||
giving 62.96 mm overall. Antenna left/right insets are 5.41/5.05 mm, giving
|
||||
17.63 mm width and a centre 0.18 mm right of the PCB centre. Both shell widths
|
||||
are 8.87 mm, with outer-edge offsets 4.16 mm left / 3.66 mm right; mouths are
|
||||
flush with the bottom PCB edge. Lines indicate mouth spans, not shell depth.
|
||||
- User accepted approximate 1.365 mm top/bottom pin-centre offsets and supplied
|
||||
approximately the same left offset. Nominal pin pitch/row spacing are retained;
|
||||
resulting right margin is 1.325 mm. This 0.04 mm asymmetry follows the selected
|
||||
modelling datum, not a precision measurement of header asymmetry.
|
||||
- **No RF keepout, courtyard, full USB envelope, mounting holes or 3D model is
|
||||
inferred.** Shell depth/height, component/underside heights and plug clearances
|
||||
remain unknown. The antenna rectangle is physical geometry, not RF guidance.
|
||||
- Routed embedded U1 now contains the four measured F.Fab shapes and the two
|
||||
antenna/USB measurement-limit annotations. Generic library captions are omitted
|
||||
to avoid overlapping existing board labels. Existing footprint fields (including
|
||||
the historical hidden status/description), reference/value positions, pads,
|
||||
nets and UUIDs are deliberately retained; this is not a full footprint replacement.
|
||||
The library is authoritative for measured geometry, not those old status strings.
|
||||
- Measured board-local silkscreen and PDF/SVG/PNG/3D previews are refreshed.
|
||||
`update_u1_graphics.py` synchronizes only owned graphics, with deterministic new
|
||||
UUIDs and lock/concurrent-change guards; `validate_u1_update.py` checks exact
|
||||
preservation against the pre-update A1 snapshot. A physical 1:1 fit check is
|
||||
still pending; USB mouths sit 2.295 mm inside the carrier bottom edge.
|
||||
|
||||
## Validation and release gates
|
||||
|
||||
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.
|
||||
The HW678 regression now checks the two measured rectangles and two USB mouth
|
||||
spans, while retaining exact pad geometry checks and prohibiting guessed
|
||||
courtyards, zones and 3D models. The local Python binding
|
||||
emitted three startup `PROPERTY_ENUM` assertions (also seen on a version-only
|
||||
import); these did not prevent loading or the checks from passing.
|
||||
|
||||
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.
|
||||
@@ -1,332 +0,0 @@
|
||||
# Compact two-layer PCB — A1 draft
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Open **`../../../../hardware/PCB/serial-carrier.kicad_pcb`** in KiCad 10. The routed board belongs to the
|
||||
existing `../../../../hardware/PCB/serial-carrier.kicad_pro` / `../../../../hardware/PCB/serial-carrier.kicad_sch` project. **Not for fabrication.**
|
||||
No Gerbers, production drills or manufacturing archive were generated.
|
||||
|
||||
## Measurement update — embedded U1, silkscreen and exports
|
||||
|
||||
The user has now supplied ESP32 main-PCB, antenna and USB mouth geometry; see
|
||||
`footprint-notes.md` and the hardware README. The library HW678 footprint contains
|
||||
these measured F.Fab bounds. Embedded U1 now has the same four shapes and two
|
||||
measurement-limit annotations; generic library captions are omitted to avoid
|
||||
colliding with existing board labels. Silkscreen, PDF and SVG/PNG/3D previews are
|
||||
refreshed. Existing footprint fields, all existing non-generated UUIDs, pads/nets,
|
||||
placement, routing, edges, RF reserve and project settings are unchanged. Old
|
||||
hidden U1 status/description fields are retained historical metadata, not evidence
|
||||
that planar measurements are missing. RF requirements, full USB envelopes and
|
||||
assembly heights remain pending; the antenna rectangle is not an RF keepout.
|
||||
|
||||
At unchanged A1 U1 placement, the measured main PCB occupies carrier-local
|
||||
X=6.635…34.725, Y=21.635…77.705 mm; the projecting antenna occupies
|
||||
X=12.045…29.675, Y=14.745…21.635 mm. USB mouths are at Y=77.705 mm,
|
||||
**2.295 mm inside the carrier bottom edge**, so flush-with-module USB does not
|
||||
mean flush-with-carrier access. Review mating-plug clearance before accepting
|
||||
placement; no relocation, rerouting or RF sign-off is implied by this update.
|
||||
|
||||
## Size and placement
|
||||
|
||||
- **Carrier bounding dimensions: 82 × 80 mm**, within the requested 100 × 100 mm.
|
||||
- **Two copper layers**, F.Cu and B.Cu. All components mount on the front.
|
||||
- Proposed **1.6 mm** carrier thickness; fabrication stackup/process not selected.
|
||||
- Stepped outline: a **36 × 19 mm top-left antenna cutaway** fits within a simple
|
||||
rectangular/square case. The lower edge is now straight, with ESP32 USB toward
|
||||
the bottom and RS-232 toward the top. A1 moved U1 down by 19 mm; this measurement
|
||||
integration retains that placement and uses the user-accepted approximate header datum.
|
||||
- Module and mating-plug overhangs are not included in the carrier dimensions.
|
||||
The final complete assembly/enclosure envelope still needs measurement.
|
||||
|
||||
Coordinates in the following table are local millimetres from the carrier's
|
||||
upper-left corner, X right/Y down, viewed from the component side. In the KiCad
|
||||
file add (100, 100) mm to obtain absolute board coordinates. All footprints are
|
||||
unrotated; their origins are defined in the footprint notes, not necessarily
|
||||
component body corners.
|
||||
|
||||
| Reference | Local origin (X, Y) | Placement |
|
||||
|---|---|---|
|
||||
| U1 | (8, 23) | ESP32 left header pin 1; antenna toward top-left cutaway, USB toward bottom edge |
|
||||
| U2 | (43, 26.670) | MAX3243 header pin 1; bare module PCB front exactly flush with carrier top edge |
|
||||
| DS1 | (43.970, 36) | OLED module upper-left PCB corner; display faces up |
|
||||
| R1 / R2 / R3 | (42.160 / 53.160 / 64.160, 32.6685) | Axial pull-ups in the gap between RS-232 and OLED |
|
||||
| SW1 / SW2 / SW3 | (41.720 / 53.720 / 65.720, 68) | Back, select, next below display; switch pad 1 origins |
|
||||
| H1 | (3.5, 22.5) | Proposed case mounting hole below antenna notch |
|
||||
| H2 | (3.5, 75) | Proposed case mounting hole near bottom-left edge |
|
||||
| H3 | (78, 4) | Proposed case mounting hole |
|
||||
| H4 | (78, 76) | Proposed case mounting hole |
|
||||
|
||||
Outline vertices: (36,0), (82,0), (82,80), (0,80), (0,19), (36,19), closed.
|
||||
The previous A0 board is retained in `../../../../hardware/PCB/validation/pcb-a0-before-reposition.kicad_pcb`.
|
||||
The board area is **58.76 cm²**; its bounding rectangle is 65.6 cm².
|
||||
|
||||
H1–H4 are user-approved **Ø3.2 mm NPTH M3 clearance holes**. The user selected
|
||||
10 mm brass standoffs into heat inserts; see the current acceptance record. They are marked board-only and excluded from the electrical
|
||||
BOM. The OLED and RS-232 module mounting holes are additional, separate holes.
|
||||
Screw heads, spacers, heat-set inserts, tool access and underside lead clearance
|
||||
must be checked against the final case and assembly heights. Exact insert dimensions remain unspecified. The user intends/reports 10 mm
|
||||
assembled module elevation, not independently measured; see user-acceptance.md.
|
||||
|
||||
## Routing and electrical status
|
||||
|
||||
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.
|
||||
- **938 segments** after incremental group-alignment repair, signal width **0.25 mm**, 3.3 V and GND width **0.50 mm**.
|
||||
- **41 through vias** after repair, **0.70 mm copper / 0.30 mm drill**.
|
||||
- **No copper pours**; ground is explicitly routed. This is an initial routing
|
||||
solution, not a reviewed return-current/EMI or power-integrity design.
|
||||
- Proposed minimum different-net clearance **0.25 mm** and copper-edge clearance
|
||||
**0.50 mm**. These are selected draft design rules, not fabrication qualification.
|
||||
- Project netclasses align interactive routing defaults with the draft: Default
|
||||
uses 0.25 mm tracks / 0.25 mm clearance, while Power matches `/+3V3` and `/GND`
|
||||
with 0.50 mm tracks. Both use 0.70 / 0.30 mm vias.
|
||||
- Schematic pad-net assignments, component values and UUID linkage are retained.
|
||||
U1's intentionally unused GPIO/USB/UART0/5 V header pads remain unused.
|
||||
|
||||
The bounded draft router prioritizes connectivity and conservative geometric
|
||||
clearance, not optimal trace topology. Do not treat DRC passage as evidence of
|
||||
current-carrying capacity, supply stability, I²C signal quality or EMC compliance.
|
||||
|
||||
## Explicit provisional mechanical choices
|
||||
|
||||
### ESP32 and RF
|
||||
|
||||
A two-layer **no-tracks/no-vias/no-pads/no-zone-fill rule area** reserves local
|
||||
X=10…31 mm, Y=19…32 mm beneath the antenna-side end of U1, adjacent to the notch. This is a deliberate carrier design
|
||||
allocation, **not a measured antenna envelope or a manufacturer RF keepout**.
|
||||
The module itself may span the rule area; footprint placement is not forbidden.
|
||||
No carrier copper enters the reserve, but copper runs close to its boundary.
|
||||
|
||||
U1's planar PCB/antenna/USB-mouth offsets are now recorded. Antenna clearance
|
||||
and USB access must still be checked with the actual board and a printed
|
||||
placement template. Move U1, change the edge or enlarge the reserve if required.
|
||||
Missing RF clearance cannot be signed off from the current footprint alone.
|
||||
|
||||
### RS-232
|
||||
|
||||
U2 uses **`Carrier:MAX3243_Reference_Provisional`** in the schematic and PCB. Its bare-module outline/header/holes are adapted from published female
|
||||
5988 CAD for the intended male 6253 module. The user explicitly confirms identical
|
||||
male/female external dimensions. The linked Adafruit STEP has now been measured
|
||||
with OpenCascade; its nominal metal geometry is used on that basis. Male header
|
||||
mapping, actual fit, tolerances and the mounting stack still need qualification.
|
||||
See `rs232-footprint-notes.md` and `db9-mechanical-notes.md` for evidence/licenses.
|
||||
|
||||
The module's **bare PCB front is flush with carrier Y=0**, as requested. The
|
||||
connector has distinct front planes, not one universal case-wall datum:
|
||||
|
||||
| Feature | Position relative to carrier top edge | Meaning |
|
||||
|---|---:|---|
|
||||
| Flange front / shell root | 1.930 mm inside | Behind the PCB edge |
|
||||
| Breakout PCB front | 0 | Flush with the carrier edge |
|
||||
| Hex screw-lock mouth faces | 2.870 mm outside | Front cable-retention hole mouths |
|
||||
| Shell nose | 3.870 mm outside | Foremost modeled shell surface |
|
||||
|
||||
The front screw-lock axes are **25.000 mm apart**, with centres **6.277620 mm
|
||||
above the module PCB top**. They are not the two 24.130 mm-spaced PCB mounting
|
||||
holes. The source models Ø3.000 mm bores but **no threads**; this does not establish
|
||||
M3 or 4-40 hardware. The main metal flange is 30.800 × 12.550 × 0.400 mm; the
|
||||
shell envelope including its root/lip is 16.878 × 9.100 × 5.800 mm.
|
||||
|
||||
`../../../../hardware/PCB/add_db9_envelope.py` places the measured metal bounding projections and screw-lock
|
||||
axes on **Dwgs.User**, relative to U2. They are not silkscreen, a courtyard or a
|
||||
case cutout. Run the helper after moving U2; it preserves copper and project
|
||||
settings. The original STEP and its extraction/validation scripts are retained.
|
||||
|
||||
**Case-wall positioning remains a design decision:** PCB-edge alignment does not
|
||||
put the flange or screw-lock mouths at that same plane. For example, making the
|
||||
hex mouths flush with the case exterior would place that exterior 2.870 mm beyond
|
||||
the carrier edge. No wall thickness, cable-backshell clearance or sealing scheme
|
||||
has been assumed. The model's anchor legs extend **2.230 mm below the module
|
||||
underside**; select a stand-off/header stack or clearance solution before assembly.
|
||||
|
||||
The user has now confirmed male pinout/header/footprint equivalence and reviewed
|
||||
connections pin by pin. U2 is assigned in both schematic and PCB; validators
|
||||
require **zero schematic parity warnings**, without exclusions.
|
||||
|
||||
### OLED and controls
|
||||
|
||||
OLED geometry, including its display boundary, is the user-fit-verified geometry.
|
||||
The module schematic/library files are unchanged. The new overall placement still
|
||||
needs an assembly fit check: adjacent resistor bodies, module underside parts,
|
||||
header/socket bodies, button actuation and finger/plunger clearance are not
|
||||
represented by complete assembly courtyards. In particular the axial resistors
|
||||
are tightly packed between the module outlines; confirm clearance and height.
|
||||
|
||||
## Device outlines on front silkscreen
|
||||
|
||||
The PCB now has **0.12 mm front-silkscreen outlines** for the OLED PCB and the
|
||||
published RS-232 reference module, plus a dashed OLED display boundary. The
|
||||
RS-232's visible rounded corners retain true arcs. Small gaps clear pad/mask
|
||||
openings, vias and adjacent resistor markings; the original full F.Fab geometry
|
||||
remains unchanged.
|
||||
|
||||
U1 now has **measured body/antenna outlines**, replacing the header-row guides
|
||||
and `HEADER GUIDES` label. Silkscreen is clipped inside the stepped carrier with
|
||||
0.22 mm additional edge/mask clearance; the off-carrier antenna projection remains
|
||||
visible only on F.Fab. Two **0.30 mm strokes** mark the 8.87 mm USB mouth spans,
|
||||
replacing coincident 0.12 mm body-edge strokes. Wider strokes receive correspondingly
|
||||
wider clearance. No USB depth or RF keepout is inferred. Existing button/resistor
|
||||
silkscreen remains unchanged.
|
||||
|
||||
These are board-local drawings, not edits to the footprint libraries. If the
|
||||
module placement or routing changes, close PCB Editor and refresh them with:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
|
||||
```
|
||||
|
||||
The script first verifies the saved U1 measured graphics against the library.
|
||||
It requires pcbnew and Shapely, replaces only its own generated graphics,
|
||||
and refuses an open PCB or concurrent board change. It leaves project settings,
|
||||
copper, pads and source F.Fab outlines unchanged. Run it **after** placement or
|
||||
routing regeneration; moving a footprint does not move these board-local drawings.
|
||||
It currently supports the draft's unrotated module placements.
|
||||
|
||||
The original A0 silkscreen-only change left its routing unchanged. A1 deliberately
|
||||
repositions U1/U2 and reroutes all connections, then refreshes these outlines.
|
||||
Silkscreen is clipped away from the new flush U2 front board edge. Physical DRC
|
||||
remains **0 violations**, **0 unconnected**, with the same U2 footprint-parity
|
||||
warning. PDF and both previews have been refreshed.
|
||||
|
||||
## Review outputs
|
||||
|
||||
PCB presentation labels have been removed: title suffix `draft`, revision suffix
|
||||
`DRAFT`, front-silkscreen `DRAFT - NOT FOR FAB`, and the title-comment prefix
|
||||
`NOT FOR FABRICATION -` (including the PDF exporter). The generator no longer
|
||||
emits these labels. Specific technical cautions, dimensions, geometry, hidden
|
||||
metadata and footprint identifiers remain; artifact filenames are unchanged.
|
||||
This is a presentation change, **not fabrication approval**. The schematic and
|
||||
its exports are outside this PCB-only cleanup.
|
||||
|
||||
`remove_draft_markers.py` performs the guarded, idempotent cleanup and audits
|
||||
that no other parsed board content or project settings change. The U1 regression
|
||||
now permits exactly these label edits against its unchanged historical baseline,
|
||||
without relaxing geometry/pad/net/routing checks. Fresh PCB DRC, U1/silkscreen
|
||||
regressions and both footprint tests pass; the expected U2 parity warning remains.
|
||||
PDF/SVG/PNG/3D outputs were regenerated and visually reviewed after cleanup.
|
||||
|
||||
- `../../../../hardware/PCB/validation/pcb-draft.svg` / `../../../../hardware/PCB/validation/pcb-draft.png`: combined copper/outline/fab review.
|
||||
- `../../../../hardware/PCB/validation/pcb-draft-3d.png`: KiCad render of the carrier and available standard
|
||||
component models. **ESP32, OLED and RS-232 assembled module models are absent**;
|
||||
the render is not an assembled-device or enclosure fit check.
|
||||
- `../../../../hardware/PCB/pcb-placement-draft.pdf`: **1:1** front placement/pad/outline drawing. Print at
|
||||
100%, disable fit-to-page, and confirm the outer width is 82 mm before use.
|
||||
It includes the known module outlines and nominal STEP-derived DE-9 metal
|
||||
bounding projections and measured U1 planar geometry; no guessed USB depth,
|
||||
RF keepout or case-wall geometry.
|
||||
|
||||
Refresh the embedded measurement graphics and PDF without replacing the footprint:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/update_u1_graphics.py
|
||||
/usr/bin/python3 -B hardware/PCB/add_device_silkscreen.py
|
||||
/usr/bin/python3 -B hardware/PCB/export_placement_pdf.py
|
||||
```
|
||||
|
||||
Both board-writing helpers refuse PCB Editor locks and concurrent board changes,
|
||||
use `SaveBoard(..., True)`, and check unchanged project settings. The PDF exporter
|
||||
verifies embedded geometry instead of overlaying the library; only review captions
|
||||
change on its temporary plotting board. Print at 100%, not fit-to-page.
|
||||
|
||||
Preview refresh commands (repository root, KiCad and Inkscape installed):
|
||||
|
||||
```sh
|
||||
kicad-cli pcb export svg --mode-single --layers F.Cu,B.Cu,F.SilkS,F.Fab,Dwgs.User,Edge.Cuts --sketch-pads-on-fab-layers --page-size-mode 2 --output hardware/PCB/validation/pcb-draft.svg hardware/PCB/serial-carrier.kicad_pcb
|
||||
inkscape hardware/PCB/validation/pcb-draft.svg --export-type=png --export-width=1800 --export-filename=hardware/PCB/validation/pcb-draft.png
|
||||
kicad-cli pcb render --width 1600 --height 1200 --side top --quality high --output hardware/PCB/validation/pcb-draft-3d.png hardware/PCB/serial-carrier.kicad_pcb
|
||||
```
|
||||
|
||||
KiCad's SVG trailing whitespace was normalized after export (`git diff --check`
|
||||
passes); the rendered geometry is unchanged. PDF and previews were visually reviewed.
|
||||
|
||||
The board-area SVG crop omits outside-board review captions/parts of the DE-9
|
||||
projection; use the full-page PDF for the complete placement drawing.
|
||||
|
||||
## Validation and limits
|
||||
|
||||
Run from the repository root with the system Python containing KiCad bindings:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
|
||||
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_u1_update.py
|
||||
/usr/bin/python3 -B hardware/PCB/reference/db9-cad/validate.py
|
||||
```
|
||||
|
||||
The first script exports a **fresh saved-schematic netlist** and checks the actual
|
||||
PCB pad assignments, component linkage, dimensions, layer count, nominal thickness,
|
||||
mounting holes, track/via sizes and KiCad DRC. It never rewrites the PCB or project.
|
||||
It requires zero parity issues, DRC violations and unconnected items, including
|
||||
the retained user-confirmed U2 assignment. Reports are retained in
|
||||
`../../../../hardware/PCB/validation/pcb-parity-drc.json` and `../../../../hardware/PCB/validation/pcb-validation-summary.json`.
|
||||
|
||||
The second independently checks continuous copper/hole/edge geometry and original
|
||||
placement/net/UUID preservation against the initial unrouted snapshot. It needs
|
||||
locally installed NumPy and Shapely in addition to pcbnew. It is not a substitute
|
||||
for current-schematic net validation and is specific to this initial layout.
|
||||
|
||||
Measurement-integration validation with KiCad **10.0.6**:
|
||||
|
||||
- All five commands above pass. New `validate_u1_update.py` compares the complete
|
||||
parsed board against `validation/pcb-before-u1-measured.kicad_pcb`, excluding
|
||||
only owned silk and six new U1 graphics. Snapshot and project hashes are pinned;
|
||||
full pad/net/UUID/placement/routing/edge/zone/settings preservation passes.
|
||||
- Measured embedded/library geometry, silk regeneration idempotence, carrier/notch
|
||||
clipping, two exact USB mouth markers and a stale-geometry negative control pass;
|
||||
**91 generated silk items**. U1 refresh byte-idempotence also passed; an already
|
||||
current U1 now returns without writing. Evidence: `hardware/PCB/validation/pcb-u1-update.json`.
|
||||
- The historical A0 `pcb-routing-preservation.py` does **not** pass: it stops on
|
||||
its historical `serial-carrier.kicad_prl` protected hash, already stale for this
|
||||
working state. Its older exact-footprint contract also predates measured U1
|
||||
additions. Historical script/reports were not weakened or rebaselined; the new
|
||||
A1 pre-update comparison is the preservation evidence for this change.
|
||||
- PCB, project and schematic retain their existing content except the authorized
|
||||
board graphics. No firmware/assets, reroute, upload, erase or hardware test.
|
||||
|
||||
Historical measurement-integration results (current alignment results are in
|
||||
[user-acceptance.md](user-acceptance.md)):
|
||||
|
||||
- **0 physical DRC errors/warnings; 0 unconnected items.**
|
||||
- All saved-schematic pad nets match the PCB; **1 expected U2 footprint parity warning**.
|
||||
- Independent minimum different-net/copper-to-NPTH gap **0.270544 mm**; minimum
|
||||
copper/NPTH-to-board-edge gap **1.900 mm**.
|
||||
- All 9 electrical footprints and 4 board-only mounts retained; positions, outline
|
||||
and RF area revised as documented, with electrical linkage preserved.
|
||||
- Startup `PROPERTY_ENUM` assertions from the local pcbnew binding are still
|
||||
emitted, but the checks complete successfully.
|
||||
|
||||
DRC uses the existing default ignored checks: `missing_courtyard`,
|
||||
`track_not_centered_on_via`, `tuning_profile_track_geometries`,
|
||||
`footprint_filters_mismatch`, `footprint_type_mismatch`. No new exclusions were
|
||||
added to hide problems. **Missing module courtyards therefore remain an explicit
|
||||
manual release gate**. The draft rules do not represent a chosen fabricator's
|
||||
complete capability set. No electrical/hardware/fabrication tests were performed.
|
||||
|
||||
## Regeneration and project settings
|
||||
|
||||
Normal use is to **edit the native PCB in KiCad**, not regenerate it. The optional
|
||||
`../../../../hardware/PCB/generate_pcb_draft.py` recreates the *unrouted* A1 placement and discards manual
|
||||
PCB work only with explicit `--overwrite`; it refuses an open-project lock. A1 was
|
||||
made by mutating the existing board, not running that generator. All A1 saves use
|
||||
`SaveBoard(..., True)` to preserve user project settings. The original A0 creation
|
||||
populated the minimal project with rules; this revision leaves those settings and
|
||||
the schematic unchanged. Refresh device silkscreen and CAD envelope helpers after
|
||||
any intentional regeneration or placement/routing changes.
|
||||
|
||||
`../../../../hardware/PCB/route_pcb_draft.py` requires NumPy and pcbnew. By default it never saves; replacing
|
||||
existing tracks requires explicit `--route --overwrite --replace-routes`. Do not
|
||||
run that over manual routing without a backup. It is a bounded initial-draft
|
||||
router, **not** a general-purpose autorouter. See `validation/pcb-routing-summary.md`
|
||||
for algorithm limits and commands. These scripts do not need downloaded packages
|
||||
or services in the current environment and create no manufacturing outputs.
|
||||
|
||||
Before fabrication: qualify all module pin maps/footprints and connectors,
|
||||
complete assembly courtyards and RF/USB/DE-9 checks, review power/return paths,
|
||||
verify the existing USB power circuit and OLED pull-ups, confirm procurement and
|
||||
case hardware, rerun ERC/DRC, and perform an independent hardware design review.
|
||||
@@ -1,124 +0,0 @@
|
||||
# MAX3243 carrier — PROVISIONAL female CAD reference / VERIFY MALE
|
||||
|
||||
> **Current status (2026-09-22):** [User acceptance and group alignment](user-acceptance.md) supersedes the earlier pending user-fit, USB-clearance, male-map, procurement and mounting-hardware items below. Original research/evidence remains historical; acceptance is not independent qualification.
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Footprint: `Carrier:MAX3243_Reference_Provisional` in
|
||||
`../../../../hardware/PCB/Carrier.pretty/MAX3243_Reference_Provisional.kicad_mod`.
|
||||
Intended only for a <=100 × 100 mm, two-copper-layer carrier PCB draft.
|
||||
No schematic assignment or edits are made. This footprint does not establish
|
||||
whole-board fit, connector access, DRC compliance, or fabrication readiness.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
The user reports that the male **6253** module has the same dimensions. This is
|
||||
not confirmation of its pad map. Geometry below is taken from published **female
|
||||
5988** Eagle CAD, not verified male-specific CAD. Confirm the purchased module's
|
||||
identity, orientation, header numbering, outline and mounting geometry before
|
||||
release. Never infer header order from DE-9 gender or mating-face numbering.
|
||||
|
||||
Source: [Adafruit PCB repository](https://github.com/adafruit/Adafruit-RS-232-Level-Shifter-Breakout-PCB),
|
||||
commit `63c6200bb3ef17d491089cfa6441b1810d79bb20`.
|
||||
Local original: `../../../../hardware/PCB/reference/Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd`;
|
||||
SHA-256 `6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870`
|
||||
was rechecked against the file. See `adafruit-research.md` for provenance and
|
||||
female/male distinctions.
|
||||
|
||||
CAD-derived geometry adapted from the design by **Limor Fried/Ladyada for Adafruit
|
||||
Industries**, under **CC BY-SA 3.0 Unported**:
|
||||
https://creativecommons.org/licenses/by-sa/3.0/ . The CAD-derived footprint and
|
||||
these CAD-derived notes use that license. Preserve the full upstream notices in
|
||||
`../../../../hardware/PCB/reference/README.md` and `../../../../hardware/PCB/reference/license.txt` when redistributing this adaptation.
|
||||
Changes: coordinate transformation, carrier pad convention, NPTH carrier holes,
|
||||
and provisional annotations; no module circuitry or connector package copied.
|
||||
|
||||
## View, origin and exact nominal geometry
|
||||
|
||||
Front/component top view, module facing up, DE-9 toward the top (**negative Y**).
|
||||
Origin is header JP2 pin 1, not a PCB corner. X increases rightward; Y downward.
|
||||
Transform Eagle board coordinates in mm with:
|
||||
|
||||
`x = X - 1.905; y = 2.667 - Y`.
|
||||
|
||||
- Pads 1…12: `(2.54 × (N - 1), 0)`, first-to-last span 27.94 mm.
|
||||
- Full bare-module outline is on `F.Fab`, **not carrier `Edge.Cuts`**.
|
||||
Bounds: X = -1.905…29.845, Y = -26.670…2.667 (31.750 × 29.337 mm).
|
||||
Top corners are square. Bottom corners retain the source 2.540 mm radius arcs.
|
||||
- Left arc: (-1.905, 0.127) to (0.635, 2.667), centre (0.635, 0.127).
|
||||
Right arc: (27.305, 2.667) to (29.845, 0.127), centre (27.305, 0.127).
|
||||
Midpoints use radius/sqrt(2), rounded to nine decimal places in the file;
|
||||
the arcs are not chamfers or a bounding-box approximation.
|
||||
- Two unnumbered **3.00 mm NPTH carrier holes**: (1.905, -8.763) and
|
||||
(26.035, -8.763), spacing 24.130 mm. These transfer module-hole centres and
|
||||
drill diameter only. The source module holes are actually plated, 3.60 mm
|
||||
copper / 3.00 mm drill, unconnected in the source signal map. Carrier NPTH
|
||||
deliberately does not copy that plating or assign electrical connections.
|
||||
No screw, washer, standoff or tool-access clearance is implied.
|
||||
|
||||
## Provisional carrier pads and female reference map
|
||||
|
||||
Same convention documented in `footprint-notes.md` for the other carrier modules:
|
||||
**1.70 × 1.70 mm PTH pads / 1.00 mm drills**, rectangular pad 1, circular pads
|
||||
2…12, `*.Cu` and `*.Mask`, unused copper layers retained. That convention derives
|
||||
from the installed KiCad `PinHeader_1x04_P2.54mm_Vertical`; it is not a selected
|
||||
header/socket qualification. The source module itself uses 1.9304 mm round pads.
|
||||
Confirm actual pins/sockets, finished-hole tolerances, annular rings and assembly
|
||||
height before fabrication. Square pad 1 is a carrier marker, not source evidence.
|
||||
|
||||
| Pad | X (mm), Y=0 | Female label | Female CAD net |
|
||||
|---:|---:|---|---|
|
||||
| 1 | 0.00 | Vin | VCC |
|
||||
| 2 | 2.54 | GND | GND |
|
||||
| 3 | 5.08 | DCD | DCD |
|
||||
| 4 | 7.62 | RX | RXD |
|
||||
| 5 | 10.16 | TX | TXD |
|
||||
| 6 | 12.70 | DTR | DTR |
|
||||
| 7 | 15.24 | DSR | DSR |
|
||||
| 8 | 17.78 | RTS | RTS |
|
||||
| 9 | 20.32 | CTS | CTS |
|
||||
| 10 | 22.86 | RI | RI |
|
||||
| 11 | 25.40 | VLD | !INVALID |
|
||||
| 12 | 27.94 | OFF | !FORCEOFF |
|
||||
|
||||
**This is the female reference map, NOT a confirmed male map.** Footprint pads
|
||||
have numbers only, no assigned nets. OFF is active low; VLD indicates valid input
|
||||
when high. This project's logic interface requires 3.3 V Vin.
|
||||
|
||||
## Intentionally omitted / release gates
|
||||
|
||||
The footprint F.Fab outline still describes only the bare module PCB. Subsequent
|
||||
STEP B-rep measurements now establish nominal shell/flange/hex-post envelopes and
|
||||
registration; see `db9-mechanical-notes.md`. The user explicitly confirms identical
|
||||
male/female external dimensions. A1 board-local Dwgs.User projections show those
|
||||
nominal bounds, without changing this footprint's pads or deriving drills from
|
||||
the 3D model. Mating-plug/cable, case wall/cutout, assembly height and actual fit
|
||||
remain unqualified. No guessed complete courtyard or 3D mounting offset is added.
|
||||
There is **no fake courtyard**; courtyard checks cannot establish assembly fit.
|
||||
Short silkscreen labels identify PROVISIONAL / FEMALE CAD REF / VERIFY MALE.
|
||||
|
||||
Before layout release: verify male pad map and orientation against the actual
|
||||
module or authoritative male CAD; perform a 1:1 fit check; select headers and
|
||||
mounting hardware; verify assembled and mating envelopes; then establish real
|
||||
clearances/courtyard and check the complete <=100 × 100 mm two-layer layout.
|
||||
|
||||
## Validation performed
|
||||
|
||||
KiCad **10.0.6** `pcbnew.FootprintLoad` successfully loaded the footprint using
|
||||
system Python (`/usr/bin/python3 -B`) in a read-only inline check. Assertions passed
|
||||
for all 12 numbered pad positions against source JP2 package/element coordinates,
|
||||
female signal-contactref order, pad sizes/drills/shapes and copper/mask presence,
|
||||
no assigned header nets, both NPTH centres/sizes/drills, and all six transformed
|
||||
source outline segments including both 90-degree, 2.540 mm radius arcs and centres.
|
||||
Also checked absence of courtyard, Edge.Cuts, zones and 3D models.
|
||||
|
||||
The first check assumed arc endpoint order was preserved; KiCad normalizes arc
|
||||
orientation. The corrected check matches endpoint pairs independent of order and
|
||||
verifies radius, angle and centre; it passed without changing geometry. The
|
||||
binding emitted the same three startup `PROPERTY_ENUM` assertions documented for
|
||||
other module validation; these did not prevent loading or successful checks.
|
||||
No hardware fit test, male electrical verification, board DRC or firmware build
|
||||
was performed. Existing `../../../../hardware/PCB/validate_footprints.py` was not changed and does not
|
||||
cover this new footprint. Only this notes file and the new footprint were written;
|
||||
schematic, project and lock files were left untouched.
|
||||
@@ -1,129 +0,0 @@
|
||||
# Carrier group alignment and user acceptance — 2026-09-22
|
||||
|
||||
This is the current acceptance record. It supersedes pending-user-confirmation
|
||||
items in the earlier research and A0/A1 notes; it does not replace their original
|
||||
manufacturer evidence or claim new hardware tests, fabrication approval or certification.
|
||||
|
||||
## Saved placement
|
||||
|
||||
Coordinates below are **carrier-local mm**, X right/Y down. Add (100,100) for
|
||||
KiCad coordinates. All footprint rotations remain zero. Body centres, not
|
||||
header/pad origins, define the common vertical axis **X=56.970 mm**.
|
||||
|
||||
| Item | Old footprint origin | New footprint origin | Movement |
|
||||
|---|---|---|---|
|
||||
| U2 RS-232 | (43,26.670) | unchanged | none |
|
||||
| DS1 OLED | (43,36) | (43.970,36) | +0.970 X |
|
||||
| R1 | (43,34) | (42.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| R2 | (54,34) | (53.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| R3 | (65,34) | (64.160,32.6685) | −0.840 X, −1.3315 Y |
|
||||
| SW1 | (43,68) | (41.720,68) | −1.280 X |
|
||||
| SW2 | (55,68) | (53.720,68) | −1.280 X |
|
||||
| SW3 | (67,68) | (65.720,68) | −1.280 X |
|
||||
|
||||
U1 remains (8,23); carrier outline, four carrier mounting holes, RF reserve and
|
||||
confirmed USB geometry are unchanged. Resistors remain a horizontal group with
|
||||
11 mm pitch; buttons remain a horizontal group with 12 mm pitch. Neither group
|
||||
was rearranged into a vertical stack.
|
||||
|
||||
The published RS-232 bare PCB spans X=41.095…72.845, Y=0…29.337, including
|
||||
its rounded rear corners beyond the header row. Its centre X=56.970 is retained
|
||||
to minimize module movement and preserve connector/case registration. OLED PCB
|
||||
bounds are X=43.970…69.970, Y=36…62. The OLED's offset display window is not
|
||||
used as the module centre.
|
||||
|
||||
The nominal resistor bodies are 6.3 × 2.5 mm, centred 3.81 mm right of pad 1.
|
||||
Their group spans X=42.820…71.120, Y=31.4185…33.9185. The 6.663 mm gap between
|
||||
module boards is therefore split into **2.0815 mm clearance / 2.5 mm body /
|
||||
2.0815 mm clearance**. Adjacent resistor bodies have 4.7 mm horizontal clearance.
|
||||
These are nominal planar CAD clearances, not new tolerance or height measurements.
|
||||
|
||||
Omron B3F-1000 nominal bodies are 6 × 6 mm with centres 3.25 mm right and
|
||||
2.25 mm below their pad-1 origins. Group body bounds X=41.970…71.970,
|
||||
Y=67.250…73.250; OLED-to-button body gap **5.250 mm**, adjacent button body
|
||||
gaps **6 mm**. Footprint courtyard and lead envelopes are wider than the bodies;
|
||||
the alignment audit uses the actual F.Fab bodies, not a pad-origin average.
|
||||
|
||||
## Closed by explicit user acceptance/report
|
||||
|
||||
- All component dimensions and physical fit match visually; USB placement and
|
||||
clearance are correct. This is user acceptance, not an independently performed
|
||||
new print-fit or hardware test by the agent.
|
||||
- Female headers are described as 8.5 mm and standard male headers as adding
|
||||
2.5 mm. User intends/reports **10 mm assembled elevation**. The arithmetic sum
|
||||
is 11 mm, but mating height cannot be inferred by simply adding those descriptions.
|
||||
**10 mm is not independently measured or precision-verified.** User reports
|
||||
RS-232 mounting prongs clear the carrier at the intended assembly height.
|
||||
- Four Ø3.2 mm M3 carrier holes approved; **10 mm brass standoffs into heat
|
||||
inserts**, M3 daughterboard screws, and **M2 display hardware** selected.
|
||||
This does not establish DE-9 cable-retention thread type, exact screw lengths,
|
||||
insert dimensions or the final printed case wall/cutout.
|
||||
- **Omron B3F-1000 switches purchased.** Existing standard KiCad footprint is
|
||||
retained; documented duplicate-pad mapping is unchanged.
|
||||
- **2 kΩ, 1% metal-film resistors purchased.** R1–R3 values updated in schematic,
|
||||
PCB and BOM; nominal DIN0207 6.3 × 2.5 mm body / 7.62 mm lead pitch retained
|
||||
under the visual-fit confirmation. Exact resistor MPN and purchased wattage
|
||||
were not supplied. Design requirement remains at least 0.125 W. At nominal
|
||||
3.3 V, each pressed external pull-up draws 1.65 mA and dissipates 5.445 mW;
|
||||
these calculations do not quantify regulator margin or GPIO internal pull-ups.
|
||||
- Male Adafruit module has identical reference pinout/header mapping/footprint
|
||||
per user, who reviewed module connections pin by pin. Existing schematic U2
|
||||
assignment `Carrier:MAX3243_Reference_Provisional` is preserved; clean parity
|
||||
is required, not waived. Original female CAD provenance is unchanged.
|
||||
- Nonisolated USB power arrangement explicitly accepted and reported already
|
||||
tested by user. **Successful operation does not establish backfeed safety**, in
|
||||
particular with simultaneous powered hosts. No isolation/backfeed protection
|
||||
was added or claimed.
|
||||
- Existing 3.3 V supply successfully powers the modules per user. This is operating
|
||||
evidence, not measured regulator current/thermal/transient headroom.
|
||||
- User confirms backside OLED I²C pull-ups and operation without extras. **Their
|
||||
resistance and termination rail remain unmeasured.** No values/rail are invented
|
||||
and no external I²C pull-ups added.
|
||||
- Current RF reserve and power/ground routing accepted based on visual and
|
||||
operational experience. This is not manufacturer RF qualification, measured
|
||||
signal/power integrity, regulator-margin proof, EMC testing or certification.
|
||||
|
||||
## Verification and remaining limits
|
||||
|
||||
Incremental routing starts from `hardware/PCB/validation/pcb-before-group-alignment.kicad_pcb`.
|
||||
It removes copper obstructed by moved pads, reconnects retained routes and trims
|
||||
KiCad-reported dangling stubs. **476/587 original copper items remain exactly
|
||||
unchanged**; 111 original items removed. No whole-board regeneration or route
|
||||
reset was used. `pcb-group-migration.json` records the removed UUIDs and repairs;
|
||||
`validate_group_alignment.py` checks the full parsed non-copper delta, retained
|
||||
copper UUID/content, body centrelines/gaps, and unchanged project settings.
|
||||
Original A0 and measured-U1 snapshots/reports are retained, not rebaselined.
|
||||
|
||||
Run from repository root:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B hardware/PCB/validate_schematic.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_pcb_draft.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_group_alignment.py
|
||||
/usr/bin/python3 -B hardware/PCB/validation/pcb-routing-check.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_u1_update.py
|
||||
/usr/bin/python3 -B hardware/PCB/validate_footprints.py
|
||||
/usr/bin/python3 -B hardware/PCB/reference/db9-cad/validate.py
|
||||
```
|
||||
|
||||
Validation completed with KiCad 10.0.6: schematic ERC **0 errors/warnings**;
|
||||
PCB DRC **0 physical violations, 0 unconnected items, 0 parity issues**. All
|
||||
17 connected nets, 9 electrical components, 44 U1 pins and 15 firmware GPIO
|
||||
contracts checked. Current routing: **938 segments, 41 vias**; unchanged width
|
||||
rules (0.25 mm signal / 0.50 mm power and ground), 0.70/0.30 mm vias. Independent
|
||||
minimum different-net/copper-to-NPTH gap **0.252762 mm**, minimum board-edge gap
|
||||
**1.900 mm**; RF reserve remains clear. Exact group/preservation and U1/silk
|
||||
regressions, two footprint tests and DB9 CAD/source checks passed. KiCad bindings
|
||||
emit known startup enum-property assertions but all checks complete successfully.
|
||||
|
||||
DB9 projections and 94 device-silkscreen items refreshed. Placement PDF (1:1),
|
||||
PCB SVG/PNG/3D and schematic PDF/SVG/PNG regenerated; PNG previews visually
|
||||
reviewed. The 3D view still lacks complete module models and is not assembly
|
||||
qualification. No removed PCB presentation/draft markers were reintroduced.
|
||||
|
||||
Current acceptance closes the user-review gates above. Still not independently
|
||||
qualified: regulator margin, USB backfeed safety, pull-up values/rail, RF/EMC,
|
||||
complete tolerance/height/case/actuator envelopes, finished-hole fabrication
|
||||
process and certification. Missing module courtyards remain a known model limit;
|
||||
DRC does not validate complete assembled-module or case fit. No new hardware
|
||||
tests, firmware changes, Gerbers, fabrication order or manufacturing approval.
|
||||
@@ -1,256 +0,0 @@
|
||||
# A1 top-left-notch / bottom-USB routing — current result
|
||||
|
||||
Paths in prose point to the retained hardware artifacts or neighboring development notes.
|
||||
Commands keep their stated working directory; repository-root paths remain root-relative.
|
||||
|
||||
Date: 2026-09-21. Actual revised board: `../../../../../hardware/PCB/serial-carrier.kicad_pcb`.
|
||||
|
||||
**38/38 connections across 17 multi-pad nets; 0 physical DRC violations,
|
||||
0 unconnected items; exactly 1 expected U2 footprint parity warning.**
|
||||
The warning is the board-only `Carrier:MAX3243_Reference_Provisional` assignment
|
||||
versus the intentionally blank schematic footprint, not a net mismatch.
|
||||
|
||||
## Geometry and preservation
|
||||
|
||||
Coordinates below are local to absolute origin (100,100) mm; all dimensions mm.
|
||||
|
||||
- Carrier: 82 x 80, outline `[(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)]`.
|
||||
- U1 origin: (8,23), formerly (8,4). Header end-pad centres now Y=76.340;
|
||||
USB guide Y=83 is 3 mm below the bottom edge. No ESP32 body was invented.
|
||||
- U2 origin: (43,26.670), formerly (43,30). Actual reference F.Fab bare-PCB
|
||||
front edge is absolute Y=100.000, flush with carrier local Y=0.
|
||||
- Nominal male metal external dimensions are user-confirmed: shell overhang
|
||||
3.870, hex-mouth overhang 2.870, flange front 1.930 behind the carrier edge.
|
||||
With this placement their local Y positions are -3.870, -2.870 and +1.930.
|
||||
Integration added seven board-local Dwgs.User graphics for the CAD metal
|
||||
projections and screw-lock axes.
|
||||
- H1: (3.5,22.5), H2: (3.5,75). H3/H4 unchanged at (78,4)/(78,76).
|
||||
- OLED, resistors and switches unchanged.
|
||||
- RF reserve: local X=10..31 / Y=19..32, absolute X=110..131 / Y=119..132,
|
||||
both layers; no tracks, vias or fills inside it. Boundary remains provisional.
|
||||
|
||||
The existing A0 board was mutated, **not regenerated**. The one-time
|
||||
`../../../../../hardware/PCB/validation/pcb-a0-before-reposition.kicad_pcb` backup remains byte-for-byte intact.
|
||||
`../../../../../hardware/PCB/validation/pcb-routing-input.kicad_pcb` is now the A1 unrouted audit input, not A0.
|
||||
`../../../../../hardware/PCB/validation/pcb-routing-preservation.py` verified all 13 complete serialized footprints
|
||||
identical except the four authorized top-level placements, including schematic
|
||||
fields, pad/net definitions, electrical UUID paths and all footprint graphics.
|
||||
Non-owned board-graphic UUIDs and unrelated graphics are preserved. ESP/RS/RF
|
||||
annotations moved with placement; the post-routing silkscreen refresh replaced
|
||||
91 owned graphics. Its edge clipping avoids silk across U2's flush front edge.
|
||||
|
||||
All board saves used `SaveBoard(..., True)`. Project `.pro`, `.prl`, schematic,
|
||||
symbol/footprint libraries and library tables retain their original hashes.
|
||||
README and pcb-draft-notes were intentionally updated during integration. The generator's constants/text were updated but it was **not
|
||||
run**; its automatic project/netclass writes were removed. PCB Editor lock was
|
||||
absent at migration, routing, silkscreen refresh and final preservation check.
|
||||
|
||||
## Routing and independent measurements
|
||||
|
||||
- F.Cu/B.Cu; **565 segments, 22 through vias**.
|
||||
- **451 signal segments at 0.25 mm; 114 +3V3/GND segments at 0.5 mm**.
|
||||
- Vias: 0.7 mm copper / 0.3 mm drill. No copper pours needed.
|
||||
- Continuous measured minimum different-net/NPTH clearance: **0.270544 mm**.
|
||||
- Minimum copper/NPTH-to-edge gap: **1.900 mm**.
|
||||
- Minimum copper/NPTH-to-RF-reservation gap: **0.025 mm**.
|
||||
- Minimum via-drill edge to other drilled-hole edge: **0.820544 mm**.
|
||||
|
||||
Two bounded passes, each with 240 seconds / 1,200,000 expansions per search:
|
||||
|
||||
1. 31.18 seconds, 38/38, 674 segments / 16 vias, physical DRC clean. One GND
|
||||
connection required the router's 0.25 mm fallback; strict width validation
|
||||
correctly failed. Retained as `../../../../../hardware/PCB/validation/pcb-routing-a1-pass1.kicad_pcb` for evidence.
|
||||
2. 32.44 seconds, 38/38, 565 segments / 22 vias. Short GND connections (<=15 mm)
|
||||
now route before longer signal fanout; long GND links remain last. All power
|
||||
and ground now use 0.5 mm; no fallback. This is the saved current result.
|
||||
|
||||
The grid remains 0.05 mm, physical clearance 0.25 mm plus 0.02 mm routing margin.
|
||||
No rip-up/unbounded retry or external network router was used.
|
||||
|
||||
## Validation / rerun commands
|
||||
|
||||
From `hardware/PCB`:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B validate_pcb_draft.py
|
||||
/usr/bin/python3 -B validation/pcb-routing-check.py
|
||||
/usr/bin/python3 -B validation/pcb-routing-preservation.py
|
||||
kicad-cli pcb drc --format json --all-track-errors --severity-all --exit-code-violations -o validation/pcb-routing-all-drc.json serial-carrier.kicad_pcb
|
||||
```
|
||||
|
||||
All passed on A1. Validators perform fresh KiCad DRC, fresh schematic-netlist
|
||||
comparison, geometry checks, continuous copper-distance checks, and routing
|
||||
save-refusal tests. DRC was read-only to preserve project settings; no copper
|
||||
zones exist to refill. Existing ignored DRC checks were not changed; see reports.
|
||||
The preservation audit includes the intentional documentation updates and seven
|
||||
CAD projection graphics while retaining electrical and project preservation checks.
|
||||
|
||||
Rerouting remains explicitly destructive to existing/manual copper and requires:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B route_pcb_draft.py --route --overwrite --replace-routes --seconds 240 --max-expansions 1200000
|
||||
/usr/bin/python3 -B add_device_silkscreen.py
|
||||
```
|
||||
|
||||
Then rerun the validators. The one-shot `pcb-routing-reposition.py --apply`
|
||||
refuses anything except the original A0 snapshot and must **not** be rerun on A1.
|
||||
|
||||
Current evidence: `../../../../../hardware/PCB/validation/pcb-routing-a1-pass2.log`, `../../../../../hardware/PCB/validation/pcb-routing-run.json`,
|
||||
`../../../../../hardware/PCB/validation/pcb-routing-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-all-drc.json`, `../../../../../hardware/PCB/validation/pcb-parity-drc.json`,
|
||||
`../../../../../hardware/PCB/validation/pcb-validation-summary.json`, `../../../../../hardware/PCB/validation/pcb-routing-check.json`,
|
||||
`../../../../../hardware/PCB/validation/pcb-routing-reposition.json`, and `../../../../../hardware/PCB/validation/pcb-routing-preservation.json`.
|
||||
The current post-integration board SHA-256 is recorded in
|
||||
`../../../../../hardware/PCB/validation/pcb-routing-check.json`; the CAD projection additions do not change copper.
|
||||
|
||||
**Still a draft, not fabrication-ready.** ESP32 geometry/antenna/USB access,
|
||||
module pinout/header fit, full assembly/height clearances, power integrity and
|
||||
hardware operation remain unqualified. Nominal RS-232 metal dimensions alone
|
||||
are not assembly qualification. Placement PDF, SVG/PNG and carrier-only 3D
|
||||
preview were refreshed after CAD envelope integration. No Gerbers generated.
|
||||
|
||||
---
|
||||
|
||||
# Historical A0 routing result — superseded by A1 above
|
||||
|
||||
The following records the earlier A0 routing/integration. Its dimensions, counts,
|
||||
measurements and no-save reproduction do not describe current A1. A0 `pass1`,
|
||||
`pass2`, `pass3`, `baseline` and `dry-run` files are historical; use the A1 and
|
||||
current reports listed above.
|
||||
|
||||
Date: 2026-09-20. KiCad 10.0.6; system `/usr/bin/python3`.
|
||||
|
||||
## Saved result
|
||||
|
||||
Actual copper routing is saved in `../../../../../hardware/PCB/serial-carrier.kicad_pcb`, not just a preview
|
||||
or an exported proposal. **38/38 required spanning-tree connections across 17
|
||||
multi-pad nets routed; KiCad reports 0 unconnected items, 0 errors, 0 warnings.**
|
||||
Intentional single-pad `unconnected-(...)` nets retain their original names and
|
||||
are not connected to anything else.
|
||||
|
||||
- Two copper layers, F.Cu and B.Cu.
|
||||
- 569 straight track segments: 425 at 0.25 mm for signals and 144 at 0.5 mm for
|
||||
+3V3/GND. No narrow power-track fallback was necessary.
|
||||
- 20 through vias, 0.7 mm copper diameter / 0.3 mm drill.
|
||||
- No copper pours: all ground connections were routed explicitly.
|
||||
- Original RF rule area, x110..131 / y100..113 mm on both copper layers, preserved.
|
||||
No tracks, vias or fills enter the reservation. Minimum copper-to-reservation
|
||||
gap measured independently is 0.025 mm; this is geometric compliance with the
|
||||
specified provisional boundary, **not validation of an antenna's RF needs**.
|
||||
- All 9 electrical footprints and 4 standalone mounting footprints preserved.
|
||||
Pad positions, shapes, sizes, drills, types, nets, functions, pin types and UUIDs;
|
||||
footprint placement, orientation, library IDs, values, UUIDs and schematic UUID
|
||||
paths; net table, outline and RF rule-area geometry/flags match the input.
|
||||
- Generator, README, schematic, project and libraries were not edited by routing.
|
||||
No external network dependency or manufacturing exports were used.
|
||||
|
||||
## Validation performed
|
||||
|
||||
From `hardware/PCB`:
|
||||
|
||||
```sh
|
||||
kicad-cli pcb drc --format json --refill-zones --save-board -o validation/pcb-routing-drc.json serial-carrier.kicad_pcb
|
||||
kicad-cli pcb drc --format json --all-track-errors --severity-all --exit-code-violations -o validation/pcb-routing-all-drc.json serial-carrier.kicad_pcb
|
||||
/usr/bin/python3 -B validation/pcb-routing-check.py
|
||||
```
|
||||
|
||||
Both DRC runs: **0 violations and 0 unconnected items**. No DRC settings or
|
||||
exclusions were added or changed. Existing project rules specify 0.25 mm minimum
|
||||
clearance, 0.5 mm copper-edge clearance, 0.25 mm minimum track width and 0.7 mm
|
||||
minimum via diameter.
|
||||
|
||||
Independent continuous-geometry audit (`../../../../../hardware/PCB/validation/pcb-routing-check.json`):
|
||||
|
||||
| Check | Measured minimum | Requirement |
|
||||
|---|---:|---:|
|
||||
| Different-net copper / copper-to-NPTH clearance | 0.275 mm | 0.25 mm |
|
||||
| Copper or NPTH edge to board edge | 0.525 mm | 0.5 mm |
|
||||
| Copper / NPTH edge to RF reserve | 0.025 mm | No intersection |
|
||||
| Via drill edge to another drilled hole edge | 0.840175 mm | 0.25 mm |
|
||||
|
||||
The audit uses exact straight-line/point/rectangle distance calculations with
|
||||
copper radii, rather than the router's raster masks. It also verifies dimensions,
|
||||
source-preservation invariants, and three overwrite-refusal cases. Requires
|
||||
locally installed `pcbnew`, NumPy and Shapely; the router itself only needs
|
||||
`pcbnew` and NumPy.
|
||||
|
||||
DRC still honors the project's pre-existing ignored checks:
|
||||
`missing_courtyard`, `track_not_centered_on_via`,
|
||||
`tuning_profile_track_geometries`, `footprint_filters_mismatch`, and
|
||||
`footprint_type_mismatch`. `--severity-all` does not enable ignored checks.
|
||||
The routing subtask compared electrical UUID linkage and pad-net assignments
|
||||
with the input board. Subsequent integration also ran fresh schematic-parity DRC
|
||||
and added missing symbol metadata fields to the PCB without changing copper.
|
||||
That check reports exactly one expected U2 footprint warning (provisional PCB
|
||||
footprint versus intentionally blank schematic assignment); no net mismatch.
|
||||
See `../../../../../hardware/PCB/validation/pcb-parity-drc.json` and `../../../../../hardware/PCB/validation/pcb-validation-summary.json`.
|
||||
|
||||
## Method and bounded passes
|
||||
|
||||
`../../../../../hardware/PCB/route_pcb_draft.py` uses a two-layer 8-direction raster A* search, an explicit
|
||||
via cost, exact-pad-centre terminal stubs, conservative foreign-copper and hole
|
||||
obstacles, and only collinear path compression. It preserves the L-shaped edge
|
||||
and RF reservation. Per-net minimum spanning trees connect every physical pad,
|
||||
including duplicate switch contacts; short links precede longer links and GND is
|
||||
last. It does not optimize return-current paths, topology or total via count.
|
||||
|
||||
1. Initial 0.1 mm raster / 0.04 mm safety margin / 250,000 expansions per search:
|
||||
29/38 connections; DRC 0 violations, 9 unconnected.
|
||||
2. Increased search allowance to 1,200,000 expansions: 30/38; DRC 0 violations,
|
||||
8 unconnected. Some raster header passages were blocked, not just expensive.
|
||||
3. Final 0.05 mm raster / 0.02 mm additional safety margin, with 0.25 mm physical
|
||||
clearance unchanged: 38/38 in approximately 26.5 seconds; DRC fully connected.
|
||||
Each pass had a 240-second routing deadline. No unresolved routing remains.
|
||||
|
||||
A KiCad Python binding ownership problem in the first replacement attempt caused
|
||||
an exception/segfault before any board save. Replacement now uses `board.Delete`
|
||||
instead of `board.Remove`; the subsequent save and no-save rerun both passed.
|
||||
The known startup PROPERTY_ENUM assertions do not prevent validation.
|
||||
|
||||
## Rerun safety
|
||||
|
||||
By default the script never saves. The existing routed board is protected even
|
||||
from a preview unless `--replace-routes` is given. A preview can be run with:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B route_pcb_draft.py --replace-routes --seconds 240
|
||||
```
|
||||
|
||||
This exact no-save workflow was tested: it again found 38/38 connections, 569
|
||||
segments and 20 vias, and left the board byte-for-byte unchanged.
|
||||
|
||||
**The following command discards all existing/manual tracks and vias**, then
|
||||
replaces them with fresh draft routing. Back up manual work first:
|
||||
|
||||
```sh
|
||||
/usr/bin/python3 -B route_pcb_draft.py --route --overwrite --replace-routes --seconds 240 --max-expansions 1200000
|
||||
```
|
||||
|
||||
The script refuses a PCB-editor lock, unexpected outline/RF/board structure,
|
||||
unsupported pad orientation/layers, unapproved existing routes, or detected board
|
||||
changes before save. Candidate serialization must preserve immutable geometry
|
||||
and linkage before replacing the board. It deliberately does not run DRC itself:
|
||||
rerun the validation commands after any routing change. Runtime/expansion limits
|
||||
can produce a partial board; inspect both the run report and actual DRC, not just
|
||||
the script exit status.
|
||||
|
||||
## Evidence and remaining limitations
|
||||
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-input.kicad_pcb`: unrouted input snapshot, after baseline DRC save.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-baseline.json`: 38 unconnected / no violations before routing.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-pass1-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-pass2-drc.json`: intermediate DRC.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-pass2.log`, `../../../../../hardware/PCB/validation/pcb-routing-pass3.log`: bounded routing progress.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-run.json`: final saved-route metrics and connection list.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-drc.json`, `../../../../../hardware/PCB/validation/pcb-routing-all-drc.json`: final KiCad DRC evidence.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-check.py`, `../../../../../hardware/PCB/validation/pcb-routing-check.json`: repeatable independent audit.
|
||||
- `../../../../../hardware/PCB/validation/pcb-routing-dry-run.log`: successful final no-save reproduction.
|
||||
|
||||
Routing-only DRC-saved snapshot SHA-256 (before integration copied schematic
|
||||
metadata fields): `bf94b6c9b00c43a97f933054758a2c9d6541981ae09f2f285dcdb567bd73d9b1`.
|
||||
The current post-integration hash is recorded by `../../../../../hardware/PCB/validation/pcb-routing-check.json`;
|
||||
geometry, pad-net linkage and copper are unchanged by the metadata copy.
|
||||
|
||||
**Still DRAFT / NOT FOR FABRICATION.** Provisional module/connector geometry,
|
||||
antenna envelope, assembly/courtyard clearances, USB access, male RS-232 connector
|
||||
fit, physical pin mapping, power integrity, EMI and hardware operation are not
|
||||
qualified by routing or DRC. All previously documented mechanical/electrical
|
||||
release gates remain. No physical-device or manufacturing validation performed.
|
||||
@@ -1,68 +0,0 @@
|
||||
# Bounded ordinary HTTPS idle retention
|
||||
|
||||
Source/build/test paths in code spans and commands are relative to the repository root
|
||||
unless a different working directory is stated; Markdown links are relative to this document.
|
||||
|
||||
Current owner/SDK contract. The user accepted idle cleanup as working; [overall acceptance](roadmap.md#phase8-acceptance-evidence) records current status and evidence limits. No individual unreported soak, fault or reserve result is inferred.
|
||||
|
||||
## Evidence and scope
|
||||
|
||||
User-authorized response to the admission capture: post-TLS occupancy **6/6, ordinary4 / serial2 / admin0**, ordinary connection ages **50–74 seconds**; admin ticket returned in **14 ms**, no subsequent TLS/upgrade observed, ticket unconsumed. These ages measure connection lifetime, not time since the last request. They support investigating retained ordinary sockets, not claiming every captured socket was idle. Installed HTTPD excludes its listening fd from `select` while full with LRU disabled. New connections can therefore wait before TLS or upgrade even when ticket issuance was fast.
|
||||
|
||||
This policy releases **expired idle ordinary HTTPS connections**, not arbitrary ordinary requests to make room. The policy uses the existing six sockets, LRU-disabled operation, one-second receive/send and five-second TLS-handshake timeouts without expanding task stacks or queue capacities. Current overall handler capacity is 39. No new task. UART0 recovery, independent USB access, serial broker ownership, tickets, authentication and both WebSocket transports are unchanged.
|
||||
|
||||
## Usage and timeout semantics
|
||||
|
||||
- Automatically enabled for every successfully started HTTPS server, including when diagnostic tracing is disabled and either optional transport is unavailable. No new console command, setting or credential migration.
|
||||
- **15 seconds of observed ordinary idle retention**, checked by a **one-second ESP timer** that requests work on HTTPD. The three existing five-second browser status-poll intervals leave room for normal keepalive reuse instead of a TLS handshake per request.
|
||||
- A new post-TLS connection that has sent no request gets the same full idle window. Time spent establishing TLS does not consume it.
|
||||
- The first owner sweep observing a new connection or a changed successful-request completion marker starts a fresh window. The marker covers every ordinary route, including login, assets, status, typed settings, tickets and keepalive errors that return successfully—not just diagnostic wrappers. Failed requests are deleted by the normal SDK path instead.
|
||||
- HTTPD pending bytes, TLS pending bytes or a readable TCP fd reset the observation window. Negative TLS pending results and `select` errors conservatively reset it too. No bytes are read or discarded by the probe.
|
||||
- At the threshold, after current SDK classification and zero-time readiness checks, HTTPD calls `shutdown(current_fd, SHUT_RDWR)`. A successful shutdown is latched; a failed call retries on the next probe. The SDK's subsequent read/delete path owns socket close, TLS destruction, diagnostic close notification and freeing the slot. The probe never calls `close`, overrides TLS cleanup, or queues a session-close pointer.
|
||||
- **WebSockets (serial and admin), closing WebSockets and async requests are exempt.** The check uses actual SDK flags, not diagnostic metadata. Successful explicit 101/classification and request cleanup finish before the owner can sweep, so there is no ordinary-idle interval during upgrade admission.
|
||||
- Under an available owner and timely successful work delivery, expect 15 seconds plus the initial observation delay (nominally up to one second), the next probe delay (nominally up to one second), and SDK read/cleanup latency. This is a conservative sampled idle policy, **not a strict wall-clock timeout or admission SLA**.
|
||||
|
||||
### Deliberate limitations
|
||||
|
||||
1. Parsing an incomplete request, synchronous response sends, leftover-body purge and TLS handshakes serialize on HTTPD. The probe cannot interrupt them. Existing per-read/per-send timeouts remain; a peer trickling input can extend overall processing beyond one timeout. This change does not provide a slowloris deadline or solve TLS/memory/global-socket pressure.
|
||||
2. Four continuously active ordinary connections plus two WebSockets can still fill all six slots. They are not evicted. Likewise, an admission attempt immediately after fresh saturation can hit the browser's existing 15-second timeout before the conservative window expires; the user may still need to retry. Older idle retained sockets are eligible on the next delivered probe.
|
||||
3. As with any HTTP keepalive timeout, bytes arriving **after** the last readiness check can race a shutdown. Already executing HTTPD requests/responses are protected; future client intent cannot be predicted. A client may need a new connection. No new application-level mutation retry/replay is added. Response completion here means synchronous HTTPD completion, not proof of peer receipt/TCP acknowledgement.
|
||||
4. `httpd_queue_work` with `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` disabled uses loopback UDP. A reported queue error releases the reservation and retries next tick. **A successful send is not an execution acknowledgement**: an accepted-but-lost control message leaves one reservation pending, disables further probes, and requires successful HTTPS stop/restart to restore probing. There is no speculative reservation timeout: it could accumulate delayed callbacks and violate the one-probe bound. This is explicitly regression-tested, not hidden behind a hard retention guarantee. HTTPD blockage or queue loss also cannot be repaired by raising sockets/LRU/timeouts here.
|
||||
5. A failed stop leaves cleanup detached and ownership retained for a later stop retry; it does not restart probes on a partially stopped server. Timer allocation/start failure gates HTTPS start with its error rather than silently starting without the policy. UART0/USB recovery is unaffected.
|
||||
|
||||
## Exact installed SDK audit
|
||||
|
||||
All SDK references below are under `~/.platformio/packages/framework-espidf/components/`; installed framework is ESP-IDF **5.5.0** (`3.50500.0`). Production private access remains solely in `src/web_httpd_adapter.c`, with the existing compile-time version guard extended to require idle-lifecycle re-audit.
|
||||
|
||||
- `esp_http_server/src/httpd_main.c`: `httpd_server` selects the listener only with capacity or LRU enabled. Control work runs **before** current data sessions, then accept. `httpd_process_session` skips async sessions and synchronously runs `httpd_sess_process`; errors delete the session before any subsequent owner work. Accept invokes TLS synchronously through `open_fn`.
|
||||
- `esp_http_server/src/httpd_sess.c`: `httpd_sess_process` calls `httpd_req_new`, then `httpd_req_delete`, and only after both return success assigns `session->lru_counter = ++hd->lru_counter`. This happens even with LRU disabled. New sessions zero the slot, including the marker; the global counter resets when all sessions are deleted. Application calls to `httpd_sess_update_lru_counter` are confined to verified serial-WebSocket send work (`web_serial_transport.c`), which the sweep exempts. This marker is not a timestamp and not an fd-generation token.
|
||||
- `esp_http_server/src/httpd_parse.c`: `httpd_req_new` synchronously parses and invokes the URI handler. `httpd_req_delete` drains any remaining body; cleanup clears `hd_req_aux.sd` and request pointers. A return from a diagnostic handler wrapper or a response-send call is earlier than this boundary. The sweep requires HTTPD's thread identity and no current `hd_req_aux.sd`, and skips `for_async_req`. Current ordinary handlers do not use async requests, out-of-owner sends, or unfinished chunked responses. Re-audit that contract if introduced.
|
||||
- `esp_http_server/include/esp_http_server.h` has no global synchronous post-request-cleanup hook. Its event notifications are not such a hook: `esp_http_server_dispatch_event` posts to the event loop. `HTTP_SERVER_EVENT_SENT_DATA` is emitted by `httpd_resp_send` and per `httpd_resp_send_chunk`, **before** handler return/body purge/cleanup. Send/receive overrides belong to HTTPS and do not expose a safe completion hook. URI matching and error handlers likewise cannot supply an all-route post-cleanup boundary.
|
||||
- `esp_https_server/src/https_server.c`: successful `httpd_ssl_open` stores the transport context and installs TLS send/recv/pending functions before synchronous `HTTPD_SSL_USER_CB_SESS_CREATE`. The application callback invalidates any old row for that fd even if the TLS pointer, socket-slot address and counter value were reused. If fd lookup unexpectedly fails, all observations reset conservatively. The close callback remains the diagnostic observer; SDK destruction remains intact. `httpd_ssl_pending` calls `esp_tls_get_bytes_avail` without consuming data; errors can post an existing SDK error event.
|
||||
- `httpd_sess_trigger_close` resolves fd to a raw reusable `sock_db *`, then queues `httpd_sess_close`. Its zero-counter/LRU guard does not prove the same connection still occupies that slot. **Not used by this policy.** Direct owner shutdown retains the slot until SDK read cleanup and has no deferred fd/pointer argument that could later close a replacement.
|
||||
- `httpd_queue_work` uses `cs_send_to_ctrl_sock` / `sendto` in `esp_http_server/src/util/ctrl_sock.c`. The actual generated config leaves `CONFIG_HTTPD_QUEUE_WORK_BLOCKING` undefined/off, selecting the nonblocking queue mode. The idle initializer explicitly rejects builds with that blocking option on. Successful `httpd_stop` waits for `THREAD_STOPPED`, frees HTTPD and ends possible old callback execution; failed stop is not a retirement boundary.
|
||||
|
||||
## Ownership and bounded storage
|
||||
|
||||
`src/web_httpd_idle.{c,h}` owns one persistent ESP timer, six static observation rows, lifecycle gate/generation and queued/submitting flags. It uses no request data, secret, dynamic per-connection allocation, payload buffer, new task or additional socket. Timer callback performs only short metadata locking and at most one queue submission; all private session access, readiness and shutdown run on HTTPD.
|
||||
|
||||
`src/web_server.c` serializes lifecycle. Prepare initializes rows **before** SSL startup; the TLS callback and sweep thereafter share the same owner. Attach publishes a nonzero, nonwrapping `uintptr_t` server generation, passed by value as opaque work argument—not a mutable shared descriptor or raw fd. At most one queued/executing probe is reserved; a separate submitting flag remains set until `httpd_queue_work` returns even if work already finished. That closes the callback-before-submit-return race.
|
||||
|
||||
Detach first prevents submissions, then waits at most one second for any submitting call to return. Fence timeout forbids SSL destruction, retaining the handle for retry. An already executing sweep may finish safely while stop waits for HTTPD. Only successful SDK stop retires a discarded queued reservation; restart gets a new generation even if the server handle is reused. A stale generation cannot sweep or clear a newer reservation. No counter wrap or generic off-owner session-list query is accepted.
|
||||
|
||||
`src/web_httpd_adapter.{c,h}` defines the six-row bound and 15-second policy and performs the version-pinned owner sweep. `src/CMakeLists.txt` adds only the new module. Diagnostics remains unchanged internally; server composes idle identity reset followed by existing diagnostic publication.
|
||||
|
||||
## Regression and resource limits
|
||||
|
||||
`tests/web_httpd_idle/run.py` exercises production lifecycle/sweep paths, installed SDK request cleanup, host socketpair/readiness/shutdown/fd reuse and deterministic TLS/timer/queue doubles. `tests/web_admin_transport/server_lifecycle.py` checks server composition. Historical host validation passed; these references do not claim a new run or real target scheduler/TLS timing.
|
||||
|
||||
Bounded storage: six observation rows (144 bytes), one persistent timer (installed non-profiled layout 32 bytes before allocator overhead), lifecycle/generation flags and one reserved probe. SDK control UDP/mailbox allocation is transient and separate. No per-connection payload/task/stack/socket growth. Actual HTTPD/timer margins and runtime overhead/reserve floors require target measurement, not host sizes. See [latest firmware resources](roadmap.md#phase8-acceptance-evidence).
|
||||
|
||||
## Regression procedure — not execution evidence
|
||||
|
||||
1. Start with diagnostic capture disabled: establish two serial WebSockets and ordinary HTTPS fetches. Verify idle ordinary sockets disappear after the observation window while both serial clients/lease remain unchanged. Enable capture only as needed to compare close/open occupancy; ages remain connection ages, not idle timestamps.
|
||||
2. Reproduce the original ordinary4/serial2 full-slot case; wait beyond the idle window, then issue/open admin. Record client ticket/TLS/upgrade timings and occupancy without recording tickets/cookies. Verify no repeat reload loop is needed for already-old idle saturation. Separately test fresh saturation and acknowledge the existing 15-second browser timeout limit.
|
||||
3. Leave status polling active for several minutes: no five-second TLS reconnect churn; both WebSockets and binary serial data/broker isolation survive. Repeat Settings/account-key operations, large assets, login/logout and two browser contexts. Compare with polling paused to distinguish genuinely idle slots.
|
||||
4. Slow incomplete headers/bodies, pipelined requests, slow response readers and slow/failed TLS handshakes: no probe-driven close of an executing response or admitted WebSocket. Record owner delays; do not infer an overall request deadline from the unchanged one-second receive timeout.
|
||||
5. Repeated close/reopen/fd reuse, full-mix stop/start and certificate rotation via supported UART0/SSH/browser lifecycle paths. Confirm UART0 and USB remain available, queued work never affects replacement connections, failed-stop retries retain ownership, and no start allocates a second server.
|
||||
6. Capture settled/loaded/post-cleanup internal/DMA/PSRAM values and HTTPD/ESP-timer minimum-free stack. Soak at the accepted full client mix. Investigate control queue loss separately if probing appears stuck; successful stop/start is the safe recovery, not an eviction/capacity increase.
|
||||
@@ -1,397 +0,0 @@
|
||||
# Implementation roadmap
|
||||
|
||||
Source/build/test paths in code spans and commands are relative to the repository root
|
||||
unless a different working directory is stated; Markdown links are relative to this document.
|
||||
|
||||
This document tracks the implementation and hardware-validation plan for the ESP32 Serial Swiss Army Knife. It describes intended sequencing rather than a release schedule. Completion requires implementation, documentation and build validation, plus relevant hardware tests or an explicitly recorded user waiver. A waiver is not a hardware-test pass.
|
||||
|
||||
## Status legend
|
||||
|
||||
- **Complete** — implemented and accepted, with target-hardware validation or an explicit phase-specific user waiver recorded below.
|
||||
- **In progress** — implementation or validation is actively underway, but the overall phase is not complete.
|
||||
- **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks.
|
||||
- **Planned** — accepted project direction, not yet implemented.
|
||||
- **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment.
|
||||
|
||||
## Project-wide design constraints
|
||||
|
||||
These constraints apply across all phases:
|
||||
|
||||
- UART0 remains the trusted physical administration and recovery console.
|
||||
- UART1 and the MAX3243 provide the single physical RS-232 data port.
|
||||
- All serial transports use the central session broker: one writer, multiple observers.
|
||||
- A slow or failed client must not block UART reception or unrelated clients.
|
||||
- Serial data paths remain binary-transparent; transports must not reserve in-band control sequences.
|
||||
- Network serial access must be authenticated and encrypted. No unauthenticated plaintext TCP serial service is planned.
|
||||
- Secrets must not be exposed by routine status output, command completion, or logs.
|
||||
- Resource use must remain bounded and observable on the ESP32-S3 N16R8 target.
|
||||
- New work must preserve USB CDC and UART0 recovery when Wi-Fi or a network service fails.
|
||||
- Project code remains `GPL-3.0-only`; third-party license notices and compatibility must be preserved.
|
||||
|
||||
## Phase overview
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|---|---|---|
|
||||
| 0 | RS-232 hardware characterization and diagnostics | **Complete** |
|
||||
| 1 | Persistent UART1 serial-service foundation | **Complete** |
|
||||
| 2 | Transport-neutral session broker | **Complete** |
|
||||
| 3 | Native USB CDC-ACM serial transport | **Complete** |
|
||||
| 4 | Wi-Fi station/AP foundation and network diagnostics | **Complete** |
|
||||
| 5A | Authenticated HTTPS administration foundation | **Complete** |
|
||||
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
|
||||
| 6 | Authenticated SSH serial transport | **Complete** |
|
||||
| 7 | Local display and button interface | **Complete** |
|
||||
| 8 | Role-based users and administrative access | **Complete** |
|
||||
| 9 | Small intermittent-use security baseline | **Complete (user signoff 2026-09-18; new hardware check waived)** |
|
||||
| 10 | Simple admin web firmware upload | **Complete (explicit user acceptance 2026-09-18; upload and normal operation verified)** |
|
||||
| 12 | Dual-stack networking and local service discovery | **Complete (explicit user validation; fresh boot and full client mix at 230400 baud)** |
|
||||
|
||||
|
||||
## Completed phases
|
||||
|
||||
### Phase 0 — RS-232 hardware characterization
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Central board pin profile for the ESP32-S3 N16R8 and MAX3243 breakout.
|
||||
- MAX3243 enable, static driver, receiver, valid-voltage, and modem-signal checks.
|
||||
- UART loopback across supported framing and baud-rate combinations.
|
||||
- Hardware CTS transmit gating and RTS receive-backpressure tests.
|
||||
- Exclusive RS-232 port ownership and fault-safe cleanup.
|
||||
- RGB status indication for diagnostic state.
|
||||
- Phase 0 hardware diagnostics, including signal status, are isolated under the `debug` CLI submenu.
|
||||
|
||||
Detailed procedures are in [Electrical tests](../electrical_tests.md) and [Hardware wiring](../wiring.md).
|
||||
|
||||
### Phase 1 — Serial-service foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Persistent, versioned UART configuration with safe defaults.
|
||||
- UART1 lifecycle management and exclusive ownership.
|
||||
- Nonblocking RX and TX software streams.
|
||||
- Baud rate, data bits, parity, stop bits, RTS/CTS, DTR policy, and RTS threshold controls.
|
||||
- Runtime counters and safe restart after working-configuration changes.
|
||||
- Explicit save/load/default/reset behavior; edits are not silently persisted.
|
||||
|
||||
### Phase 2 — Session broker
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Up to eight generation-safe broker clients.
|
||||
- Exactly one writer lease with multiple observers.
|
||||
- Bounded per-client output and event queues.
|
||||
- Slow-observer isolation and drop accounting.
|
||||
- Writer grant, release, denial, revocation, and forced administrative reassignment.
|
||||
- DTR `on-connect` integration.
|
||||
- Partial-transfer and retryable no-progress semantics for transports.
|
||||
- Broker payload storage placed preferentially in PSRAM while FreeRTOS control structures remain in internal RAM.
|
||||
|
||||
### Phase 3 — Native USB CDC-ACM
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Native ESP32-S3 USB CDC-ACM serial transport.
|
||||
- Automatic broker connection when the host opens the port with DTR asserted.
|
||||
- Opportunistic writer acquisition with observer fallback.
|
||||
- Binary-transparent bidirectional serial data.
|
||||
- Host line-coding visibility for diagnostics; UART1 remains under explicit serial configuration control.
|
||||
- Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls.
|
||||
|
||||
### Phase 4 — Wi-Fi foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Four persistent station profiles with priority ordering.
|
||||
- WPA2/WPA3 mixed mode and WPA3-SAE-required mode.
|
||||
- Explicit no-echo secret entry through UART0.
|
||||
- AP policies `off`, `fallback`, and `always`.
|
||||
- Random persistent fallback-AP password and MAC-derived default SSID.
|
||||
- Bounded profile attempts, DHCP deadlines, failover, and retry backoff.
|
||||
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
|
||||
- Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`.
|
||||
|
||||
Post-validation enhancements implemented; regression validation pending:
|
||||
|
||||
- Edits to disabled station profiles are staged without restarting the active radio; enable-state changes and edits affecting enabled radio policy apply asynchronously.
|
||||
- Configurable STA-only `sak-<suffix>.local` mDNS hostname with an independent versioned NVS record, live hostname updates, automatic withdrawal/restoration across STA IPv4 transitions, and nonfatal failure isolation. The responder initializes at most once and keeps its task stack internal while general metadata prefers PSRAM.
|
||||
|
||||
A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP.
|
||||
|
||||
### Phase 5A — Authenticated HTTPS foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS.
|
||||
- Persistent random shared `admin` credential, later retained only as migration/recovery material by Phase 8.
|
||||
- Persistent device-specific ECDSA P-256 certificate and private key.
|
||||
- Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery.
|
||||
- Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths.
|
||||
- Credential rotation revocation hooks shared with WebSocket and SSH sessions.
|
||||
|
||||
### Phase 5B — Offline browser terminal
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Vendored xterm.js and FitAddon assets; no Internet or CDN dependency.
|
||||
- CSP-compatible page structure with scripts served as separate resources.
|
||||
- Authenticated status and one-time WebSocket-ticket APIs.
|
||||
- Binary WebSocket serial frames and broker writer/observer integration.
|
||||
- Two bounded browser-terminal sessions.
|
||||
- Writer request/release controls and role indication.
|
||||
- Bounded TLS/socket behavior and slow-observer isolation.
|
||||
- Viewport-constrained terminal fitting without recursive page growth.
|
||||
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
||||
|
||||
Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected.
|
||||
|
||||
### Phase 6 — Authenticated SSH serial transport
|
||||
|
||||
Implemented and target-hardware validated:
|
||||
|
||||
- wolfSSH on TCP port 22 with a separate persistent ECDSA P-256 host key.
|
||||
- Two bounded interactive shell/PTY sessions; no `exec`, file transfer, forwarding, or subsystems.
|
||||
- Authenticated, binary-transparent broker forwarding with opportunistic writer acquisition and observer fallback.
|
||||
- Bounded handshake attempts/deadlines, receive work, buffers, and a single wolfSSH owner task pinned to CPU 1.
|
||||
- UART0 lifecycle, session, counter, and host-key administration.
|
||||
- PSRAM-preferred allocations with internal fallback and internal task stacks.
|
||||
- Bounded broker/WebSocket work, software mbedTLS AES, and software wolfCrypt AES/SHA to avoid the validated shared-hardware-crypto/PSRAM watchdog failure while retaining ESP-IDF SHA/MPI acceleration.
|
||||
- Concurrent USB, WebSocket, and SSH operation validated with USB and SSH alternately holding the writer lease.
|
||||
|
||||
### Phase 7 — Local display and buttons
|
||||
|
||||
Implemented and target-hardware validated:
|
||||
|
||||
- Optional 128×64 SSD1315-compatible I²C OLED on GPIO11/12 and active-low buttons on GPIO10/13/14; assignments remain centralized in `board_pins.h`.
|
||||
- Display service with a static framebuffer, bounded dirty-page commits, and separate yellow status rows 0–15 plus blue content rows 16–63.
|
||||
- Low-priority status/control task built from copied public snapshots; no service lock is held across I²C and the UI never becomes a broker client.
|
||||
- Overview, RS-232, broker, and network/service pages with no credential or key material.
|
||||
- Confirmation-protected local controls for service lifecycle, Wi-Fi reconnect/profile rotation, writer release, display off, and reboot; the UI cannot assign a writer or edit secrets/configuration.
|
||||
- Persistent dim/off settings, bounded boot animation, consumed wake presses, stuck-button quarantine, and one-shot confirmations.
|
||||
- Missing, disconnected, or unresponsive display hardware remains nonfatal and can recover through one bounded reprobe.
|
||||
- Concurrent serial, UART0, USB, WebSocket, SSH, and injected display/button fault behavior validated. See [Electrical tests](../electrical_tests.md) and [Hardware wiring](../wiring.md).
|
||||
|
||||
### Phase 8 — Role-based users and administrative access — Complete
|
||||
|
||||
Implemented and accepted: a bounded, persistent user system replaces the single shared network credential. Both roles can use the authenticated web serial/status interface. Over SSH, `user` routes to the broker-backed serial stream while `admin` routes exclusively to the administration shell and receives no broker client or writer lease. UART0 remains the physical recovery and bootstrap authority.
|
||||
|
||||
Completed implementation (8A–8C target-hardware validated; 8D explicitly accepted at 8D.22 on 2026-09-13):
|
||||
|
||||
1. **Phase 8A — User database and UART0 administration — Complete**
|
||||
- Versioned NVS database for up to eight `user`/`admin` accounts, random account IDs and authentication generations, final-admin protection, and three Ed25519/P-256 keys per account.
|
||||
- Salted PBKDF2-HMAC-SHA256 password verifiers, bounded no-echo entry, one-time generated passwords, and secret-free account/key status.
|
||||
- Historical migration/bootstrap supported the initial cutover. Current firmware instead commits missing storage empty, provisions the first admin with normal UART0 `user add`, and provides UART0-only unavailable-database recovery; legacy credential/bootstrap commands are removed.
|
||||
- Migration, bootstrap, CRUD, persistence, and command basics target-hardware validated; the full fault-injection matrix remains regression coverage. NVS remains unencrypted and offline guessing remains possible.
|
||||
2. **Phase 8B — Role-aware HTTPS and SSH authentication — Complete**
|
||||
- HTTPS and SSH authenticate through the common database and copied secret-free principals; unavailable user storage fails closed. The initial HTTPS Basic path was replaced by cookie authentication in 8D.
|
||||
- One-time principal-bound WebSocket tickets and ongoing principal-currentness checks prevent stale admission or input.
|
||||
- Account mutations request targeted WebSocket/SSH revocation; authentication generations provide fail-safe invalidation while unrelated accounts remain connected.
|
||||
- Password/key login, ticket behavior, targeted revocation, recovery, and concurrent transport operation are target-hardware validated. Legacy credentials were subsequently removed without changing established TLS identity or valid users.
|
||||
3. **Phase 8C — SSH administrative shell — Complete**
|
||||
- Authenticated `admin` SSH shell sessions route to a bounded administration worker and never create a broker client or acquire a serial writer lease. Normal `user` sessions retain the existing broker-backed serial stream.
|
||||
- UART0 and admin SSH now submit complete lines to one fixed-length request queue. A single dispatcher task is the sole caller of ESP-IDF's non-reentrant `esp_console_run()` and therefore executes the same registered command handlers for both entry routes. The former separately implemented reduced SSH command dispatcher has been removed.
|
||||
- The worker uses fixed per-session command/input and output buffers. Queue records contain copied secret-free principals and generation-tagged session tokens; late work is discarded after disconnect, slot reuse, role change, password/key mutation, or deletion. Task-local standard streams route canonical handler output into the applicable bounded SSH ring, and only the SSH owner task calls wolfSSH APIs.
|
||||
- Transport-neutral bounded prompts now support interactive user passwords/keys and Wi-Fi secrets over admin SSH without exposing hidden input or allowing another command while a prompt is active. Ping callbacks enqueue typed bounded events and the dispatcher alone formats their output. Four-entry per-session history and whole-line Tab completion are RAM-only and wiped on disconnect.
|
||||
- Authenticated administrators receive the operational registry, including HTTPS material rotation/reset, reboot, ping, and SSH lifecycle/session/host-key mutation. Self-terminating reboot and SSH actions are deferred until acknowledgement output drains, block further shell input, and execute through existing synchronous owner APIs from a separate bounded control task. First-admin provisioning and explicit `user recover --force` remain physical-UART0 operations (`user bootstrap` was later removed); admin SSH also rejects generating a replacement password for its own account.
|
||||
- `ssh sessions` and `ssh counters` identify broker versus admin-console routes, worker command state, queued admin output, admission failures, and input backpressure. `exit` and Ctrl+D on an empty command line request bounded deferred self-disconnect after best-effort application-buffer draining. Admin sessions are checked for a current `admin` principal before command execution and during the active-session reconciliation.
|
||||
- Keep SFTP, SCP, `exec`, forwarding, subsystems, and unauthenticated shells disabled.
|
||||
- Target-hardware validation passed for route separation, history/Tab editing, interactive visible/hidden prompts, output/backpressure, generated and entered user/password/key management including the longest ECDSA P-256 import, ping event routing, deferred reboot/SSH lifecycle drain behavior, bootstrap/recovery rejection, targeted self/other-user revocation during queued work, UART0/SSH administration serialization, and concurrent USB/WebSocket/user-SSH/admin-SSH operation. Stress at 460800 baud with SSH and WebSocket clients caused substantial expected packet drops and slower display controls, but did not exhaust memory or require lowering the supported baud-rate range.
|
||||
4. **Phase 8D — Integrated web administration — Complete**
|
||||
- User explicitly signed off tested firmware at **8D.22 on 2026-09-13**, superseding earlier per-slice pending acceptance/review gates. Cookie login/logout replaces Basic; bounded digest-only sessions, same-origin/CSRF checks, principal currentness and targeted revocation protect serial and admin routes.
|
||||
- Admin-only Serial/Admin selection and Settings preserve connected terminal observation and serial writer ownership. The browser shell shares the serialized UART0/admin-SSH dispatcher, with bounded output, explicit admission and retained frontend restrictions—not unrestricted shell parity.
|
||||
- Typed Serial, Accounts/password/authorized-key, Network/Wi-Fi/mDNS, Display, Broker and SSH settings; confirmed HTTPS stop/restart/reboot and HTTPS/SSH identity rotation. Canonical owners compare reserved service/identity/target generations, preserve commit/failure semantics and expose no stored secrets. Admitted work is not cancelled by later logout/timeout; uncertain mutations are never automatically replayed.
|
||||
- Accessible Serial/Wi-Fi/client/writer contextual controls reuse existing controllers. Writer transfer is explicit and generation-safe; one writer, isolated observers, binary transparency, UART0 administrative recovery and network-independent native USB UART1 remain invariants.
|
||||
- Scope removed by user: **8D.15** dedicated typed network diagnostics (shell diagnostics retained) and the unimplemented **8D.19** ordinary browser-session/native-USB controls (SSH controls retained). Browser identity reset/recovery/export is excluded; canonical recovery remains available.
|
||||
- Acceptance evidence and legacy storage compatibility are condensed below. [Web administration](web_administration.md) retains current API/owner/recovery contracts; [regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) retain practical checks, not claims of execution.
|
||||
|
||||
#### Phase8 acceptance evidence
|
||||
|
||||
**8D.22 signoff, 2026-09-13:** the user explicitly accepted the tested firmware, superseding earlier per-slice target, review and integration gates for the retained scope above. Earlier scoped acceptance covered browser login/logout, shared administration, Serial/account/Network presentation and legacy cleanup. Prior host regression suites and independent reviews were reported passing; this consolidation asserts no new build or test execution.
|
||||
|
||||
Prior final Phase 8 build: **PASS, 100,556 B linked RAM / 1,828,573 B flash, CPU 160 MHz**. The combined binary WebSocket-send fix was separately accepted at **160 MHz / 230400 baud with the full client mix including browser admin**; retain combined send and bounded failed-send isolation, not the earlier frequency-only experiment. Ordinary HTTPS idle cleanup was accepted, not every possible admission failure or owner delay.
|
||||
|
||||
Latest reported Phase 8 telemetry (bytes):
|
||||
|
||||
| Sample / capability | Free | Minimum free | Largest block |
|
||||
|---|---:|---:|---:|
|
||||
| Boot internal 8-bit | 59,808 | 58,840 | 31,744 |
|
||||
| Boot internal DMA | 52,052 | 51,084 | 31,744 |
|
||||
| Boot PSRAM | 8,196,968 | 8,183,972 | 8,126,464 |
|
||||
| Loaded after burst, internal 8-bit | 31,508 | 2,052 | 18,432 |
|
||||
| Loaded after burst, internal DMA | 23,752 | 460 | 18,432 |
|
||||
| Loaded after burst, PSRAM | 8,136,624 | 8,065,972 | 7,995,392 |
|
||||
|
||||
Loaded SSH minimum-free stack: **15,028 B**. This sample had two SSH sessions (serial/admin), two serial WebSockets and USB, with SSH as writer. Browser admin had been used then closed; it was **not active in this capture**. Web send/queue/protocol and SSH IO errors were zero; one SSH handshake failure and one session revocation were reported without an established cause.
|
||||
|
||||
**Limits, not reopened acceptance gates:** missing latest broker/serial counters prevent an exact zero-drop or byte-integrity inference. No latest browser-admin-active full-mix, individual fault-injection, exact duration, soak, cleanup-cycle or reserve-floor result is inferred. Internal/DMA lifetime minima remain a nonblocking transient-headroom follow-up, not approved reserves or proof of allocation failure: capability pools overlap and per-region minima may be non-simultaneous, so do not sum them. HTTPD/dispatcher stack margins, correlated peaks and allocation-reserve policy remain unmeasured/unapproved here. SDK TLS `-0x004C` means NET_RECV_FAILED, not an OOM diagnosis; stale cookies as the cause of two boot authentication failures remain unconfirmed.
|
||||
|
||||
#### Phase8 legacy credential compatibility
|
||||
|
||||
Cleanup was accepted **2026-09-08**, with unchanged certificate fingerprint and usable preexisting test users. This is not an unreported provisioning, recovery, power-loss or all-key pass.
|
||||
|
||||
- **Users:** missing `user_db/database` is committed as an empty v1 database, with no legacy account/password import. First-admin provisioning is physical UART0 `user add <username> admin` (optional `--generate`); entered passwords use hidden confirmation and generated passwords appear once. UART0-only `user recover --force` rebuilds only an unavailable database empty, refuses healthy databases (including empty ones), and leaves other configuration and TLS/SSH identities untouched; follow with normal `user add`.
|
||||
- Valid existing v1 user bytes load without rewrite or changes to accounts, roles, IDs, authentication generations, verifiers or authorized keys; migrated `user` accounts are not promoted. Private `v1_admin_marker` preserves the old wire byte and is derived from admin count on mutation, not a new role/schema/public bootstrap state. Public bootstrap and legacy migration/synchronization APIs are removed; final-admin protection, conditional mutations, principal currentness and targeted revocation remain.
|
||||
- **TLS:** at unchanged `web_sec/material`, a private reader validates the **1,392-byte v1** layout and migrates to **1,340-byte TLS-only v2**, preserving exact private-key DER, certificate DER, SHA-256 fingerprint and material generation—not rotating identity. The candidate is validated and committed before publication; temporary credential-bearing input is wiped. Malformed/unknown material, read/crypto failures and migration write/commit failures fail closed without regeneration or overwrite; truly missing material may be generated normally.
|
||||
- `web credentials show`, `web credentials rotate --force` and `user bootstrap` are removed. `web certificate info` / `web certificate rotate --force` retain frontend policy. `web reset --force` replaces TLS identity only, not accounts/passwords/verifiers; replacement/restart may close browser sessions and require renewed certificate trust/login, without revoking unrelated SSH sessions. No public legacy credential reader, display, rotation or authentication path remains.
|
||||
- **Downgrade/security:** older v1-only firmware cannot read v2 HTTPS storage; user-database v1 compatibility does not make HTTPS downgrade safe or restore removed credentials. Logical NVS replacement is **not secure flash wiping**: historical plaintext legacy credentials may remain, Wi-Fi secrets and TLS/SSH private keys remain unencrypted, and PBKDF2 verifiers permit offline guessing. No erase is required or authorized by cleanup.
|
||||
- Prior host tests used NVS fault doubles and a generated identity wrapped in the historical layout, **not an independently captured old-device fixture**; they do not prove flash durability or power-loss behavior. Focused runners: `tests/web_security/run.py`, `tests/admin_console_boundary/accounts.py`, `tests/admin_ssh_policy/run.py`. See [legacy-removal regressions](user_administration_tests.md#current-legacy-removal-regression-procedure).
|
||||
|
||||
### Phase 9 — Security and production hardening
|
||||
|
||||
**Complete — user signoff 2026-09-18; new hardware check explicitly waived.** Reduced scope: an intermittently powered wireless serial cable on a trusted network, not a permanently exposed Internet service. The earlier extensive Phase 9 implementation was abandoned in favor of the accepted pre-Phase-9 baseline. Short exposure reduces risk; it does not make upstream defects harmless or certify this firmware secure.
|
||||
|
||||
Small implementation:
|
||||
|
||||
- Keep the baseline PlatformIO/ESP-IDF and managed-library versions and upstream sources. No generated dependency patches, custom SSH state machine, crypto-policy replacement, allocator hooks, or SDK migration in this phase.
|
||||
- Explicitly disable core dumps and select silent panic reboot through standard `sdkconfig.defaults` options. Keep ordinary logging; reduced crash diagnostics are a deliberate trade-off. Existing generated configurations must be checked because defaults do not override saved selections.
|
||||
- Follow the [operational checklist](../security_operations.md): unique credentials, trusted identity verification, no Internet forwarding, networking off/power off after use, and existing rotation/recovery procedures.
|
||||
- Review upstream maintenance releases occasionally and before planned upgrades. Evaluate reachable issues against actual use; any stock dependency upgrade is a separate build/device-tested task, not a reason to accumulate local backports. Preserve existing license notices.
|
||||
|
||||
**Excluded:** secure boot, NVS/flash/PSRAM encryption, eFuse changes, partition changes, exhaustive CVE/zeroization audits, new authentication throttling, and production certification. Existing authentication, session isolation and recovery behavior remain unchanged. There is no protection against physical extraction or replacement of firmware. Phase 10 uses administrator-selected firmware, without a separate image-signing system.
|
||||
|
||||
**Host/build evidence (2026-09-18):** normal `pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the original 20241119 toolchains: **94,196 B linked RAM / 1,828,565 B flash**. Generated configuration confirms no dumps and silent panic reboot; compilation inputs contain none of the abandoned Phase 9 source overlays or wolf crypto guards. No device operation or target validation was performed.
|
||||
|
||||
**Acceptance (2026-09-18):** the user signed off this small Phase 9 and explicitly waived a new whole-phase hardware check of boot, UART0 recovery, native USB serial, HTTPS/SSH login and normal serial sharing. Application code is unchanged, **but crash-diagnostic defaults changed**; this is a waiver, not evidence of target or panic-path validation. The prior build/configuration evidence above remains the validation record; no new hardware test was performed. A separately controlled panic check with synthetic secrets remains optional, not a completion gate; do not add a production panic endpoint or erase flash. Disabling dumps does not erase old contents.
|
||||
|
||||
### Phase 10 — Simple admin web firmware upload
|
||||
|
||||
**Complete — explicit user acceptance on 2026-09-18: firmware upload works and normal operation is verified.** The admin **Settings → HTTPS / Reboot** firmware card uploads the locally built `.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`. Install this OTA-enabled firmware by wire first, then use web application uploads. There is no release server, automatic download or update service. [Firmware update and recovery](#phase10-update-and-recovery) below covers operation; [regression guidance](#phase10-regression-guidance) covers future checks.
|
||||
|
||||
Implemented scope:
|
||||
|
||||
- Raw `POST /api/firmware` requires exact `Content-Type: application/octet-stream`, known exact `Content-Length`, cookie, same-origin Origin and `X-CSRF-Token`; no multipart/JSON, Basic authentication or ordinary `user` access. Reject queries, ambiguous headers, chunked transfer and `Expect`; UI code leaves browser-managed headers alone. Admission precedes body/flash work; session/principal currentness is rechecked before boot selection. The card confirms upload/reboot and shows progress/results without automatic retries. HTTPD capacity is 40 method/path handlers.
|
||||
- Standard ESP-IDF OTA APIs (`esp_ota_begin`, `esp_ota_write`, `esp_ota_end`, `esp_ota_set_boot_partition`) stream one upload through a 4 KiB buffer into the inactive slot (two 4 MiB app slots). A reboot task with a 2 KiB stack is preallocated before erase. Lifecycle/identity reservations and ordinary software-reboot gating exclude competing work; no custom flash protocol or whole-image RAM buffer.
|
||||
- Accepts an ESP32-S3 application binary for this board/layout, not an ELF, ZIP, merged full-flash image, bootloader or partition-table image. Browser hints require a nonempty `.bin` of at most 4 MiB; the server bounds raw length against actual destination capacity before trusting HTTPD's narrowed length. Checks completeness, target/header and exact SDK image length/integrity, including mandatory appended SHA-256, before selection. The digest is not publisher authentication; neither it nor filename checks prove board compatibility or a working application.
|
||||
- Before selection, rejected/incomplete uploads and receive/write/validation failures abort any live OTA handle without selecting the candidate; the inactive slot may be erased/partially written. Boot-metadata failure (`firmware_commit_failed`) needs inspection and carries SDK transactional uncertainty. `200 {"ok":true,"rebooting":true}` means validation/selection succeeded; successful synchronous send schedules restart after 500 ms, retaining reservations, but proves neither browser receipt nor boot success. Response failure after selection schedules no restart, releases reservations for manual reboot and latches further admissible uploads to 409 `firmware_selected_reboot_required`; the latch survives HTTPS stop/start until device reset.
|
||||
- Synchronous HTTPD receive/flash blocks other HTTPD work: browser sessions can stall/drop. The 120-second total receive-loop and ten-second stall checks use the existing one-second socket timeout, not an absolute deadline: synchronous SDK erase/write/validation and scheduling are not preempted. The browser's 180-second timeout cannot cancel committed work. UART0/native USB remain independent paths, not guarantees of uninterrupted serial timing during flash; reboot interrupts all transports. Software exclusion cannot prevent physical reset, power loss or panic.
|
||||
- **Preserve NVS:** write only the inactive application slot and the OTA selection metadata. Do not erase the chip, rewrite the partition table/bootloader, or touch `nvs`, `nvs_key`, PHY, storage or other data partitions. Existing users, passwords, Wi-Fi/serial settings and HTTPS/SSH identities remain stored, as with an application-only wired update without erase. This preserves stored bytes; the uploaded firmware must still understand the existing schemas and must not itself erase/migrate them incompatibly.
|
||||
- Keep wired USB-to-UART flashing documented as recovery if the uploaded application does not boot or no longer serves the web UI. Basic image validation cannot prevent a valid but broken application from requiring wired recovery.
|
||||
|
||||
**Not in scope:** secure boot, image signatures/signing-key management, anti-rollback/version-downgrade enforcement, automatic rollback/post-boot health-confirmation machinery, remote release discovery, partition migration or NVS backup/restore. Use upstream OTA support without dependency patches. The administrator is responsible for selecting trusted, compatible firmware, including when deliberately installing an older build.
|
||||
|
||||
**Historical integration build and host evidence (parent-reported):** `pio run` **PASS**, **94,220 B linked RAM / 1,847,645 B flash**. Against Phase 9's 94,196 B / 1,828,565 B, this is **+24 B RAM / +19,080 B flash**. Final backend 88 cases plus the pinned SDK begin/abort failure-contract test, UI 169 groups, server lifecycle 44, admin transport 25, console lifecycle, SSH runtime and cookie lifecycle checks passed. Additional cookie base/admin/display/lifecycle, HTTPD idle cleanup 18, and SSH management/runtime/security checks passed; the extra cookie `--admin` run initially lacked a reboot symbol in its fixture, corrected in test-only code before passing. Host doubles are not target evidence. The later concise-UI fix was copy-only; its latest reported UI regression passed all **169 groups**, with no rebuild after the text change. The build figures above remain historical; this documentation update ran no build or tests.
|
||||
|
||||
**Acceptance (2026-09-18):** after the firmware upload implementation and concise-UI fix, the user confirmed: “That works perfectly. And the usual operation is also verified.” Phase 10 is complete by this explicit acceptance of working upload and verified normal operation. Do not infer specific fault-injection, NVS before/after comparisons, power-loss or wired-recovery passes. The [regression guidance](#phase10-regression-guidance) below is reusable future guidance, not an acceptance blocker.
|
||||
|
||||
#### Phase10 update and recovery
|
||||
|
||||
1. Keep **USB-to-UART** available for UART0 administration/flashing; native USB CDC is network-independent UART1 access, not administration. Install the updater by wire first: older firmware without the route cannot install its own first web updater. The custom OTA layout is required; [one-time old-layout migration](../getting_started.md#partition-migration-and-erasing) is separate and destructive.
|
||||
2. Build trusted, schema-compatible `esp32-s3-devkitc-1-n16r8` firmware with `pio run`; choose **`.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin`**, never a merged image or a renamed non-application file. Observe the [downgrade warning](#phase8-legacy-credential-compatibility).
|
||||
3. Save desired RAM-only settings and record nonsecret configuration/public HTTPS/SSH fingerprints. Arrange a maintenance window and stable power/network; sign in as `admin` over trusted HTTPS, open **Settings → HTTPS / Reboot → Firmware update**, choose the file and confirm **Upload and reboot…**.
|
||||
4. Wait for validation/reboot, not just 100% transmitted bytes. Restore network reachability, reload and explicitly sign in; verify the running application, saved configuration and identities before another upload.
|
||||
5. A lost response, timeout, page close, sign-out or browser abort proves neither cancellation nor failure. The UI locks uncertain outcomes without replay; reload clears only browser locks, not the device latch. Inspect through UART0 and deliberately reboot if appropriate: any later reset can boot an already selected image.
|
||||
6. For initial wired installation or recovery when boot/HTTPS fails, connect USB-to-UART, select a known-good compatible checkout, run `pio run --target upload`, then `pio device monitor -b 115200`. **Do not erase for ordinary updates or recovery**; there is no automatic recovery flashing/rollback.
|
||||
|
||||
Replies: **400** malformed/incompatible/incomplete input; **401/403** authentication/Origin/CSRF or `admin_required`; **408** `firmware_timeout`; **409** selected-image latch; **413/415** destination capacity/content type; **500** `firmware_write_failed`/`firmware_commit_failed`; **503** busy/unavailable/resources. Unread rejected bodies close rather than drain; bounded secret-free JSON may never reach a disconnected browser.
|
||||
|
||||
#### Phase10 regression guidance
|
||||
|
||||
These are reusable checks, **not recorded passes or outstanding acceptance gates**. Record only performed outcomes; host flash/network/scheduling doubles are not hardware evidence. Focused host suites: `python3 tests/web_firmware_update/run.py` and `python3 tests/web_ui_session/run.py` (the SDK begin/abort case covers a live handle published on erase failure).
|
||||
|
||||
- Exercise wired-first install, known-good upload/reboot/new build and explicit reconnect; compare saved users/roles, password/key login, Wi-Fi/serial/display/hostname settings and public HTTPS/SSH fingerprints without recording secrets.
|
||||
- Interrupt/cancel receive; reject corrupt, truncated, wrong-target, non-application and oversized inputs without unintended selection/restart, then deliberately perform a valid update. Reject unauthenticated/ordinary-user, wrong-Origin and missing/wrong-CSRF requests before flash. Never attempt power interruption at commit without wired recovery ready.
|
||||
- Exercise competing upload, HTTPS lifecycle/identity and ordinary reboot exclusion; HTTPD requests may wait rather than promptly return busy. With controlled post-selection response-failure injection, verify no automatic restart, subsequent 409 (also after HTTPS restart), then manual reboot; an arbitrary disconnect does not establish this fault case.
|
||||
- Observe web stalls/drops and serial/network recovery, single-writer isolation, UART0 recovery and native USB UART1 access without networking; demonstrate known-good wired recovery without erase and recheck configuration/identities. Do not claim uninterrupted traffic.
|
||||
|
||||
## Current and planned phases
|
||||
|
||||
**Phases 8, 9, 10 and 12 are complete** for their accepted scopes. Phase 9 includes the explicit new-hardware-check waiver above; Phase 10 includes explicit user acceptance of upload and normal operation. Phase 12 includes explicit user validation and the fresh-boot/full-client evidence below. Optional features must not weaken completed serial and recovery paths. General release gates below guide future work, not claims that every fault, soak, recovery or reserve measurement was performed for completed phases.
|
||||
|
||||
### Phase 12 — Advanced network integration
|
||||
|
||||
**Complete by explicit user validation:** “I just validated Phase 12 successfully.” Accepted baseline:
|
||||
|
||||
- Dual-stack local access and DNS-SD discovery: advertise available HTTPS and SSH services using the shared `sak-<suffix>.local` hostname, with appropriate IPv4 `A` and IPv6 `AAAA` records. Preserve IPv4 access and verify IPv6 support throughout HTTPS, WebSocket and SSH. Clients choose address-family preference and fallback; DNS-SD cannot mandate IPv6 preference. Certificate-name integration and trust remain separate concerns.
|
||||
- Send the configured device hostname (`sak-<suffix>`, without `.local`) through DHCPv4 Host Name option 12 so a suitably configured DHCP/DNS server can publish the lease address in its own DNS zone. Set the STA netif hostname before DHCP starts, define how hostname changes reach subsequent DHCP exchanges, and verify the transmitted option and server-side DNS registration. DNS publication and the DNS domain remain server policy; mDNS does not provide this integration.
|
||||
- IPv6 baseline: link-local addressing and SLAAC-provided ULA/global addresses, with mDNS/DNS-SD for local discovery; no DHCPv6 requirement. Preserve link-local interface scope and keep advertisements synchronized with usable addresses and service availability. Registration of SLAAC addresses in infrastructure DNS is a separate optional feature, outside this baseline; SLAAC does not provide automatic hostname registration.
|
||||
- Validate IPv4-only, IPv6-only and dual-stack operation, including reconnects, address changes and service lifecycle changes. Discovery remains local-link by default; preserve UART0 and native USB recovery regardless of network state.
|
||||
|
||||
The device is not intended to become a general-purpose router. Captive-portal interception, unauthenticated DNS redirection, NAPT, and a plaintext serial listener remain out of scope unless the project requirements are explicitly revised.
|
||||
|
||||
#### Implementation and operational boundaries
|
||||
|
||||
- The Wi-Fi owner installs the full configured hostname before STA connection and updates it after hostname edits. DHCPv4 option 12 uses `sak-<suffix>`, not `.local`. Rename does not force DHCP restart: the next DHCP exchange carries the new name, and DNS registration/cache cleanup remain server policy. Existing hostname limits and NVS formats are unchanged.
|
||||
- STA enables SLAAC and IPv6 link-local creation; either IPv4 or a preferred IPv6 address establishes `ONLINE`. **Link-local-only counts as online**, prevents IPv4-only timeout/failover, and can retire the fallback AP after the existing stability interval. This indicates local address availability, not Internet reachability or a default route. Web, CLI and OLED distinguish absent IPv4 from IPv6 availability. A diagnostic follow-up adds up to three preferred numeric IPv6 addresses to the snapshot, copied and cleared with the same observation; `wifi status` and browser Network settings print them in link-local/ULA/GUA groups; overview/OLED status retains availability flags. The settings JSON is bounded to 2304 bytes, with no extra netif calls on HTTPD.
|
||||
- HTTPS/WebSocket use IDF's existing dual-stack listener. SSH explicitly uses one dual-stack listener, retaining the existing two-session limit. Bracketed IPv6 HTTPS authorities are canonicalized and bound to the existing Host/Origin/session checks; scoped IPv6 literal URLs are rejected. Prefer the `.local` hostname for link-local browser access, subject to client resolver support. No certificate regeneration, new trust mechanism, or automatic IPv6 preference is introduced.
|
||||
- STA-only mDNS advertises `_https._tcp:443` and `_ssh._tcp:22` according to listener availability, without TXT metadata. Record and address-family changes converge through the existing Wi-Fi owner, including offline reconciliation; service setters do no component work. The responder is not restarted for ordinary changes. Initialization failure remains latched/nonfatal.
|
||||
- mDNS 1.12.0 exposes no readiness acknowledgement and can silently drop queued family actions. Absent families are disabled on each one-second owner pass; available families are re-enabled/reprobed on a 30-second repair cadence. Upstream synchronous calls can delay this cadence. There can be a transient stale/zero-A response window after IPv4 loss before disable is processed. AAAA records follow upstream **valid-address** semantics, including deprecated-but-still-valid addresses, not preferred-only filtering. Client caches expire independently.
|
||||
- `cmake/mdns_membership.*` applies one version/hash-guarded, build-local source overlay to mDNS 1.12.0: balance per-family multicast leaves and unwind joins after PCB creation failure. Managed sources and dependency versions remain unchanged. An upstream mismatch fails configuration for deliberate review; see `tests/mdns_membership/README.md` for maintenance/removal and regression evidence. This is not the abandoned Phase 9 patch set.
|
||||
- `sdkconfig.defaults` explicitly enables IPv4/IPv6 and increases mDNS service capacity from one to two. Existing saved configurations override defaults: verify `CONFIG_LWIP_IPV4=y`, `CONFIG_LWIP_IPV6=y`, `CONFIG_MDNS_MAX_SERVICES=2` and STA-only predefined mDNS interfaces before building. The local N16R8 saved configuration was updated accordingly. No new task, transport socket, broker slot, serial buffer, partition, filesystem or generated web asset was added.
|
||||
- The accepted baseline initially retained IPv4-only traceroute and limited family/scope selection. The later requested [dual-stack diagnostics follow-up](../command_reference.md#dual-stack-network-diagnostics) adds `-4`/`-6` to ping/traceroute/nslookup across UART0, admin SSH and browser Admin, scoped IPv6 probes and explicit A+AAAA lookup. This follow-up is built/host-tested, not covered retroactively by the acceptance capture below. DHCPv6, infrastructure DNS registration of SLAAC addresses, and new IPv6 resolver provisioning remain outside scope.
|
||||
|
||||
#### Validation evidence and acceptance
|
||||
|
||||
`pio run` passed on PlatformIO 6.12.0 / ESP-IDF 5.5.0 with the guarded mDNS source compiled: **94,396 B linked RAM / 1,853,025 B flash**, +176 B RAM / +5,380 B flash versus the recorded Phase 10 build. These are static link sizes, not runtime heap or stack headroom. Initial target compilation rejected lock-free bool atomics; the implementation now uses short portMUX sections. Existing SDK Kconfig notes remain.
|
||||
|
||||
Host checks passed: Wi-Fi/address/hostname reconciliation, mDNS lifecycle/family repair, actual patched multicast-membership functions and CMake guards, SSH dual-stack/management/runtime/security, HTTPS authority parser (689 cases on host and actual installed lwIP parser), cookie authorization variants, Network settings, browser UI/CSP (171 groups), HTTPS lifecycle (45 groups and eight status projections), firmware upload (88 cases plus SDK contract), broker diagnostics, and session-store/serial integration. Network-settings host fakes were updated after the new netif dependency exposed a compilation failure; the rerun passed. These are not packet-level or hardware evidence.
|
||||
|
||||
Follow-up after the user's live status report: both preferred-address flags were `yes`, while plain client `ping` selected IPv4. Source verification confirms the flags require actual preferred addresses; this is not evidence of failed SLAAC or verified IPv6 reachability. `wifi status` now prints the addresses for diagnosis. Latest follow-up `pio run` passed: **94,444 B linked RAM / 1,853,685 B flash** (+48 / +660 versus the initial Phase 12 build). Browser Admin console now permits canonical Wi-Fi commands, including disruptive edits and hidden prompts, as explicitly requested. Focused policy/browser Wi-Fi prompt, address snapshot/rendering, Network/cookie and UI regressions passed. The broader console-boundary suite passes its console/certificate stages but has a pre-existing SSH-adapter fixture compilation failure for missing `web_firmware_update_reserve_reboot`; no production change was made to hide it.
|
||||
|
||||
Latest address-display build: `pio run` **94,444 B linked RAM / 1,854,485 B flash**; Network API/owner checks and **174 browser groups plus CSP** passed. These remain build/host results, separate from the user's device evidence below.
|
||||
|
||||
##### User-reported device acceptance
|
||||
|
||||
User explicitly validated Phase 12 after confirming numeric IPv6 ping/HTTPS, Avahi AAAA resolution and normal IPv6 hostname access following the Fedora `authselect` mDNS correction. The earlier hostname failure was a client resolver configuration issue, not missing device IPv6 addresses. The acceptance capture adds:
|
||||
|
||||
- **Fresh boot:** HTTPS, SSH and mDNS running without reported startup failures; zero SSH sessions, serial stopped, no broker clients, USB attached but not host-open. The configured UART profile is **230400 baud, 8N1, RTS/CTS, DTR active, RTS threshold 96**.
|
||||
- **Full client mix:** two authenticated public-key SSH sessions with IPv6 ULA peers (one serial observer, one admin console); two serial WebSockets (one writer, one observer); one active browser admin WebSocket; native USB host-open as an observer. Four broker clients, exactly one writer, all reported pending/event counts zero. Serial running with RX-available/TX-pending zero, CTS asserted and valid RS-232 voltage at observation time.
|
||||
- SSH: two successful handshakes, zero handshake/authentication failures or timeouts, zero stream I/O failures and zero session revocations. Stream counters **RX 68 / accepted 65 / rejected 3 / TX 161,209 bytes**; two broker writer revocations were reported. The rejected bytes are retained as evidence, not silently described as zero loss or assigned an unverified cause.
|
||||
- Web serial: two connections, **27 accepted RX frames/bytes**, zero rejected RX frames/bytes; **782 binary TX frames / 318,230 bytes**, 11 control frames / 948 bytes. Send/queue/protocol/close failure counters zero. Two writer requests were denied; the final broker snapshot still shows one writer. Browser admin: one connection, **106 RX / 3,721 TX bytes**, zero send/queue/protocol/authorization/backpressure failures. No response errors reported.
|
||||
- HTTPS and SSH both report running, not transitioning, `ESP_OK`; mDNS reports expected announcement and `ESP_OK`. Expected announcement alone is not packet-level service-record proof; the earlier successful Avahi lookup separately establishes the reported AAAA lookup.
|
||||
|
||||
All heap figures are bytes, copied from the user's observations:
|
||||
|
||||
| Observation | Heap capability | Free | Lifetime minimum-free | Largest block |
|
||||
|---|---|---:|---:|---:|
|
||||
| Fresh boot | Internal 8-bit | 65,880 | 64,912 | 31,744 |
|
||||
| Fresh boot | Internal DMA | 58,124 | 57,156 | 31,744 |
|
||||
| Fresh boot | External PSRAM | 8,196,732 | 8,187,980 | 8,126,464 |
|
||||
| Full client mix | Internal 8-bit | 35,408 | 8,528 | 22,528 |
|
||||
| Full client mix | Internal DMA | 27,652 | 772 | 22,528 |
|
||||
| Full client mix | External PSRAM | 8,111,952 | 8,072,612 | 7,995,392 |
|
||||
|
||||
SSH owner stack: **20,480 B** configured, **18,476 B minimum-free** at fresh-boot observation and **16,284 B minimum-free** under the full mix.
|
||||
|
||||
**Evidence limits:** minimum-free is the conservative sum of matching heap regions' lifetime minima, not a simultaneous free-space measurement or guaranteed allocation reserve. Internal 8-bit and DMA capabilities overlap and must not be added as independent pools. The **772 B DMA lifetime minimum remains a headroom watch item**, not an observed allocation failure or acceptance blocker. Boot output contains two TLS `-0x004C` receive errors and unauthenticated-request failures; the capture does not establish their cause or connect them to memory exhaustion. Command snapshots are non-atomic; differing cumulative request/authentication counts must not be treated as one aligned measurement interval. No UART overflow/per-observer drop counters, soak duration, byte-for-byte capture integrity, exhaustive fault injection, DHCP packet capture/server-zone update, or every address-family/service transition is established by this excerpt. User acceptance is recorded without inventing those passes.
|
||||
|
||||
##### Reusable regression guidance
|
||||
|
||||
The following procedures are retained for future regression testing, **not outstanding acceptance blockers or a claim that every item was executed**. Keep UART0 and USB recovery available:
|
||||
|
||||
1. On IPv4, IPv6-only and dual-stack STA networks, exercise HTTPS login, serial/admin WebSockets, SSH user/admin access and one-writer/observer isolation. Test hostname access plus IPv4 and unscoped ULA/GUA IPv6 literals; verify SSH host keys and HTTPS identity rather than trusting discovery.
|
||||
2. Capture DHCP DISCOVER/REQUEST option 12 at boot, rename/renew and reconnect, including a maximum-length suffix. With a configured DHCP/DNS server, verify the resulting zone entry and its server-controlled update/removal behavior.
|
||||
3. Browse `_https._tcp` and `_ssh._tcp` and inspect A/AAAA/SRV answers over both families. Stop/restart each service; rename; drop/reacquire DHCP while IPv6 survives; change/deprecate/expire RA prefixes; disconnect/reconnect. Confirm eventual withdrawal/restoration, valid-address AAAA semantics and no persistent zero-A response. Exercise repeated family transitions and, where practical, missed-event/action repair.
|
||||
4. Check link-local-only readiness and the fallback-AP transition explicitly. Confirm the client preserves interface scope; do not infer browser link-local success from `ONLINE` alone. Test multicast filtering/AP isolation separately from firmware address readiness.
|
||||
5. Repeat the previously accepted 230400-baud mixed USB/WebSocket/SSH workload with discovery and address changes. Capture current/minimum internal, DMA and PSRAM availability, task stack margins, UART overflow and per-client drop counters. No new runtime memory reserve or maximum-baud performance guarantee is claimed.
|
||||
|
||||
Phase 12 is complete by the explicit user validation above. This acceptance update changes documentation only; the agent did not run a new build, test, upload, erase or device operation.
|
||||
|
||||
## Cross-phase release gates
|
||||
|
||||
Every phase should satisfy the following before being marked complete, with any user-waived check explicitly recorded in that phase rather than reported as passed:
|
||||
|
||||
1. A clean release build for the ESP32-S3 N16R8 target.
|
||||
2. No new compiler or project diagnostics attributable to the change.
|
||||
3. Focused automated tests where practical and documented hardware tests where hardware behavior is involved.
|
||||
4. UART0, native USB CDC, and previously completed network transports still operate.
|
||||
5. Broker writer/observer semantics and binary transparency remain intact.
|
||||
6. Failure, disconnect, stop/start, reboot, and stale-session cleanup paths are exercised.
|
||||
7. Runtime memory, stack, socket, and queue bounds are measured under maximum supported concurrency.
|
||||
8. Secrets remain absent from ordinary status output and logs.
|
||||
9. [Command reference](../command_reference.md), [Hardware wiring](../wiring.md), electrical tests, and this roadmap are updated as applicable.
|
||||
10. New dependencies receive security, maintenance, and GPL-3.0 compatibility review.
|
||||
|
||||
## Explicitly deferred work
|
||||
|
||||
The following are not implemented merely because flash partitions or library support exist:
|
||||
|
||||
- NVS, flash, or PSRAM encryption.
|
||||
- Secure boot or production eFuse provisioning.
|
||||
- Automatic OTA downloads, image signing, post-boot health confirmation and automatic rollback. The simple admin upload is complete in Phase 10; these advanced update features remain deferred.
|
||||
- Core-dump collection or secret-safe core-dump processing.
|
||||
- Filesystem mounting.
|
||||
|
||||
- SFTP, SCP, SSH `exec`, forwarding, or subsystems.
|
||||
- General routing, NAPT, captive-portal interception, or unauthenticated serial access.
|
||||
|
||||
Deferring these features is intentional: each changes the security model, recovery behavior, or bounded-resource assumptions and therefore requires its own design and validation gate.
|
||||
@@ -1,138 +0,0 @@
|
||||
# Web administration contracts
|
||||
|
||||
Current implementation contracts; [Phase 10 firmware upload is complete by explicit user acceptance on 2026-09-18](roadmap.md#phase-10--simple-admin-web-firmware-upload), confirming upload and normal operation, not specific fault or recovery checks. Phase status and executed-evidence limits belong in the [roadmap](roadmap.md#phase-8--role-based-users-and-administrative-access--complete) and [acceptance record](roadmap.md#phase8-acceptance-evidence), not in implementation timelines. [Regression procedures](user_administration_tests.md#integrated-web-administration-regression-procedure) describe checks, not results. Source is authoritative; start with the [code map](../agent/code-map.md).
|
||||
|
||||
## Authentication and admission
|
||||
|
||||
- HTTPS-only, same-origin login replaces Basic authentication entirely. Four digest-only opaque sessions have absolute one-hour expiry; four pre-login challenges last 120 seconds. Login is globally bounded to five credential attempts per 60 seconds. Live sessions/challenges/tickets are not evicted to admit another client.
|
||||
- Raw tokens travel only in the host-only `__Host-` cookie with Secure, HttpOnly, SameSite=Strict, Path=/ and explicit lifetime; no Domain. Session records retain copied secret-free principals, CSRF state and nonzero, nonreused originating-session IDs. Passwords, raw cookies/tickets, verifiers and private keys never enter routine snapshots/logs/completion/display.
|
||||
- Mutation admission requires current cookie/principal, strict Origin and CSRF validation; administration additionally requires current `admin`. Normal users retain serial/status but cannot invoke administration directly. Authentication POST fetches use CORS mode with fixed same-origin URLs and same-origin credentials: do not accept Origin `null` to compensate for browser no-referrer behavior.
|
||||
- Session-store initialization is part of admitted HTTPS start; authentication failure gates HTTPS. Failed start/accepted stop disables and wipes session state. Logout invalidates only the originating session before socket cleanup; account mutation invalidates that account's sessions/tickets, including deletion/recreation, without revoking unrelated accounts. Currentness checks remain authoritative if best-effort notifications fail.
|
||||
- Four serial tickets and two admin tickets are digest-only, single-use, 30-second, session/principal-bound records. Cookie/Origin/ticket/currentness and transport admission precede explicit WebSocket 101. Store RNG/SHA/database calls run outside short spinlocks; IDs/expiry/epochs fence stale publication without nested store/transport locks.
|
||||
- HTTPD remains bounded to six sockets, two serial WebSockets, one admin WebSocket and 40 method/path handlers; LRU eviction is disabled. Sessions, sockets, tickets and the two shared remote-console slots are separate capacity limits. Optional settings/admin failures preserve unrelated routes where their initialization contract permits; UART0 and native USB remain independent of web readiness.
|
||||
|
||||
`web_httpd_adapter` alone accesses private IDF 5.5.0 HTTPD state. It rejects duplicate/ambiguous headers, postpones 101 until admission, and wipes consumed header scratch while preserving right-aligned unread bytes. Optional Settings registration stages descriptor/name allocations before publishing either, avoiding the pinned public registration failure path. Re-audit these private boundaries on SDK upgrades and same-version SDK patches: the version guard does not detect patches that retain the same version number. HTTPD response headers are pointer-backed, not copied; both `Set-Cookie` value buffers must remain valid and distinct through response send. Do not reuse or wipe those buffers before sending completes. Do not enable header/ticket debug logging. Auth documents, scripts and sensitive responses are no-store with CSP/no-referrer/frame-denial protections; authored loader changes require matching CSP hashes. Generated assets are not a normal documentation/build output.
|
||||
|
||||
## Terminal and console ownership
|
||||
|
||||
Switching Serial/Admin/Settings never creates a second serial client, requests/releases the writer lease or reconnects serial. Hidden connected terminals keep draining; only selected terminal input is sent. Both browser terminals have separate 5,000-line scrollback and 64 KiB callback-accounted pending-output bounds with visible browser-drop counts. Admin input is bounded to 4 KiB admission and 512-byte frames. Admin open/reopen is explicit; admin close is isolated. Disconnect pauses serial reconnect but retains login; logout/expiry/revocation closes affected sockets normally.
|
||||
|
||||
The document binds terminal state to its first validated username/role/session-stable CSRF tuple. A different identity requires a clean document before showing retained output. Pagehide fences sockets/work and hides scrollback until same-session revalidation; admin is not automatically reopened. Fit readiness caches only success and uses at most three generation-fenced animation-frame retries per external request.
|
||||
|
||||
`admin_ssh_console` is the sole `esp_console_run()` caller for UART0, admin SSH and browser admin. Two fixed remote-console slots are shared across SSH/browser, not two per transport. Queue records carry copied principals and transport-qualified generation tokens. Owner currentness is checked outside console locks before commands and during prompts (250 ms polling plus validation/scheduling latency), then identity is rechecked. This is operation admission, not cancellation or rollback of arbitrary executing handlers. Consumed output and retired prompt/history state are wiped.
|
||||
|
||||
HTTPD alone owns browser-admin socket IO and its 1,552-byte PSRAM-only payload. A 20 ms timer queues at most one owner poll, not a new task. Close uses current-owner shutdown, not queued reusable `sock_db *` pointers. Detach fences submitters; queued state retires only after successful HTTPD destruction. Failed destruction retains ownership and prevents unsafe restart/reuse.
|
||||
|
||||
### Browser-shell policy
|
||||
|
||||
Typed Settings permissions do not expand shell permissions. Parsed canonical arguments, not raw prefixes or completion suggestions, control admission:
|
||||
|
||||
- Browser `web` permits only `web status`, `web stop`, and exact `web certificate rotate --force`; certificate info/reset, diagnostics/performance and other web forms are denied.
|
||||
- Browser `wifi` uses the canonical command handler, including bare `wifi` (status), profile/AP settings, persistence, lifecycle and network diagnostics. Profile/AP secret entry uses the shared hidden prompt: no input echo, history or completion of secret bytes; cancellation, disconnect and failed currentness discard the input. Explicit `wifi ap show-secret` reveals the AP password only in the invoking admin terminal; routine status and completion do not reveal credentials. Browser `mdns` still permits only exact `mdns status`. Root and `wifi`-prefixed ping/traceroute/nslookup support the same `-4`/`-6` and device-local IPv6 scope syntax as UART0/admin SSH; see [dual-stack diagnostics](../command_reference.md#dual-stack-network-diagnostics). They run on the shared dispatcher, not HTTPD or the client browser; no extra endpoint or permission expansion is involved.
|
||||
- Browser `user` permits status/list/show and interactive add/password plus forced role/delete for **other accounts only**. Self changes, generated passwords, key commands and recovery are denied there; typed Accounts supports the separately bounded self/generated/key workflows.
|
||||
- Browser SSH stop/disconnect/reset and host-key mutation are denied; typed SSH Settings has its own safe owner path. Do not claim full browser-shell parity.
|
||||
- Browser `reboot` and owner-relative `exit` are supported. First-admin provisioning uses normal `user add` on UART0; unavailable-database recovery is UART0-only. The legacy `user bootstrap` and web credential commands no longer exist.
|
||||
|
||||
Wi-Fi changes use the existing canonical manager path, not deferred browser drain/acknowledgement. Stop, reconnect, profile/AP edits, load/defaults/reset can disconnect network clients before output arrives. A lost result does not cancel admitted work: reconnect and inspect before retrying; use UART0 if networking is unavailable. Changes are RAM-only until `wifi save`, except `wifi reset`, which also persists defaults. Native USB remains independent UART1 access, not an administration console.
|
||||
|
||||
Deferred self-affecting shell actions use the existing bounded drain/control path (up to ten seconds plus a short delay), not guaranteed peer delivery. Browser certificate rotation hands a typed action after drain/200 ms to the existing 12 KiB dispatcher, never crypto/NVS on the 4 KiB control stack. Pending input is discarded through execution, and an executing slot remains reserved across self-detach. UART0/admin SSH retain canonical recovery actions.
|
||||
|
||||
## Typed settings API and operation lifetime
|
||||
|
||||
All routes below are under `/api/settings/`. Each domain has bodyless GET snapshot plus GET/POST operation unless noted. Requests reject queries, malformed/oversized/duplicate/unknown fields and inappropriate bodies; mutation routes require bounded JSON with current admin/Origin/CSRF. GET allows absent Origin but rejects mismatch. Snapshot contention/unavailability is not an empty successful projection. HTTPD performs bounded admission/encoding only; canonical mutations run on the existing four-entry administration dispatcher using **IDs only**, not command strings, credentials, request pointers or socket handles.
|
||||
|
||||
| Domain and source | Snapshot / operation suffix | Request / snapshot / result buffer bounds (bytes) |
|
||||
|---|---|---|
|
||||
| `web_serial_settings`, serial service | `serial` / `serial-operation` | 256 / 256 / 96 |
|
||||
| `web_account_settings`, user database | `accounts` / `account-operation` | 768 / 1024 (accounts), 512 (keys) / 96 |
|
||||
| `web_network_settings`, Wi-Fi + mDNS | `network` / `network-operation` | 768 / 2304 / 128 |
|
||||
| `web_display_settings`, local status UI | `display` / `display-operation` | 256 / 128 / 96 |
|
||||
| `web_broker_settings`, session broker | `broker` / `broker-operation` | 256 / 2048 / 96 |
|
||||
| `web_ssh_settings`, SSH owner/security | `ssh` / `ssh-operation` | 256 / 768 / 96 |
|
||||
| `web_lifecycle_settings`, HTTPS owner/security | `lifecycle` / `lifecycle-operation` | 256 / 320 / 96 |
|
||||
|
||||
Mutation bodies use at most four receive attempts. Each domain retains one original-login-bound pending/result slot, nonreused operation IDs and an executing reservation. Results are replaceable, login-isolated observations, not durable history or idempotency keys. Another tab can replace a completed result. Dispatcher currentness and the 30-second dequeue deadline precede canonical owner admission. **Admitted work may finish after logout, revocation, timeout or navigation.** A failed response, 401 or disappearance of a result proves neither success nor cancellation; inspect current state before any deliberate retry.
|
||||
|
||||
Accounts and Network each use a one-second timer to cancel/wipe queued, non-executing secrets after 30 seconds plus scheduling latency. Shared input wipes on dequeue/rejection; executing locals wipe on return. This is not a hard wall-clock erasure/execution guarantee. Other domain deadlines are dequeue checks, not new cancellation timers. UI fences late/session-changed responses, bounds request/result checking to 15 seconds and never automatically replays/restores mutations. Serial/Accounts/Network/Display/Broker use bounded automatic checks where implemented (at most ten one-second GETs); SSH and HTTPS/Reboot require manual Check Result/Refresh.
|
||||
|
||||
### Serial and Display
|
||||
|
||||
Serial Apply/Defaults change RAM; Save persists working device state, not browser drafts. Load follows canonical defaults/fallback behavior. Reset uses canonical apply/persist/best-effort rollback. Reconfiguration/stop discards serial-service RX/TX and task-local pending data but preserves broker clients, lease and already-fanned output. USB with DTR may restart a stopped service. `/api/status` uses a zero-wait consistent serial snapshot (`running:null` if unavailable).
|
||||
|
||||
Display accepts dim/off timeouts 0–86400 seconds and an expected nonzero configuration generation. All writers, including CLI/legacy Apply, share a zero-wait owner reservation; NVS runs outside critical sections. Save stabilizes selected RAM; Load retains canonical fallback without rewriting NVS; Reset commits defaults **before** RAM publication, leaving RAM unchanged on failure. Buttons/diagnostic holds update activity, not configuration generation. Configuration needs an available UI task, not a physically present panel, and never takes over I2C.
|
||||
|
||||
### Accounts and authorized keys
|
||||
|
||||
The database owns eight accounts, final-admin protection and conditional target username/account-ID/auth-generation checks inside its mutation lock. Create/password/role/delete and key mutations share canonical commit/invariant logic. HTTPD uses zero-wait secret-free projections, not blocking CLI snapshots. Successful mutations request target-only web/SSH revocation; self mutation can revoke access before result retrieval.
|
||||
|
||||
Separate bodyless POST `accounts/generate-password` returns one 24-character value without mutation or retained retrieval. The browser uses a 60-second context-bound saved acknowledgement before separate submission; this is UX, not delivery proof, and JavaScript strings cannot be securely wiped. POST `accounts/keys` returns only slot/type/SHA-256 fingerprints for the selected identity. Key add/delete/clear share the operation slot; public-key import is at most 384 decoded text bytes in the 768-byte request and uses canonical Ed25519/P-256 validation. Three stable key indices may be sparse; never interpret response-array position as an index. No stored key blob, verifier or password is exported.
|
||||
|
||||
### Network
|
||||
|
||||
Wi-Fi config/runtime is one zero-wait consistent projection; mDNS is a separate projection, not cross-domain atomic authorization. Four stable profiles carry enabled/priority/security/SSID/password-configured metadata. `mixed` means WPA2-or-stronger, not open. `announced` is expected STA announcement, not verified DNS. Runtime `ipv6_addresses` contains up to three preferred addresses from the same snapshot as the availability flags, encoded as eight lowercase four-digit hextets without a zone. Network settings display separate link-local, ULA and GUA lists; absent groups show `none`. Link-local access needs the client's interface scope, and address presence does not assert a route or Internet reachability. Encoding uses the existing snapshot only, not netif/driver calls on HTTPD; the 2304-byte response buffer adds 256 bytes of bounded stack storage.
|
||||
|
||||
SSID wire values are reversible **bytes**, maximum 32: printable ASCII, standard single-character JSON escapes and `\u00HH`, with no raw non-ASCII, non-byte Unicode or surrogates. NUL/non-UTF-8 round-trip. UI text is UTF-8-encoded before byte serialization; exact reversible text or literal hex preserves existing bytes and BOM, with no silent replacement/truncation.
|
||||
|
||||
Flat operations select one domain/target:
|
||||
|
||||
- `wifi-patch` + Wi-Fi generation: optional boot policy, AP policy/channel/SSID/password/clear; `profile-patch` additionally selects stable profile 0–3 and optional enabled/priority/security.
|
||||
- `wifi-save|wifi-load` require Wi-Fi generation. `start|stop|reconnect|next-profile` use canonical connection controls, not explicit selected-profile connection.
|
||||
- `mdns-set` requires its generation and suffix; `mdns-save|mdns-load|mdns-defaults` require its generation. Suffix is 1–55 lowercase ASCII letters/digits/hyphens, no leading/trailing hyphen, producing `sak-<suffix>`.
|
||||
|
||||
Omitted fields preserve current bytes under the Wi-Fi mutex. Password Keep omits, Replace is 8–63 printable ASCII bytes, disabled-STA Clear is explicit; replacement and clear cannot coexist. AP clear is always denied, even while off. No saved PSK or length is returned. Wi-Fi compare/merge/whole-candidate validation and required queue admission precede RAM publication. Save stabilizes selected bytes; Load reads stored configuration only and cannot generate fallback secrets. No browser Wi-Fi reset/default-secret/export route exists.
|
||||
|
||||
Edits require explicit Save. Disabled-profile-only edits do not restart active radio; enabled-policy/AP changes follow canonical asynchronous restart. Start/Stop change RAM enabled-at-boot; Reconnect/Next are no-ops while stopped. Next follows enabled priority order with wrap; editor selection is not connection selection. mDNS independently owns generation/persistence; the Wi-Fi manager owns radio/reannouncement. mDNS RAM change followed by queue failure is reported without rollback; its Load may choose deterministic MAC-derived defaults.
|
||||
|
||||
Network result fields are `id/action/state/error`: `pending`, `accepted`, `ok`, `failed`, `cancelled`, `stale`, `invalid`, `loaded_defaults`, `applied_not_queued`, or login-isolated `idle`. `accepted` means RAM/owner-queue admission, **not** association/DHCP/DNS completion. `ok` denotes explicit Save. Loss can precede response; recover via STA/AP and inspect before retrying. UART0 administers recovery; native USB provides network-independent UART1, not an admin console.
|
||||
|
||||
### Broker and contextual controls
|
||||
|
||||
Broker management atomically copies eight compact client rows, writer and lease generation under one zero-wait mutex acquisition, without reading UART payloads or consuming events. Conditional assignment compares the selected nonzero target ID and lease generation under the same force-writer lock before any effects. Stale/absent targets conflict; same-current-target assignment is a no-op only after validation. Accepted serial TX is not recalled by transfer.
|
||||
|
||||
IDs have three slot bits and 29 generation bits; exhausted client slots retire until reboot rather than wrap. The separate 32-bit lease version saturates at UINT32_MAX, survives counter clear and advances on grant/release/revoke before advisory event delivery. Forced transfer can advance twice; it is not a count. Saturation blocks typed assignment but leaves ordinary request/release/disconnect and canonical recovery force available. Reboot invalidates old browser sessions.
|
||||
|
||||
Serial/Wi-Fi/Broker quick views reuse one nonmodal settings host and existing controllers/drafts, not parallel editors. Hover/focus/click/tap never mutate. Network quick mode excludes password controls. Full-page drafts are protected; promotion preserves controller/selection, departure fences reads/timers without cancelling admitted work. Dismissal restores the prior terminal without stealing unrelated focus; explicit Escape/Close returns focus to its trigger.
|
||||
|
||||
Broker contextual reads use one timer five seconds after successful completion and a five-second whole-read deadline, stopping on errors/uncertainty/departure. Explicit target/lease selection is not rebased by refresh. Sticky stale/absence latches require deliberate reselection even if later snapshots match again. Confirmation captures exact versions before session revalidation; guarded aria-disabled controls preserve focus during updates.
|
||||
|
||||
## Service lifecycle and identity rotation
|
||||
|
||||
Service generations are saturated, distinct from identity and session generations, and advance on admitted canonical lifecycle attempts including failures; counter clear/reinit cannot revive stale versions. Public zero-wait metadata authorizes nothing. Conditional controls compare under the canonical owner reservation; no snapshot-check/unconditional-mutate gap is allowed. Reboot invalidates originating logins and boot-local reservation IDs.
|
||||
|
||||
### HTTPS and reboot
|
||||
|
||||
GET `lifecycle` has seven fields: `generation`, `running`, `transitioning`, `controllable`, `identity_generation`, `fingerprint`, `rotatable`. Stored HTTPS fingerprint is 64 lowercase hex SHA-256 digits. Stop/restart/reboot require exactly action + service generation; rotate also requires identity generation. Unknown/duplicate/escaped/coerced fields and zero/saturated versions reject. Unavailable identity yields generation zero/empty fingerprint/not rotatable without removing ordinary service controls.
|
||||
|
||||
Self-cutting HTTP actions use **successful synchronous send return → one nonreused-ID HTTPD callback → existing dispatcher**. Send return is not browser receipt. The callback never waits or performs lifecycle work, and captures no request/fd/reusable operation pointer. A two-second ACK admission deadline and post-validation 30-second dequeue deadline are not execution bounds. Accepted-but-lost HTTPD work retains one reservation until callback arrival or successful HTTPD destruction; failed destruction cannot release it or accumulate more callbacks. The original login/current admin is revalidated before owner admission.
|
||||
|
||||
`web_server_stop_current()`/`web_server_restart_current()` compare and reserve under the server mutex. Restart retains transition ownership through stop/start; failed stop skips start, failed cleanup retains handles and canonical recovery, and reinit preserves the error. `web_server_reboot_current()` reserves the HTTPS generation then uses canonical `esp_restart()` outside locks, not an HTTPD stop wait or self-console cleanup.
|
||||
|
||||
`web_server_replace_identity(service_generation, identity_generation, reset, &committed)` reserves **service before identity**, before any crypto/storage. Both nonzero generations mean conditional healthy-running rotation; both zero retain CLI semantics; conditional Reset is invalid. Canonical CLI/browser-shell rotation and direct security rotate/reset share nonreused task-owner-bound identity reservation tokens. Only the owner can replace once/release; exhaustion fails closed until reboot. Crypto/NVS run outside service/security mutexes and spinlocks; commit precedes live publication and old-key wipe. Reservation remains held while reserved start copies the committed pair.
|
||||
|
||||
HTTPS ordering is **commit → stop → restart**. Precommit generation/RNG/storage failure leaves identity, HTTPD and logins unchanged (service version may advance after admission). Postcommit lifecycle failure never rolls back identity; failed stop can leave old served and new stored fingerprints different. Running replacement restarts HTTPS; canonical stopped rotation stays stopped, while CLI TLS-only reset can recover unavailable identity and starts a stopped service. Browser has no Reset/recovery/export action.
|
||||
|
||||
Save drafts; rotation/restart invalidates all web logins and closes both browser routes. Inspect `web certificate info` through trusted UART0, verify fingerprint before renewing trust, then reload/sign in freshly. Accepting a warning alone is not trusted verification. Use canonical UART0/admin SSH `web stop` / `web start` for retained-server recovery. Network/SSH/USB are not stopped by HTTPS-only operations; whole-device reboot interrupts all transports and loses unsaved RAM.
|
||||
|
||||
### Application firmware upload
|
||||
|
||||
The admin-only firmware card is in **Settings → HTTPS / Reboot**. It sends a raw `POST /api/firmware` with `application/octet-stream`, known length, session cookie, same-origin Origin and `X-CSRF-Token`; it is not a JSON Settings operation or dispatcher/result-slot workflow. Standard SDK OTA APIs stream through a 4 KiB buffer to the inactive application slot and select it only after validation/currentness checks. Only that slot and `otadata` are written; NVS/data partitions are untouched.
|
||||
|
||||
One upload reserves HTTPS lifecycle/identity and excludes ordinary software reboot. The synchronous HTTPD handler can stall/drop browser serial/admin sessions. Its 120-second total receive-loop and ten-second stall checks are not preemptive flash deadlines or uninterrupted-traffic guarantees. Successful response send schedules a delayed reboot, not proof of peer receipt. Failed response after commit leaves the image selected, schedules no automatic reboot, releases reservations for manual reboot, and latches subsequent admissible uploads to 409 `firmware_selected_reboot_required` until reset. Never automatically retry an uncertain outcome.
|
||||
|
||||
See [firmware update](roadmap.md#phase-10--simple-admin-web-firmware-upload) for wired-first installation, exact image selection, manual recovery, acceptance limits and reusable future hardware regression checks. Phase 10 is accepted; there are no signature/version policies or automatic rollback.
|
||||
|
||||
### SSH
|
||||
|
||||
GET `ssh` supplies service/session state plus identity generation, fixed P-256 algorithm, unpadded OpenSSH `SHA256:` base64 fingerprint and rotatable flag. Service actions use exactly `action`, `generation`, `target`; rotate adds `identity_generation` and requires target zero. Start/stop and exact-session disconnect use published state, saturated service generation and the command mutex; exhausted SSH session slots retire rather than wrap. Disconnect success is an owner close request, not completed teardown. HTTPD never calls wolfSSH or waits for the SSH task.
|
||||
|
||||
`ssh_transport_replace_identity()` compares/reserves **service then identity before stop or storage**, holding the existing command mutex across **stop → generate/commit/publish → conditional restart**. Canonical UART0/deferred admin-SSH rotation/reset and direct security mutations share the task-bound nonreused identity reservation. Crypto/NVS run outside security locks/spinlocks. Both nonzero versions select conditional rotation (also while stopped); both zero preserve canonical semantics, with no conditional Reset.
|
||||
|
||||
Failed stop/timeout skips mutation and never starts again; pending owner work is not cancelled. Persistence failure after successful stop may already have disconnected clients and attempts old-identity restart if previously running. Committed replacement remains committed if restart fails. Stopped rotation stays stopped; canonical reset can recover unavailable material/start stopped SSH. Only the SSH owner frees runtime context after all slots retire, before clearing cleanup admission; start rejects orphan handles. wolfSSH copies DER into its context; caller/candidate/superseded private bytes are wiped.
|
||||
|
||||
SSH changes leave invoking HTTPS available, so they use the ordinary ID-dispatcher/result path, not HTTPS's self-cutting ACK gate. UI confirms both versions and all-SSH/session scope, retains stale selection and manual 15-second/no-replay flow. A failed result may represent partial effects. Verify new trust through UART0 `ssh host-key info` before updating known_hosts; no HTTPS relogin is inherently required. Browser Reset/recovery/key export and user authorized-key changes are separate, excluded from host rotation.
|
||||
|
||||
## Diagnostics, recovery and scope boundaries
|
||||
|
||||
See [admission diagnostics](web_admission_diagnostics.md), [ordinary HTTPS idle cleanup](https_idle_cleanup.md), [throughput diagnostics](web_throughput_diagnostics.md) and [legacy storage compatibility](roadmap.md#phase8-legacy-credential-compatibility). Broker read means transport handoff, not peer receipt; capture non-consuming counters before disconnect. TLS `-0x004C` is generic NET_RECV_FAILED, not evidence of OOM. Resource minima and counter observations require attribution, not inferred causes.
|
||||
|
||||
Phase 8D.15's dedicated typed network-diagnostics UI/API was removed: diagnostics remain shell-based, subject to frontend policy. The unimplemented 8D.19 ordinary browser-session/native-USB control expansion was removed; existing SSH controls remain. No full shell parity, browser identity recovery/reset/export, encryption or secure boot is implied by Phase 8 acceptance. Phase 10 application upload is complete by explicit user acceptance of upload and normal operation; unreported fault, NVS-comparison, power-loss and recovery checks are not implied. UART0 is the administrative recovery authority; native USB is binary-transparent, network-independent UART1 access. Neither permits bypassing the broker's single writer or recalling already-admitted work.
|
||||
@@ -1,35 +0,0 @@
|
||||
# Web admission diagnostics
|
||||
|
||||
Source/build/test paths in code spans and commands are relative to the repository root
|
||||
unless a different working directory is stated; Markdown links are relative to this document.
|
||||
|
||||
Current default-disabled instrumentation, not an admission fix or an allocation-failure detector. `src/web_diagnostics.{c,h}` observes public synchronous HTTPS create/close callbacks and the four serial/admin ticket/upgrade handlers. It adds no owner task, queued probe or socket-capacity change. See [ordinary idle cleanup](https_idle_cleanup.md) for the independent cleanup policy and [acceptance evidence](roadmap.md#phase8-acceptance-evidence) for user reports.
|
||||
|
||||
## Usage and interpretation
|
||||
|
||||
Use UART0 (preferred during network stalls) or authenticated admin SSH:
|
||||
|
||||
```text
|
||||
web diagnostics clear
|
||||
web diagnostics enable
|
||||
web diagnostics show
|
||||
```
|
||||
|
||||
Enable before reproducing the failed third connection. At failure, run `web diagnostics show` promptly, alongside existing `memory`/`web counters`/browser Network timing evidence; repeat the snapshot if admission remains stalled. Then `web diagnostics disable` freezes event retention (live occupancy continues updating); `web diagnostics show` prints retained history, and `web diagnostics clear` erases history/counters without changing live sockets or enable state. The setting is RAM-only and defaults off after boot. Existing browser-shell policy deliberately denies these commands, even though shared completion offers the fixed forms. There is no HTTP diagnostic endpoint.
|
||||
|
||||
- Six connection records are maintained from boot even with capture disabled, so enabling on an already loaded server does not mislabel existing sockets. Each successful TLS connection gets a monotonically increasing, non-wrapping, firmware-lifetime 64-bit `conn` sequence independent of fd, cookie, ticket, broker or user identity. Clear, disable and HTTPS restart do not reset it. Internal TLS object identity is used only during synchronous cleanup; no pointer is exported or dereferenced by the console.
|
||||
- Snapshot output gives its boot-relative `snapshot_us`, post-TLS occupancy split ordinary/serial-WS/admin-WS and each live fd/connection sequence/open time/age. `kind=0/1/2` means ordinary/serial/admin. Ordinary includes every successful TLS socket not yet observed as upgraded, including idle keep-alives, assets/login/settings and sockets not yet used for HTTP. Classification is actual public `httpd_ws_get_fd_info()` state after upgrade-handler return, **not inferred from `ESP_OK`**. No URI string is inspected.
|
||||
- The fixed 32-entry overwrite ring records successful TLS open, TLS transport-context close, and enter/result for serial/admin ticket and upgrade handlers. `t_us` is boot-relative observation time; result `dt_us` measures the underlying handler only (including its work/IO, excluding entry resource sampling); close `dt_us` is successful-TLS connection lifetime. Open/enter durations are zero. Open/result occupancy includes the connection; close occupancy excludes it. Event IDs survive clear; `overwritten` counts evicted retained records since clear. `unmatched` counts duplicate create/unmatched close; `lost` counts untrackable creates (getter failure, metadata/sequence exhaustion). Nonzero anomaly counters mean occupancy is not trustworthy as complete evidence. Counters saturate.
|
||||
- `rc` is the exact handler return, **not HTTP status or ticket issuance outcome**: sending a 401/403/503 can return `ESP_OK`. Match browser HTTP status and existing rejection/issuance counters; there is no ticket-value correlation, request ID, authentication identity, header/body/query logging, or allocation-failure attribution. Upgrade success is visible in the occupancy classification. Ring overwrite or clear/toggle during an operation can leave unpaired enter/result records; do not invent a duration for a missing result.
|
||||
- Each retained event samples free/largest bytes for internal 8-bit, internal DMA and PSRAM 8-bit, plus the current HTTPD task's minimum-free stack **in ESP-IDF bytes**. Capability scans run outside the diagnostic lock. Samples are sequential, not an atomic heap snapshot; overlapping internal/DMA pools must not be added. Stack watermark includes diagnostic call overhead and is not dispatcher margin. `show` does not query a live task handle: resources are historical event samples, not fresh heap values at show time. Use `memory` for current system heap.
|
||||
- Console snapshots copy only local diagnostic metadata under a short portMUX, never inspect HTTPD session internals or wait for its owner. Printing occurs outside the lock. At most 32 ID-qualified rows and six live records are printed, even during churn; concurrent clear/overwrite is reported as “no longer retained”. Capture epochs reject samples crossing enable/disable/clear, and sequence checks prevent a stale upgrade result from reclassifying a reused fd. No queued diagnostic work exists, hence no outstanding probe or stale queue lifetime to retire on restart. Minimal connection bookkeeping and upgrade classification remain active while disabled; heap scans and event recording do not. Instrumentation still has CPU/static RAM cost, not zero perturbation.
|
||||
|
||||
## SDK audit and explicit blind spots
|
||||
|
||||
Audited installed PlatformIO ESP-IDF **5.5.0**, `components/esp_https_server/src/https_server.c` (`httpd_ssl_open`, `httpd_ssl_close`), `components/esp_http_server/src/httpd_sess.c` (`httpd_sess_delete`) and `httpd_main.c`. HTTPS performs synchronous TLS creation before the configured `open_fn` and `user_cb` create callback. It installs a transport-context destructor; that destructor invokes the public close callback before deleting TLS/freeing the context. HTTPD's default close closes the fd, then clears contexts, then frees its session slot. The diagnostic close therefore marks a cleanup observation, not a FIN timestamp or causal close reason. It uses the stored fd, not a getter on an already-closed socket. **Neither `open_fn` nor `close_fn` is replaced**, preserving all existing HTTPS cleanup ownership and failure behavior. Callbacks execute synchronously under the existing HTTPD lifecycle; successful stop finishes cleanup before restart, while failed/partial stop retains remaining live metadata. No asynchronous fd-only events are consumed, avoiding event-delay/fd-reuse ambiguity.
|
||||
|
||||
This deliberately bounded first slice does **not** measure TCP connect/accept/listen backlog, pending clients when IDF stops accepting at capacity, handshake begin/duration/failure, TLS allocation failure or aggregate lwIP socket pressure. The public configured open hook is post-TLS too; adding it would not fix these blind spots. No owner-queued client-list probe is added. Occupancy is an owner-published **successful-TLS lower bound**, not the complete HTTPD session table while a handshake is in progress. A clean **6/6** snapshot supports established-connection saturation at that instant (three WS + three ordinary is directly distinguishable); fewer than six does not exonerate admission/TLS/global socket pressure. Correlate time with browser evidence and existing secret-free TLS errors; do not call this pre-TLS tracing or claim the root cause is proven. Existing capacities, receive/send/handshake timeouts and accepted admission issue remain unchanged.
|
||||
|
||||
## Regression entry points and limits
|
||||
|
||||
`python3 tests/web_diagnostics/run.py` covers bounded metadata/ring behavior and SDK guards; `python3 tests/web_admin_transport/server_lifecycle.py` covers lifecycle composition. These are reusable host commands, not a new execution claim. Doubled TLS/scheduling and host tests cannot establish target admission latency, reserve floors or fault causes. Capture on UART0 alongside `memory`, `web counters`, browser timings and non-consuming broker/serial counters, without cookies/tickets/headers. Generic TLS NET_RECV_FAILED (`-0x004C`) and NET_CONN_RESET (`-0x0050`) are not OOM diagnoses.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Web serial throughput diagnostics
|
||||
|
||||
Source/build/test paths in code spans and commands are relative to the repository root
|
||||
unless a different working directory is stated; Markdown links are relative to this document.
|
||||
|
||||
## Current contract and accepted fix
|
||||
|
||||
Default-disabled instrumentation measures broker fan-out and binary WebSocket TX, not CPU usage, peer receipt or pure scheduler latency. The user separately accepted the combined binary header/payload-send fix at **160 MHz / 230400 baud with full mix including browser admin**. [Acceptance evidence](roadmap.md#phase8-acceptance-evidence) distinguishes that report from the latest capture and unmeasured reserves. The old frequency-only experiment is not the retained solution.
|
||||
|
||||
`web_httpd_ws_send_binary` is an IDF-5.5.0-pinned owner-only session-override send with a bounded 516-byte header/payload scratch copy and 512-byte payload. Generation validation and one outstanding item per slot remain. A non-full send installs a reject-only override before close/shutdown, preventing SDK automatic PONG/CLOSE from reentering TLS with different arguments after incomplete output, even if shutdown fails. No retry/replay; HTTPD owns TLS destruction. Text/control/admin retain the SDK sender. One API call does not promise one TLS record/packet or peer receipt. Keep CPU160, priorities, scheduling and 4096-byte broker/512-byte web payload bounds unless separately changing them with evidence.
|
||||
|
||||
## Broker accounting
|
||||
|
||||
`broker counters` retains global totals and adds one row per active client: generation-safe ID, type, pending bytes, output high-water mark (HWM), UART bytes considered for that client, queued bytes, read bytes and dropped bytes. `broker clients` helps map the IDs to active transports; `web performance show` supplies each web slot's broker ID.
|
||||
|
||||
- HWM is peak output occupancy, bounded by 4,096 bytes. `broker clear-counters` clears totals but seeds each active HWM with current pending occupancy, not zero. It does not drain queues or change ownership.
|
||||
- `read` means handed to a transport, not received or rendered by a peer. Queued counts successful copies; UART counts bytes considered while the client was connected.
|
||||
- Disconnected per-client rows disappear and slot/generation reuse resets them. Global counters retain disconnected traffic until cleared, including accepted-but-unread output discarded on disconnect. Thus global drops need not mean only overflow, and queued plus dropped need not equal UART fan-out after disconnect discards.
|
||||
- Capture before disconnect. **Do not use `broker read` for observation:** it consumes data intended for that transport. Snapshot/counter commands do not consume serial data.
|
||||
|
||||
## Independent web performance capture
|
||||
|
||||
Use physical UART0 for the least intrusive capture. Commands are also available through the authenticated admin SSH registry. `web performance enable|disable|show|clear` is separate from admission tracing (`web diagnostics enable|disable|show|clear`) and from ordinary `web counters|clear-counters`.
|
||||
|
||||
Performance capture defaults disabled. Enable resumes aggregates; disable freezes aggregates; clear resets aggregates while preserving enable state. Each control operation advances a nonwrapping epoch and fences in-flight samples. Exhaustion fails closed. Slot generation and broker identity also fence reuse; this is not a disconnected-session archive.
|
||||
|
||||
Each active one of two fixed slots shows fd, generation, broker ID, pending/measured-pending/executing state and current-epoch `pending_age_us`. Age starts at reservation-path entry and includes an executing send. If `measured_pending=0`, zero age is unavailable, not evidence of immediate service. Disabling invalidates current-epoch pending age; capture a live `show` only if investigating a stuck queue, accepting its perturbation.
|
||||
|
||||
Only serial **binary TX** is measured. Text/control frames are not samples, though they can delay subsequent binary work. No payloads, passwords, keys, cookies, tickets, verifier material or other authentication secrets are retained or printed.
|
||||
|
||||
| Printed timing | Exact interpretation |
|
||||
|---|---|
|
||||
| `queue->callback-entry` | Reservation-path entry to HTTPD callback entry, timestamped before the callback takes the transport lock. Includes submission-path work, not just HTTPD queue residence; owned callbacks retired without sending can count. |
|
||||
| `send-call` | Time around the actual synchronous HTTPD-owner send API call. Return is not peer acknowledgement, browser receipt or rendering. |
|
||||
| `completion->first-drain-attempt-return` | Successful send-call completion to return of the first subsequent broker read attempt, including empty/error attempts. |
|
||||
| `completion->next-nonempty-drain-return (includes idle)` | Completion to the next successful nonempty broker read return, possibly after empty attempts and source idle time. |
|
||||
| `completion->first-attempt-nonempty-return` | Subset where the first subsequent read returns data. Excludes observed empty attempts, but does not prove backlog existed at send completion. |
|
||||
|
||||
Each timing reports count, sum in microseconds, integer estimated average (`sum/count`, zero for no samples) and maximum. Completion intervals are not scheduler-only: they include transport work, broker read/mutex time and intervening control-frame work, and may include idle time even in the first-attempt-nonempty subset. They do not isolate a particular task, TLS operation or network cause.
|
||||
|
||||
`queued_frames/bytes` counts transport reservations, including reported queue failures; `queue_errors` records those failures. `sent_frames/bytes` counts successful send-call returns. `send_errors` includes owner-context rejection, whereas send-call timings require an actual API call. `retired` records measured callbacks retired without sending. Saturating aggregates set `saturated=1`; totals and averages then cease to be reliable. Epoch fencing intentionally prevents old work contaminating a new capture, so do not demand equality across toggles or in-flight clears.
|
||||
|
||||
## Reproducible UART0 capture
|
||||
|
||||
1. Record firmware/build identity, baud/framing/flow control, source burst size, browser versions, client IDs/roles and full-mix topology. Establish all serial clients and both admin routes first. Quiet the UART source and allow queues and connection activity to settle. Do not reconnect/login during the trace.
|
||||
2. Optionally stop independent admission tracing before the timed capture. Then reset in this order, while quiet:
|
||||
|
||||
```text
|
||||
web diagnostics disable
|
||||
web performance clear
|
||||
web performance enable
|
||||
broker clear-counters
|
||||
serial clear-counters
|
||||
web clear-counters
|
||||
```
|
||||
|
||||
The first command is optional; record whether admission tracing was enabled. Resets are sequential, not an atomic cross-service snapshot.
|
||||
3. Send the known burst once, stop the source and wait for transport queues to drain. Avoid repeated console/status printing during the burst. A quiet final broker pending count of zero alone does not prove browser receipt; retain peer byte counts too.
|
||||
4. Stop performance capture first, then capture all counters **before disconnecting any client**:
|
||||
|
||||
```text
|
||||
web performance disable
|
||||
web performance show
|
||||
broker counters
|
||||
serial counters
|
||||
web counters
|
||||
broker clients
|
||||
```
|
||||
|
||||
Disable already prints the performance snapshot; `show` is an explicit repeatable frozen-aggregate record. Keep connection counts and source/peer totals with the sample.
|
||||
5. Only after saving evidence, change to one browser, keeping the other full-mix participants and serial settings equivalent. Quiet/settle and repeat the complete reset/burst/drain/stop/capture sequence. Do not compare a fresh capture to lifetime counters.
|
||||
6. Separately compare equivalent performance-enabled and performance-disabled bursts with fresh ordinary counter resets. Keep performance disabled for the latter; do not treat retained performance aggregates as that run's measurements. Keep admission tracing state equal and record it.
|
||||
|
||||
At 230400 baud, 8N1, 4,096 bytes represents approximately **177.8 ms** of continuous input retention from an empty broker queue, ignoring flow-control pauses. Compare per-client drops/HWM with latency maxima against this scale, not as a hard deadline or proof of cause; occupancy, idle gaps and timings cover different boundaries. For future regressions, gather per-client attribution and controlled comparisons before changing buffers, priorities, scheduling or CPU.
|
||||
|
||||
## Resources and regression references
|
||||
|
||||
Two fixed diagnostic records plus nonwrapping epoch/timestamp/slot fences and broker HWM fields add bounded storage, no new instrumentation allocations/tasks/queues/payloads. Disabled is not zero overhead: gate branches remain and broker occupancy/HWM is always active. Enabled timing and short aggregate locks have runtime costs not established by host tests. The combined-send local scratch adds HTTPD stack use; actual reserve remains a target measurement.
|
||||
|
||||
`tests/session_broker_diagnostics/run.py` covers counters and generation-safe management; `tests/web_serial_performance/run.py` covers production send/drain paths, epochs, retirement, SDK wire compatibility and failed-send/control-reply isolation. `tests/admin_ssh_policy/run.py` covers SSH-allowed/browser-denied tracing commands. Prior focused/broad host validation and firmware builds passed; no tests were run during documentation consolidation. These harnesses do not prove real TLS/scheduler overhead, peer-byte integrity or long-soak behavior. Use the capture procedure above for future attribution, retaining explicit workload/duration/counter provenance.
|
||||
@@ -4,7 +4,7 @@ These manual procedures verify the OLED and buttons, MAX3243 breakout, UART1 dat
|
||||
|
||||
> **Safety:** With power removed, install only the wiring required by the selected test. DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are driven outputs. Never connect one of these outputs to another driven output. Keep temporary Dupont wiring short and secure. Power the OLED only from 3.3 V because module-mounted I²C pull-ups may connect SDA and SCL to the OLED `VCC` rail.
|
||||
|
||||
## OLED and button checks
|
||||
## 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.
|
||||
|
||||
@@ -31,7 +31,7 @@ If the expected address does not respond, treat the result as a nonfatal hardwar
|
||||
|
||||
### 3. Initialization and display patterns
|
||||
|
||||
Initialize the supported address found by the probe: `0x3c` or `0x3d`. An unexpected scan address is a finding to investigate, not an address accepted by `debug display init`. The tested module uses 7-bit `0x3c`, equivalently 8-bit `0x78` (write) and `0x79` (read):
|
||||
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
|
||||
@@ -51,7 +51,7 @@ Confirm that clear and fill affect the full 128×64 area, checker and grid have
|
||||
|
||||
### 4. Rendered status/content layout
|
||||
|
||||
Run the panel-layout pattern:
|
||||
Run the Phase 7B panel-layout pattern:
|
||||
|
||||
```text
|
||||
debug display pattern layout
|
||||
@@ -75,7 +75,7 @@ debug display pattern clear
|
||||
debug display row 16
|
||||
```
|
||||
|
||||
On the tested two-color module, row 15 is the last yellow addressable row and row 16 is the first blue addressable row. The two colored areas are separated by a narrow physical black divider. Confirm that your module matches this layout: the UI uses separate 128×16 yellow and 128×48 blue panels. Also test another endpoint row if needed with `debug display row <0..63>` to confirm row addressing and orientation.
|
||||
**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
|
||||
|
||||
@@ -133,7 +133,7 @@ Confirm that each page remains entirely within the blue content panel. The yello
|
||||
|
||||
The UI refreshes at 4 Hz maximum. Status pages are observational and must not expose passwords, credentials, tickets, or key material. A short Select press opens the separate Controls menu described below; merely navigating status pages must not change a service, alter writer ownership, or inject serial data. With UART0, USB CDC, WebSocket, and SSH active, navigate pages and confirm that the display continues to update without disrupting serial traffic or UART0 recovery.
|
||||
|
||||
With the default dim/off settings (check `display status`), leave all three buttons untouched and verify:
|
||||
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.
|
||||
@@ -142,7 +142,7 @@ With the default dim/off settings (check `display status`), leave all three butt
|
||||
|
||||
`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
|
||||
### 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.
|
||||
|
||||
@@ -159,13 +159,11 @@ For Wi-Fi lifecycle calls, confirm the immediate result says `Requested`, then u
|
||||
|
||||
Run these checks with UART0 available. Repeat appropriate stop/revoke cases with USB CDC, WebSocket, and SSH clients connected; verify the intended session/service is interrupted, unrelated recovery paths remain responsive, and no action injects serial data.
|
||||
|
||||
### 10. Boot animation, persistence, and fault recovery
|
||||
### 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.
|
||||
|
||||
Record your current `display status` settings before this test: it changes saved dim/off delays and finishes by saving defaults. Restore and save your preferred values afterward if needed.
|
||||
|
||||
Shorten the delays for a bounded dim/off test:
|
||||
Then shorten the delays for a bounded aging-policy test:
|
||||
|
||||
```text
|
||||
display status
|
||||
@@ -177,7 +175,7 @@ reboot
|
||||
|
||||
After reboot, `display status` must report the stored 5/10-second values. Verify dim at five seconds, off at ten seconds, and one consumed wake press. Run `display load`, then test `display defaults` without saving and confirm 300/600 seconds are restored only in RAM. Run `display reset`, reboot, and confirm the defaults persisted. Verify invalid combinations such as dim `10` with off `5` are rejected without changing the working values. Set either timeout to `0`, save/reboot, and confirm that transition is disabled; finish with `display reset`.
|
||||
|
||||
Exercise the following fault checks while keeping UART0 available. Button-triggered display recovery requires a running local UI task and an initialized I²C bus. If `debug display status` reports `bus=unavailable`, correct the wiring with power off and reboot; a button press cannot recreate the bus.
|
||||
Exercise the following fault matrix while keeping UART0 available:
|
||||
|
||||
| Fault/stress | Procedure | Required result |
|
||||
|---|---|---|
|
||||
@@ -188,11 +186,7 @@ Exercise the following fault checks while keeping UART0 available. Button-trigge
|
||||
| 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 a concurrent-transport check, connect USB CDC, one WebSocket terminal, and one SSH terminal while UART1 carries sustained bidirectional traffic. Navigate the UI, run the persistence checks, exercise service stop/start and writer revocation, and inject the display/stuck-button faults above. Confirm UART0 remains responsive, the UI never appears as a broker client or injects serial bytes, writer ownership is always the expected client or none, and serial/broker drop counters do not increase unexpectedly. Record `memory`, transport counters, `wifi counters`, and `debug display status` before and after the run.
|
||||
|
||||
## Before RS-232 diagnostics
|
||||
|
||||
Disconnect external DE-9 equipment and use UART0 for these checks. After each power-up, run `serial stop` before any RS-232 `debug` command, including `debug status`. The diagnostics refuse access while the serial service owns the port. If cleanup reports a port fault or a UART driver that could not be removed, reboot before continuing.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Firmware update
|
||||
|
||||
The administrator can upload a locally built **application binary** through HTTPS. There is no release server, automatic download, or automatic update service.
|
||||
|
||||
Related guides: [Web administration](web_administration.md), [account provisioning](user_administration.md), [command reference](command_reference.md), and [security operations](security_operations.md). Implementation details are in the [web development contract](devel/web_administration.md#application-firmware-upload); historical acceptance and regression guidance remain in [roadmap Phase 10](devel/roadmap.md#phase-10--simple-admin-web-firmware-upload).
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Keep **USB-to-UART** available for UART0 administration and wired flashing. Native USB CDC is UART1 serial access, not the administration/flashing recovery path described here.
|
||||
- Install OTA-enabled firmware **by wire first**. Firmware without the upload route cannot install its own first web updater. The custom OTA partition layout is required. [One-time migration from the old default layout](getting_started.md#partition-migration-and-erasing) is a separate, destructive procedure—not an ordinary update step.
|
||||
- Select trusted firmware for **`esp32-s3-devkitc-1-n16r8`**, compatible with this board, partition layout, and existing stored configuration schemas. Check the [legacy downgrade warning](devel/roadmap.md#phase8-legacy-credential-compatibility): older v1-only firmware cannot read the newer TLS-only HTTPS material. An older build is not automatically a safe recovery choice.
|
||||
- Save desired RAM-only serial, Wi-Fi, hostname, and display settings. Record nonsecret configuration and verified public HTTPS/SSH fingerprints. Keep passwords separately in a password manager; there is no supported NVS backup/restore facility.
|
||||
- Arrange a maintenance window, stable power, and stable network access. Stop work that depends on uninterrupted serial traffic. Use trusted HTTPS and an `admin` account, verifying device identity as described in [security operations](security_operations.md).
|
||||
|
||||
## Build and upload
|
||||
|
||||
1. From the trusted project checkout, build:
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
2. Select exactly the application output:
|
||||
|
||||
```text
|
||||
.pio/build/esp32-s3-devkitc-1-n16r8/firmware.bin
|
||||
```
|
||||
|
||||
Do not select an ELF, ZIP, merged full-flash binary, bootloader, or partition-table image. Renaming another file to `.bin` does not make it suitable. The browser requires a nonempty `.bin` no larger than **4 MiB**; the device also checks the actual destination capacity and image structure/integrity.
|
||||
3. Sign in as `admin` and open **Settings → HTTPS / Reboot → Firmware update**.
|
||||
4. Choose the file in **Application .bin**, select **Upload and reboot…**, and confirm. Keep power connected.
|
||||
5. Wait for validation and reboot—not just **100%** transmitted bytes. The UI may first report **Upload complete. Validating firmware…**, then **Firmware accepted; rebooting. Reconnect and sign in shortly.**
|
||||
6. Restore network reachability, use **Reconnect after reboot** or reload, and explicitly sign in again. Verify the running application, saved configuration, account access, and HTTPS/SSH identities before another upload. Check UART0 if the result is unclear.
|
||||
|
||||
Upload handling is synchronous and can stall or drop browser serial/admin connections and other web work. UART0 and native USB are independent recovery/data paths, not guarantees of uninterrupted timing during flash operations. Reboot interrupts **all** transports.
|
||||
|
||||
## What is preserved—and what is not guaranteed
|
||||
|
||||
The updater writes only the inactive application slot and OTA selection metadata. It does not rewrite the partition table/bootloader or erase NVS, PHY, storage, or other data partitions. Saved users, credentials, settings, and device identities remain stored. This preserves stored bytes, not compatibility with arbitrary firmware: the uploaded application must understand existing schemas and must not itself erase or incompatibly migrate them.
|
||||
|
||||
The server checks target/header, completeness, exact image length, and SDK image integrity, including the appended SHA-256 digest, before selecting the image. **A digest is not publisher authentication.** Neither validation nor a `.bin` filename proves that the firmware is trusted, fully board-compatible, or able to boot and operate correctly.
|
||||
|
||||
There is **no image-signature/signing-key policy, secure-boot guarantee, anti-rollback/version-downgrade enforcement, automatic rollback, or post-boot health-confirmation mechanism** in this update workflow. There is no partition migration or NVS backup/restore. A valid but broken application can require wired recovery.
|
||||
|
||||
## Lost response, timeout, or failed upload
|
||||
|
||||
**Do not blindly retry or power-cycle an uncertain update.** A lost response, page close, sign-out, timeout, or browser abort proves neither cancellation nor failure. The image may already be selected, and any later reset can boot it.
|
||||
|
||||
- Before boot selection, rejected/incomplete images and receive/write/validation failures abort the upload without selecting the candidate. The inactive slot may already be erased or partially written.
|
||||
- `firmware_commit_failed` means boot-metadata selection failed with SDK transactional uncertainty. Inspect the device before any reset or retry; do not infer that selection definitely stayed unchanged.
|
||||
- A successful response means validation and boot selection succeeded. Successful server send schedules reboot after about 500 ms, but does not prove browser receipt or successful boot.
|
||||
- If sending the response fails **after selection**, the device schedules no automatic reboot and rejects further admissible uploads with **409 `firmware_selected_reboot_required`** until device reset. HTTPS stop/start does not clear this latch. Inspect via UART0 and deliberately reboot when appropriate.
|
||||
- Reloading clears browser-side uncertainty locks only; it does not undo committed work or clear the device's selected-image latch.
|
||||
|
||||
The device's ten-second receive-stall and 120-second receive-loop checks are not hard flash-operation deadlines. Synchronous erase/write/validation can extend elapsed time. The browser's 180-second timeout cannot cancel committed work. Software reboot/lifecycle exclusions cannot prevent physical reset, power loss, or panic.
|
||||
|
||||
### Interpreting errors
|
||||
|
||||
| Response | Meaning / next step |
|
||||
|---|---|
|
||||
| 400 | Malformed, incompatible, or incomplete image/request. Check the build artifact before a deliberate new attempt. |
|
||||
| 401 / 403 | Authentication, Origin/CSRF, or administrator access failure. Restore a valid admin login; if upload had begun, inspect state first. |
|
||||
| 408 `firmware_timeout` | Device receive deadline exceeded. Inspect state and network conditions. |
|
||||
| 409 `firmware_selected_reboot_required` | Image already selected; inspect and deliberately reboot, rather than upload again. |
|
||||
| 413 / 415 | Image exceeds destination capacity or wrong content type. Use the supported application binary/browser workflow. |
|
||||
| 500 `firmware_write_failed` / `firmware_commit_failed` | Flash/selection failure. Use UART0 to inspect; commit failure carries selection uncertainty. |
|
||||
| 503 | Busy, unavailable partition/service, or insufficient resources. Resolve the condition before a deliberate retry. |
|
||||
|
||||
A disconnected browser may never receive the error. Competing web requests may wait during upload rather than promptly report busy.
|
||||
|
||||
## Wired recovery
|
||||
|
||||
If the new application does not boot or no longer serves HTTPS:
|
||||
|
||||
1. Connect the USB-to-UART path and select a known-good **schema- and layout-compatible** project checkout.
|
||||
2. Run the documented wired workflow:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
3. Inspect startup, restore connectivity, verify saved settings and identities, and test normal operation before returning to web updates.
|
||||
|
||||
**Do not erase for ordinary updates or recovery.** Erasing destroys persisted configuration and credentials. There is no automatic recovery flashing or rollback. If the partition layout itself needs migration, use the separate migration procedure knowingly rather than treating erase as generic troubleshooting.
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# Getting started
|
||||
|
||||
This guide takes a supported, wired board from its first firmware installation to USB or network serial access. If firmware is already installed, start at [Open the recovery console](#3-open-the-recovery-console). Review [security operations](security_operations.md) before enabling network access.
|
||||
|
||||
## 1. Check the hardware
|
||||
|
||||
Use the ESP32-S3-DevKitC-1-compatible **N16R8** target (16 MB flash, 8 MB octal PSRAM) and the Adafruit MAX3243 breakout described in [Hardware wiring](wiring.md). Follow that GPIO map, not a generic ESP32 pinout.
|
||||
|
||||
- Wire with power disconnected. Power the breakout and optional OLED from **3.3 V**, not 5 V.
|
||||
- Never connect RS-232 voltage directly to ESP32 GPIOs. The MAX3243 is a level shifter, not a galvanic isolator.
|
||||
- Select a straight-through cable for a DCE peer or a suitable null-modem cable for another DTE peer. A gender changer alone does not cross signals.
|
||||
- Use [Electrical tests](electrical_tests.md) to verify a new assembly before attaching valuable equipment. Active RS-232 diagnostics require the serial service to be stopped.
|
||||
|
||||
There are two distinct USB paths:
|
||||
|
||||
| Board connector | Typical Linux device | Use |
|
||||
|---|---|---|
|
||||
| USB-to-UART bridge | `/dev/ttyUSB*` | Firmware installation, UART0 logs, and the `serial-tool>` administration/recovery console |
|
||||
| Native USB | `/dev/ttyACM*` | UART1 serial data shared with browser and SSH clients |
|
||||
|
||||
Names and connector labels vary between boards. Identify the actual port rather than assuming the example device name. Consult the board schematic before powering through both connectors.
|
||||
|
||||
## 2. Install firmware by wire
|
||||
|
||||
Install [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html), obtain a trusted project checkout, and run these commands from its root. PlatformIO downloads the configured toolchain and dependencies on the first build, so initial setup needs network access.
|
||||
|
||||
```sh
|
||||
pio run
|
||||
```
|
||||
|
||||
Connect the **USB-to-UART** port, then upload:
|
||||
|
||||
```sh
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
If automatic port selection is ambiguous, specify your actual bridge port; for example:
|
||||
|
||||
```sh
|
||||
pio run --target upload --upload-port /dev/ttyUSB0
|
||||
```
|
||||
|
||||
Do not erase for ordinary installation over a compatible layout or for routine updates. If migrating from the former default/factory layout or unrelated firmware, read [Partition migration and erasing](#partition-migration-and-erasing) first. Subsequent application-only updates can use the [browser firmware updater](firmware_update.md).
|
||||
|
||||
## 3. Open the recovery console
|
||||
|
||||
Use the bridge port at **115200 baud**:
|
||||
|
||||
```sh
|
||||
pio device monitor -b 115200
|
||||
```
|
||||
|
||||
Alternatively, on Linux with picocom installed and the actual bridge at `/dev/ttyUSB0`:
|
||||
|
||||
```sh
|
||||
picocom /dev/ttyUSB0 -b 115200
|
||||
```
|
||||
|
||||
After an unattended boot, press Enter once in an ANSI-capable terminal to enable line editing, history, cursor movement, and hierarchical Tab completion. At `serial-tool>`, run:
|
||||
|
||||
```text
|
||||
help
|
||||
serial status
|
||||
wifi status
|
||||
```
|
||||
|
||||
This console controls the ESP32, not the equipment attached to RS-232. Keep it available while changing network settings or credentials. Physical UART0 access is not protected by network account passwords.
|
||||
|
||||
## 4. Set the equipment's serial parameters
|
||||
|
||||
Match UART1 to the attached equipment. For example, for **115200 baud, 8N1, no hardware flow control**:
|
||||
|
||||
```text
|
||||
serial set baud 115200
|
||||
serial set data-bits 8
|
||||
serial set parity none
|
||||
serial set stop-bits 1
|
||||
serial set flow none
|
||||
serial status
|
||||
serial save
|
||||
```
|
||||
|
||||
Use `serial set flow rts-cts` only when the peer and cable support it. DTR is configured separately with `serial set dtr inactive`, `active`, or `on-connect`; choose the behavior your equipment requires. See the [serial command reference](command_reference.md#serial-service) for supported values.
|
||||
|
||||
Working serial edits are RAM-only until `serial save`. Reconfiguration or stopping the service discards pending serial-service data, so do it while traffic is idle. USB terminal baud/framing choices **do not configure physical UART1**.
|
||||
|
||||
## 5. Use native USB without networking
|
||||
|
||||
Connect the native USB port and open its CDC device with a serial terminal that asserts DTR. This creates the `usb-cdc` client, starts UART1 if needed, and requests writer ownership. If another client already owns it, USB joins as an observer.
|
||||
|
||||
On UART0, inspect:
|
||||
|
||||
```text
|
||||
usb status
|
||||
broker clients
|
||||
```
|
||||
|
||||
Exactly **one client can write** to UART1; others receive the incoming serial stream. Coordinate with the current writer before requesting control with `usb request-writer`; use `usb release-writer` to give it up. A slow observer may lose its own buffered output without blocking other clients.
|
||||
|
||||
Native USB remains usable when network services fail. It is not the account-recovery console and does not provide uninterrupted operation through reboot.
|
||||
|
||||
## 6. Provision network access
|
||||
|
||||
On trusted UART0, create an administrator and, if desired, a separate SSH serial account:
|
||||
|
||||
```text
|
||||
user add maint admin
|
||||
user add operator user
|
||||
```
|
||||
|
||||
Enter unique passwords at the hidden prompts. There is **no default shared login**. Account changes persist immediately. See [User administration](user_administration.md) for password rules, SSH keys, and recovery.
|
||||
|
||||
Inspect `wifi profiles` first to avoid replacing an existing profile. For a new device, this example configures slot 0; replace the example SSID with your trusted network's name:
|
||||
|
||||
```text
|
||||
wifi profile set 0 0 mixed "Your network name"
|
||||
wifi profile secret 0
|
||||
wifi profile enable 0
|
||||
wifi start
|
||||
wifi status
|
||||
```
|
||||
|
||||
Enter the Wi-Fi password at the secret prompt, not in the command line. Connection changes are asynchronous: check `wifi status` again for the resulting connection and addresses. Once correct, run:
|
||||
|
||||
```text
|
||||
wifi save
|
||||
mdns status
|
||||
web certificate info
|
||||
ssh host-key info
|
||||
```
|
||||
|
||||
Record the public identity fingerprints through this trusted local connection. Wi-Fi and mDNS hostname settings have separate Save commands. For device-AP policy and password setup, see the [Wi-Fi reference](command_reference.md#wi-fi); revealing an AP secret with `wifi ap show-secret` should only be done in a private, unrecorded console.
|
||||
|
||||
### Browser
|
||||
|
||||
Open `https://` followed by the hostname reported by `mdns status`, or a reachable IP address. Compare the browser certificate fingerprint with the local `web certificate info` output **before entering credentials**. Sign in and follow the [browser guide](web_administration.md) to connect Serial and manage control. Connecting or reconnecting automatically requests the writer lease; check **Role** before typing.
|
||||
|
||||
### SSH
|
||||
|
||||
Compare the client's host-key fingerprint with `ssh host-key info` before trusting it. Use an interactive SSH connection to the device address with the desired account:
|
||||
|
||||
- `user` accounts open the UART1 stream.
|
||||
- `admin` accounts open device administration, not UART1.
|
||||
|
||||
For example, replace `DEVICE_ADDRESS` with the reported hostname or IP:
|
||||
|
||||
```sh
|
||||
ssh operator@DEVICE_ADDRESS
|
||||
```
|
||||
|
||||
A serial SSH connection starts UART1 if needed and automatically requests writer ownership. If USB or another client already owns it, SSH remains an observer and typed input is discarded, not queued for later. Check ownership before typing. Releasing the other writer does not automatically promote this session; deliberately assign it control from administration, or reconnect after the lease is free.
|
||||
|
||||
SSH supports interactive shells, not exec commands, file transfer, or forwarding. Use the [SSH reference](command_reference.md#ssh-serial-transport) for connection details. Inspect the writer with `broker clients` on an administration connection; coordinate before deliberately transferring control through [Broker settings](web_administration.md#serial-observer-and-writer) or `broker force-writer <client-id>`.
|
||||
|
||||
## If something does not work
|
||||
|
||||
| Symptom | First checks |
|
||||
|---|---|
|
||||
| No `serial-tool>` prompt | Check that you opened the bridge, not native USB; use 115200 baud and press Enter. |
|
||||
| USB opens but cannot transmit | Check host DTR, `usb status`, and the current writer in `broker clients`. |
|
||||
| Garbled or missing serial data | Check UART1 settings, cable crossing, ground, and RTS/CTS support. Use the safe loopback procedures before changing wiring. |
|
||||
| Browser or SSH is unreachable | Inspect `wifi status`, `web status`, and `ssh status` on UART0; test a reachable numeric address to separate discovery from connectivity. |
|
||||
| Hostname works on one computer only | Check that the other client supports local mDNS resolution for the required IPv4/IPv6 family. See [network discovery](command_reference.md#mdns). |
|
||||
| Forgotten account password | Use normal `user password` on UART0. Do not erase or invoke unavailable-database recovery for a healthy database. |
|
||||
| Lost response after a settings change | Reconnect and inspect actual state before retrying; disconnection does not cancel accepted work. |
|
||||
|
||||
## Partition migration and erasing
|
||||
|
||||
**Erasing the whole flash destroys all persisted configuration, accounts, Wi-Fi credentials, and device identities.** Record what you need to reprovision first. Do not use it as routine troubleshooting or an ordinary update step.
|
||||
|
||||
The first migration from the former default/factory partition layout to this project's custom OTA layout requires an erase. Migration from unrelated firmware or another layout may also require it. Only when deliberately performing such a migration or a complete destructive reset:
|
||||
|
||||
```sh
|
||||
pio run --target erase
|
||||
pio run --target upload
|
||||
```
|
||||
|
||||
Then provision the device again and verify its new HTTPS/SSH fingerprints locally. See [flash layout](devel/flash_layout.md) for the partition map and [firmware recovery](firmware_update.md#wired-recovery) for recovery that does not routinely erase NVS.
|
||||
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -0,0 +1,325 @@
|
||||
# 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.
|
||||
|
||||
## Status legend
|
||||
|
||||
- **Complete** — implemented and validated on the target hardware.
|
||||
- **In progress** — implementation or validation is actively underway, but the overall phase is not complete.
|
||||
- **Implemented; validation pending** — code is present and builds, but the current implementation still needs the listed hardware checks.
|
||||
- **Planned** — accepted project direction, not yet implemented.
|
||||
- **Under evaluation** — useful candidate whose feasibility, security, or resource cost must be measured before it becomes a commitment.
|
||||
|
||||
## Project-wide design constraints
|
||||
|
||||
These constraints apply across all phases:
|
||||
|
||||
- UART0 remains the trusted physical administration and recovery console.
|
||||
- UART1 and the MAX3243 provide the single physical RS-232 data port.
|
||||
- All serial transports use the central session broker: one writer, multiple observers.
|
||||
- A slow or failed client must not block UART reception or unrelated clients.
|
||||
- Serial data paths remain binary-transparent; transports must not reserve in-band control sequences.
|
||||
- Network serial access must be authenticated and encrypted. No unauthenticated plaintext TCP serial service is planned.
|
||||
- Secrets must not be exposed by routine status output, command completion, or logs.
|
||||
- Resource use must remain bounded and observable on the ESP32-S3 N16R8 target.
|
||||
- New work must preserve USB CDC and UART0 recovery when Wi-Fi or a network service fails.
|
||||
- Project code remains `GPL-3.0-only`; third-party license notices and compatibility must be preserved.
|
||||
|
||||
## Phase overview
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|---|---|---|
|
||||
| 0 | RS-232 hardware characterization and diagnostics | **Complete** |
|
||||
| 1 | Persistent UART1 serial-service foundation | **Complete** |
|
||||
| 2 | Transport-neutral session broker | **Complete** |
|
||||
| 3 | Native USB CDC-ACM serial transport | **Complete** |
|
||||
| 4 | Wi-Fi station/AP foundation and network diagnostics | **Complete** |
|
||||
| 5A | Authenticated HTTPS administration foundation | **Complete** |
|
||||
| 5B | Offline xterm.js WebSocket serial terminal | **Complete** |
|
||||
| 6 | Authenticated SSH serial transport | **Complete** |
|
||||
| 7 | Local display and button interface | **Complete** |
|
||||
| 8 | Role-based users and administrative access | **In progress (8A–8C complete; current 8D guided workflows implemented, all 8D target validation pending)** |
|
||||
| 9 | Security and production hardening | **Planned** |
|
||||
| 10 | Authenticated, rollback-capable OTA | **Planned** |
|
||||
| 11 | BLE serial transport and provisioning evaluation | **Planned** |
|
||||
| 12 | Advanced network integration | **Under evaluation** |
|
||||
| 13 | Optional filesystem-backed features | **Under evaluation** |
|
||||
|
||||
## Completed phases
|
||||
|
||||
### Phase 0 — RS-232 hardware characterization
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Central board pin profile for the ESP32-S3 N16R8 and MAX3243 breakout.
|
||||
- MAX3243 enable, static driver, receiver, valid-voltage, and modem-signal checks.
|
||||
- UART loopback across supported framing and baud-rate combinations.
|
||||
- Hardware CTS transmit gating and RTS receive-backpressure tests.
|
||||
- Exclusive RS-232 port ownership and fault-safe cleanup.
|
||||
- RGB status indication for diagnostic state.
|
||||
- Phase 0 hardware diagnostics, including signal status, are isolated under the `debug` CLI submenu.
|
||||
|
||||
Detailed procedures are in [Electrical tests](electrical_tests.md) and [Hardware wiring](wiring.md).
|
||||
|
||||
### Phase 1 — Serial-service foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Persistent, versioned UART configuration with safe defaults.
|
||||
- UART1 lifecycle management and exclusive ownership.
|
||||
- Nonblocking RX and TX software streams.
|
||||
- Baud rate, data bits, parity, stop bits, RTS/CTS, DTR policy, and RTS threshold controls.
|
||||
- Runtime counters and safe restart after working-configuration changes.
|
||||
- Explicit save/load/default/reset behavior; edits are not silently persisted.
|
||||
|
||||
### Phase 2 — Session broker
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Up to eight generation-safe broker clients.
|
||||
- Exactly one writer lease with multiple observers.
|
||||
- Bounded per-client output and event queues.
|
||||
- Slow-observer isolation and drop accounting.
|
||||
- Writer grant, release, denial, revocation, and forced administrative reassignment.
|
||||
- DTR `on-connect` integration.
|
||||
- Partial-transfer and retryable no-progress semantics for transports.
|
||||
- Broker payload storage placed preferentially in PSRAM while FreeRTOS control structures remain in internal RAM.
|
||||
|
||||
### Phase 3 — Native USB CDC-ACM
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Native ESP32-S3 USB CDC-ACM serial transport.
|
||||
- Automatic broker connection when the host opens the port with DTR asserted.
|
||||
- Opportunistic writer acquisition with observer fallback.
|
||||
- Binary-transparent bidirectional serial data.
|
||||
- Host line-coding visibility for diagnostics; UART1 remains under explicit serial configuration control.
|
||||
- Disconnect cleanup, bounded buffering, counters, and UART0 lifecycle controls.
|
||||
|
||||
### Phase 4 — Wi-Fi foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Four persistent station profiles with priority ordering.
|
||||
- WPA2/WPA3 mixed mode and WPA3-SAE-required mode.
|
||||
- Explicit no-echo secret entry through UART0.
|
||||
- AP policies `off`, `fallback`, and `always`.
|
||||
- Random persistent fallback-AP password and MAC-derived default SSID.
|
||||
- Bounded profile attempts, DHCP deadlines, failover, and retry backoff.
|
||||
- Root and `wifi` aliases for `ping`, `nslookup`, and `traceroute`.
|
||||
- Wi-Fi/lwIP payload allocation configured to prefer PSRAM with capacities pinned in `sdkconfig.defaults`.
|
||||
|
||||
Post-validation enhancements implemented; regression validation pending:
|
||||
|
||||
- Edits to disabled station profiles are staged without restarting the active radio; enable-state changes and edits affecting enabled radio policy apply asynchronously.
|
||||
- Configurable STA-only `sak-<suffix>.local` mDNS hostname with an independent versioned NVS record, live hostname updates, automatic withdrawal/restoration across STA IPv4 transitions, and nonfatal failure isolation. The responder initializes at most once and keeps its task stack internal while general metadata prefers PSRAM.
|
||||
|
||||
A WPA3-required profile correctly rejects an AP that does not advertise the required Protected Management Frame capability; mixed mode is the compatible choice for such an AP.
|
||||
|
||||
### Phase 5A — Authenticated HTTPS foundation
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- HTTPS-only service on TCP port 443 using ESP-IDF mbedTLS.
|
||||
- Persistent random shared `admin` credential, later retained only as migration/recovery material by Phase 8.
|
||||
- Persistent device-specific ECDSA P-256 certificate and private key.
|
||||
- Physical-console credential retrieval, rotation, certificate inspection, rotation, and recovery.
|
||||
- Explicit failure behavior that preserves UART0, USB, serial, and Wi-Fi recovery paths.
|
||||
- Credential rotation revocation hooks shared with WebSocket and SSH sessions.
|
||||
|
||||
### Phase 5B — Offline browser terminal
|
||||
|
||||
Implemented and hardware-validated:
|
||||
|
||||
- Vendored xterm.js and FitAddon assets; no Internet or CDN dependency.
|
||||
- CSP-compatible page structure with scripts served as separate resources.
|
||||
- Authenticated status and one-time WebSocket-ticket APIs.
|
||||
- Binary WebSocket serial frames and broker writer/observer integration.
|
||||
- Two bounded browser-terminal sessions.
|
||||
- Writer request/release controls and role indication.
|
||||
- Bounded TLS/socket behavior and slow-observer isolation.
|
||||
- Viewport-constrained terminal fitting without recursive page growth.
|
||||
- Validation with multiple clients, ANSI colors, advanced escape sequences, and full-screen terminal software.
|
||||
|
||||
Post-validation enhancement implemented; browser regression validation pending: the combined Connect/Disconnect control explicitly closes the terminal WebSocket and pauses automatic reconnect until Connect is selected.
|
||||
|
||||
### Phase 6 — Authenticated SSH serial transport
|
||||
|
||||
Implemented and target-hardware validated:
|
||||
|
||||
- wolfSSH on TCP port 22 with a separate persistent ECDSA P-256 host key.
|
||||
- Two bounded interactive shell/PTY sessions; no `exec`, file transfer, forwarding, or subsystems.
|
||||
- Authenticated, binary-transparent broker forwarding with opportunistic writer acquisition and observer fallback.
|
||||
- Bounded handshake attempts/deadlines, receive work, buffers, and a single wolfSSH owner task pinned to CPU 1.
|
||||
- UART0 lifecycle, session, counter, and host-key administration.
|
||||
- PSRAM-preferred allocations with internal fallback and internal task stacks.
|
||||
- Bounded broker/WebSocket work, software mbedTLS AES, and software wolfCrypt AES/SHA to avoid the validated shared-hardware-crypto/PSRAM watchdog failure while retaining ESP-IDF SHA/MPI acceleration.
|
||||
- Concurrent USB, WebSocket, and SSH operation validated with USB and SSH alternately holding the writer lease.
|
||||
|
||||
### Phase 7 — Local display and buttons
|
||||
|
||||
Implemented and target-hardware validated:
|
||||
|
||||
- Optional 128×64 SSD1315-compatible I²C OLED on GPIO11/12 and active-low buttons on GPIO10/13/14; assignments remain centralized in `board_pins.h`.
|
||||
- Display service with a static framebuffer, bounded dirty-page commits, and separate yellow status rows 0–15 plus blue content rows 16–63.
|
||||
- Low-priority status/control task built from copied public snapshots; no service lock is held across I²C and the UI never becomes a broker client.
|
||||
- Overview, RS-232, broker, and network/service pages with no credential or key material.
|
||||
- Confirmation-protected local controls for service lifecycle, Wi-Fi reconnect/profile rotation, writer release, display off, and reboot; the UI cannot assign a writer or edit secrets/configuration.
|
||||
- Persistent dim/off settings, bounded boot animation, consumed wake presses, stuck-button quarantine, and one-shot confirmations.
|
||||
- Missing, disconnected, or unresponsive display hardware remains nonfatal and can recover through one bounded reprobe.
|
||||
- Concurrent serial, UART0, USB, WebSocket, SSH, and injected display/button fault behavior validated. See [Electrical tests](electrical_tests.md) and [Hardware wiring](wiring.md).
|
||||
|
||||
## Current and planned phases
|
||||
|
||||
Phase 8A through 8C are complete and target-hardware validated. The current Phase 8D browser-session, admin-shell, typed serial/user/Wi-Fi/display, broker-client, and writer-transfer implementation is present, but all Phase 8D target-hardware validation remains pending. Later work remains planned or under evaluation. Optional features must not weaken the completed serial and recovery paths.
|
||||
|
||||
### Phase 8 — Role-based users and administrative access
|
||||
|
||||
Replace the single shared network credential with a bounded, persistent user system. 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.
|
||||
|
||||
Implementation sequence:
|
||||
|
||||
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.
|
||||
- Transactional migration of the legacy credential to an unprivileged account, explicit UART0 bootstrap, and UART0-only recovery that replaces only the unavailable user blob.
|
||||
- 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 Basic plus SSH password/public-key authentication use the common database and copied secret-free principals; unavailable user storage fails closed.
|
||||
- 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.
|
||||
- Legacy credentials remain migration/recovery-only after bootstrap. Password/key login, ticket behavior, targeted revocation, recovery, and concurrent transport operation are target-hardware validated.
|
||||
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 recovery-secret display, 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. Initial `user bootstrap` and explicit `user recover --force` remain physical-UART0 operations; 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 — In progress**
|
||||
- **Implemented, validation pending:** replace browser-facing HTTP Basic authentication with a same-origin HTTPS login page, explicit logout, and bounded opaque server-side sessions. Store only a digest of each random session token with a copied secret-free principal, expiry, CSRF state, and authentication-generation binding. Send the raw token only in a host-only `__Host-` cookie with `Secure`, `HttpOnly`, `SameSite=Strict`, `Path=/`, no `Domain`, and an explicit lifetime; never retain passwords, Basic headers, raw tokens, verifiers, or SSH-key blobs in snapshots or logs.
|
||||
- **Implemented, validation pending:** logout invalidates the current server-side session, expires its cookie, closes that session's serial and administrative WebSockets, and redirects to login. Password/role/key mutation, deletion, recreation, and explicit revocation invalidate the affected account's web sessions and tickets without disturbing unrelated accounts. Require the CSRF token plus strict same-origin checks for every state-changing request, including logout, and rate-limit login attempts with bounded secret-free accounting.
|
||||
- **Implemented, validation pending:** an admin-only **Serial terminal**/**Admin shell** selector. The administrative route uses a short-lived, single-use, admin-principal-bound ticket and a bounded WebSocket frontend for the same serialized command registry used by UART0 and admin SSH; it is not a generic HTTP command-execution endpoint. Normal users retain the existing serial interface and cannot mint, upgrade, or invoke administrative routes. Remote policy still rejects physical-only `user bootstrap` and `user recover --force`.
|
||||
- **Implemented, validation pending:** switching the visible terminal between Serial and Admin changes only the displayed terminal route. It must not disconnect the browser's serial broker client, release its writer lease, or silently stop serial observation. Keep the writer/observer badge plus Request control/Release control visible in both modes so an administrator knows the retained state and cannot unintentionally lose the lease to another client. Explicit Disconnect, logout, revocation, session expiry, or an explicit release/transfer operation still performs normal broker cleanup.
|
||||
- **Implemented, validation pending:** the admin-only Settings area uses typed, bounded APIs rather than generated CLI strings. Full serial framing/lifecycle/load/default/save controls and Wi-Fi lifecycle/profile-rotation actions remain available.
|
||||
- **Implemented, validation pending:** `/api/admin/users` provides guided account CRUD, role changes, entered or one-time generated passwords, and authorized Ed25519/P-256 key add/remove. Existing-account mutations carry both the optimistic database generation and stable user ID, so stale state and delete/recreate races fail without retargeting. `user_admin_service` serializes CLI and web mutations with `admin_command_gate`; after a committed mutation it requests best-effort web and SSH revocation without rolling back the database if notification fails.
|
||||
- **Implemented, validation pending:** `/api/admin/wifi-config` provides typed station profile, enable/disable/delete, AP policy/SSID/channel, write-only station/AP secret, and Save operations. Reads disclose only `secret_set` flags. Every edit is a full validated compare-and-swap against the expected nonzero working generation, Save persists exactly the expected generation, and generation conflicts or exhaustion fail closed.
|
||||
- **Implemented, validation pending:** `/api/admin/display` provides typed display-aging Apply/Save/Load/Defaults/Reset operations with the same validation and persistence semantics as the console. All five operations serialize with console display writers through `admin_command_gate`.
|
||||
- **Implemented, validation pending:** the common body-backed URL-form parser decodes in place and remains bounded to 512 bytes and 10 unique fields. The browser clears entered and generated secrets when Settings closes or an operation fails; stale user and Wi-Fi editors reload current state without replaying the mutation.
|
||||
- **Implemented, validation pending:** contextual admin quick settings on the existing status cards. Hover, keyboard focus, or click on **Serial** opens a popover with current framing/lifecycle state, safe common edits, apply/save semantics, and a link to full Serial settings. The **Wi-Fi** card similarly exposes connection/profile controls and a link to full Network settings without revealing saved secrets by default. Touch and keyboard users must receive the same functionality as pointer hover, with Escape/outside-click dismissal and no action triggered merely by opening a popover.
|
||||
- **Implemented, validation pending:** hover, focus, or click on **Broker clients** opens a live, secret-free client list with IDs, transport, writer/observer state, and bounded queue/drop information. Activating **Active writer** opens an admin-only transfer dialog listing current eligible clients; transfer is an explicit confirmed generation-safe administrative assignment, never a side effect of opening or hovering. Stale/disconnected targets fail visibly without changing the current lease. Normal users may retain ordinary aggregate status but receive neither client-management details nor mutation controls.
|
||||
- **Implemented, validation pending:** HTTPS lifecycle operations are fully serialized and generation-tagged. TLS certificate/material replacement requires a post-commit TLS refresh; explicit newer start/stop intent wins races. Admin-transport HTTPD API work is disabled and tracked during teardown, failed HTTPD stop retains ownership for retry, and incomplete post-stop admin finalization remains pending and is retried before a later start.
|
||||
- **Implemented, validation pending:** preserve strict CSP, no-referrer/frame-denial policy, no-store responses for login/session/admin material, secret-safe JSON encoding, one-time generated-password display, and bounded request/response bodies. Keep UART0 recovery, native USB UART1 access, and existing SSH behavior available if web sessions or administration cannot initialize.
|
||||
- **Implemented, validation pending:** secret-free counters/snapshots cover session capacity, expiry, login failure/backoff, logout, invalidation, CSRF/origin rejection, admin-console admission/backpressure, typed-API failure, and writer-transfer races.
|
||||
- **Not provided as guided forms:** broader service/session administration, network diagnostics, security and danger-zone operations, and unusual hardware/debug commands remain available through the canonical Admin shell under its existing remote policy.
|
||||
|
||||
Completion requires login/logout and account switching without browser credential-cache dependence; stale-session, CSRF, origin, capacity, expiry, and revocation tests; hidden and server-rejected normal-user administration; shared admin-console serialization and backpressure; terminal switching that demonstrably preserves the browser broker client and writer lease; accessible Serial/Wi-Fi/client/writer popovers; generation-safe explicit writer transfer; guided user/key, Wi-Fi secret/CAS, display validation, bounded-parser, and HTTPS teardown/finalizer tests; concurrent USB/WebSocket/user-SSH/admin-SSH/browser-admin operation; and continued UART0 recovery. None of the current Phase 8D target-hardware checks has passed yet.
|
||||
|
||||
### Phase 9 — Security and production hardening
|
||||
|
||||
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.
|
||||
|
||||
Planned work:
|
||||
|
||||
- Define development, test, and production security profiles.
|
||||
- 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.
|
||||
|
||||
### Phase 10 — Authenticated OTA and rollback
|
||||
|
||||
Use the existing dual 4 MiB application slots and `otadata` partition to add safe firmware updates.
|
||||
|
||||
Planned work:
|
||||
|
||||
- Accept only authenticated, integrity-checked firmware images compatible with the selected secure-boot policy.
|
||||
- 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.
|
||||
|
||||
### Phase 11 — BLE
|
||||
|
||||
Evaluate and, if resource limits permit, add BLE as another bounded transport or provisioning path.
|
||||
|
||||
Planned work:
|
||||
|
||||
- Decide whether BLE is primarily a serial transport, a provisioning interface, or both.
|
||||
- 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.
|
||||
|
||||
### Phase 12 — Advanced network integration
|
||||
|
||||
These features are candidates, not current commitments:
|
||||
|
||||
- Additional DNS-SD service advertisement and any certificate-name integration beyond the implemented configurable STA hostname.
|
||||
- Enterprise Wi-Fi support, subject to credential-storage and certificate-validation design.
|
||||
- IPv6 behavior and diagnostics beyond the current basic support.
|
||||
- WireGuard feasibility evaluation, including RAM, CPU, licensing, key storage, routing, and recovery impact.
|
||||
|
||||
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 13 — Optional filesystem-backed features
|
||||
|
||||
The `storage` partition is reserved but not currently mounted. Possible uses must be justified individually:
|
||||
|
||||
- LittleFS-backed web assets to decouple large static files from the application image.
|
||||
- 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.
|
||||
|
||||
## Cross-phase release gates
|
||||
|
||||
Every phase should satisfy the following before being marked complete:
|
||||
|
||||
1. A clean release build for the ESP32-S3 N16R8 target.
|
||||
2. No new compiler or project diagnostics attributable to the change.
|
||||
3. Focused automated tests where practical and documented hardware tests where hardware behavior is involved.
|
||||
4. UART0, native USB CDC, and previously completed network transports still operate.
|
||||
5. Broker writer/observer semantics and binary transparency remain intact.
|
||||
6. Failure, disconnect, stop/start, reboot, and stale-session cleanup paths are exercised.
|
||||
7. Runtime memory, stack, socket, and queue bounds are measured under maximum supported concurrency.
|
||||
8. Secrets remain absent from ordinary status output and logs.
|
||||
9. [Command reference](command_reference.md), [Hardware wiring](wiring.md), electrical tests, and this roadmap are updated as applicable.
|
||||
10. New dependencies receive security, maintenance, and GPL-3.0 compatibility review.
|
||||
|
||||
## Explicitly deferred work
|
||||
|
||||
The following are not implemented merely because flash partitions or library support exist:
|
||||
|
||||
- NVS, flash, or PSRAM encryption.
|
||||
- Secure boot or production eFuse provisioning.
|
||||
- OTA download, image confirmation, or rollback policy.
|
||||
- Core-dump collection or secret-safe core-dump processing.
|
||||
- Filesystem mounting.
|
||||
|
||||
- SFTP, SCP, SSH `exec`, forwarding, or subsystems.
|
||||
- General routing, NAPT, captive-portal interception, or unauthenticated serial access.
|
||||
|
||||
Deferring these features is intentional: each changes the security model, recovery behavior, or bounded-resource assumptions and therefore requires its own design and validation gate.
|
||||
@@ -1,101 +0,0 @@
|
||||
# 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 provisioning, daily use, credential changes, and recovery with the existing
|
||||
firmware. It does not provide protection against physical extraction or firmware replacement.
|
||||
|
||||
## Provision once, locally
|
||||
|
||||
- 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 12–64 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.
|
||||
|
||||
## Maintenance precautions
|
||||
|
||||
Before installing firmware, review its documented changes and security notices.
|
||||
If a relevant security issue is reported, stop network use until an appropriate
|
||||
update or mitigation is available; native USB serial remains an option.
|
||||
Keep wired UART0 recovery available when updating, and do not assume an update
|
||||
preserves configuration unless its migration guidance says so.
|
||||
|
||||
See the [command reference](command_reference.md) for command details.
|
||||
This checklist is operating guidance, not a security audit or verification of any
|
||||
individual device's flash/eFuse configuration. Contributor maintenance and evidence
|
||||
notes are in the [developer index](devel/README.md#security-maintenance-and-evidence).
|
||||
@@ -1,118 +0,0 @@
|
||||
# User administration
|
||||
|
||||
Provision accounts locally first, then use the browser or authenticated admin SSH for routine administration.
|
||||
|
||||
Related guides: [Web administration](web_administration.md), [firmware updates](firmware_update.md), [command reference](command_reference.md#role-based-users), [security operations](security_operations.md), and the [web development contract](devel/web_administration.md).
|
||||
|
||||
## Choose roles and keep local recovery
|
||||
|
||||
| Account role | HTTPS | SSH |
|
||||
|---|---|---|
|
||||
| `user` | Serial terminal and status | UART1 serial stream through the broker |
|
||||
| `admin` | Serial, Admin, and Settings | Device administration shell, not UART1 |
|
||||
|
||||
Use a separate `user` account for ordinary SSH serial work. Account roles do not override the broker's one-writer/multiple-observer rule. Admin SSH is not a serial broker client.
|
||||
|
||||
Keep physical **UART0 administration** available on a trusted computer. Native USB CDC is a different, network-independent UART1 data path, not an account-recovery console. Physical local access is not protected by network account authentication.
|
||||
|
||||
## Create the first administrator
|
||||
|
||||
A new device with missing user-database storage starts with a persisted empty database. There is no default shared login, credential import, or `user bootstrap` command. Existing valid user databases retain their accounts unchanged; an older role-`user` account is not silently promoted.
|
||||
|
||||
On UART0:
|
||||
|
||||
```text
|
||||
user status
|
||||
user list
|
||||
user add maint admin
|
||||
user show maint
|
||||
```
|
||||
|
||||
Enter and confirm a unique password at the no-echo prompts. Passwords must contain **12–64 printable ASCII characters**. `Ctrl-C` cancels a prompt. Usernames must match `[a-z][a-z0-9_-]{0,15}`: 1–16 characters, starting with a lowercase letter, followed by lowercase letters, digits, underscore, or hyphen.
|
||||
|
||||
To generate a password instead:
|
||||
|
||||
```text
|
||||
user add maint admin --generate
|
||||
```
|
||||
|
||||
Use one creation form, not both for the same name. Generated passwords are displayed once: save the value in a password manager, avoid terminal recording, and do not share the output. Successful account changes persist immediately; no separate Save command is needed.
|
||||
|
||||
Before testing network login, compare `web certificate info` and `ssh host-key info` from trusted UART0 with the browser/SSH client's fingerprints. Follow [security operations](security_operations.md), then [sign in through HTTPS](web_administration.md#connect-and-sign-in). For SSH, connect using an interactive shell with the chosen account; exec/subsystem and file-transfer workflows are not supported.
|
||||
|
||||
## Add and maintain accounts
|
||||
|
||||
Examples for UART0 or an authenticated admin SSH shell:
|
||||
|
||||
```text
|
||||
user add operator user
|
||||
user list
|
||||
user show operator
|
||||
user password operator
|
||||
```
|
||||
|
||||
The database holds at most **eight accounts**. Account summaries expose roles and public-key fingerprints, not passwords, salts, or verifiers. Passwords cannot be recovered from status output; set a replacement when necessary.
|
||||
|
||||
For an intentional role change or removal:
|
||||
|
||||
```text
|
||||
user role operator admin --force
|
||||
user role operator user --force
|
||||
user delete operator --force
|
||||
```
|
||||
|
||||
These are separate examples, not a sequence to run routinely. Check the target and intended access before using `--force`. The final administrator cannot be deleted or demoted. Password, role, key, and deletion changes invalidate the affected username's outstanding tickets and active web/SSH sessions; unrelated accounts are not intentionally revoked. Your own change may disconnect you before you see its result. Inspect state and test the intended login before repeating an uncertain operation.
|
||||
|
||||
`user password <username> --generate` displays a replacement once. Admin SSH rejects generation for its own account to avoid losing the value during revocation; use trusted UART0 or the supported browser workflow instead.
|
||||
|
||||
### Browser account management
|
||||
|
||||
Sign in as `admin`, open **Settings → Accounts**, and select the intended account for password, role, deletion, or authorized-key operations. Changes commit on successful submission; they are not RAM-only drafts awaiting a global Save.
|
||||
|
||||
The generated-password workflow first displays a new value without changing the account. Store it securely and acknowledge that it is saved, then explicitly submit the create/password change. Generation alone does not set a password; the saved acknowledgement is not proof that the subsequent operation succeeded. Self changes may end your login immediately.
|
||||
|
||||
The browser **Admin shell** has narrower account permissions than Accounts settings: status/list/show and selected interactive changes to other accounts are supported, but self changes, generated passwords, key commands, and database recovery are denied there. Use typed Accounts controls or UART0 rather than trying to bypass these restrictions.
|
||||
|
||||
## Authorize an SSH client key
|
||||
|
||||
1. On a trusted client, generate or choose an OpenSSH **Ed25519** or **ECDSA P-256** key. Keep the private key on that client; import only the single-line `.pub` public key.
|
||||
2. On UART0 or authenticated admin SSH, run:
|
||||
|
||||
```text
|
||||
user key add operator
|
||||
```
|
||||
|
||||
3. Paste the public-key line at the prompt. `Ctrl-C` cancels. Alternatively use the Accounts key-import controls in the browser.
|
||||
4. Run `user show operator` and compare the reported key type and `SHA256:` fingerprint with the client's public key. Test a new SSH login with the intended username before closing local recovery access.
|
||||
|
||||
Each account supports **three keys**. Supported types are `ssh-ed25519` and `ecdsa-sha2-nistp256`; RSA and OpenSSH certificate keys are not supported. Duplicate keys within an account are rejected. The same key can be assigned to multiple accounts, but access then follows the username selected at login.
|
||||
|
||||
Delete by the stable index shown in `user show`, not by its position in a displayed list:
|
||||
|
||||
```text
|
||||
user key delete operator 0 --force
|
||||
```
|
||||
|
||||
Use `user key clear operator --force` only when intentionally removing all that account's keys. Key changes persist immediately and revoke that account's sessions. Changing a password does **not** remove authorized keys; review both after compromise. Authorized client keys are separate from the device's SSH host identity.
|
||||
|
||||
## Forgotten passwords and database failure
|
||||
|
||||
For a healthy database, use UART0 and normal commands such as `user password maint` to restore access. First inspect `user status` and `user list`; do not erase flash or reset identities to repair a forgotten account password.
|
||||
|
||||
Only if database initialization has failed and the database is **unavailable**, deliberate UART0 recovery is:
|
||||
|
||||
```text
|
||||
user recover --force
|
||||
user status
|
||||
user add maint admin
|
||||
```
|
||||
|
||||
**Recovery destroys the user database's contents and rebuilds it empty.** It refuses a healthy database, including a healthy empty database, and is unavailable over SSH/browser. It does not erase unrelated Wi-Fi/serial configuration or HTTPS/SSH identities. Recreate required accounts and authorized keys afterward. Check each command result; storage failures are not evidence of successful recovery.
|
||||
|
||||
`web reset --force` and `ssh reset --force` recover their respective device identities, not user accounts. Neither is a whole-device factory reset.
|
||||
|
||||
## Persistence and security limits
|
||||
|
||||
Keep a private reprovisioning record of usernames/roles, public keys, settings, and verified device fingerprints; store passwords separately. There is no supported configuration/credential/identity backup-export command. Application-only [firmware updates](firmware_update.md) preserve stored bytes, but compatibility with the new firmware still matters.
|
||||
|
||||
NVS credentials are not protected against physical extraction by this workflow. Password verifiers can be attacked offline, and logical replacement does not securely erase historical flash contents. Keep the device and local ports controlled, avoid Internet exposure, and follow the [security checklist](security_operations.md).
|
||||
@@ -1,45 +1,6 @@
|
||||
# User administration and authentication tests
|
||||
|
||||
Source/build/test paths in code spans and commands are relative to the repository root
|
||||
unless a different working directory is stated; Markdown links are relative to this document.
|
||||
|
||||
Reusable procedures, **not execution evidence**. [Phase 8 is accepted](roadmap.md#phase8-acceptance-evidence); overall 8D.22 user sign-off does not assert that every checklist item below ran. Historical 8A/B sections describe their original baselines only; current cookie authentication, removed bootstrap/credentials and UART0-only recovery override them. Use current contracts and the integrated regression section for present behavior. Never log generated/entered passwords, cookies/tickets or private/verifier material.
|
||||
|
||||
## Current Broker management regression procedure — 8D.16
|
||||
|
||||
The [Broker contract](web_administration.md#broker-and-contextual-controls) defines current API, generations, wrap safety and bounds. Retain these checks for future regressions; acceptance is recorded separately.
|
||||
|
||||
- Verify admin-only detailed client rows against non-consuming UART0 snapshots/counters; direct normal-user management GET/POST denied. Observe zero/one/full client mix without reading UART payloads as a probe.
|
||||
- Opening, selecting, cancelling, refreshing and navigating must leave writer and both terminals unchanged. Explicit full-page Refresh clears selection; contextual refresh retains selected target/version and sticky stale/absence latches until deliberate reselection. Native confirmation must identify exact target and snapshot writer.
|
||||
- Assign among USB/SSH/two browser serial clients with both admins connected. Disconnect/reuse target while confirmation is open; race writer release/reacquire, competing requests, shell force and local release. Stale confirmation must fail without changing the intervening lease; Refresh/reselect/confirm explicitly.
|
||||
- Test separate-login result isolation, same-login tab replacement, pending capacity, lost ACK/result, bounded polling/manual recovery, logout/expiry/revocation and HTTPS stop/restart. Never infer cancellation from connection loss or replay automatically.
|
||||
- At signed-off 160 MHz/230400 baud, check full-mix binary traffic/drop isolation, UART0/USB recovery and optional-route failure isolation. Capture exact revision/mix and internal/DMA/PSRAM plus HTTPD/dispatcher margins; host tests/build do not approve reserves or target throughput for an unmeasured workload.
|
||||
|
||||
## Current Network settings regression procedure — 8D.12/8D.13
|
||||
|
||||
The [Network contract](web_administration.md#network) defines the current byte codec, owner/persistence semantics and uncertainty. Retain these regression procedures without inferring individual execution from overall sign-off.
|
||||
|
||||
- Verify admin-only Settings/Network and direct-route normal-user denial, current cookie/principal, body/query/framing/Origin/CSRF checks, unavailable/contended snapshots and generation races against CLI/local controls.
|
||||
- Round-trip UTF-8 and arbitrary SSID bytes through text/hex, including NUL/BOM/non-UTF-8 and 32-byte boundaries. No saved PSK or length may appear in responses, status/logs/completion/local display. Keep omits credentials; Replace never accepts blank; disabled-STA Clear (including disable+clear) works; enabled-STA/AP clear is denied even when AP policy is off. Check transient-input expiry and context/session clearing.
|
||||
- Distinguish RAM Apply, explicit Save, stored-only Wi-Fi Load and reboot persistence. Missing/invalid/failing Wi-Fi storage must not install generated defaults or new AP secrets. Exercise stale generations, queue failure/drop accounting and NVS failures without secret logging. No Wi-Fi reset/default/export action exists.
|
||||
- Prepare UART0 and USB before confirming disruptive actions. Test Start/Stop (including RAM boot policy), Reconnect/Next, AP policies and stopped no-ops. The selected profile is an edit target, not explicit connection selection; Next uses canonical priority/wrap. Cancel confirmations and exercise lost ACK/401/disconnect, manual Check Result/Refresh, another-tab result replacement and no automatic replay. `accepted` is not online; delivery before disconnection is not guaranteed. Reconnect via STA/AP; UART0 administers recovery, USB preserves independent UART1 access.
|
||||
- Test mDNS generation/Set/Save/Load/Defaults, live/offline reannouncement, next STA IP, init/live failure isolation and `applied_not_queued`. Verify actual client DNS and changed-hostname browser trust/login, not merely `announced`. Confirm no unintentional Wi-Fi secret reset.
|
||||
- With USB/two web serial/SSH serial and both admin routes, verify hidden output draining and writer/observer preservation through Settings navigation. Separate actual network-disruption losses from serial/broker regressions. Exercise optional Network route/timer failure and stop/restart without taking down unrelated routes. Browser-shell restrictions remain unchanged.
|
||||
- Capture boot/full-mix internal/DMA/PSRAM free/minimum/largest blocks and memory floors during TLS/Network operations; timer heap/slot costs, repeated-operation cleanup/soak and **HTTPD/dispatcher stack margins** remain required. Record exact revision/client mix and nonsecret counters, including broker and manager queue drops. Host tests do not establish target reserve or hard scheduling/cancellation guarantees.
|
||||
|
||||
## Current legacy-removal regression procedure
|
||||
|
||||
The legacy bootstrap/credential/reconciliation instructions in the phase baselines below are **historical only**, superseded by [legacy credential removal](roadmap.md#phase8-legacy-credential-compatibility) and the current [command reference](../command_reference.md). Basic authentication is also historical; current HTTPS uses cookie login. The current overrides apply to later Phase 8C/browser procedures too: no `user bootstrap` or `web credentials` command remains, first-admin creation uses normal UART0 `user add`, and recovery rebuilds empty. Never treat the checklist below as evidence of execution.
|
||||
|
||||
1. On a disposable controlled NVS image with only `user_db/database` missing, boot and confirm an empty database is persisted with zero accounts/admins and no imported credential. Reboot and confirm it remains empty. Keep physical UART0 attached; do not factory-erase the device for this test.
|
||||
2. Run `user add maint admin` on UART0, check hidden password confirmation and cancellation, then confirm account/password persistence after reboot. Separately exercise `user add operator user --generate` and secure one-time display. Final-administrator delete/demotion must still fail. Existing SSH own-password generation restrictions and typed browser generated-password support remain unchanged.
|
||||
3. Load a valid existing v1 user image and verify accounts, roles, IDs, auth generations, verifiers and keys are unchanged, including a formerly migrated role-`user` account. No bootstrap status should appear and no account should be silently promoted. No web material change may synchronize a verifier.
|
||||
4. On a disposable malformed user image, confirm authentication fails closed without automatically overwriting storage. On UART0 run `user recover --force`, confirm empty storage, then `user add maint admin`. Recovery must refuse healthy databases, including healthy empty storage, and be unavailable through SSH/browser. Verify serial/Wi-Fi configuration and TLS/SSH identities remain intact. Inject read/write/commit failures where available and check failure isolation and complete committed records, not partial live mutations.
|
||||
5. Upgrade valid 1,392-byte v1 `web_sec/material`; verify persisted 1,340-byte TLS-only v2 and exact certificate/key DER, fingerprint and generation retention in a controlled fixture without logging private data. Reboot and confirm identity continuity. Malformed/unknown/cryptographically invalid records and migration read/write/commit failures must fail closed without fallback replacement. Do not interpret a failed commit as proof that no flash write occurred.
|
||||
6. Confirm help/completion/status expose no legacy credential/bootstrap operation or secret. Removed commands must reject without mutation. Exercise retained user generation and `web certificate rotate --force` through supported frontends. `web reset --force` must change TLS only, require new certificate trust/login after HTTPS restart, leave user credentials/generations unchanged, and not revoke unrelated SSH sessions.
|
||||
7. Recheck UART0/native USB availability and broker one-writer/isolated-observer behavior with network authentication unavailable. Record only nonsecret counters and telemetry. Older v1-only firmware cannot read HTTPS v2; logical NVS replacement is not secure flash erasure and no factory erase is required.
|
||||
|
||||
Acceptance and prior evidence limits are recorded in [legacy compatibility](roadmap.md#phase8-legacy-credential-compatibility) and [overall acceptance](roadmap.md#phase8-acceptance-evidence); the preceding procedure is not an execution log.
|
||||
This document retains phase-specific regression procedures. The Phase 8A and 8B sections describe historical baselines rather than the current end-to-end behavior. Phase 8C's shared UART0/admin-SSH shell is validated. The current Phase 8D worktree adds explicit browser sessions, the canonical browser admin shell, typed serial/user/Wi-Fi/display workflows, and broker controls; use the Phase 8D section for pending browser validation. Never include generated or entered passwords, Wi-Fi secrets, private keys, tickets, cookies, CSRF values, or verifier material in test logs.
|
||||
|
||||
## Historical Phase 8A baseline — role-based database and UART0 administration
|
||||
|
||||
@@ -205,9 +166,9 @@ While an administrative command is queued or running, use UART0 to change that a
|
||||
|
||||
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
|
||||
## Phase 8D integrated web administration
|
||||
|
||||
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.
|
||||
**Validation status: all current Phase 8D target-hardware validation is pending.** The browser-session/admin-shell foundation and typed serial, user, Wi-Fi, display, client, and writer workflows are implemented. None of the checks below should be marked passed until exercised on target hardware.
|
||||
|
||||
### 1. Integrated login and authorization
|
||||
|
||||
@@ -217,7 +178,7 @@ Authenticate as both roles through the same-origin login page, explicitly log ou
|
||||
|
||||
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.
|
||||
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. Paste a line longer than 256 bytes followed by a dangerous valid-command suffix before the newline; the whole overlong line must be discarded through CR/LF and the suffix must not execute. Physical-only bootstrap/recovery commands remain rejected. Closing only the admin-console route must leave the browser serial client and its lease intact.
|
||||
|
||||
### 3. Quick settings and client popovers
|
||||
|
||||
@@ -225,18 +186,39 @@ For an administrator, open the **Serial** and **Wi-Fi** card popovers by hover,
|
||||
|
||||
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
|
||||
### 4. Guided users, roles, passwords, keys, and revocation
|
||||
|
||||
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.
|
||||
1. Through Settings, create disposable `user` and `admin` accounts with both entered and generated passwords. Confirm the generated value is displayed once, is absent from subsequent account reads, and is cleared when Settings closes or an operation fails. Exercise list/select, role change, entered password replacement, account deletion, and recreation.
|
||||
2. Add disposable Ed25519 and ECDSA P-256 public keys. Confirm only type, slot, and fingerprint are returned afterward. Re-adding a key to the same account must return a conflict without changing its generation or key list; adding that same key to a second account must succeed independently. Remove each key and verify the matching private key no longer authenticates for that account.
|
||||
3. Open the same account editor in two administrator sessions. Commit a mutation in the first, then submit the stale form from the second. It must receive a conflict, reload the current user list, clear entered password/key material, and not replay the request. Delete and recreate the username before submitting another stale form; the stable user-ID check must prevent it from targeting the replacement account.
|
||||
4. Keep affected browser and SSH sessions plus unrelated-account sessions active. After each committed password, role, key, or delete mutation, confirm best-effort web/SSH revocation is attempted, affected principals lose access, and unrelated sessions continue. Where revocation-hook failure can be injected, confirm the database commit remains authoritative and stale sessions close at their next currentness check rather than rolling back the mutation.
|
||||
5. With at least two administrators, change the signed-in administrator's entered password or role and confirm self-revocation prevents further admin requests. Generated replacement of the current remote administrator must be rejected. Then reduce the database to one administrator and verify attempts to demote or delete that final administrator are rejected without a generation change or session revocation.
|
||||
|
||||
### 5. Concurrency and failure isolation
|
||||
### 5. Guided Wi-Fi configuration and secret handling
|
||||
|
||||
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.
|
||||
1. Read `/api/admin/wifi-config` and inspect browser state, HTTP responses, URLs, routine logs, and status endpoints. Saved station/AP secrets must never appear; only `secret_set` flags may indicate their presence.
|
||||
2. Exercise station SSID/priority/security apply, enable, disable, delete, and secret replacement for each slot. Exercise AP policy, SSID, channel, and secret replacement. Confirm disabled-profile-only edits remain staged without disrupting the active radio, while effective-policy changes follow the existing asynchronous restart behavior.
|
||||
3. Save a known working generation, reboot, and confirm it persists. With two editors at the same generation, let one commit and then have the stale editor submit a profile/AP edit or Save. The stale request must return a conflict without changing RAM or NVS. The browser must clear both secret fields, reload current state, and require explicit re-entry rather than replaying the secret-bearing request. Where generation exhaustion can be injected, confirm edit and Save both fail closed.
|
||||
4. Close Settings and force server, authorization, validation, and conflict failures after entering station/AP secrets. Confirm entered values are cleared and do not reappear when Settings is reopened.
|
||||
|
||||
### 6. Service identity, lost acknowledgement and recovery
|
||||
### 6. Guided display aging
|
||||
|
||||
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.
|
||||
Exercise typed Apply, Save, Load saved, Defaults, and confirmed Reset. Verify valid zero-disabled transitions and valid increasing dim/off delays, then reject values above 86400 and any case where both transitions are enabled but off is not greater than dim. Invalid requests must leave the active configuration unchanged. Confirm Apply is RAM-only, Save survives reboot, Load restores the saved value, Defaults applies 300/600 seconds without persistence, and Reset applies and persists those defaults. Concurrently submit browser operations and display-writer commands from UART0 or a remote Admin shell; confirm `admin_command_gate` serializes each complete operation, with no mixed RAM/NVS result or lost update. Repeat with the local-UI configuration service unavailable and confirm a bounded failure without affecting UART0, USB, serial, Wi-Fi, or HTTPS.
|
||||
|
||||
### 7. Display, API bounds and optional failures
|
||||
### 7. Bounded typed requests and HTTPS lifecycle 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.
|
||||
For `/api/admin/serial`, `/api/admin/users`, `/api/admin/wifi-config`, `/api/admin/display`, and the writer endpoint, send malformed URL encoding, unknown/missing fields, duplicate fields, 11 fields, 513-byte bodies, stale sessions, wrong CSRF, wrong Origin, and normal-user requests. Confirm the 512-byte/10-unique-field body parser rejects them without side effects and that secret-bearing values never enter the request URL.
|
||||
|
||||
Run `web stop`, `web certificate rotate --force`, and `web reset --force` from the browser Admin shell. Administrative output must use bounded best-effort draining; certificate/material replacement must still perform the mandatory TLS refresh if the invoking frontend disappears or its drain times out. Verify the new certificate is served after reconnect and that a newer explicit start/stop intent wins a lifecycle-generation race.
|
||||
|
||||
Exercise these injected teardown paths separately:
|
||||
|
||||
1. Force admin-transport detach timeout followed by successful HTTPD stop. Confirm post-stop finalization runs only after HTTPD destruction; if finalization times out, the next start retries it and does not attach a new server until it succeeds.
|
||||
2. Force HTTPD stop failure. Confirm the handle remains owned, no second HTTPS server starts, transport-owned HTTPD calls stay disabled/tracked, and a later Stop can retry safely.
|
||||
3. Hold an admin HTTPD API operation in flight during detach. Confirm teardown does not free or finalize its server state early and no retired work calls HTTPD after successful destruction.
|
||||
|
||||
Service/session controls beyond the guided serial/Wi-Fi actions, network diagnostics, security/danger operations, and unusual hardware/debug commands have no guided forms; validate them through the canonical Admin shell under existing remote-command policy.
|
||||
|
||||
### 8. 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 Settings and popovers. Verify one writer, isolated observers, bounded memory/queues, serialized user mutations, 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.
|
||||
@@ -1,78 +0,0 @@
|
||||
# Web administration
|
||||
|
||||
Use the browser to operate the UART1 serial terminal and, with an administrator account, manage the device. Use a trusted LAN or the device's password-protected AP—not an Internet-exposed connection.
|
||||
|
||||
Related guides: [Account provisioning](user_administration.md), [firmware updates](firmware_update.md), [command reference](command_reference.md), and [security operations](security_operations.md). Protocol and implementation details are in the preserved [development contract](devel/web_administration.md).
|
||||
|
||||
## Connect and sign in
|
||||
|
||||
1. Provision an account through physical **UART0** first; see [user administration](user_administration.md). Native USB CDC carries UART1 data and is not the administration console.
|
||||
2. Connect to the device's network and open its HTTPS address (port 443), using its configured hostname or reachable IP address. The terminal assets are served by the device; Internet access is not required.
|
||||
3. Before entering credentials, compare the browser's SHA-256 certificate fingerprint with `web certificate info` on trusted UART0. Do not blindly accept an unexpected certificate warning.
|
||||
4. Sign in with your account username and password. Both `user` and `admin` can use Serial; only `admin` can use Admin and Settings. SSH client keys do not replace the browser password.
|
||||
|
||||
A login expires **one hour after sign-in**, even while active. There are four login-session slots, two browser serial connections, and one browser admin connection; these are separate limits. Login attempts are globally limited to five per 60 seconds. If admission fails, close unused connections or sign out unused sessions and wait rather than repeatedly submitting credentials.
|
||||
|
||||
**Disconnect** closes the serial connection and pauses its automatic reconnect; **Connect** resumes it. Neither signs you out. Use **Sign out** when finished. If sign-out is not confirmed because the network failed, do not assume server-side logout succeeded; retry when reachable.
|
||||
|
||||
## Serial: observer and writer
|
||||
|
||||
All transports share one physical RS-232 port. Exactly one broker client can write; other clients observe the incoming UART1 stream.
|
||||
|
||||
Connecting or reconnecting Serial automatically requests control. If no other client owns the writer lease, your connection becomes writer. Check **Role** after every reconnect; **Release control** applies to the current connection, not future reconnects.
|
||||
|
||||
- Check **Role**, **Active writer**, and the connection status before typing. The dashboard's Writer/Observer role is not your account's `admin`/`user` role.
|
||||
- Choose **Request control** to request the writer lease. An administrator account does not automatically make this serial connection the writer. If another client owns control, coordinate with that operator.
|
||||
- Type only when writer status is confirmed. Observer input is disabled.
|
||||
- Choose **Release control** when finished transmitting so another client can write.
|
||||
- Administrators can inspect **Broker** settings or the writer/client quick views and explicitly assign control to a selected current client. Refresh and deliberately reselect after a stale-target warning. Transfer cannot recall bytes already accepted for transmission.
|
||||
|
||||
Slow observers can lose their own output without blocking other clients. The browser has bounded output buffering and 5,000 lines of scrollback per terminal; it is not a durable recording. Observe reported drop counts when completeness matters. Serial transport remains binary-transparent: there are no in-band administration escape sequences.
|
||||
|
||||
## Serial, Admin, and Settings
|
||||
|
||||
Switching views does **not** reconnect Serial or request/release its writer lease. Hidden connected terminals continue receiving output; keyboard input goes only to the selected terminal.
|
||||
|
||||
- **Serial** is the connected equipment's UART1 stream.
|
||||
- **Admin** is the ESP32 command console, not the connected equipment. Choose **Open admin** explicitly. Closing Admin leaves Serial connected. One browser admin connection and two remote-console slots shared with admin SSH bound availability.
|
||||
- **Settings** provides typed controls for Serial settings, Accounts, Network, Display, Broker, SSH, and HTTPS / Reboot. Opening a page, selecting a client, or opening a quick view does not apply changes.
|
||||
|
||||
Use the [command reference](command_reference.md) for commands, but do not assume full UART0/SSH parity in the browser shell. Browser account commands are limited to selected operations on other accounts; use **Accounts** for supported self/password-generation/key workflows. Browser SSH identity/service mutations belong in typed **SSH** settings. Browser `web` commands are restricted to status, stop, and certificate rotation; recovery remains on UART0 or reachable admin SSH. Wi-Fi commands include hidden secret prompts and can immediately disconnect you. Avoid explicit secret-display commands in recorded/shared terminals.
|
||||
|
||||
## Apply, Save, and persistent state
|
||||
|
||||
| Area | Operator rule |
|
||||
|---|---|
|
||||
| Serial settings | Apply and Defaults change working RAM. Save persists the device's working values, not unsent browser drafts. Reconfiguration or stop discards pending serial-service data, though broker clients and writer ownership remain. |
|
||||
| Network | Wi-Fi edits require explicit Save; hostname/mDNS settings have separate persistence. Start/Stop also change the RAM boot-enabled policy. Save only after checking the desired working state. |
|
||||
| Display | Apply changes working configuration; Save persists it. Load reads saved state; Reset persists defaults. |
|
||||
| Accounts and authorized keys | Successful mutations are committed immediately; there is no separate account Save. They revoke the affected account's network sessions. |
|
||||
| HTTPS/SSH identities | Successful replacement persists immediately. Rotation is not an account-password change. |
|
||||
| Broker/service controls | These operate on live connections/services; they are not saved configuration profiles. |
|
||||
|
||||
Load, Defaults, and Reset are not synonyms. Read each confirmation and the [command reference](command_reference.md) before discarding configuration. Network quick views intentionally omit password controls; use the full Network page. USB host baud/framing selections do not configure UART1; use Serial settings.
|
||||
|
||||
Save desired working settings before reboot or [firmware upload](firmware_update.md). Saved configuration survives an ordinary reboot; unsaved RAM settings and browser drafts may not.
|
||||
|
||||
## Disruptive changes and uncertain results
|
||||
|
||||
Plan a maintenance window and keep UART0 available before changing networking, identities, services, or firmware.
|
||||
|
||||
- **Network:** profile/AP changes, stop, or reconnect may cut off the result. An accepted operation is not proof of Wi-Fi association, address acquisition, or name resolution. Restore connectivity and inspect actual state before retrying.
|
||||
- **Accounts:** changing your own password, role, or keys may revoke your login before the result appears. See [account management](user_administration.md).
|
||||
- **HTTPS stop/restart:** closes all web logins and both browser terminal routes. A stopped web service cannot be restarted through its own page; use `web start` on UART0 or still-reachable admin SSH.
|
||||
- **HTTPS certificate rotation:** verify the new fingerprint through UART0 before renewing browser trust. A failure after storage commit can leave a new stored identity even if restart fails; a retained server may still serve the old certificate. Do not repeat rotation blindly.
|
||||
- **SSH controls/host-key rotation:** can close SSH sessions. Verify a changed host-key fingerprint with `ssh host-key info` on UART0 before updating client trust. HTTPS is not inherently restarted by SSH rotation.
|
||||
- **Reboot:** interrupts every transport, including native USB and UART operation during restart. Reconnect and sign in explicitly afterward.
|
||||
- **Firmware upload:** can stall or drop browser sessions before reboot. Follow the separate [firmware procedure](firmware_update.md).
|
||||
|
||||
A timeout, lost response, navigation, logout, or revoked login does **not** cancel already admitted work. Settings results are temporary observations, not a durable operation history. Use **Check Result**/**Refresh** where offered (SSH and HTTPS / Reboot require manual checking), then inspect actual device state. Never replay a destructive action merely because its acknowledgement disappeared.
|
||||
|
||||
## Recovery
|
||||
|
||||
1. If the page or session fails, restore network access, reload, and sign in again. Check identity warnings locally before entering credentials.
|
||||
2. If networking or HTTPS is unavailable, use physical UART0 for status and repair. Reachable admin SSH can also recover HTTPS with `web stop` / `web start` when appropriate.
|
||||
3. Repair forgotten passwords with normal UART0 account commands. `user recover --force` is destructive unavailable-database recovery, not a password-reset shortcut; see [user administration](user_administration.md).
|
||||
4. If a firmware update prevents boot or web access, use [wired recovery](firmware_update.md#wired-recovery) as described in the firmware guide.
|
||||
|
||||
Native USB remains network-independent UART1 access when network services fail, but cannot repair accounts or identities and does not promise uninterrupted data through flash operations or reboot. Keep local ports physically controlled. See [security operations](security_operations.md) for backup limits, identity recovery, and physical-extraction risks.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hardware wiring
|
||||
|
||||
Use these connections for the hardware profile below. Do not apply this GPIO map to another board without checking its schematic and pin assignments.
|
||||
This document records the GPIO assignments for the initial hardware profile. Additional profiles should use separate sections rather than treating this GPIO map as universal.
|
||||
|
||||
## Profile: ESP32-S3-DevKitC-1 N16R8 with Adafruit MAX3243
|
||||
|
||||
@@ -87,7 +87,7 @@ The breakout's `OFF` pin drives the MAX3243 active-low `!FORCEOFF` input. High o
|
||||
|
||||
GPIO8 and GPIO9 are not adjacent on the official J1 header. GPIO3 and GPIO46 lie between them and are boot-strapping pins, so follow the printed GPIO labels rather than counting header positions.
|
||||
|
||||
### Administration and native USB connectors
|
||||
### Development and native USB connectors
|
||||
|
||||
| DevKit connector | ESP32-S3 path | Typical Linux device | Purpose |
|
||||
|---|---|---|---|
|
||||
@@ -100,9 +100,9 @@ The USB-to-UART bridge's DTR/RTS controls serve automatic boot/reset and do not
|
||||
|
||||
Native USB CDC DTR controls the lifetime of the `usb-cdc` broker client but is not forwarded to physical DE-9 DTR. Physical DTR follows the `serial` configuration. CDC RTS is status information only; GPIO15/DE-9 RTS remains UART1 receive flow control when `flow=rts-cts` is enabled.
|
||||
|
||||
## OLED and button wiring
|
||||
## Phase 7A OLED and button wiring
|
||||
|
||||
Use the following connections for the 128×64 I²C OLED and three local buttons. The selected module acknowledges at 7-bit I²C address `0x3c` (8-bit `0x78` write / `0x79` read) and has separate yellow rows 0–15 and blue rows 16–63, divided by a narrow physical black separator:
|
||||
Phase 7A hardware validation used the following connections for the 128×64 I²C OLED and three local buttons. The selected module acknowledges at 7-bit I²C address `0x3c` (8-bit `0x78` write / `0x79` read) and has separate yellow rows 0–15 and blue rows 16–63, divided by a narrow physical black separator:
|
||||
|
||||
| Device connection | ESP32-S3 connection | Electrical behavior | Purpose |
|
||||
|---|---:|---|---|
|
||||
@@ -133,8 +133,8 @@ Before applying power, verify whether the module already includes SDA and SCL pu
|
||||
|
||||
## Electrical verification
|
||||
|
||||
See [Electrical tests](electrical_tests.md) for OLED/button checks, persistent dim/off settings, optional-display and stuck-button fault recovery, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker/concurrent-transport testing.
|
||||
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.
|
||||
|
||||
## Using a different board or transceiver
|
||||
## Future hardware profiles
|
||||
|
||||
Alternative boards—such as the LILYGO T-Display-S3—or different RS-232 transceivers require separate profiles. Review display, buttons, USB, flash/PSRAM wiring, boot-strapping pins, and onboard peripherals before assigning GPIOs.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Import("env")
|
||||
|
||||
env.Replace(COMPILATIONDB_INCLUDE_TOOLCHAIN=True)
|
||||
@@ -1,8 +0,0 @@
|
||||
# KiCad per-user state, locks and automatic backups
|
||||
*.kicad_prl
|
||||
*.lck
|
||||
~*.lck
|
||||
*-backups/
|
||||
*.bak
|
||||
__pycache__/
|
||||
.history/
|
||||
@@ -1,116 +0,0 @@
|
||||
(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)) )))
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -1,62 +0,0 @@
|
||||
(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; nominal header centres with user-measured main PCB/antenna bounds and USB shell mouth spans on F.Fab; approximate 1.365 mm top/left registration; no GPIO mapping, complete USB envelope, courtyard or RF keepout; PROVISIONAL connector fit: 1.70 mm pads / 1.00 mm drills from installed KiCad PinHeader_1x04_P2.54mm_Vertical, awaiting selected actual pin/socket datasheet")
|
||||
(tags "module provisional THT")
|
||||
(attr through_hole)
|
||||
(property "Reference" "REF**" (at 12.7 -3) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(property "Value" "HW678_2x22_Provisional" (at 12.7 57) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "PROVISIONAL CONNECTOR FIT" (at 12.7 25) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_text user "ANTENNA UP / USB DOWN" (at 12.7 28) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))
|
||||
(fp_rect (start -1.365 -1.365) (end 26.725 54.705) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_rect (start 4.045 -8.255) (end 21.675 -1.365) (stroke (width 0.1) (type solid)) (fill none) (layer "F.Fab"))
|
||||
(fp_line (start 2.795 54.705) (end 11.665 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_line (start 14.195 54.705) (end 23.065 54.705) (stroke (width 0.3) (type solid)) (layer "F.Fab"))
|
||||
(fp_text user "ANTENNA BOUNDS - NOT RF KEEPOUT" (at 12.7 -9.5) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(fp_text user "USB SHELL MOUTHS ONLY" (at 12.7 59) (layer "F.Fab") (effects (font (size 0.8 0.8) (thickness 0.12))))
|
||||
(pad "1" thru_hole rect (at 0.00 0.00) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "2" thru_hole circle (at 0.00 2.54) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(pad "3" thru_hole circle (at 0.00 5.08) (size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask") (remove_unused_layers no))
|
||||
(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))
|
||||
)
|
||||
@@ -1,34 +0,0 @@
|
||||
(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"))
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
(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"))
|
||||
)
|
||||
@@ -1,305 +0,0 @@
|
||||
# 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 A1 carrier draft with a top-left antenna notch,
|
||||
USB toward the bottom and RS-232 toward the top.** It is **not fabrication-ready**.
|
||||
See **[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md)** for placement, chosen dimensions/rules, provisional
|
||||
mechanical assumptions and validation. A review drawing is `pcb-placement-draft.pdf`.
|
||||
|
||||
The schematic contains the complete ESP32 development-board interface, MAX3243
|
||||
breakout logic interface, I²C OLED, three Omron B3F-1000 buttons and three 2 kΩ
|
||||
button pull-ups. USB connectors, regulator, reset/boot circuit, DE-9 and MAX3243
|
||||
charge pump are already on their respective modules and are not duplicated on
|
||||
the carrier. All 3.3 V and ground connections are explicit named nets.
|
||||
|
||||
## Current user acceptance and placement
|
||||
|
||||
See [user-acceptance.md](../../docs/devel/hardware/PCB/user-acceptance.md) for the exact group placements, nominal body clearances and evidence limits. Common body centreline is carrier-local X=56.970 mm; resistor bodies are centred in the RS-232/OLED gap with 2.0815 mm nominal clearance to each PCB. U1, U2, outline, carrier holes and USB placement are unchanged. R1–R3 are purchased 2 kΩ 1% metal-film parts; Omron B3F-1000 switches are also purchased.
|
||||
|
||||
The user accepts visual fit/USB clearance, male module mapping, M3 carrier/daughterboard and M2 display hardware, current RF reserve and power/ground routing. Intended/reported assembled module elevation is 10 mm, not an independently verified sum of the 8.5 mm female and described 2.5 mm male contributions. USB nonisolation is accepted; backfeed safety, regulator margin and RF qualification are not established. OLED backside pull-ups are confirmed by user, but their values/rail are unmeasured. Earlier pending-status language below is historical where superseded by this record.
|
||||
|
||||
PCB presentation uses **A3 landscape (420 × 297 mm)** instead of A4; the
|
||||
carrier stays at absolute X=100…182, Y=100…180 mm. No origin, footprint,
|
||||
routing or zone was moved. The 1:1 placement PDF includes the worksheet/title
|
||||
block, clear of the PCB; `validation/pcb-placement-page.png` previews the full
|
||||
page. Print at actual size on A3 for the 1:1 fit check, not “fit to A4”.
|
||||
`update_presentation.py` removes the accepted USB/geometry/pinout/assembly
|
||||
pending labels with lock/concurrent-edit guards and an exact-content audit.
|
||||
RF qualification and connector/case-cutout cautions remain.
|
||||
|
||||
## Files and use
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `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](../../docs/devel/hardware/PCB/pcb-draft-notes.md), `pcb-placement-draft.pdf` | PCB design decisions/limits and 1:1 placement review |
|
||||
| `validation/pcb-draft.png`, `validation/pcb-draft-3d.png` | Copper/fab preview and carrier-only 3D render |
|
||||
| `validate_pcb_draft.py` | Fresh schematic-to-PCB net checks, bounds/layers and DRC/parity report |
|
||||
| `generate_pcb_draft.py`, `route_pcb_draft.py` | Optional destructive-regeneration tools with explicit overwrite guards |
|
||||
| `add_device_silkscreen.py` | Refresh board-local OLED/RS-232 and measured ESP32 outlines/USB mouth markers after placement/routing; preserves copper and project settings |
|
||||
| [db9-mechanical-notes.md](../../docs/devel/hardware/PCB/db9-mechanical-notes.md), `reference/db9-cad/` | STEP-derived metal dimensions, originals, source licenses and repeatable extraction/validation |
|
||||
| `update_u1_graphics.py`, `validate_u1_update.py` | Guarded measured-U1 graphics integration and exact pre-update preservation / geometry / silkscreen regressions |
|
||||
| `export_placement_pdf.py` | 1:1 PDF from saved board geometry; refuses stale embedded U1 graphics |
|
||||
| `remove_draft_markers.py` | Guarded presentation-label cleanup with exact-content preservation audit; does not qualify fabrication |
|
||||
| `add_db9_envelope.py` | Refresh nominal connector metal projections on Dwgs.User; not a case cutout or assembly clearance |
|
||||
| `Carrier.kicad_sym`, `sym-lib-table` | Project-local module, switch and resistor symbols |
|
||||
| `Carrier.pretty/`, `fp-lib-table` | OLED/ESP32/RS-232-reference provisional footprints; standard KiCad 10 switch/resistor/mounting-hole library references |
|
||||
| `oled-fit-check.svg` | User-fit-verified OLED mechanical template |
|
||||
| [footprint-notes.md](../../docs/devel/hardware/PCB/footprint-notes.md), [rs232-footprint-notes.md](../../docs/devel/hardware/PCB/rs232-footprint-notes.md) | Footprint origins, provisional pad/drill choices, validation and RS-232 adaptation attribution |
|
||||
| [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md), [button-notes.md](../../docs/devel/hardware/PCB/button-notes.md), `reference/` | Manufacturer evidence, pad maps and source/license records |
|
||||
| `bom-draft.csv` | Draft procurement list and unresolved selections |
|
||||
| `validate_schematic.py`, `validation/` | Repeatable netlist/firmware/ERC checks and exported evidence |
|
||||
| `validate_footprints.py` | Native KiCad footprint geometry regression checks via system Python/pcbnew |
|
||||
| `generate_schematic.py` | Optional stdlib-only initial design generator; not needed to open/edit in KiCad |
|
||||
|
||||
Development notes live in [docs/devel](../../docs/devel/README.md); the
|
||||
[routing evidence summary](../../docs/devel/hardware/PCB/validation/pcb-routing-summary.md)
|
||||
is retained there too. Keep CAD, libraries, assets and scripts together here so
|
||||
project-relative libraries resolve. Standard KiCad
|
||||
10 footprint libraries must be installed for the THT buttons, resistors and mounting holes.
|
||||
SKiDL 2.3.0 is installed, but no SKiDL dependency is required for this native design.
|
||||
|
||||
**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 1–22 and right row is 23–44, 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 1–4 are GND, VCC, SCL, SDA, front/display view with header at top.
|
||||
- U2 pin numbering follows published Adafruit **5988 female** CAD for the selected
|
||||
**6253 male** module. The user confirmed the same pinout, header mapping and
|
||||
footprint as the reference. Both schematic and PCB now assign
|
||||
`Carrier:MAX3243_Reference_Provisional`; clean footprint parity is required.
|
||||
The identifier is unchanged; this assignment is not hardware or fabrication qualification. See [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md)
|
||||
and [pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md); this is not a generic DE-9 footprint.
|
||||
- No-connect crosses mean unused on the carrier, not disconnected internally.
|
||||
Native USB GPIO19/20, UART0 GPIO43/44, memory-conflicting GPIO35–37 and the 5 V
|
||||
header are not connected to carrier circuitry.
|
||||
- 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 R1–R3, **2 kΩ, 1%, at least 0.125 W**, from each button input to 3.3 V.
|
||||
They provide nominal 1.65 mA contact current / 5.445 mW dissipation while pressed,
|
||||
rather than relying on the internal pull-up for the silver-contact rated load.
|
||||
Firmware's internal pull-ups and debouncing remain enabled; no firmware changes
|
||||
are required. Final supply tolerance/load budget still needs checking.
|
||||
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](../../docs/devel/hardware/PCB/footprint-notes.md) for scope and limitations.
|
||||
|
||||
OLED PCB/header/display/mounting-hole geometry was physically fit-verified by
|
||||
the user. This does **not** validate connector drills, male RS-232 geometry,
|
||||
electrical operation, power budget, procurement or fabrication.
|
||||
|
||||
The routed PCB draft has **0 physical DRC violations and 0 unconnected items**;
|
||||
a fresh netlist-to-PCB check passes with **zero schematic-parity issues**,
|
||||
including the user-confirmed U2 footprint assignment. No DRC exclusions were
|
||||
added. Missing courtyards are ignored by default DRC
|
||||
and remain a manual release check. Run `/usr/bin/python3 -B
|
||||
hardware/PCB/validate_pcb_draft.py` from the repository root; see
|
||||
[pcb-draft-notes.md](../../docs/devel/hardware/PCB/pcb-draft-notes.md) for the complete verification scope and remaining gates.
|
||||
No firmware build or device tests were run: this PCB work did not modify firmware.
|
||||
|
||||
## Evidence and components
|
||||
|
||||
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. Measured planar outline and visual fit accepted by user; finished-hole process remains unqualified. |
|
||||
| RS-232 module | Adafruit MAX3243 full-pinout breakout; male is product 6253, female is 5988 | Eagle pin map/PCB geometry and STEP metal dimensions extracted. User confirms identical male pinout/header/footprint, visual fit and pin-by-pin review. Reference footprint assigned in schematic and PCB. |
|
||||
| Display | 128×64 yellow/blue I²C OLED, address 0x3c; Amazon ASIN B0F13SSSFB | 26 mm square board, 22.50 mm hole spacing, Ø2 mm holes and centred header 1.50 mm from top edge are user-fit-verified. Local footprint assigned; header pad/drill selection remains provisional. |
|
||||
| Three buttons | Omron B3F-1000; previous, select, next; normally open to ground, active low | Datasheet and `Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x` pad mapping verified. Purchased per user; external purchased 2 kΩ 1% metal-film pull-ups included. |
|
||||
|
||||
Adafruit manufacturer CAD starting point:
|
||||
https://learn.adafruit.com/adafruit-rs-232-full-pinout-level-shifter-breakout/downloads
|
||||
|
||||
Published female-module Eagle CAD and its header map were retrieved and verified;
|
||||
see [adafruit-research.md](../../docs/devel/hardware/PCB/adafruit-research.md) for pinned revision, hashes, attribution and geometry.
|
||||
The original files and CC BY-SA 3.0 notices are retained in `reference/`; they
|
||||
remain under their upstream license. Omron reference documents retain their
|
||||
manufacturer notices. The CAD-parts repository's STEP and MIT license are separately
|
||||
retained under `reference/db9-cad/`. External metal geometry is used for the male
|
||||
module on the user's explicit equivalence confirmation. The user separately
|
||||
confirmed electrical mapping and visual fit; connector threads, tolerances and
|
||||
final case cutout are not independently qualified.
|
||||
|
||||
## Verified logical connections for the existing firmware profile
|
||||
|
||||
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.
|
||||
- User reports successful 3.3 V module operation and accepts nonisolated USB power.
|
||||
Regulator margin and USB backfeed safety remain unverified, especially with two powered hosts.
|
||||
- User confirms backside OLED I²C pull-ups and operation without extras.
|
||||
Resistance and termination rail are unmeasured; no external values are invented.
|
||||
- Firmware enables internal button pull-ups; this carrier additionally uses 2 kΩ
|
||||
external pull-ups for the selected buttons' contact-current requirement.
|
||||
- RS-232 OFF uses the breakout's onboard pull-up; GPIO9 releases it to enable.
|
||||
- The breakout is not galvanically isolated; signal grounds are common.
|
||||
- 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 confirms dimensions, pinout/header/footprint
|
||||
equivalence and completed a pin-by-pin module connection review.
|
||||
- Carrier for complete modules: soldered module headers for final assembly,
|
||||
sockets for beta testing without a case. Header pitch, row spacing, drill size
|
||||
and socket clearance must be validated against selected hardware.
|
||||
- Power only through the ESP32 development board's USB connectors, typically a
|
||||
powerbank or the serial peer's USB port, with common ground. Do not introduce
|
||||
another carrier power input or assume simultaneous USB supplies are isolated.
|
||||
- User purchased Omron B3F-1000 THT buttons and 2 kΩ 1% metal-film resistors.
|
||||
- Case will be 3D-printed after PCB finalization. User approves M3 carrier holes,
|
||||
10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display
|
||||
hardware. Exact insert/boss dimensions and screw lengths are not supplied.
|
||||
|
||||
## User-supplied mechanical evidence
|
||||
|
||||
- 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 actual user measurements supersede the seller's 28 × 63 mm drawing:
|
||||
main PCB **56.07 × 28.09 mm**, excluding the antenna projection. Top/component
|
||||
view, antenna up and USB down: antenna projects **6.89 mm** above the main PCB,
|
||||
inset **5.41 mm left / 5.05 mm right**. Derived antenna width **17.63 mm**,
|
||||
overall length **62.96 mm**, antenna centre **0.18 mm right** of PCB centre.
|
||||
- Header end offsets were initially approximately 1.5 mm; user accepted **1.365 mm**
|
||||
top/bottom to preserve the breadboard-confirmed 53.34 mm span. Left-edge-to-left-row
|
||||
centre distance is also approximately **1.365 mm**. Preserve 25.40 mm row spacing:
|
||||
the resulting right margin is **1.325 mm**, not an independently measured offset.
|
||||
These modelling offsets are not a claim of micron-level measurement accuracy.
|
||||
- Both USB-C metal shells are **8.87 mm wide**, with opening faces flush with the
|
||||
main PCB bottom edge. Left shell's outside left edge is **4.16 mm** from the PCB
|
||||
left edge; right shell's outside right edge is **3.66 mm** from the PCB right edge.
|
||||
User accepts USB placement and mating-plug clearance visually; shell depth/height remain unmeasured.
|
||||
- The library and routed board's embedded U1 now record these bounds on F.Fab;
|
||||
USB mouths are width spans only, not complete connector outlines. Measured
|
||||
board-local silkscreen, 1:1 placement PDF and SVG/PNG/3D previews are refreshed. See
|
||||
`../../docs/devel/hardware/PCB/footprint-notes.md` for coordinates and limits.
|
||||
- OLED actual PCB outline measured by user: 26 × 26 mm, superseding the seller's
|
||||
28.7 × 27.8 mm dimensions. Seller's 4.1 mm height remains unverified.
|
||||
- OLED front view, connector at top: GND, VCC, SCL, SDA from left to right,
|
||||
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.
|
||||
|
||||
## Current acceptance checklist and evidence limits
|
||||
|
||||
- [x] Component dimensions and visual physical fit accepted by user, including USB placement/clearance and RS-232 prong clearance.
|
||||
- [x] Male Adafruit pinout/header/footprint equivalence and pin-by-pin module connection review confirmed by user; existing U2 schematic assignment retained with clean parity.
|
||||
- [x] M3 carrier holes, 10 mm brass standoffs into heat inserts, M3 daughterboard screws and M2 display hardware approved.
|
||||
- [x] Omron B3F-1000 and 2 kΩ 1% metal-film resistors purchased per user.
|
||||
- [x] Existing 3.3 V supply operation and OLED onboard pull-ups reported; no extra I²C pull-ups needed in reported operation.
|
||||
- [x] Nonisolated USB power, existing RF reserve and power/ground routing accepted by user.
|
||||
- [ ] Independently verified regulator margin, USB backfeed safety, OLED pull-up resistance/rail and manufacturer RF/EMC qualification are not available.
|
||||
- [ ] Detailed fastener lengths/insert dimensions, final case cutout, tolerance/height envelopes and fabrication-process qualification remain outside this acceptance.
|
||||
|
||||
The intended/reported 10 mm assembled elevation is not independently measured;
|
||||
8.5 + 2.5 = 11 mm is not evidence of actual mated height. User acceptance closes
|
||||
the corresponding review gates, not these factual evidence limits. Missing
|
||||
module courtyards still limit automated assembly checks. No new hardware tests,
|
||||
firmware changes, Gerbers, order or certification are implied.
|
||||
@@ -1,93 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add nominal STEP-derived metal bounding projections on Dwgs.User only.
|
||||
|
||||
Not silkscreen, a courtyard, or a case cutout. See ../../docs/devel/hardware/PCB/db9-mechanical-notes.md for
|
||||
source attribution, selected face subsets and the user-confirmed male/female
|
||||
external-geometry equivalence. No module height or case-wall thickness assumed.
|
||||
"""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
PCB=ROOT/"serial-carrier.kicad_pcb"
|
||||
NS=uuid.UUID("ba7274a8-c35d-46bc-908d-cd86708fc6c4")
|
||||
|
||||
|
||||
def main():
|
||||
lock=ROOT/"~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before refreshing the CAD envelopes")
|
||||
before=PCB.read_bytes()
|
||||
project=ROOT/"serial-carrier.kicad_pro"
|
||||
project_before=project.read_bytes()
|
||||
board=p.LoadBoard(str(PCB))
|
||||
u2=next(f for f in board.GetFootprints() if f.GetReference()=="U2")
|
||||
assert u2.GetOrientationDegrees()==0, "Review transform for rotated module"
|
||||
x0,y0=p.ToMM(u2.GetPosition().x),p.ToMM(u2.GetPosition().y)
|
||||
data=json.loads((ROOT/"reference/db9-cad/measurements.json").read_text())
|
||||
owned={str(uuid.uuid5(NS,str(i))) for i in range(64)}
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if drawing.m_Uuid.AsString() in owned:
|
||||
board.Delete(drawing)
|
||||
count=0
|
||||
|
||||
def point(x,y):
|
||||
# STEP/Eagle axes +Y toward connector; KiCad local Y is downward.
|
||||
return p.VECTOR2I(p.FromMM(x0+x-1.905),p.FromMM(y0+2.667-y))
|
||||
|
||||
def add(item):
|
||||
nonlocal count
|
||||
assert count<64
|
||||
item.SetLayer(p.Dwgs_User)
|
||||
item.SetUuid(p.KIID(str(uuid.uuid5(NS,str(count)))))
|
||||
board.Add(item)
|
||||
count+=1
|
||||
|
||||
for feature in ("front_flange_exposed_faces", "shell_outer_including_root_and_lip",
|
||||
"left_front_hex_post", "right_front_hex_post"):
|
||||
xmin,ymin,_,xmax,ymax,_=data[feature]["bounds"]
|
||||
rect=p.PCB_SHAPE(board)
|
||||
rect.SetShape(p.SHAPE_T_RECT)
|
||||
rect.SetStart(point(xmin,ymax)); rect.SetEnd(point(xmax,ymin))
|
||||
rect.SetWidth(p.FromMM(0.1))
|
||||
add(rect)
|
||||
for axis in data["front_lock_axes"]:
|
||||
x=axis["location"][0]
|
||||
# These mark front-facing Y axes, not holes through the carrier PCB.
|
||||
line=p.PCB_SHAPE(board)
|
||||
line.SetShape(p.SHAPE_T_SEGMENT)
|
||||
line.SetStart(point(x,26.907)); line.SetEnd(point(x,32.707))
|
||||
line.SetWidth(p.FromMM(0.08))
|
||||
add(line)
|
||||
label=p.PCB_TEXT(board)
|
||||
label.SetText("CAD METAL ENVELOPES - NOT CASE CUTOUT")
|
||||
label.SetPosition(p.VECTOR2I(p.FromMM(x0+13.97),p.FromMM(y0-17.67)))
|
||||
label.SetTextSize(p.VECTOR2I(p.FromMM(0.65),p.FromMM(0.65)))
|
||||
label.SetTextThickness(p.FromMM(0.1))
|
||||
add(label)
|
||||
for drawing in board.GetDrawings():
|
||||
if not isinstance(drawing,p.PCB_TEXT):
|
||||
continue
|
||||
value=drawing.GetText()
|
||||
if value.startswith("DE-9 OUT /"):
|
||||
drawing.SetPosition(p.VECTOR2I(p.FromMM(x0+13.97),p.FromMM(y0-20.67)))
|
||||
elif value=="82 x 80 mm / 2 copper layers":
|
||||
drawing.SetPosition(p.VECTOR2I(p.FromMM(141),p.FromMM(91)))
|
||||
elif value.startswith("Nominal RS-232 metal verified;"):
|
||||
drawing.SetPosition(p.VECTOR2I(p.FromMM(141),p.FromMM(93.5)))
|
||||
temp=ROOT/"serial-carrier-envelope-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp),board,True)
|
||||
if lock.exists() or PCB.read_bytes()!=before:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert project.read_bytes()==project_before
|
||||
temp.replace(PCB)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
print(f"Added {count} nominal CAD projection graphics on Dwgs.User; no copper/project changes")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Refresh board-local device outlines without modifying libraries or project settings.
|
||||
|
||||
Uses pcbnew and Shapely from system Python. Only owned F.SilkS graphics are
|
||||
replaced; source F.Fab outlines, copper, footprints and board edges are retained.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import math
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import LineString, Point, box
|
||||
from shapely.ops import unary_union, polygonize
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
NS = uuid.UUID("67964aa4-c3a5-45d8-9b54-250fb68e7c41")
|
||||
WIDTH = 0.12
|
||||
GAP = 0.22 # Additional clearance from mask openings and existing silk strokes.
|
||||
|
||||
|
||||
def xy(point):
|
||||
return p.ToMM(point.x), p.ToMM(point.y)
|
||||
|
||||
|
||||
def vec(point):
|
||||
return p.VECTOR2I(p.FromMM(point[0]), p.FromMM(point[1]))
|
||||
|
||||
|
||||
def outline_lines(shape):
|
||||
if shape.GetShape() == p.SHAPE_T_RECT:
|
||||
x1, y1 = xy(shape.GetStart())
|
||||
x2, y2 = xy(shape.GetEnd())
|
||||
return [LineString([(x1,y1),(x2,y1)]), LineString([(x2,y1),(x2,y2)]),
|
||||
LineString([(x2,y2),(x1,y2)]), LineString([(x1,y2),(x1,y1)])]
|
||||
if shape.GetShape() == p.SHAPE_T_SEGMENT:
|
||||
return [LineString([xy(shape.GetStart()), xy(shape.GetEnd())])]
|
||||
return []
|
||||
|
||||
|
||||
def add_outlines(board):
|
||||
verify(board)
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(4096)}
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if drawing.m_Uuid.AsString() in owned:
|
||||
board.Delete(drawing)
|
||||
footprints = {f.GetReference(): f for f in board.GetFootprints()}
|
||||
obstacles = []
|
||||
for fp in board.GetFootprints():
|
||||
for pad in fp.Pads():
|
||||
if not pad.IsOnLayer(p.F_Mask):
|
||||
continue
|
||||
assert pad.GetOrientationDegrees() % 90 == 0
|
||||
x,y = xy(pad.GetPosition())
|
||||
expansion = max(0, p.ToMM(pad.GetSolderMaskExpansion(p.F_Mask)))
|
||||
if pad.GetShape() == p.PAD_SHAPE_CIRCLE:
|
||||
geom = Point(x,y).buffer(p.ToMM(pad.GetSize().x)/2, quad_segs=32)
|
||||
else:
|
||||
bb=pad.GetBoundingBox()
|
||||
geom=box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
|
||||
obstacles.append(geom.buffer(expansion+GAP+WIDTH/2))
|
||||
for graphic in fp.GraphicalItems():
|
||||
if isinstance(graphic,p.PCB_SHAPE) and graphic.GetLayer()==p.F_SilkS:
|
||||
for line in outline_lines(graphic):
|
||||
obstacles.append(line.buffer(p.ToMM(graphic.GetWidth())/2+GAP+WIDTH/2))
|
||||
# Clear even tented vias for a robust visible guide, independent of mask settings.
|
||||
for track in board.GetTracks():
|
||||
if isinstance(track,p.PCB_VIA):
|
||||
obstacles.append(Point(xy(track.GetPosition())).buffer(p.ToMM(track.GetWidth(p.F_Cu))/2+GAP+WIDTH/2,quad_segs=32))
|
||||
blocked=unary_union(obstacles)
|
||||
edges = [LineString([xy(d.GetStart()), xy(d.GetEnd())])
|
||||
for d in board.GetDrawings() if d.GetLayer() == p.Edge_Cuts]
|
||||
polygons = list(polygonize(edges))
|
||||
assert len(polygons) == 1, 'Expected a single closed carrier outline'
|
||||
# U2's bare PCB front is flush with the carrier edge; do not print across it.
|
||||
silk_area = polygons[0].buffer(-(GAP + WIDTH / 2))
|
||||
count=0
|
||||
|
||||
def add(shape):
|
||||
nonlocal count
|
||||
assert count < 4096
|
||||
shape.SetLayer(p.F_SilkS)
|
||||
shape.SetUuid(p.KIID(str(uuid.uuid5(NS,str(count)))))
|
||||
board.Add(shape)
|
||||
count+=1
|
||||
|
||||
def line(a,b,width=WIDTH):
|
||||
extra = max(0, (width-WIDTH)/2)
|
||||
area = silk_area.buffer(-extra) if extra else silk_area
|
||||
obstacles = blocked.buffer(extra) if extra else blocked
|
||||
clipped=LineString([a,b]).intersection(area).difference(obstacles)
|
||||
pieces=list(clipped.geoms) if hasattr(clipped,"geoms") else [clipped]
|
||||
for piece in pieces:
|
||||
if piece.geom_type!="LineString" or piece.length < 0.25:
|
||||
continue
|
||||
shape=p.PCB_SHAPE(board)
|
||||
shape.SetShape(p.SHAPE_T_SEGMENT)
|
||||
shape.SetStart(vec(piece.coords[0])); shape.SetEnd(vec(piece.coords[-1]))
|
||||
shape.SetWidth(p.FromMM(width))
|
||||
add(shape)
|
||||
|
||||
for ref in ("DS1","U2"):
|
||||
assert footprints[ref].GetOrientationDegrees()==0, "Review outlines for rotated modules"
|
||||
for source in footprints[ref].GraphicalItems():
|
||||
if not isinstance(source,p.PCB_SHAPE) or source.GetLayer()!=p.F_Fab:
|
||||
continue
|
||||
if source.GetShape()==p.SHAPE_T_ARC:
|
||||
# Test small spans for clearance, but retain true circular arcs
|
||||
# for each visible run rather than replacing corners with chords.
|
||||
cx,cy=xy(source.GetCenter())
|
||||
sx,sy=xy(source.GetStart()); mx,my=xy(source.GetArcMid())
|
||||
start=math.atan2(sy-cy,sx-cx)
|
||||
sweep=2*math.atan2((sx-cx)*(my-cy)-(sy-cy)*(mx-cx),
|
||||
(sx-cx)*(mx-cx)+(sy-cy)*(my-cy))
|
||||
radius=math.hypot(sx-cx,sy-cy)
|
||||
steps=max(1,math.ceil(abs(math.degrees(sweep))))
|
||||
def at(t):
|
||||
angle=start+sweep*t/steps
|
||||
return cx+radius*math.cos(angle),cy+radius*math.sin(angle)
|
||||
run=None
|
||||
for i in range(steps+1):
|
||||
span=LineString([at(i),at(i+0.5),at(i+1)])
|
||||
clear=i<steps and silk_area.covers(span) and not span.intersects(blocked)
|
||||
if clear and run is None:
|
||||
run=i
|
||||
if not clear and run is not None:
|
||||
if radius*abs(sweep)*(i-run)/steps>=0.25:
|
||||
arc=p.PCB_SHAPE(board)
|
||||
arc.SetShape(p.SHAPE_T_ARC)
|
||||
arc.SetArcGeometry(vec(at(run)),vec(at((run+i)/2)),vec(at(i)))
|
||||
arc.SetWidth(p.FromMM(WIDTH))
|
||||
add(arc)
|
||||
run=None
|
||||
continue
|
||||
for edge in outline_lines(source):
|
||||
# Display boundary is visually distinct from the PCB perimeter.
|
||||
dashed = ref=="DS1" and round(edge.bounds[0]-xy(footprints[ref].GetPosition())[0],2) in (1.0,25.5)
|
||||
if dashed:
|
||||
distance=0.0
|
||||
while distance < edge.length:
|
||||
line(edge.interpolate(distance).coords[0],edge.interpolate(min(distance+0.8,edge.length)).coords[0])
|
||||
distance+=1.3
|
||||
else:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
# Union removes the shared body/antenna edge duplication. Mouth spans replace
|
||||
# the coincident bottom-edge strokes, with their measured library line weight;
|
||||
# this marks width only, without inventing connector depth or an RF keepout.
|
||||
sources=[g for g in footprints["U1"].GraphicalItems()
|
||||
if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab]
|
||||
mouths=[g for g in sources if g.GetShape()==p.SHAPE_T_SEGMENT]
|
||||
mouth_lines=unary_union([edge for g in mouths for edge in outline_lines(g)])
|
||||
perimeter=unary_union([edge for g in sources if g.GetShape()==p.SHAPE_T_RECT
|
||||
for edge in outline_lines(g)]).difference(mouth_lines)
|
||||
for edge in perimeter.geoms:
|
||||
line(edge.coords[0],edge.coords[-1])
|
||||
for mouth in mouths:
|
||||
line(xy(mouth.GetStart()),xy(mouth.GetEnd()),p.ToMM(mouth.GetWidth()))
|
||||
return count
|
||||
|
||||
|
||||
def main():
|
||||
lock=ROOT/"~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before updating silkscreen")
|
||||
before=PCB.read_bytes()
|
||||
project=ROOT/"serial-carrier.kicad_pro"
|
||||
project_before=project.read_bytes()
|
||||
board=p.LoadBoard(str(PCB))
|
||||
count=add_outlines(board)
|
||||
temp=ROOT/"validation/pcb-routing-silk-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp),board,True)
|
||||
if lock.exists() or PCB.read_bytes()!=before:
|
||||
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
|
||||
assert project.read_bytes()==project_before,"Project settings changed"
|
||||
temp.replace(PCB)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
print(f"Added {count} board-local silkscreen graphics; project settings preserved.")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -1,9 +0,0 @@
|
||||
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,Dimensions and visual fit including USB placement/clearance user accepted
|
||||
U2,1,MAX3243 full-pinout RS-232 breakout male,Adafruit,6253,Carrier:MAX3243_Reference_Provisional,Assigned in schematic and PCB; male pinout/header/footprint equivalence and pin-by-pin review user confirmed
|
||||
DS1,1,128x64 yellow-blue I2C OLED 26mm PCB,User supplied,ASIN B0F13SSSFB,Carrier:OLED_26mm_I2C_Provisional,Module geometry and visual fit user confirmed; onboard I2C pull-ups present but values/rail unmeasured
|
||||
SW1 SW2 SW3,3,Normally-open 6mm THT tactile switch,Omron,B3F-1000,Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x,User reports purchased; datasheet/footprint mapping checked
|
||||
R1 R2 R3,3,2k ohm 1 percent metal-film axial resistor,TBD,User purchased; exact MPN and wattage unspecified,Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,Visual dimensions accepted; retained nominal 6.3x2.5mm body and 7.62mm pitch; design rating at least 0.125W
|
||||
H1 H2 H3 H4,4,M3 carrier mounting locations with 3.2mm NPTH,User selected,M3 screws and 10mm brass standoffs into heat inserts,MountingHole:MountingHole_3.2mm_M3,User approved; board-only holes excluded from electrical component count
|
||||
Module mounting hardware,TBD,M3 daughterboard screws and M2 OLED hardware,User selected,Exact insert and screw lengths unspecified,,User approved; case wall and detailed fastener specifications not defined
|
||||
Module headers,TBD,8.5mm female headers and standard male headers,User selected,Male described as adding 2.5mm,,User intended/reported assembled elevation 10mm; not independently measured; arithmetic sum 11mm is not verified mating height; RS232 prongs clear per user
|
||||
|
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export a 1:1 placement PDF with measured HW678 geometry, without editing CAD.
|
||||
|
||||
Export the saved board's verified measured U1 graphics and A3 worksheet.
|
||||
No library overlay or temporary caption hides stale CAD.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import hashlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
PDF = ROOT / "pcb-placement-draft.pdf"
|
||||
|
||||
|
||||
def export():
|
||||
original = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
from update_presentation import expected_tree
|
||||
from validate_u1_update import parse, canonical
|
||||
assert canonical(expected_tree(parse(PCB))) == canonical(parse(PCB)), "Update PCB presentation first"
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-placement-") as temporary:
|
||||
draft = Path(temporary) / PCB.name
|
||||
result = Path(temporary) / PDF.name
|
||||
if not p.SaveBoard(str(draft), board, True):
|
||||
raise RuntimeError("Cannot save temporary plotting board")
|
||||
subprocess.run([
|
||||
"kicad-cli", "pcb", "export", "pdf", "--mode-single", "--include-border-title",
|
||||
"--layers", "F.Fab,F.SilkS,Dwgs.User,Edge.Cuts",
|
||||
"--sketch-pads-on-fab-layers", "--black-and-white", "--scale", "1",
|
||||
"--output", str(result), str(draft),
|
||||
], check=True)
|
||||
if PCB.read_bytes() != original:
|
||||
raise RuntimeError("Source PCB changed during export; PDF not replaced")
|
||||
PDF.write_bytes(result.read_bytes())
|
||||
print(f"Exported {PDF}; scale 1:1; source PCB unchanged")
|
||||
print(f"Source PCB SHA256: {hashlib.sha256(original).hexdigest()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export()
|
||||
@@ -1 +0,0 @@
|
||||
{"ARCHIVE_NAME": "", "EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false, "OPEN BROWSER": true, "BACKUP_OPT": true}
|
||||
@@ -1,6 +0,0 @@
|
||||
(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"))
|
||||
)
|
||||
@@ -1,209 +0,0 @@
|
||||
#!/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, footprint libraries or project settings.
|
||||
SaveBoard(..., True) deliberately preserves the user's existing project rules.
|
||||
"""
|
||||
import argparse
|
||||
|
||||
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 = [(36,0), (82,0), (82,80), (0,80), (0,19), (36,19)]
|
||||
PLACEMENT = {
|
||||
"U1": ("Carrier:HW678_2x22_Provisional", 8, 23),
|
||||
"U2": ("Carrier:MAX3243_Reference_Provisional", 43, 26.670),
|
||||
"DS1": ("Carrier:OLED_26mm_I2C_Provisional", 43.97, 36),
|
||||
"R1": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 42.16, 32.6685),
|
||||
"R2": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 53.16, 32.6685),
|
||||
"R3": ("Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal", 64.16, 32.6685),
|
||||
"SW1": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 41.72, 68),
|
||||
"SW2": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 53.72, 68),
|
||||
"SW3": ("Button_Switch_THT:SW_TH_Tactile_Omron_B3F-100x", 65.72, 68),
|
||||
}
|
||||
MOUNTS = [(3.5,22.5), (3.5,75), (78,4), (78,76)]
|
||||
# Deliberate allocation, NOT a measured antenna envelope or an RF guarantee.
|
||||
RF_RESERVE = [(10,19), (31,19), (31,32), (10,32)]
|
||||
|
||||
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 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 ROOT.joinpath("~serial-carrier.kicad_pcb.lck").exists():
|
||||
parser.error("PCB is open in KiCad; close it before regenerating")
|
||||
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)
|
||||
# Proposed in-memory draft rules only; never overwrite the user's project settings.
|
||||
title=board.GetTitleBlock()
|
||||
title.SetTitle("Serial Swiss Army Knife - 82x80 mm carrier")
|
||||
title.SetRevision("A1")
|
||||
title.SetComment(0,"")
|
||||
pad_nets={}
|
||||
for source_net in source.findall("nets/net"):
|
||||
net=p.NETINFO_ITEM(board,source_net.get("name"))
|
||||
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]
|
||||
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,44))
|
||||
elif ref=="U2":
|
||||
fp.Reference().SetPosition(xy(56.97,14.670))
|
||||
elif ref=="DS1":
|
||||
fp.Reference().SetPosition(xy(56.97,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,26,p.Dwgs_User,1.2)
|
||||
text(board,"VERIFY ANTENNA",20.5,29,p.Dwgs_User,1)
|
||||
text(board,"ESP32 HW678",20.7,48)
|
||||
|
||||
text(board,"DE-9 OUT / NOMINAL MALE",56.97,-1.830,p.Dwgs_User,1)
|
||||
text(board,"MAX3243 REF",56.97,17.670)
|
||||
text(board,"26mm OLED",56,52)
|
||||
for label,x in zip(("BACK","SELECT","NEXT"),(46.25,58.25,70.25)):
|
||||
text(board,label,x,75, size=0.9)
|
||||
|
||||
text(board,"82 x 80 mm / 2 copper layers",41,-5,p.Dwgs_User,1.5)
|
||||
from update_presentation import CAPTION
|
||||
text(board,CAPTION,41,-2.5,p.Dwgs_User,0.9)
|
||||
board.BuildConnectivity()
|
||||
assert p.SaveBoard(str(PCB),board,True)
|
||||
# Separate process avoids repeated-load SWIG lifetime issues in pcbnew.
|
||||
import sys
|
||||
subprocess.run([sys.executable, "-B", str(ROOT/"update_presentation.py")], check=True)
|
||||
print("Created",PCB,"with 9 circuit footprints, 4 proposed M3 mounts, 2 copper layers.")
|
||||
print("Placement draft only. User-confirmed fit and pin map; RF/electrical/fabrication qualification not established.")
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -1,169 +0,0 @@
|
||||
#!/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 ../../docs/devel/hardware/PCB/footprint-notes.md"))\n (lib (name "Button_Switch_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Button_Switch_THT.pretty")(options "")(descr "KiCad standard THT switches"))\n (lib (name "Resistor_THT")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/Resistor_THT.pretty")(options "")(descr "KiCad standard axial resistors"))\n (lib (name "MountingHole")(type "KiCad")(uri "${KICAD10_FOOTPRINT_DIR}/MountingHole.pretty")(options "")(descr "KiCad standard mounting holes"))\n)\n')
|
||||
# Preserve project settings on regeneration.
|
||||
project_file = ROOT / (PROJECT + ".kicad_pro")
|
||||
if not project_file.exists():
|
||||
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]
|
||||
if ref == "U2":
|
||||
footprint = "Carrier:MAX3243_Reference_Provisional"
|
||||
instance = [f'(symbol (lib_id {q("Carrier:"+name)}) (at {n(x)} {n(y)} 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid(ref))})',
|
||||
prop("Reference",ref,x,y-hh-5.08),prop("Value",value,x,y-hh-2.54),prop("Footprint",footprint,x,y,True),prop("Datasheet","",x,y,True)]
|
||||
for k,v in (extra or {}).items():
|
||||
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":"User confirmed male pinout/header/footprint and pin-by-pin connections","Datasheet_source":"../../docs/devel/hardware/PCB/adafruit-research.md; reference female 5988 CAD"})
|
||||
place("OLED_I2C_26mm","DS1","128x64 I2C OLED / 0x3C",228.6,152.4,{"1":"GND","2":"+3V3","3":"OLED_SCL","4":"OLED_SDA"},{"Status":"Module outline/header/display user fit-verified; pad/drill fit provisional"})
|
||||
for index,(net,function) in enumerate((("BTN_PREVIOUS","Previous / back"),("BTN_SELECT","Select / confirm"),("BTN_NEXT","Next")),1):
|
||||
y=88.9+(index-1)*35.56
|
||||
place("R",f"R{index}","2k 1%",327.66,y,{"1":"+3V3","2":net},{"Rating":"Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified"})
|
||||
place("SW_NO",f"SW{index}","B3F-1000",327.66,y+15.24,{"1":net,"2":"GND"},{"Manufacturer":"Omron","MPN":"B3F-1000","Function":function,"Procurement":"User reports purchased"})
|
||||
note(function,302.26,y+21.59,1.016)
|
||||
|
||||
note("MODULE CARRIER - INITIAL SCHEMATIC",15.24,15.24,2.54)
|
||||
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.\nMale mapping user-reviewed; reference footprint assigned.\nDE-9 and charge pump are already on the module.",177.8,64.77,1.016)
|
||||
note("Controls: active-low, firmware-debounced",292.1,57.15,1.778)
|
||||
note("External 2k 1% pull-ups: ~1.65 mA at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending.",292.1,64.77,1.016)
|
||||
note("DS1: front view GND / VCC / SCL / SDA",177.8,128.27,1.524)
|
||||
note("Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups.",177.8,165.1,1.016)
|
||||
note("POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nUSB nonisolated accepted; backfeed safety not verified.\nKeep both USB connectors and reset/boot buttons accessible.",15.24,185.42)
|
||||
note("RS-232 / ELECTRICAL LIMITS\nTX/RX and modem labels are at the ESP32 logic side.\nOFF_N is active low; GPIO9 must remain open-drain.\nBreakout provides its OFF pull-up. No galvanic isolation.\nNo carrier connection to raw RS-232 voltages.\nCarrier GND, USB GND and RS-232 signal ground are common.\nNever power OLED or MAX3243 logic from 5 V.",152.4,185.42)
|
||||
note("USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\nERC is not hardware or fabrication approval.",292.1,185.42)
|
||||
items.append(')')
|
||||
ROOT.joinpath(PROJECT+".kicad_sch").write_text('\n'.join(items)+'\n')
|
||||
ROOT.joinpath("validation/expected-nets.json").write_text(json.dumps(expected,indent=2,sort_keys=True)+'\n')
|
||||
print("Generated native KiCad schematic, project-local symbol library and tables.")
|
||||
@@ -1,37 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,6 +0,0 @@
|
||||
Designator,Footprint,Quantity,Value,LCSC Part #
|
||||
DS1,OLED_26mm_I2C_Provisional,1,128x64 I2C OLED / 0x3C,
|
||||
"R1, R2, R3",R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal,3,2k 1%,
|
||||
"SW1, SW2, SW3",SW_TH_Tactile_Omron_B3F-100x,3,B3F-1000,B3F-1000
|
||||
U1,HW678_2x22_Provisional,1,HW678 / S3-N16R8,
|
||||
U2,MAX3243_Reference_Provisional,1,Adafruit MAX3243 / male 6253,
|
||||
|
@@ -1,13 +0,0 @@
|
||||
DS1:1
|
||||
H1:1
|
||||
H2:1
|
||||
H3:1
|
||||
H4:1
|
||||
R1:1
|
||||
R2:1
|
||||
R3:1
|
||||
SW1:1
|
||||
SW2:1
|
||||
SW3:1
|
||||
U1:1
|
||||
U2:1
|
||||
|
@@ -1,133 +0,0 @@
|
||||
P CODE 00
|
||||
P UNITS CUST 0
|
||||
P arrayDim N
|
||||
317/BTN_SELECT VIA MD0118PA00X+063602Y-066122X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+059528Y-052362X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+058563Y-054055X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+057047Y-053681X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+059154Y-053465X0276Y0000R000S3
|
||||
317/GND VIA MD0118PA00X+057874Y-054626X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+056811Y-051909X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+061535Y-053681X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+060650Y-052776X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+057677Y-051909X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+058839Y-050886X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+056260Y-052638X0276Y0000R000S3
|
||||
317/+3V3 VIA MD0118PA00X+059921Y-052047X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+053091Y-051575X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+056713Y-050453X0276Y0000R000S3
|
||||
317/RS_DSR VIA MD0118PA00X+051791Y-052421X0276Y0000R000S3
|
||||
317/RS_RI VIA MD0118PA00X+059154Y-054567X0276Y0000R000S3
|
||||
317/RS_RI VIA MD0118PA00X+053091Y-055571X0276Y0000R000S3
|
||||
317/RS_DTR VIA MD0118PA00X+051870Y-053917X0276Y0000R000S3
|
||||
317/RS_DTR VIA MD0118PA00X+058189Y-052402X0276Y0000R000S3
|
||||
317/RS_RTS VIA MD0118PA00X+050866Y-054921X0276Y0000R000S3
|
||||
317/RS_RTS VIA MD0118PA00X+053878Y-054921X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+059567Y-053169X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+060079Y-053524X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+057697Y-055866X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+058189Y-053976X0276Y0000R000S3
|
||||
317/RS_CTS VIA MD0118PA00X+051240Y-056417X0276Y0000R000S3
|
||||
317/RS_TX VIA MD0118PA00X+054705Y-053228X0276Y0000R000S3
|
||||
317/RS_TX VIA MD0118PA00X+057343Y-052402X0276Y0000R000S3
|
||||
317/RS_RX VIA MD0118PA00X+056004Y-052933X0276Y0000R000S3
|
||||
317/RS_RX VIA MD0118PA00X+056850Y-052303X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+063858Y-052362X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+062500Y-053209X0276Y0000R000S3
|
||||
317/RS_VALID VIA MD0118PA00X+060689Y-054921X0276Y0000R000S3
|
||||
317/RS_OFF_N VIA MD0118PA00X+058563Y-058917X0276Y0000R000S3
|
||||
317/RS_OFF_N VIA MD0118PA00X+056398Y-059213X0276Y0000R000S3
|
||||
317/OLED_SDA VIA MD0118PA00X+051949Y-064272X0276Y0000R000S3
|
||||
317/OLED_SDA VIA MD0118PA00X+053071Y-063602X0276Y0000R000S3
|
||||
317/OLED_SCL VIA MD0118PA00X+044823Y-063130X0276Y0000R000S3
|
||||
317/BTN_NEXT VIA MD0118PA00X+051969Y-067244X0276Y0000R000S3
|
||||
317/BTN_NEXT VIA MD0118PA00X+053169Y-066929X0276Y0000R000S3
|
||||
317/BTN_SELECT SW2 -1 D0394PA00X+060520Y-066142X0669Y0000R000S0
|
||||
317/BTN_SELECT SW2 -1 D0394PA00X+063079Y-066142X0669Y0000R000S0
|
||||
317/GND SW2 -2 D0394PA00X+060520Y-067913X0669Y0000R000S0
|
||||
317/GND SW2 -2 D0394PA00X+063079Y-067913X0669Y0000R000S0
|
||||
367N/C H1 D1260UA00X+040748Y-048228X1260Y0000R000S0
|
||||
317/+3V3 U1 -1 D0394PA00X+042520Y-048425X0669Y0669R000S0
|
||||
317/+3V3 U1 -2 D0394PA00X+042520Y-049425X0669Y0000R000S0
|
||||
317D-(U1-EN-PAD3) U1 -3 D0394PA00X+042520Y-050425X0669Y0000R000S0
|
||||
317/RS_DCD U1 -4 D0394PA00X+042520Y-051425X0669Y0000R000S0
|
||||
317/RS_DSR U1 -5 D0394PA00X+042520Y-052425X0669Y0000R000S0
|
||||
317/RS_RI U1 -6 D0394PA00X+042520Y-053425X0669Y0000R000S0
|
||||
317/RS_DTR U1 -7 D0394PA00X+042520Y-054425X0669Y0000R000S0
|
||||
317/RS_RTS U1 -8 D0394PA00X+042520Y-055425X0669Y0000R000S0
|
||||
317/RS_CTS U1 -9 D0394PA00X+042520Y-056425X0669Y0000R000S0
|
||||
317/RS_TX U1 -10 D0394PA00X+042520Y-057425X0669Y0000R000S0
|
||||
317/RS_RX U1 -11 D0394PA00X+042520Y-058425X0669Y0000R000S0
|
||||
317/RS_VALID U1 -12 D0394PA00X+042520Y-059425X0669Y0000R000S0
|
||||
3171-GPIO3-PAD13) U1 -13 D0394PA00X+042520Y-060425X0669Y0000R000S0
|
||||
317-GPIO46-PAD14) U1 -14 D0394PA00X+042520Y-061425X0669Y0000R000S0
|
||||
317/RS_OFF_N U1 -15 D0394PA00X+042520Y-062425X0669Y0000R000S0
|
||||
317/BTN_PREVIOUS U1 -16 D0394PA00X+042520Y-063425X0669Y0000R000S0
|
||||
317/OLED_SDA U1 -17 D0394PA00X+042520Y-064425X0669Y0000R000S0
|
||||
317/OLED_SCL U1 -18 D0394PA00X+042520Y-065425X0669Y0000R000S0
|
||||
317/BTN_SELECT U1 -19 D0394PA00X+042520Y-066425X0669Y0000R000S0
|
||||
317/BTN_NEXT U1 -20 D0394PA00X+042520Y-067425X0669Y0000R000S0
|
||||
317-(U1-5V-PAD21) U1 -21 D0394PA00X+042520Y-068425X0669Y0000R000S0
|
||||
317/GND U1 -22 D0394PA00X+042520Y-069425X0669Y0000R000S0
|
||||
317/GND U1 -23 D0394PA00X+052520Y-048425X0669Y0000R000S0
|
||||
317-GPIO43-PAD24) U1 -24 D0394PA00X+052520Y-049425X0669Y0000R000S0
|
||||
317-GPIO44-PAD25) U1 -25 D0394PA00X+052520Y-050425X0669Y0000R000S0
|
||||
3171-GPIO1-PAD26) U1 -26 D0394PA00X+052520Y-051425X0669Y0000R000S0
|
||||
3171-GPIO2-PAD27) U1 -27 D0394PA00X+052520Y-052425X0669Y0000R000S0
|
||||
317-GPIO42-PAD28) U1 -28 D0394PA00X+052520Y-053425X0669Y0000R000S0
|
||||
317-GPIO41-PAD29) U1 -29 D0394PA00X+052520Y-054425X0669Y0000R000S0
|
||||
317-GPIO40-PAD30) U1 -30 D0394PA00X+052520Y-055425X0669Y0000R000S0
|
||||
317-GPIO39-PAD31) U1 -31 D0394PA00X+052520Y-056425X0669Y0000R000S0
|
||||
317-GPIO38-PAD32) U1 -32 D0394PA00X+052520Y-057425X0669Y0000R000S0
|
||||
317-GPIO37-PAD33) U1 -33 D0394PA00X+052520Y-058425X0669Y0000R000S0
|
||||
317-GPIO36-PAD34) U1 -34 D0394PA00X+052520Y-059425X0669Y0000R000S0
|
||||
317-GPIO35-PAD35) U1 -35 D0394PA00X+052520Y-060425X0669Y0000R000S0
|
||||
3171-GPIO0-PAD36) U1 -36 D0394PA00X+052520Y-061425X0669Y0000R000S0
|
||||
317-GPIO45-PAD37) U1 -37 D0394PA00X+052520Y-062425X0669Y0000R000S0
|
||||
317-GPIO48-PAD38) U1 -38 D0394PA00X+052520Y-063425X0669Y0000R000S0
|
||||
317-GPIO47-PAD39) U1 -39 D0394PA00X+052520Y-064425X0669Y0000R000S0
|
||||
317-GPIO21-PAD40) U1 -40 D0394PA00X+052520Y-065425X0669Y0000R000S0
|
||||
317-GPIO20-PAD41) U1 -41 D0394PA00X+052520Y-066425X0669Y0000R000S0
|
||||
317-GPIO19-PAD42) U1 -42 D0394PA00X+052520Y-067425X0669Y0000R000S0
|
||||
317/GND U1 -43 D0394PA00X+052520Y-068425X0669Y0000R000S0
|
||||
317/GND U1 -44 D0394PA00X+052520Y-069425X0669Y0000R000S0
|
||||
367N/C H3 D1260UA00X+070079Y-040945X1260Y0000R000S0
|
||||
367N/C U2 D1181UA00X+057049Y-046420X1181Y0000R000S0
|
||||
367N/C U2 D1181UA00X+066549Y-046420X1181Y0000R000S0
|
||||
317/+3V3 U2 -1 D0394PA00X+056299Y-049870X0669Y0669R000S0
|
||||
317/GND U2 -2 D0394PA00X+057299Y-049870X0669Y0000R000S0
|
||||
317/RS_DCD U2 -3 D0394PA00X+058299Y-049870X0669Y0000R000S0
|
||||
317/RS_RX U2 -4 D0394PA00X+059299Y-049870X0669Y0000R000S0
|
||||
317/RS_TX U2 -5 D0394PA00X+060299Y-049870X0669Y0000R000S0
|
||||
317/RS_DTR U2 -6 D0394PA00X+061299Y-049870X0669Y0000R000S0
|
||||
317/RS_DSR U2 -7 D0394PA00X+062299Y-049870X0669Y0000R000S0
|
||||
317/RS_RTS U2 -8 D0394PA00X+063299Y-049870X0669Y0000R000S0
|
||||
317/RS_CTS U2 -9 D0394PA00X+064299Y-049870X0669Y0000R000S0
|
||||
317/RS_RI U2 -10 D0394PA00X+065299Y-049870X0669Y0000R000S0
|
||||
317/RS_VALID U2 -11 D0394PA00X+066299Y-049870X0669Y0000R000S0
|
||||
317/RS_OFF_N U2 -12 D0394PA00X+067299Y-049870X0669Y0000R000S0
|
||||
367N/C H4 D1260UA00X+070079Y-069291X1260Y0000R000S0
|
||||
317/+3V3 R2 -1 D0315PA00X+060299Y-052232X0630Y0000R000S0
|
||||
317/BTN_SELECT R2 -2 D0315PA00X+063299Y-052232X0630Y0000R000S0
|
||||
367N/C H2 D1260UA00X+040748Y-068898X1260Y0000R000S0
|
||||
317/+3V3 R3 -1 D0315PA00X+064630Y-052232X0630Y0000R000S0
|
||||
317/BTN_NEXT R3 -2 D0315PA00X+067630Y-052232X0630Y0000R000S0
|
||||
317/BTN_NEXT SW3 -1 D0394PA00X+065244Y-066142X0669Y0000R000S0
|
||||
317/BTN_NEXT SW3 -1 D0394PA00X+067803Y-066142X0669Y0000R000S0
|
||||
317/GND SW3 -2 D0394PA00X+065244Y-067913X0669Y0000R000S0
|
||||
317/GND SW3 -2 D0394PA00X+067803Y-067913X0669Y0000R000S0
|
||||
317/+3V3 R1 -1 D0315PA00X+055969Y-052232X0630Y0000R000S0
|
||||
317/BTN_PREVIOUS R1 -2 D0315PA00X+058969Y-052232X0630Y0000R000S0
|
||||
317/BTN_PREVIOUS SW1 -1 D0394PA00X+055795Y-066142X0669Y0000R000S0
|
||||
317/BTN_PREVIOUS SW1 -1 D0394PA00X+058354Y-066142X0669Y0000R000S0
|
||||
317/GND SW1 -2 D0394PA00X+055795Y-067913X0669Y0000R000S0
|
||||
317/GND SW1 -2 D0394PA00X+058354Y-067913X0669Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+057370Y-054232X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+057370Y-063091X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+066228Y-054232X0787Y0000R000S0
|
||||
367N/C DS1 D0787UA00X+066228Y-063091X0787Y0000R000S0
|
||||
317/GND DS1 -1 D0394PA00X+060299Y-054134X0669Y0669R000S0
|
||||
317/+3V3 DS1 -2 D0394PA00X+061299Y-054134X0669Y0000R000S0
|
||||
317/OLED_SCL DS1 -3 D0394PA00X+062299Y-054134X0669Y0000R000S0
|
||||
317/OLED_SDA DS1 -4 D0394PA00X+063299Y-054134X0669Y0000R000S0
|
||||
999
|
||||
@@ -1,10 +0,0 @@
|
||||
Designator,Mid X,Mid Y,Rotation,Layer
|
||||
DS1,156.97,-148.95,0.0,top
|
||||
R1,145.97,-132.6685,0.0,top
|
||||
R2,156.97,-132.6685,0.0,top
|
||||
R3,167.97,-132.6685,0.0,top
|
||||
SW1,144.97,-170.25,0.0,top
|
||||
SW2,156.97,-170.25,0.0,top
|
||||
SW3,168.97,-170.25,0.0,top
|
||||
U1,120.7,-149.67,0.0,top
|
||||
U2,156.97,-121.9635,0.0,top
|
||||
|
@@ -1,32 +0,0 @@
|
||||
## 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.
|
||||
|
Before Width: | Height: | Size: 340 KiB |
@@ -1,753 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Adafruit Industries
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,2 +0,0 @@
|
||||
# Adafruit_CAD_Parts
|
||||
STEP, Fusion 360, and STL files for various boards, components and parts
|
||||
@@ -1,86 +0,0 @@
|
||||
"""Read pinned STEP B-rep; write reproducible solid/face measurements beside it."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from OCP.STEPControl import STEPControl_Reader
|
||||
from OCP.IFSelect import IFSelect_RetDone
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_SOLID, TopAbs_FACE
|
||||
from OCP.TopoDS import TopoDS
|
||||
from OCP.Bnd import Bnd_Box
|
||||
from OCP.BRepBndLib import BRepBndLib
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import GeomAbs_Cylinder, GeomAbs_Plane
|
||||
from OCP.BRepCheck import BRepCheck_Analyzer
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
|
||||
def bounds(s):
|
||||
b = Bnd_Box()
|
||||
BRepBndLib.AddOptimal_s(s, b, False, False)
|
||||
return list(b.Get())
|
||||
|
||||
def xyz(p):
|
||||
return [p.X(), p.Y(), p.Z()]
|
||||
|
||||
r = STEPControl_Reader()
|
||||
assert r.ReadFile(str(HERE / '5988 RS-232 Level Shifter.step')) == IFSelect_RetDone
|
||||
r.TransferRoots()
|
||||
entities = dict(re.findall(r'#(\d+)\s*=\s*(.*?);', (HERE / '5988 RS-232 Level Shifter.step').read_text(), re.S))
|
||||
styles = {}
|
||||
for text in entities.values():
|
||||
if text.startswith('STYLED_ITEM('):
|
||||
refs = re.findall(r'#(\d+)', text)
|
||||
styles[refs[-1]] = refs[:-1]
|
||||
|
||||
def colors(refs, seen=None):
|
||||
seen = set() if seen is None else seen
|
||||
result = []
|
||||
for ref in refs:
|
||||
if ref in seen:
|
||||
continue
|
||||
seen.add(ref)
|
||||
text = entities[ref]
|
||||
if text.startswith('COLOUR_RGB('):
|
||||
result.append(text.replace('\n', ''))
|
||||
else:
|
||||
result.extend(colors(re.findall(r'#(\d+)', text), seen))
|
||||
return result
|
||||
|
||||
e = TopExp_Explorer(r.OneShape(), TopAbs_SOLID)
|
||||
out = []
|
||||
while e.More():
|
||||
s = TopoDS.Solid_s(e.Current())
|
||||
item = dict(id=len(out)+1, bounds=bounds(s), valid=BRepCheck_Analyzer(s).IsValid(), faces=[])
|
||||
f = TopExp_Explorer(s, TopAbs_FACE)
|
||||
while f.More():
|
||||
face = TopoDS.Face_s(f.Current())
|
||||
a = BRepAdaptor_Surface(face)
|
||||
# These unique bodies retain CLOSED_SHELL face order on this pinned import.
|
||||
shell_id = {1: '14428', 12: '14431', 13: '14432', 14: '14433'}.get(item['id'])
|
||||
refs = re.findall(r'#(\d+)', entities[shell_id]) if shell_id else []
|
||||
step_id = refs[len(item['faces'])] if refs else '0'
|
||||
if refs:
|
||||
surface_id = re.findall(r'#(\d+)', entities[step_id])[-1]
|
||||
surface = entities[surface_id]
|
||||
expected = str(a.GetType()).split('_')[-1].upper()
|
||||
assert surface.startswith({'PLANE': 'PLANE(', 'CYLINDER': 'CYLINDRICAL_SURFACE(', 'CONE': 'CONICAL_SURFACE(', 'TORUS': 'TOROIDAL_SURFACE('}[expected])
|
||||
if a.GetType() == GeomAbs_Cylinder:
|
||||
assert abs(float(surface.rsplit(',', 1)[1].rstrip(')')) - a.Cylinder().Radius()) < 1e-8
|
||||
row = dict(id=len(item['faces'])+1, bounds=bounds(face), type=str(a.GetType()), orientation=str(face.Orientation()))
|
||||
row.update(step_id=step_id, colors=colors(styles.get(step_id, [])))
|
||||
if a.GetType() == GeomAbs_Cylinder:
|
||||
c = a.Cylinder()
|
||||
row.update(radius=c.Radius(), location=xyz(c.Location()), axis=xyz(c.Axis().Direction()))
|
||||
elif a.GetType() == GeomAbs_Plane:
|
||||
p = a.Plane()
|
||||
row.update(location=xyz(p.Location()), normal=xyz(p.Axis().Direction()))
|
||||
item['faces'].append(row)
|
||||
f.Next()
|
||||
if shell_id:
|
||||
assert len(item['faces']) == len(refs)
|
||||
out.append(item)
|
||||
e.Next()
|
||||
(HERE / 'geometry.json').write_text(json.dumps(out, indent=2)+'\n')
|
||||
for s in out:
|
||||
print(s['id'], 'valid', s['valid'], 'bounds', [round(v,6) for v in s['bounds']], 'faces', len(s['faces']))
|
||||
@@ -1,207 +0,0 @@
|
||||
{
|
||||
"front_flange_exposed_faces": {
|
||||
"solid": 12,
|
||||
"faces": [
|
||||
8,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14
|
||||
],
|
||||
"bounds": [
|
||||
0.47499999999999787,
|
||||
27.006999999999998,
|
||||
1.5726202399843388,
|
||||
31.275000000000002,
|
||||
27.407,
|
||||
14.1226202399843
|
||||
],
|
||||
"dimensions": [
|
||||
30.800000000000004,
|
||||
0.40000000000000213,
|
||||
12.549999999999962
|
||||
]
|
||||
},
|
||||
"shell_straight_outer_surface": {
|
||||
"solid": 12,
|
||||
"faces": [
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
],
|
||||
"bounds": [
|
||||
8.035981107410171,
|
||||
28.006999999999998,
|
||||
3.89762023998434,
|
||||
23.71401889258985,
|
||||
32.607,
|
||||
11.79762023998434
|
||||
],
|
||||
"dimensions": [
|
||||
15.678037785179677,
|
||||
4.600000000000001,
|
||||
7.9
|
||||
]
|
||||
},
|
||||
"shell_outer_including_root_and_lip": {
|
||||
"solid": 12,
|
||||
"faces": [
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
81,
|
||||
82,
|
||||
83,
|
||||
84,
|
||||
85,
|
||||
86,
|
||||
87,
|
||||
88,
|
||||
103,
|
||||
104,
|
||||
105,
|
||||
106,
|
||||
107,
|
||||
108,
|
||||
109,
|
||||
110
|
||||
],
|
||||
"bounds": [
|
||||
7.4359811074101705,
|
||||
27.406999999999996,
|
||||
3.29762023998434,
|
||||
24.31401889258985,
|
||||
33.207,
|
||||
12.397620239984342
|
||||
],
|
||||
"dimensions": [
|
||||
16.87803778517968,
|
||||
5.800000000000004,
|
||||
9.100000000000001
|
||||
]
|
||||
},
|
||||
"left_front_hex_post": {
|
||||
"solid": 12,
|
||||
"faces": [
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
57,
|
||||
58,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
89
|
||||
],
|
||||
"bounds": [
|
||||
0.7191887617277004,
|
||||
27.407,
|
||||
5.547620239984319,
|
||||
6.030811238272291,
|
||||
32.207,
|
||||
10.147620239984366
|
||||
],
|
||||
"dimensions": [
|
||||
5.31162247654459,
|
||||
4.800000000000001,
|
||||
4.600000000000048
|
||||
]
|
||||
},
|
||||
"right_front_hex_post": {
|
||||
"solid": 12,
|
||||
"faces": [
|
||||
62,
|
||||
63,
|
||||
64,
|
||||
65,
|
||||
66,
|
||||
67,
|
||||
68,
|
||||
69,
|
||||
70,
|
||||
90
|
||||
],
|
||||
"bounds": [
|
||||
25.71918876172773,
|
||||
27.407,
|
||||
5.547620239984328,
|
||||
31.030811238272328,
|
||||
32.207,
|
||||
10.14762023998434
|
||||
],
|
||||
"dimensions": [
|
||||
5.311622476544599,
|
||||
4.800000000000001,
|
||||
4.600000000000012
|
||||
]
|
||||
},
|
||||
"front_lock_axes": [
|
||||
{
|
||||
"id": 60,
|
||||
"bounds": [
|
||||
1.875,
|
||||
27.407,
|
||||
6.34762023998434,
|
||||
4.875,
|
||||
32.007,
|
||||
9.34762023998434
|
||||
],
|
||||
"type": "GeomAbs_SurfaceType.GeomAbs_Cylinder",
|
||||
"orientation": "TopAbs_Orientation.TopAbs_REVERSED",
|
||||
"step_id": "13822",
|
||||
"colors": [
|
||||
"COLOUR_RGB('Opaque(245,245,246)',0.96078431372549,0.96078431372549,0.964705882352941)"
|
||||
],
|
||||
"radius": 1.5,
|
||||
"location": [
|
||||
3.375,
|
||||
32.207,
|
||||
7.84762023998434
|
||||
],
|
||||
"axis": [
|
||||
0.0,
|
||||
-1.0,
|
||||
0.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"bounds": [
|
||||
26.875,
|
||||
27.407,
|
||||
6.34762023998434,
|
||||
29.875,
|
||||
32.007,
|
||||
9.34762023998434
|
||||
],
|
||||
"type": "GeomAbs_SurfaceType.GeomAbs_Cylinder",
|
||||
"orientation": "TopAbs_Orientation.TopAbs_REVERSED",
|
||||
"step_id": "13831",
|
||||
"colors": [
|
||||
"COLOUR_RGB('Opaque(245,245,246)',0.96078431372549,0.96078431372549,0.964705882352941)"
|
||||
],
|
||||
"radius": 1.5,
|
||||
"location": [
|
||||
28.375,
|
||||
32.207,
|
||||
7.84762023998434
|
||||
],
|
||||
"axis": [
|
||||
0.0,
|
||||
-1.0,
|
||||
0.0
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
[
|
||||
{
|
||||
"file": "5988 RS-232 Level Shifter.step",
|
||||
"url": "https://raw.githubusercontent.com/adafruit/Adafruit_CAD_Parts/5b8a21fb5e2e48478faf8901077be812f5224066/5988%20RS-232%20Level%20Shifter/5988%20RS-232%20Level%20Shifter.step",
|
||||
"sha256": "8eab9dc591a985fc987ceec2d7534e5e259263c73bebc64c0c3cd847e135906f"
|
||||
},
|
||||
{
|
||||
"file": "LICENSE",
|
||||
"url": "https://raw.githubusercontent.com/adafruit/Adafruit_CAD_Parts/5b8a21fb5e2e48478faf8901077be812f5224066/LICENSE",
|
||||
"sha256": "f60436644e066301d4c2e8f1f0674a157b1f60e4fd5586717b99dde255cc18cd"
|
||||
},
|
||||
{
|
||||
"file": "README.md",
|
||||
"url": "https://raw.githubusercontent.com/adafruit/Adafruit_CAD_Parts/5b8a21fb5e2e48478faf8901077be812f5224066/README.md",
|
||||
"sha256": "9ed9beac7600623b7b9bd0d02e1fdcd86610ce22966723179e6c6b618e399dce"
|
||||
}
|
||||
]
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Validate registration and selected face subsets; no board writes."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
D = json.loads((HERE / 'geometry.json').read_text())
|
||||
assert len(D) == 15 and all(s['valid'] for s in D)
|
||||
for source in json.loads((HERE / 'sources.json').read_text()):
|
||||
assert hashlib.sha256((HERE / source['file']).read_bytes()).hexdigest() == source['sha256']
|
||||
board = HERE.parent / 'Adafruit RS-232 Full Pinout Level-Shifter Breakout.brd'
|
||||
assert hashlib.sha256(board.read_bytes()).hexdigest() == '6a7b35ef909f5a2d243c326c34db3957aaa01275bf2aac8e0c749ff18162e870'
|
||||
root = ET.parse(board).getroot()
|
||||
b = root.find('drawing/board')
|
||||
jp = b.find("elements/element[@name='JP2']")
|
||||
pkg = b.find("libraries/library[@name='microbuilder']/packages/package[@name='1X12_ROUND_76MIL']")
|
||||
for pad in pkg.findall('pad'):
|
||||
x, y = float(jp.get('x')) + float(pad.get('x')), float(jp.get('y')) + float(pad.get('y'))
|
||||
assert any(abs(f.get('radius', 0)-0.5)<1e-8 and abs(f['location'][0]-x)<1e-8 and abs(f['location'][1]-y)<1e-8 for f in D[0]['faces'])
|
||||
for x in [3.81, 27.94]:
|
||||
assert any(abs(f.get('radius',0)-1.5)<1e-8 and abs(f['location'][0]-x)<1e-8 and abs(f['location'][1]-11.43)<1e-8 for f in D[0]['faces'])
|
||||
assert all(abs(a-b)<1e-8 for a,b in zip(D[0]['bounds'], [0,0,0,31.75,29.337,1.57]))
|
||||
|
||||
subsets = {
|
||||
'front_flange_exposed_faces': [8,11,12,13,14],
|
||||
'shell_straight_outer_surface': list(range(18,26)),
|
||||
'shell_outer_including_root_and_lip': list(range(18,26))+list(range(81,89))+list(range(103,111)),
|
||||
'left_front_hex_post': list(range(53,62))+[89],
|
||||
'right_front_hex_post': list(range(62,71))+[90],
|
||||
}
|
||||
result = {}
|
||||
for name, ids in subsets.items():
|
||||
faces = [D[11]['faces'][i-1] for i in ids]
|
||||
assert all(any('Opaque(245,245,246)' in c for c in f['colors']) for f in faces)
|
||||
bounds = [min(f['bounds'][i] for f in faces) for i in range(3)] + [max(f['bounds'][i] for f in faces) for i in range(3,6)]
|
||||
result[name] = dict(solid=12, faces=ids, bounds=bounds, dimensions=[bounds[i+3]-bounds[i] for i in range(3)])
|
||||
left, right = [D[11]['faces'][i-1] for i in [60,69]]
|
||||
assert abs(right['location'][0]-left['location'][0]-25)<1e-8
|
||||
assert left['radius'] == right['radius'] == 1.5
|
||||
result['front_lock_axes'] = [left, right]
|
||||
(HERE / 'measurements.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
print('PASS: source hashes, 15 valid solids, all 12 Eagle header centres, PCB bounds, mounting holes, metal-appearance subsets and front lock cylinders.')
|
||||
for name, row in result.items():
|
||||
if isinstance(row,dict):
|
||||
print(name, [round(v,6) for v in row['bounds']], 'size', [round(v,6) for v in row['dimensions']])
|
||||
@@ -1,359 +0,0 @@
|
||||
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/.
|
||||
@@ -1,175 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
"""One-shot body-centred placement with incremental copper repair, not regeneration.
|
||||
|
||||
Writes a candidate only. Review its DRC before promoting it to the native board.
|
||||
Requires the retained pre-alignment snapshot and no editor locks.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Point, LineString, box
|
||||
import route_pcb_draft as r
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
BASE = ROOT/'validation/pcb-before-group-alignment.kicad_pcb'
|
||||
MOVES = {'DS1': ((143,136),(143.97,136)),
|
||||
**{f'R{i+1}': ((143+11*i,134),(142.16+11*i,132.6685)) for i in range(3)},
|
||||
**{f'SW{i+1}': ((143+12*i,168),(141.72+12*i,168)) for i in range(3)}}
|
||||
|
||||
|
||||
def unlocked():
|
||||
assert not list(ROOT.glob('*.lck')), 'KiCad editor lock exists'
|
||||
assert not list(ROOT.glob('.~lock.*')), 'Document editor lock exists'
|
||||
|
||||
|
||||
def geom(item):
|
||||
if isinstance(item,p.PAD):
|
||||
if item.GetShape()==p.PAD_SHAPE_CIRCLE:
|
||||
return Point(r.pos(item)).buffer(p.ToMM(item.GetSize().x)/2,quad_segs=32)
|
||||
bb=item.GetBoundingBox()
|
||||
return box(p.ToMM(bb.GetX()),p.ToMM(bb.GetY()),p.ToMM(bb.GetRight()),p.ToMM(bb.GetBottom()))
|
||||
if isinstance(item,p.PCB_VIA):
|
||||
return Point(r.pos(item)).buffer(.35,quad_segs=32)
|
||||
return LineString([xy(item.GetStart()),xy(item.GetEnd())]).buffer(p.ToMM(item.GetWidth())/2,quad_segs=32)
|
||||
|
||||
|
||||
def xy(v):
|
||||
return (p.ToMM(v.x),p.ToMM(v.y))
|
||||
|
||||
|
||||
def layers(item):
|
||||
return set(r.LAYERS) if isinstance(item,(p.PAD,p.PCB_VIA)) else {item.GetLayer()}
|
||||
|
||||
|
||||
def groups(board, net):
|
||||
items=[a for f in board.GetFootprints() for a in f.Pads() if a.GetNetCode()==net]
|
||||
items += [t for t in board.GetTracks() if t.GetNetCode()==net]
|
||||
shapes=[geom(i) for i in items]
|
||||
parents=list(range(len(items)))
|
||||
def root(i):
|
||||
while parents[i]!=i:
|
||||
i=parents[i]
|
||||
return i
|
||||
for i,a in enumerate(items):
|
||||
for j in range(i):
|
||||
if layers(a)&layers(items[j]) and shapes[i].distance(shapes[j])<1e-6:
|
||||
parents[root(i)]=root(j)
|
||||
result={}
|
||||
for i,a in enumerate(items):
|
||||
result.setdefault(root(i),[]).append(a)
|
||||
return list(result.values())
|
||||
|
||||
|
||||
def anchors(group):
|
||||
result=[]
|
||||
for a in group:
|
||||
points=[r.pos(a)] if isinstance(a,(p.PAD,p.PCB_VIA)) else [xy(a.GetStart()),xy(a.GetEnd())]
|
||||
result.extend((point,layers(a)) for point in points)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
unlocked()
|
||||
before=r.PCB.read_bytes()
|
||||
assert before==BASE.read_bytes(), 'Input differs from preserved baseline'
|
||||
protected={name:(ROOT/name).read_bytes() for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl','serial-carrier.kicad_sch')}
|
||||
board=p.LoadBoard(str(r.PCB))
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
for ref,(old,new) in MOVES.items():
|
||||
assert r.pos(fps[ref])==old
|
||||
fps[ref].SetPosition(r.point(new))
|
||||
if ref.startswith('R'):
|
||||
fps[ref].SetValue('2k 1%')
|
||||
pads=[a for f in board.GetFootprints() for a in f.Pads()]
|
||||
removed=[]
|
||||
# Only remove copper made illegal by a moved pad/hole. Everything else is
|
||||
# initially retained, including all unrelated nets and manual routing.
|
||||
moved_pads=[a for ref in MOVES for a in fps[ref].Pads()]
|
||||
for t in list(board.GetTracks()):
|
||||
if any((not a.GetNetCode() or a.GetNetCode()!=t.GetNetCode()) and
|
||||
geom(t).distance(geom(a))<.25-1e-6 for a in moved_pads):
|
||||
removed.append(t.m_Uuid.AsString())
|
||||
board.Delete(t)
|
||||
|
||||
repaired=[]
|
||||
deadline=time.monotonic()+600
|
||||
for ref,(old,new) in MOVES.items():
|
||||
for pad in fps[ref].Pads():
|
||||
if not pad.GetNumber(): continue
|
||||
target=r.pos(pad)
|
||||
start=(round(target[0]+old[0]-new[0],6),round(target[1]+old[1]-new[1],6))
|
||||
if geom(pad).covers(Point(start)): continue
|
||||
attached=[t for t in board.GetTracks() if t.GetNetCode()==pad.GetNetCode() and start in (xy(t.GetStart()),xy(t.GetEnd()))]
|
||||
if not attached: continue
|
||||
width=.5 if pad.GetNetname() in ('/+3V3','/GND') else .25
|
||||
blocked,via=r.masks(board,pads,pad.GetNetCode(),width)
|
||||
allowed=set.union(*(layers(t) for t in attached))
|
||||
for z,layer in enumerate(r.LAYERS):
|
||||
if layer not in allowed: blocked[r.node(start,z)]=True
|
||||
path,_=r.astar(blocked,via,start,target,deadline,500000)
|
||||
assert path, ('Cannot extend moved pad',ref,pad.GetNumber())
|
||||
r.add_path(board,path,start,target,pad.GetNetCode(),width)
|
||||
repaired.append(dict(net=pad.GetNetname(),start=start,end=target))
|
||||
nets=sorted({a.GetNetCode() for a in pads if a.GetNumber() and a.GetNetCode()}, key=lambda n: (board.FindNet(n).GetNetname() in ('/+3V3','/GND'), not board.FindNet(n).GetNetname().startswith('/OLED'), n))
|
||||
for net in nets:
|
||||
while len(parts:=groups(board,net))>1:
|
||||
options=[]
|
||||
for i,aa in enumerate(parts):
|
||||
for bb in parts[i+1:]:
|
||||
for a,la in anchors(aa):
|
||||
for b,lb in anchors(bb):
|
||||
options.append(((a[0]-b[0])**2+(a[1]-b[1])**2,a,la,b,lb))
|
||||
options.sort(key=lambda v:v[0])
|
||||
name=parts[0][0].GetNetname()
|
||||
width=.5 if name in ('/+3V3','/GND') else .25
|
||||
blocked,via=r.masks(board,pads,net,width)
|
||||
success=False
|
||||
for _,a,la,b,lb in options[:400]:
|
||||
mask=blocked.copy()
|
||||
for z,layer in enumerate(r.LAYERS):
|
||||
if layer not in la: mask[r.node(a,z)]=True
|
||||
if layer not in lb: mask[r.node(b,z)]=True
|
||||
path,_=r.astar(mask,via,a,b,deadline,250000)
|
||||
if path:
|
||||
r.add_path(board,path,a,b,net,width)
|
||||
repaired.append(dict(net=name,start=a,end=b))
|
||||
print(repaired[-1],flush=True)
|
||||
success=True
|
||||
break
|
||||
assert time.monotonic()<deadline, 'Bounded routing deadline reached'
|
||||
assert success, ('Cannot reconnect retained copper',name)
|
||||
unlocked()
|
||||
assert r.PCB.read_bytes()==before
|
||||
assert all((ROOT/n).read_bytes()==data for n,data in protected.items())
|
||||
out=ROOT/'validation/pcb-group-candidate.kicad_pcb'
|
||||
# Let KiCad identify true dangling stubs after repair. Do not prune valid
|
||||
# retained routes before reconnecting the moved pads.
|
||||
from add_device_silkscreen import add_outlines
|
||||
drc=ROOT/'validation/pcb-group-candidate-drc.json'
|
||||
for attempt in range(20):
|
||||
add_outlines(board)
|
||||
assert p.SaveBoard(str(out),board,True)
|
||||
subprocess.run(['kicad-cli','pcb','drc','--format','json','--all-track-errors','--severity-all','-o',str(drc),str(out)],check=True,timeout=60)
|
||||
results=json.loads(drc.read_text())
|
||||
ids={i['uuid'] for v in results['violations'] if v['type']=='track_dangling' for i in v['items']}
|
||||
if not ids:
|
||||
assert not results['unconnected_items'], results['unconnected_items']
|
||||
assert all(v['type']=='lib_footprint_issues' for v in results['violations']), results['violations']
|
||||
break
|
||||
for t in list(board.GetTracks()):
|
||||
if t.m_Uuid.AsString() in ids:
|
||||
removed.append(t.m_Uuid.AsString())
|
||||
board.Delete(t)
|
||||
else:
|
||||
raise AssertionError('Dangling-stub cleanup did not converge')
|
||||
report=dict(baseline_sha256=hashlib.sha256(before).hexdigest(),moves_absolute_mm=MOVES,
|
||||
removed_track_uuids=removed,repaired_connections=repaired,
|
||||
retained_tracks=sum(t.m_Uuid.AsString() not in removed for t in p.LoadBoard(str(BASE)).GetTracks()),
|
||||
protected_sha256={n:hashlib.sha256(data).hexdigest() for n,data in protected.items()})
|
||||
(ROOT/'validation/pcb-group-migration.json').write_text(json.dumps(report,indent=2)+'\n')
|
||||
|
||||
if __name__=='__main__':
|
||||
main()
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Remove presentation-only PCB draft labels; retain all technical cautions."""
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
TITLE = "Serial Swiss Army Knife - 82x80 mm carrier"
|
||||
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
|
||||
SILK = "DRAFT - NOT FOR FAB"
|
||||
|
||||
|
||||
def remove_labels(board):
|
||||
title = board.GetTitleBlock()
|
||||
assert title.GetTitle() in (TITLE, TITLE + " draft")
|
||||
assert title.GetRevision() in ("A1", "A1 DRAFT")
|
||||
assert title.GetComment(0) in ("", COMMENT, "NOT FOR FABRICATION - " + COMMENT)
|
||||
title.SetTitle(TITLE)
|
||||
title.SetRevision("A1")
|
||||
if title.GetComment(0):
|
||||
title.SetComment(0, COMMENT)
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if isinstance(drawing, p.PCB_TEXT) and drawing.GetText() == SILK:
|
||||
assert drawing.GetLayer() == p.F_SilkS
|
||||
board.Remove(drawing)
|
||||
|
||||
|
||||
def expected_tree(tree):
|
||||
"""Exact permitted delta for preservation audits, independent of pcbnew edits."""
|
||||
import copy
|
||||
result = copy.deepcopy(tree)
|
||||
for item in list(result):
|
||||
if not isinstance(item, list):
|
||||
continue
|
||||
if item[0] == "title_block":
|
||||
for field in item[1:]:
|
||||
replacements = {
|
||||
('title', '"' + TITLE + ' draft"'): '"' + TITLE + '"',
|
||||
('rev', '"A1 DRAFT"'): '"A1"',
|
||||
('comment', '"NOT FOR FABRICATION - ' + COMMENT + '"'): '"' + COMMENT + '"',
|
||||
}
|
||||
replacement = replacements.get((field[0], field[-1]))
|
||||
if replacement is not None:
|
||||
field[-1] = replacement
|
||||
elif item[0] == "gr_text" and item[1] == '"' + SILK + '"':
|
||||
assert ['layer', '"F.SilkS"'] in item
|
||||
result.remove(item)
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
from validate_u1_update import parse, canonical
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise RuntimeError("Close PCB Editor before changing labels")
|
||||
original = PCB.read_bytes()
|
||||
settings = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
|
||||
expected = expected_tree(parse(PCB))
|
||||
board = p.LoadBoard(str(PCB))
|
||||
remove_labels(board)
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-labels-", dir=ROOT) as temporary:
|
||||
result = Path(temporary) / PCB.name
|
||||
assert p.SaveBoard(str(result), board, True)
|
||||
assert canonical(parse(result)) == canonical(expected), "Change beyond exact label edits"
|
||||
assert canonical(expected_tree(parse(result))) == canonical(expected), "Label cleanup not idempotent"
|
||||
if lock.exists() or PCB.read_bytes() != original:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert all(path.read_bytes() == data for path, data in settings.items()), "Project settings changed"
|
||||
result.replace(PCB)
|
||||
print("Removed draft presentation labels; exact preservation and idempotence passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,369 +0,0 @@
|
||||
#!/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 < 119 + edge)
|
||||
# Expand the RF reservation by copper radius plus numerical safety margin.
|
||||
r = radius + MARGIN
|
||||
mask |= (X >= 110 - r) & (X <= 131 + r) & (Y >= 119 - r) & (Y <= 132 + 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()
|
||||
project = ROOT / 'serial-carrier.kicad_pro'
|
||||
project_before = project.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 = [(136, 100), (182, 100), (182, 180), (100, 180), (100, 119), (136, 119)]
|
||||
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, 119), (131, 119), (131, 132), (110, 132)} 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):
|
||||
# Reserve short ground escapes before signal fanout can box them in.
|
||||
# Long ground links still run last, preserving space for signal paths.
|
||||
priority = (name == '/GND' and length > 15, 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'
|
||||
assert p.SaveBoard(str(candidate), board, True)
|
||||
assert project.read_bytes() == project_before, 'project settings changed'
|
||||
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()
|
||||
@@ -1,703 +0,0 @@
|
||||
{
|
||||
"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.005,
|
||||
"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": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"field_name_whitespace": "warning",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"ground_pin_not_ground": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"isolated_pin_label": "warning",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
"lib_symbol_mismatch": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"same_local_global_label": "warning",
|
||||
"similar_label_and_power": "warning",
|
||||
"similar_labels": "warning",
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "ignore",
|
||||
"stacked_pin_name": "warning",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"undefined_netclass": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"annotate_start_num": 0,
|
||||
"annotation": {
|
||||
"method": 0,
|
||||
"sort_order": 0
|
||||
},
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from BOM",
|
||||
"name": "${EXCLUDE_FROM_BOM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Board",
|
||||
"name": "${EXCLUDE_FROM_BOARD}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Simulation",
|
||||
"name": "${EXCLUDE_FROM_SIM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Position Files",
|
||||
"name": "${EXCLUDE_FROM_POS_FILES}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"include_excluded_from_bom": true,
|
||||
"name": "Default Editing",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Referenz"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"hop_over_size_choice": 0,
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"reuse_designators": true,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "serial-carrier.kicad_sch",
|
||||
"name": "serial-carrier",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
],
|
||||
"used_designators": "",
|
||||
"variants": []
|
||||
},
|
||||
"sheets": [],
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
(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" "Carrier:MAX3243_Reference_Provisional" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet" "" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Status" "User confirmed male pinout/header/footprint and pin-by-pin connections" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(property "Datasheet_source" "adafruit-research.md; reference female 5988 CAD" (at 228.6 96.52 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "14d1ea7b-f53a-592f-9305-545890b94099"))
|
||||
(pin "2" (uuid "b2027da1-66f1-5c08-b29b-b63e30a160ff"))
|
||||
(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" "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" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 88.9 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "79a88116-1a52-5fcb-855e-da54defa9400"))
|
||||
(pin "2" (uuid "b56a5773-68f5-5c6d-936a-8289c11714de"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R1") (unit 1)))))
|
||||
(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" "User reports purchased" (at 327.66 104.14 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "0e490c78-3450-5a5b-b7d3-d55c32de988b"))
|
||||
(pin "2" (uuid "6bbc82d1-8cfb-5fd5-832c-8df2775a89c9"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW1") (unit 1)))))
|
||||
(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" "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" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 124.46 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "c23dbef2-d61d-56fa-a158-bb9912ef48ac"))
|
||||
(pin "2" (uuid "f60eb205-60d1-5d8f-ac84-ca04f66e7106"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R2") (unit 1)))))
|
||||
(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" "User reports purchased" (at 327.66 139.7 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "19bafbc6-666f-5e53-a4ef-265057c6dc8a"))
|
||||
(pin "2" (uuid "9e3f47ba-92ac-5503-9d87-6b5590a70ccb"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW2") (unit 1)))))
|
||||
(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" "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" "Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified" (at 327.66 160.02 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "d871ee2e-bc82-5f23-a436-7b7ab74b73a9"))
|
||||
(pin "2" (uuid "96528b84-ba49-5f38-8445-b6ca2cadc93b"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "R3") (unit 1)))))
|
||||
(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" "User reports purchased" (at 327.66 175.26 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
(pin "1" (uuid "85beb3b7-e16f-5450-ba94-5711245a7b25"))
|
||||
(pin "2" (uuid "300c4196-eb91-5e58-a9aa-c84b8f7c4219"))
|
||||
(instances (project "serial-carrier" (path "/c86e0d56-4290-5f2b-b790-14926c8cdeea" (reference "SW3") (unit 1)))))
|
||||
(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.\nMale mapping user-reviewed; reference footprint assigned.\nDE-9 and charge pump are already on the module." (at 177.8 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "9b62e7ca-695a-51af-bbbe-e7ee4ee45c08"))
|
||||
(text "Controls: active-low, firmware-debounced" (at 292.1 57.15 0) (effects (font (size 1.778 1.778)) (justify left top)) (uuid "9d91985c-7375-5177-9a5e-8280e3bcaff6"))
|
||||
(text "External 2k 1% pull-ups: ~1.65 mA at 3.3 V.\nChosen for B3F-1000 rated contact load; internal\npull-ups may remain enabled. Exact resistor MPN pending." (at 292.1 64.77 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "b2dd72f5-86fc-5ce1-b09a-67a6fe9404d4"))
|
||||
(text "DS1: front view GND / VCC / SCL / SDA" (at 177.8 128.27 0) (effects (font (size 1.524 1.524)) (justify left top)) (uuid "7b57748d-38ca-5712-97f9-bf6dab6dafaf"))
|
||||
(text "Power at 3.3 V only. Onboard I2C pull-ups user-confirmed;\nresistance and rail unmeasured; no external pull-ups." (at 177.8 165.1 0) (effects (font (size 1.016 1.016)) (justify left top)) (uuid "fb23ec11-40f8-56c7-b78b-0da2d40e4b72"))
|
||||
(text "POWER / RECOVERY\nPower only through U1's existing USB ports. No carrier 5 V input.\nU1 regulator supplies +3V3; GND pins are common on the module.\nPin 1 +3V3 and pin 22 GND model the onboard supply for ERC.\nGPIO19/20 native USB and GPIO43/44 UART0 remain onboard.\nUSB nonisolated accepted; backfeed safety not verified.\nKeep both USB connectors and reset/boot buttons accessible." (at 15.24 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "cadf6b77-e888-5f04-b4f6-3873114844ca"))
|
||||
(text "RS-232 / ELECTRICAL LIMITS\nTX/RX and modem labels are at the ESP32 logic side.\nOFF_N is active low; GPIO9 must remain open-drain.\nBreakout provides its OFF pull-up. No galvanic isolation.\nNo carrier connection to raw RS-232 voltages.\nCarrier GND, USB GND and RS-232 signal ground are common.\nNever power OLED or MAX3243 logic from 5 V." (at 152.4 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "0f7da388-3edb-5cc3-9937-13492397a221"))
|
||||
(text "USER ACCEPTANCE / EVIDENCE LIMITS\nModule mapping and visual fit accepted by user.\nB3F-1000 and 2k 1% metal-film resistors purchased.\nM3 carrier/module hardware; M2 display hardware.\nRF/power routing accepted, not qualified.\nRegulator margin and USB backfeed safety unverified.\nERC is not hardware or fabrication approval." (at 292.1 185.42 0) (effects (font (size 1.27 1.27)) (justify left top)) (uuid "1df2bd9e-fbff-5047-92a9-09f33d7cd7ac"))
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
(sym_lib_table (version 7)
|
||||
(lib (name "Carrier")(type "KiCad")(uri "${KIPRJMOD}/Carrier.kicad_sym")(options "")(descr "Project-local module interfaces"))
|
||||
)
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guarded accepted-fit label cleanup and A3 worksheet; never move board items."""
|
||||
import copy
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
COMMENT = "USB / antenna geometry pending; verify module pinout / assembly"
|
||||
USB = "USB ACCESS - VERIFY"
|
||||
OLD_CAPTION = "Nominal RS-232 metal verified; ESP32 USB / antenna / assembly clearance pending"
|
||||
CAPTION = "Nominal RS-232 metal verified; RF qualification not established"
|
||||
|
||||
|
||||
def expected_tree(tree):
|
||||
result = copy.deepcopy(tree)
|
||||
for item in list(result):
|
||||
if not isinstance(item, list):
|
||||
continue
|
||||
if item[0] == "paper":
|
||||
assert item in (["paper", '"A4"'], ["paper", '"A3"'])
|
||||
item[1] = '"A3"'
|
||||
elif item[0] == "title_block":
|
||||
for field in list(item[1:]):
|
||||
if field == ["comment", "1", '"' + COMMENT + '"']:
|
||||
item.remove(field)
|
||||
elif item[0] == "gr_text":
|
||||
if item[1] == '"' + USB + '"':
|
||||
assert ['layer', '"Dwgs.User"'] in item
|
||||
result.remove(item)
|
||||
elif item[1] == '"' + OLD_CAPTION + '"':
|
||||
assert ['layer', '"Dwgs.User"'] in item
|
||||
item[1] = '"' + CAPTION + '"'
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
from validate_u1_update import parse, canonical
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise RuntimeError("Close PCB Editor before changing presentation")
|
||||
original = PCB.read_bytes()
|
||||
protected = {path: path.read_bytes() for path in ROOT.glob("serial-carrier.kicad_pr*")}
|
||||
expected = expected_tree(parse(PCB))
|
||||
with tempfile.TemporaryDirectory(prefix="carrier-presentation-", dir=ROOT) as temporary:
|
||||
candidate = Path(temporary) / PCB.name
|
||||
# PAGE_INFO is opaque in this pcbnew binding. Load the exact paper-only
|
||||
# substitution, then use the native saver for all label changes.
|
||||
text = original.decode()
|
||||
assert text.count('(paper "A4")') + text.count('(paper "A3")') == 1
|
||||
candidate.write_text(text.replace('(paper "A4")', '(paper "A3")'))
|
||||
board = p.LoadBoard(str(candidate))
|
||||
title = board.GetTitleBlock()
|
||||
assert title.GetComment(0) in (COMMENT, "")
|
||||
title.SetComment(0, "")
|
||||
for drawing in list(board.GetDrawings()):
|
||||
if isinstance(drawing, p.PCB_TEXT):
|
||||
if drawing.GetText() == USB:
|
||||
assert drawing.GetLayer() == p.Dwgs_User
|
||||
board.Remove(drawing)
|
||||
elif drawing.GetText() == OLD_CAPTION:
|
||||
drawing.SetText(CAPTION)
|
||||
assert p.SaveBoard(str(candidate), board, True)
|
||||
assert canonical(parse(candidate)) == canonical(expected), "Unexpected physical/content change"
|
||||
assert canonical(expected_tree(parse(candidate))) == canonical(expected), "Not idempotent"
|
||||
if lock.exists() or PCB.read_bytes() != original:
|
||||
raise RuntimeError("PCB changed or reopened; refusing overwrite")
|
||||
assert all(path.read_bytes() == data for path, data in protected.items()), "Settings changed"
|
||||
candidate.replace(PCB)
|
||||
print("A3 worksheet / accepted-fit labels updated; exact preservation and idempotence PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Synchronize measured U1 F.Fab graphics only; never replace the footprint."""
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
NS = uuid.UUID("36693251-e1f8-44e9-a8a9-e201bd658ceb")
|
||||
|
||||
|
||||
def library_graphics():
|
||||
fp = p.FootprintLoad(str(ROOT / "Carrier.pretty"), "HW678_2x22_Provisional")
|
||||
if fp is None:
|
||||
raise RuntimeError("Cannot load measured HW678 footprint")
|
||||
return fp
|
||||
|
||||
|
||||
def signature(g, origin):
|
||||
def point(v):
|
||||
return v.x-origin.x, v.y-origin.y
|
||||
if isinstance(g, p.PCB_SHAPE):
|
||||
return ("shape", g.GetShape(), point(g.GetStart()), point(g.GetEnd()), g.GetWidth())
|
||||
return ("text", g.GetText(), point(g.GetPosition()),
|
||||
(g.GetTextSize().x, g.GetTextSize().y), g.GetTextThickness())
|
||||
|
||||
|
||||
def measured_items(fp):
|
||||
# Generic library captions would overlap existing board labels.
|
||||
return sorted((g for g in fp.GraphicalItems() if g.GetLayer() == p.F_Fab
|
||||
and not (isinstance(g, p.PCB_TEXT) and g.GetText() in (
|
||||
"PROVISIONAL CONNECTOR FIT", "ANTENNA UP / USB DOWN"))),
|
||||
key=lambda g: signature(g, fp.GetPosition()))
|
||||
|
||||
|
||||
def verify(board):
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
assert len([g for g in u1.GraphicalItems() if g.GetLayer() == p.F_Fab]) == 6
|
||||
library = library_graphics()
|
||||
expected = [signature(g, library.GetPosition()) for g in measured_items(library)]
|
||||
actual = [signature(g, u1.GetPosition()) for g in measured_items(u1)]
|
||||
assert actual == expected, "Embedded U1 F.Fab differs from measured library; run update_u1_graphics.py"
|
||||
|
||||
|
||||
def main():
|
||||
lock = ROOT / "~serial-carrier.kicad_pcb.lck"
|
||||
if lock.exists():
|
||||
raise SystemExit("Close PCB Editor before updating U1 graphics")
|
||||
before = PCB.read_bytes()
|
||||
project = ROOT / "serial-carrier.kicad_pro"
|
||||
project_before = project.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference() == "U1")
|
||||
assert u1.GetOrientationDegrees() == 0 and u1.GetLayer() == p.F_Cu
|
||||
library = library_graphics()
|
||||
sources = measured_items(library)
|
||||
assert len(sources) == 6, "Review changed measured library before integration"
|
||||
owned = {str(uuid.uuid5(NS, str(i))) for i in range(len(sources))}
|
||||
existing = list(u1.GraphicalItems())
|
||||
assert all(g.m_Uuid.AsString() in owned for g in existing if g.GetLayer() == p.F_Fab), "Unowned U1 fabrication graphics; review manually"
|
||||
if [signature(g, u1.GetPosition()) for g in measured_items(u1)] == [
|
||||
signature(g, library.GetPosition()) for g in sources]:
|
||||
verify(board)
|
||||
print("Embedded measured U1 graphics already current; no board write")
|
||||
return
|
||||
for g in existing:
|
||||
if g.m_Uuid.AsString() in owned:
|
||||
u1.Remove(g)
|
||||
for i, source in enumerate(sources):
|
||||
graphic = source.Duplicate()
|
||||
graphic.SetParent(u1)
|
||||
graphic.SetUuid(p.KIID(str(uuid.uuid5(NS, str(i)))))
|
||||
graphic.Move(u1.GetPosition() - library.GetPosition())
|
||||
u1.Add(graphic)
|
||||
verify(board)
|
||||
temp = ROOT / "validation/pcb-u1-tmp.kicad_pcb"
|
||||
try:
|
||||
assert p.SaveBoard(str(temp), board, True)
|
||||
verify(p.LoadBoard(str(temp)))
|
||||
if lock.exists() or PCB.read_bytes() != before:
|
||||
raise RuntimeError("PCB changed or reopened; refusing to overwrite")
|
||||
assert project.read_bytes() == project_before, "Project settings changed"
|
||||
temp.replace(PCB)
|
||||
finally:
|
||||
temp.unlink(missing_ok=True)
|
||||
print("Embedded measured U1 graphics; existing footprint fields, pads and routing retained")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/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)
|
||||
shapes = [g for g in footprint.GraphicalItems() if isinstance(g, k.PCB_SHAPE)]
|
||||
expected = [
|
||||
(k.SHAPE_T_RECT, mm(-1.365, -1.365), mm(26.725, 54.705), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_RECT, mm(4.045, -8.255), mm(21.675, -1.365), k.FromMM(0.1)),
|
||||
(k.SHAPE_T_SEGMENT, mm(2.795, 54.705), mm(11.665, 54.705), k.FromMM(0.3)),
|
||||
(k.SHAPE_T_SEGMENT, mm(14.195, 54.705), mm(23.065, 54.705), k.FromMM(0.3)),
|
||||
]
|
||||
self.assertEqual(Counter((g.GetShape(), xy(g.GetStart()), xy(g.GetEnd()),
|
||||
g.GetWidth()) for g in shapes), Counter(expected))
|
||||
for shape in shapes:
|
||||
self.assertEqual(shape.GetLayer(), k.F_Fab)
|
||||
self.assertFalse(shape.IsAnyFill())
|
||||
# Only measured planar bounds/mouth spans: no invented shell depth or RF area.
|
||||
self.assertAlmostEqual(26.725 - (-1.365), 28.09)
|
||||
self.assertAlmostEqual(54.705 - (-1.365), 56.07)
|
||||
self.assertAlmostEqual(54.705 - (-8.255), 62.96)
|
||||
self.assertAlmostEqual(26.725 - 25.4, 1.325)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
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)
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
"""Exact authorized placement/value delta, retained copper and body geometry audit."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from validate_u1_update import parse, canonical, ident
|
||||
from add_device_silkscreen import NS
|
||||
from refine_group_placement import ROOT, BASE, MOVES
|
||||
|
||||
METADATA = {
|
||||
**{f'R{i}': ('Rating', 'Purchased metal-film 1%; design at least 0.125 W; actual MPN/wattage unspecified') for i in range(1,4)},
|
||||
**{f'SW{i}': ('Procurement', 'User reports purchased') for i in range(1,4)},
|
||||
'U2': ('Status', 'User confirmed male pinout/header/footprint and pin-by-pin connections'),
|
||||
}
|
||||
|
||||
|
||||
def expected_identity(board):
|
||||
"""Apply only the seven authorized placements and three resistor values."""
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
for ref,(old,new) in MOVES.items():
|
||||
fps[ref].SetPosition(p.VECTOR2I(p.FromMM(new[0]),p.FromMM(new[1])))
|
||||
if ref.startswith('R'): fps[ref].SetValue('2k 1%')
|
||||
return board
|
||||
|
||||
|
||||
def audit():
|
||||
assert hashlib.sha256(BASE.read_bytes()).hexdigest()=='b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192'
|
||||
old=parse(BASE)
|
||||
new=parse(ROOT/'serial-carrier.kicad_pcb')
|
||||
from update_presentation import expected_tree
|
||||
wanted=expected_tree(old)
|
||||
for f in wanted:
|
||||
if not isinstance(f,list) or f[0]!='footprint': continue
|
||||
ref=next(v[2].strip('"') for v in f if isinstance(v,list) and v[:2]==['property','"Reference"'])
|
||||
if ref in METADATA:
|
||||
key,value=METADATA[ref]
|
||||
next(v for v in f if isinstance(v,list) and v[:2]==['property',json.dumps(key)])[2]=json.dumps(value)
|
||||
if ref in MOVES:
|
||||
at=next(v for v in f if isinstance(v,list) and v[0]=='at')
|
||||
at[1:3]=[format(x,'.10g') for x in MOVES[ref][1]]
|
||||
if ref.startswith('R'):
|
||||
next(v for v in f if isinstance(v,list) and v[:2]==['property','"Value"'])[2]='"2k 1%"'
|
||||
silk={str(uuid.uuid5(NS,str(i))) for i in range(4096)}
|
||||
def without_copper(tree):
|
||||
return [v for v in tree if not isinstance(v,list) or v[0] not in ('segment','via')]
|
||||
assert canonical(without_copper(wanted),silk)==canonical(without_copper(new),silk), 'Unexpected non-copper edit'
|
||||
before={ident(v):v for v in old if isinstance(v,list) and v[0] in ('segment','via')}
|
||||
after={ident(v):v for v in new if isinstance(v,list) and v[0] in ('segment','via')}
|
||||
migration=json.loads((ROOT/'validation/pcb-group-migration.json').read_text())
|
||||
removed=set(before)-set(after)
|
||||
assert removed==set(before)&set(migration['removed_track_uuids'])
|
||||
assert all(before[k]==after[k] for k in before.keys()&after.keys()), 'Retained copper changed'
|
||||
assert len(before.keys()&after.keys())==476
|
||||
for name in ('serial-carrier.kicad_pro','serial-carrier.kicad_prl'):
|
||||
assert hashlib.sha256((ROOT/name).read_bytes()).hexdigest()==migration['protected_sha256'][name]
|
||||
board=p.LoadBoard(str(ROOT/'serial-carrier.kicad_pcb'))
|
||||
fps={f.GetReference():f for f in board.GetFootprints()}
|
||||
def xy(v): return p.ToMM(v.x),p.ToMM(v.y)
|
||||
# Published U2 F.Fab outline includes the bare PCB beyond the header row.
|
||||
u2pts=[xy(v) for g in fps['U2'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
|
||||
axis=(min(x for x,y in u2pts)+max(x for x,y in u2pts))/2
|
||||
bottom=max(y for x,y in u2pts)
|
||||
oled=next(g for g in fps['DS1'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetStart()==fps['DS1'].GetPosition())
|
||||
assert abs((xy(oled.GetStart())[0]+xy(oled.GetEnd())[0])/2-axis)<1e-6
|
||||
resistor_boxes=[g for i in range(1,4) for g in fps[f'R{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and g.GetShape()==p.SHAPE_T_RECT]
|
||||
assert len(resistor_boxes)==3
|
||||
left=min(xy(g.GetStart())[0] for g in resistor_boxes)
|
||||
right=max(xy(g.GetEnd())[0] for g in resistor_boxes)
|
||||
assert abs((left+right)/2-axis)<1e-6
|
||||
topgap=min(xy(g.GetStart())[1] for g in resistor_boxes)-bottom
|
||||
bottomgap=xy(oled.GetStart())[1]-max(xy(g.GetEnd())[1] for g in resistor_boxes)
|
||||
assert abs(topgap-2.0815)<1e-6 and abs(bottomgap-topgap)<1e-6
|
||||
switchpts=[xy(v) for i in range(1,4) for g in fps[f'SW{i}'].GraphicalItems() if isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab for v in (g.GetStart(),g.GetEnd())]
|
||||
assert abs((min(x for x,y in switchpts)+max(x for x,y in switchpts))/2-axis)<1e-6
|
||||
switchgap=min(y for x,y in switchpts)-xy(oled.GetEnd())[1]
|
||||
assert abs(switchgap-5.25)<1e-6
|
||||
result=dict(common_axis_absolute_x_mm=axis,resistor_to_rs232_body_gap_mm=round(topgap,6),resistor_to_oled_body_gap_mm=round(bottomgap,6),oled_to_button_body_gap_mm=switchgap,
|
||||
original_copper_items=len(before),retained_copper_items=476,removed_original_copper_items=len(removed),new_copper_items=len(after.keys()-before.keys()),
|
||||
exact_unrelated_content_preserved=True,project_settings_preserved=True,baseline_sha256=migration['baseline_sha256'],board_sha256=hashlib.sha256((ROOT/'serial-carrier.kicad_pcb').read_bytes()).hexdigest())
|
||||
(ROOT/'validation/pcb-group-alignment.json').write_text(json.dumps(result,indent=2)+'\n')
|
||||
return result
|
||||
|
||||
if __name__=='__main__': print(json.dumps(audit(),indent=2))
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/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. Schematic footprint parity
|
||||
must be clean, including the user-confirmed U2 reference footprint assignment.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
import pcbnew as p
|
||||
from update_u1_graphics import verify
|
||||
|
||||
ROOT=Path(__file__).resolve().parent
|
||||
PCB=ROOT/"serial-carrier.kicad_pcb"
|
||||
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))
|
||||
verify(board)
|
||||
from update_presentation import expected_tree, CAPTION
|
||||
from validate_u1_update import parse, canonical
|
||||
tree=parse(PCB)
|
||||
assert ['paper', '"A3"'] in tree, "A3 worksheet required to clear the title block"
|
||||
assert canonical(expected_tree(tree)) == canonical(tree), "Stale presentation labels"
|
||||
assert board.GetTitleBlock().GetComment(0) == ""
|
||||
assert any(isinstance(g,p.PCB_TEXT) and g.GetText()==CAPTION for g in board.GetDrawings())
|
||||
source=ET.parse(OUT/"pcb-source-netlist.xml").getroot()
|
||||
comps={c.get("ref"):c for c in source.findall("components/comp")}
|
||||
footprints={f.GetReference():f for f in board.GetFootprints()}
|
||||
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())}
|
||||
expected_outline=[(36,0),(82,0),(82,80),(0,80),(0,19),(36,19)]
|
||||
assert points==set(expected_outline)
|
||||
expected_edges={frozenset((a,b)) for a,b in zip(expected_outline,expected_outline[1:]+expected_outline[:1])}
|
||||
actual_edges={frozenset((round(p.ToMM(v.x)-100,5),round(p.ToMM(v.y)-100,5))
|
||||
for v in (d.GetStart(),d.GetEnd())) for d in outline}
|
||||
assert actual_edges==expected_edges
|
||||
placements={"U1":(8,23),"U2":(43,26.670),"DS1":(43.97,36),
|
||||
"R1":(42.16,32.6685),"R2":(53.16,32.6685),"R3":(64.16,32.6685),
|
||||
"SW1":(41.72,68),"SW2":(53.72,68),"SW3":(65.72,68),
|
||||
"H1":(3.5,22.5),"H2":(3.5,75),"H3":(78,4),"H4":(78,76)}
|
||||
for ref,(x,y) in placements.items():
|
||||
position=footprints[ref].GetPosition()
|
||||
assert position.x==p.FromMM(x+100) and position.y==p.FromMM(y+100),ref
|
||||
assert footprints[ref].GetOrientationDegrees()==0,ref
|
||||
assert len(board.Zones())==1
|
||||
reserve=board.Zones()[0]
|
||||
assert reserve.GetIsRuleArea() and reserve.IsOnLayer(p.F_Cu) and reserve.IsOnLayer(p.B_Cu)
|
||||
assert reserve.GetDoNotAllowTracks() and reserve.GetDoNotAllowVias() and reserve.GetDoNotAllowZoneFills()
|
||||
contour=reserve.Outline().COutline(0)
|
||||
assert {(p.ToMM(contour.CPoint(i).x),p.ToMM(contour.CPoint(i).y)) for i in range(contour.PointCount())}=={(110,119),(131,119),(131,132),(110,132)}
|
||||
# Reference bare-PCB front is 26.670 mm above the U2 pad-row origin.
|
||||
u2_front=p.ToMM(footprints['U2'].GetPosition().y)-26.670
|
||||
assert abs(u2_front-100)<1e-6
|
||||
assert any(isinstance(g,p.PCB_SHAPE) and g.GetLayer()==p.F_Fab and
|
||||
g.GetShape()==p.SHAPE_T_SEGMENT and
|
||||
g.GetStart().y==p.FromMM(100) and g.GetEnd().y==p.FromMM(100)
|
||||
for g in footprints['U2'].GraphicalItems()), 'U2 bare-PCB front not flush'
|
||||
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 wanted=="Carrier:MAX3243_Reference_Provisional"
|
||||
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 not parity,parity
|
||||
summary={
|
||||
"carrier_bounds_mm":[82,80],
|
||||
"outline_local_mm":expected_outline,
|
||||
"placements_local_mm":placements,
|
||||
"rf_reserve_absolute_mm":[110,119,131,132],
|
||||
"u2_bare_pcb_front_local_y_mm":round(u2_front-100,6),
|
||||
"nominal_user_confirmed_rs232_metal_mm":{"shell_overhang":3.870,"hex_mouth_overhang":2.870,"flange_front_inset":1.930},
|
||||
"tracks":sum(not isinstance(t,p.PCB_VIA) for t in tracks),
|
||||
"vias":sum(isinstance(t,p.PCB_VIA) for t in tracks),
|
||||
"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,
|
||||
"schematic_parity_violations":0,
|
||||
"project_settings_sha256":hashlib.sha256((ROOT/'serial-carrier.kicad_pro').read_bytes()).hexdigest(),
|
||||
"ignored_DRC_checks":report["ignored_checks"],
|
||||
"user_accepted":["module dimensions/visual fit and USB clearance", "male pinout/header/footprint and pin-by-pin review", "M3 carrier/module and M2 display hardware", "RF keepout and power/ground routing"],
|
||||
"not_independently_verified":["reported 10 mm assembled module elevation", "regulator margin", "USB backfeed safety", "OLED onboard pull-up resistance/rail", "manufacturer RF qualification", "fabrication process/certification"],
|
||||
"new_hardware_tests":False,
|
||||
}
|
||||
(OUT/"pcb-validation-summary.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print("PCB draft verified: 82 x 80 mm; 2 copper layers; 0 physical DRC violations; 0 unrouted.")
|
||||
print("All schematic pad nets and footprint assignments match. Zero schematic parity violations.")
|
||||
print("Not fabrication-ready; see ../../docs/devel/hardware/PCB/pcb-draft-notes.md and ignored checks in the DRC report.")
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/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") == "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])
|
||||
# User confirmed the male module shares the reference pinout/header/footprint.
|
||||
assert components["U2"].findtext("footprint") == "Carrier:MAX3243_Reference_Provisional"
|
||||
expected = json.loads((OUT / "expected-nets.json").read_text())
|
||||
assert set(expected) == active
|
||||
for net, nodes in expected.items():
|
||||
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.",
|
||||
"User accepted module mapping/fit and reports purchased switches/resistors.",
|
||||
"No new hardware tests; regulator margin, USB backfeed safety, RF performance",
|
||||
"and fabrication qualification are not established by this schematic check.",
|
||||
]) + "\n"
|
||||
(OUT / "summary.txt").write_text(summary)
|
||||
print(summary)
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Measured U1 integration regression and exact pre-update preservation audit."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
import pcbnew as p
|
||||
from shapely.geometry import Polygon
|
||||
from add_device_silkscreen import add_outlines, NS as SILK_NS, xy, outline_lines
|
||||
from update_u1_graphics import verify, NS as U1_NS
|
||||
from remove_draft_markers import expected_tree
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PCB = ROOT / "serial-carrier.kicad_pcb"
|
||||
BASE = ROOT / "validation/pcb-before-u1-measured.kicad_pcb"
|
||||
|
||||
|
||||
def parse(path):
|
||||
tokens = iter(re.findall(r'"(?:\\.|[^"\\])*"|[()]|[^\s()]+', path.read_text()))
|
||||
def group():
|
||||
result = []
|
||||
for token in tokens:
|
||||
if token == ')':
|
||||
return result
|
||||
result.append(group() if token == '(' else token)
|
||||
raise ValueError("Unterminated S-expression")
|
||||
assert next(tokens) == '('
|
||||
return group()
|
||||
|
||||
|
||||
def ident(item):
|
||||
return next((v[1].strip('"') for v in item if isinstance(v,list) and v[0]=='uuid'), None)
|
||||
|
||||
|
||||
def canonical(item, excluded=frozenset()):
|
||||
# KiCad can reorder independent board items when saving; compare their full
|
||||
# parsed contents, not byte order. Pad coordinates/netnames/UUIDs remain exact.
|
||||
if not isinstance(item, list):
|
||||
return item
|
||||
values = [canonical(v, excluded) for v in item
|
||||
if not isinstance(v,list) or ident(v) not in excluded]
|
||||
if item and item[0] in ("kicad_pcb", "footprint"):
|
||||
return ([v for v in values if not isinstance(v,list)] +
|
||||
sorted((v for v in values if isinstance(v,list)), key=repr))
|
||||
# Coordinates, polygon vertex order, text and other ordered values must
|
||||
# remain ordered: sorting these could conceal an X/Y swap or zone edit.
|
||||
return values
|
||||
|
||||
|
||||
def main():
|
||||
assert hashlib.sha256(BASE.read_bytes()).hexdigest() == "53e36538c0669604457b9e1a9ff1b1e20422970e8f9f356088c4581868ad5045", "Pre-update snapshot changed"
|
||||
assert hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest() == "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516", "Project settings changed"
|
||||
before = PCB.read_bytes()
|
||||
board = p.LoadBoard(str(PCB))
|
||||
verify(board)
|
||||
silk_ids = {str(uuid.uuid5(SILK_NS,str(i))) for i in range(4096)}
|
||||
u1_ids = {str(uuid.uuid5(U1_NS,str(i))) for i in range(6)}
|
||||
# The original baseline retains historical draft labels. Permit only the
|
||||
# exact requested title/revision/comment edits and removal of the silk label.
|
||||
# Keep proving the original integration against its original evidence, then
|
||||
# audit the explicitly authorized subsequent placement/routing delta.
|
||||
from validate_group_alignment import audit, BASE as GROUP_BASE
|
||||
assert canonical(expected_tree(parse(BASE)), silk_ids) == canonical(parse(GROUP_BASE), silk_ids | u1_ids), "Historical U1 integration changed"
|
||||
audit()
|
||||
generated = [g for g in board.GetDrawings() if g.m_Uuid.AsString() in silk_ids]
|
||||
assert not any(isinstance(g,p.PCB_TEXT) and g.GetText()=="HEADER GUIDES" for g in board.GetDrawings())
|
||||
carrier = Polygon([(136,100),(182,100),(182,180),(100,180),(100,119),(136,119)])
|
||||
mouths = []
|
||||
for g in generated:
|
||||
assert g.GetLayer() == p.F_SilkS
|
||||
for edge in outline_lines(g):
|
||||
assert carrier.covers(edge.buffer(p.ToMM(g.GetWidth())/2)), "Silk outside carrier/notch"
|
||||
if g.GetWidth() == p.FromMM(0.3):
|
||||
mouths.append(g)
|
||||
assert len(mouths) == 2
|
||||
assert sorted((xy(g.GetStart()),xy(g.GetEnd())) for g in mouths) == [
|
||||
((110.795,177.705),(119.665,177.705)),
|
||||
((122.195,177.705),(131.065,177.705))]
|
||||
# A second refresh must reproduce all drawing geometry and UUIDs.
|
||||
add_outlines(board)
|
||||
with tempfile.TemporaryDirectory(prefix="u1-regression-") as temporary:
|
||||
result = Path(temporary)/PCB.name
|
||||
assert p.SaveBoard(str(result),board,True)
|
||||
assert canonical(parse(result)) == canonical(parse(PCB)), "Silkscreen refresh not idempotent"
|
||||
u1 = next(f for f in board.GetFootprints() if f.GetReference()=="U1")
|
||||
shape = next(g for g in u1.GraphicalItems() if isinstance(g,p.PCB_SHAPE))
|
||||
shape.SetWidth(shape.GetWidth()+1)
|
||||
try:
|
||||
verify(board)
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("Stale embedded geometry was not rejected")
|
||||
assert PCB.read_bytes() == before
|
||||
assert not (ROOT/"~serial-carrier.kicad_pcb.lck").exists()
|
||||
summary = dict(measured_library_matches_embedded_u1=True,
|
||||
historical_u1_integration_and_subsequent_authorized_group_delta_verified=True,
|
||||
presentation_label_cleanup=True,
|
||||
silk_refresh_idempotent=True, stale_geometry_negative_control=True,
|
||||
silk_clipped_to_carrier=True, usb_mouth_markers=2,
|
||||
generated_silk_items=len(generated),
|
||||
baseline_sha256=hashlib.sha256(BASE.read_bytes()).hexdigest(),
|
||||
board_sha256=hashlib.sha256(before).hexdigest(),
|
||||
project_sha256=hashlib.sha256((ROOT/"serial-carrier.kicad_pro").read_bytes()).hexdigest())
|
||||
(ROOT/"validation/pcb-u1-group-regression.json").write_text(json.dumps(summary,indent=2)+"\n")
|
||||
print(json.dumps(summary,indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"$schema": "https://schemas.kicad.org/erc.v1.json",
|
||||
"coordinate_units": "mm",
|
||||
"date": "2026-09-22T09:55:18",
|
||||
"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"
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
{
|
||||
"+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"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 253 KiB |
|
Before Width: | Height: | Size: 337 KiB |
|
Before Width: | Height: | Size: 347 KiB |
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"common_axis_absolute_x_mm": 156.97,
|
||||
"resistor_to_rs232_body_gap_mm": 2.0815,
|
||||
"resistor_to_oled_body_gap_mm": 2.0815,
|
||||
"oled_to_button_body_gap_mm": 5.25,
|
||||
"original_copper_items": 587,
|
||||
"retained_copper_items": 476,
|
||||
"removed_original_copper_items": 111,
|
||||
"new_copper_items": 503,
|
||||
"exact_unrelated_content_preserved": true,
|
||||
"project_settings_preserved": true,
|
||||
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"board_sha256": "fa95d5747c6e455b2c5effd11d11091b1f95df015dbe73c99b14092487a034c5"
|
||||
}
|
||||
@@ -1,639 +0,0 @@
|
||||
{
|
||||
"baseline_sha256": "b9e020939fff42b5c7a3161ed89af9dc3c1b9c016268311b3db7ab407ed67192",
|
||||
"moves_absolute_mm": {
|
||||
"DS1": [
|
||||
[
|
||||
143,
|
||||
136
|
||||
],
|
||||
[
|
||||
143.97,
|
||||
136
|
||||
]
|
||||
],
|
||||
"R1": [
|
||||
[
|
||||
143,
|
||||
134
|
||||
],
|
||||
[
|
||||
142.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"R2": [
|
||||
[
|
||||
154,
|
||||
134
|
||||
],
|
||||
[
|
||||
153.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"R3": [
|
||||
[
|
||||
165,
|
||||
134
|
||||
],
|
||||
[
|
||||
164.16,
|
||||
132.6685
|
||||
]
|
||||
],
|
||||
"SW1": [
|
||||
[
|
||||
143,
|
||||
168
|
||||
],
|
||||
[
|
||||
141.72,
|
||||
168
|
||||
]
|
||||
],
|
||||
"SW2": [
|
||||
[
|
||||
155,
|
||||
168
|
||||
],
|
||||
[
|
||||
153.72,
|
||||
168
|
||||
]
|
||||
],
|
||||
"SW3": [
|
||||
[
|
||||
167,
|
||||
168
|
||||
],
|
||||
[
|
||||
165.72,
|
||||
168
|
||||
]
|
||||
]
|
||||
},
|
||||
"removed_track_uuids": [
|
||||
"38b14c4a-1117-48f1-84c2-69df406efd45",
|
||||
"0436d2d5-7e3c-4343-a104-b27263f50378",
|
||||
"0cd2f1ba-dd20-43d0-88bf-b78d25caa7f2",
|
||||
"0df43fc8-4848-4075-9ee2-84440d3d4c52",
|
||||
"114df41e-6104-445f-b52b-b623673294c3",
|
||||
"2f38905d-78e9-4853-b3d0-50b257179e7b",
|
||||
"5b48705f-d547-4a77-84fa-6e4149256b50",
|
||||
"68657908-8a9e-41e0-832d-7e05a448acea",
|
||||
"6b91cd8a-246d-4cfc-9398-242d2912fc37",
|
||||
"88c50c34-fa4a-4fb9-89c7-fb8e40fcaad2",
|
||||
"9e80e1c0-4aaa-4ebc-9689-497f9e2d3e92",
|
||||
"b88df4ea-9af3-402e-b7ca-59ce0b2065bb",
|
||||
"e1a92e9c-97e5-4400-b977-42536a8d2f2d",
|
||||
"073a79bf-6f12-4c76-ba49-fbbfb977d470",
|
||||
"18bac46c-c87a-4575-b8af-1934ea6110c4",
|
||||
"20378a0c-b433-42ba-a643-c5af22ce09ed",
|
||||
"28595a1b-947e-4fdd-be3e-d725cdabbcd4",
|
||||
"49bb5dc7-5ca7-4d9c-8bb0-79ddb15d928f",
|
||||
"79379f9c-e912-4b19-af4c-1638bb948703",
|
||||
"97355a54-7801-4a02-8b6f-e1d31d364035",
|
||||
"a2b2baa5-e50d-449a-ab05-53aaaf15c06c",
|
||||
"da453fcc-84cc-470b-bc5a-49aa69700fa2",
|
||||
"eab2ad52-ceaa-4fcb-ba9d-336bbc8745de",
|
||||
"27bfb939-d170-4d17-9aab-8d68ea6995ca",
|
||||
"6bb98a8c-ace7-48cb-8836-47b54273ebc3",
|
||||
"8f0d756a-6380-4e82-b65c-17d2f12cf462",
|
||||
"eb39c8eb-56cd-4331-b798-e769bfd19c05",
|
||||
"16c19532-1b7d-4251-a3f2-07d08811abe4",
|
||||
"45f61cb8-ad1d-4ca5-bb0d-3d26df7a61eb",
|
||||
"4dc2388f-7f09-4efc-ae8d-5593cfd0efc5",
|
||||
"527da12c-1892-4783-8e1e-bbb23491cf13",
|
||||
"643622f8-985f-41ef-9f60-97b85df04a4b",
|
||||
"7201c8b8-a924-4a78-8718-8639e1b84375",
|
||||
"82d24494-1863-4798-8dab-d6acaa6af1ef",
|
||||
"9d886175-0a22-4800-bacf-d027a2594149",
|
||||
"b87e55c6-667a-4320-b42e-5311956f3869",
|
||||
"ca3aebf5-89f6-4d95-920c-7f751b4e7227",
|
||||
"d43c4432-114e-4488-b457-1015c677dd58",
|
||||
"d5c6f4dc-918d-453a-a950-6fa7ff814a53",
|
||||
"db004298-a5ea-4295-a340-99861f12affa",
|
||||
"e058474e-eea8-44c5-9566-227ab00f9713",
|
||||
"ebf07a5b-1b77-4da4-9009-b5456894c1a6",
|
||||
"0348920f-ac8a-4881-925c-2379c1c042a3",
|
||||
"1376e497-a31b-44ba-8756-a0c3909ae497",
|
||||
"1ae9db6c-00ce-498f-a69a-4917829179a3",
|
||||
"3385b064-faba-489a-ab8f-e2efd7bc6078",
|
||||
"41b7e5dc-e873-4e78-add1-4a8aef2cf0f2",
|
||||
"4a9ea5ca-9548-464c-90aa-d5a11171700b",
|
||||
"53084fac-b12d-4262-8e8a-326bb1598e1d",
|
||||
"6278694c-ab84-4d18-966d-cdd87569b736",
|
||||
"70ecf107-6be8-4c81-944d-b9195d0e907b",
|
||||
"7d370000-a728-42ad-8fb3-07bd56f55834",
|
||||
"8a26d3a6-0e4e-47ac-914a-4e2ac03e80b8",
|
||||
"a4794925-fe72-46c4-bea5-8cdf2ead8bbe",
|
||||
"c8f89618-ed14-47ff-8e60-c489062cce99",
|
||||
"d42d944c-5031-40e7-bab4-578d2a05e07c",
|
||||
"fc51bdbd-326c-4620-b799-b1fcbfd0abfa",
|
||||
"08eda9a1-b0e4-4ed1-8d7d-cec706f0162c",
|
||||
"56f6e44c-d9f4-4144-8950-869e365e09d5",
|
||||
"575729f7-8e91-43fc-ba36-47bd1927aebb",
|
||||
"58b8f7f8-efff-41d9-a224-6c87461ab440",
|
||||
"847e753c-6cc0-4300-8d7c-a3aba8914103",
|
||||
"8b3de05f-9c1d-4e9f-82ab-64f1d17763e7",
|
||||
"c0c4c166-7646-41f9-b005-9fe5c7ded558",
|
||||
"c972b3de-08dc-4ad9-a684-3ff40523f29a",
|
||||
"e0902a82-2700-41ac-b7e3-328984d82703",
|
||||
"efb34c99-e6f3-4563-b01e-7d740f4bb0d1",
|
||||
"5a6d5bb5-ab3e-4f95-9afc-8e9b5e1651b3",
|
||||
"679beec8-cb5f-4c3c-9fab-d7d370b03f50",
|
||||
"70fb66f6-deb5-4699-bb7d-effa571a4758",
|
||||
"736e4133-a876-4d18-83ad-2a0649b0a9bf",
|
||||
"785f6aab-ebe2-4476-b49f-a75d98b64c44",
|
||||
"7a2820fc-35e9-408e-8e6d-1fa62e0f3416",
|
||||
"870bf561-8eb8-4dbe-a63b-64a573c40a28",
|
||||
"b519d784-ca35-488d-b0d2-5f4c344e7519",
|
||||
"0225087c-1bcb-4bdc-ac0b-11fe1bff8a27",
|
||||
"0237718c-7284-4cf8-8958-2e7e2e829e16",
|
||||
"18a95172-f9ce-43f8-b9dd-ab56b48eaaa8",
|
||||
"1ca667c6-9ebc-4ed5-9aeb-90c176a57a9f",
|
||||
"241e7690-286a-467b-b825-70d989d4cb66",
|
||||
"2a05aea8-421b-4c13-a82e-d49ae2a0c82e",
|
||||
"2d7c81b1-9664-4486-bb9e-46db89cc5b02",
|
||||
"310d85fb-84d9-440e-80a3-b2824fcd4d15",
|
||||
"3fbbbb13-12d9-42e8-9b0a-7c40cbeef8a5",
|
||||
"4260b587-cd60-4ca0-8a41-71e2e49f46a4",
|
||||
"52415179-03b2-49b7-9e57-1fd6965ef4f4",
|
||||
"7239518a-93d0-46dc-9114-703f779efb33",
|
||||
"7bbda78d-630f-429b-816c-5b3db4ef39d0",
|
||||
"7df7c029-9651-4323-bd67-afbf86fe336b",
|
||||
"7e68296a-607b-446e-91e0-4fd4e30f18c9",
|
||||
"a88c6fe8-c782-4db0-9c1c-afc4a1bc3f4b",
|
||||
"c058ecbb-3e79-457e-a16c-77c319e0107c",
|
||||
"ce63f9cd-80a2-422e-9d7b-251b8f54bbf7",
|
||||
"d1d00ff3-b284-45e3-836a-0f5cfb666142",
|
||||
"da267940-8d82-4b44-9d44-b28614c250cf",
|
||||
"dbfbcc12-4305-4cd7-ba73-6f6dade960e4",
|
||||
"e125d718-5926-473f-b25b-b9419465b0b1",
|
||||
"ec4f2ef8-ca69-4520-aee7-6b12e42625b1",
|
||||
"eca47b24-175d-4f12-9300-8770ec274a4d",
|
||||
"08f34dfb-69ce-4ce7-aeb3-9b04878939bd",
|
||||
"75e60ab2-4a98-4ae8-8155-068279b73dd6",
|
||||
"98485ed4-b71e-46b1-aebe-781d40674d9f",
|
||||
"9cbac9b7-9f29-4742-9e3b-8817a22ad4af",
|
||||
"ab28a160-1194-460a-bcd9-cb165a51a47e",
|
||||
"bac74aef-8326-4288-ac02-51ae39830b3d",
|
||||
"d8112662-ca8b-4828-8ad4-34d71ca66db7",
|
||||
"55f54e2b-ba8d-4c59-97e8-2e3c26ec1d98",
|
||||
"a9579dde-c548-4e39-a3a9-6de3be621548",
|
||||
"30c97d70-ce85-48b4-b576-22926efcb4ae",
|
||||
"2ca96e04-1f1e-4466-b4ed-78484f82acd2",
|
||||
"f120e834-5db9-4ce4-9e4b-3b2b0757fa3f",
|
||||
"15c5caea-afd8-4f13-ad41-65aa162be77b",
|
||||
"64cefc0c-db46-4fe9-bba7-c43c8901e416",
|
||||
"0806ce46-3896-4c68-ab6b-e07f137928b3"
|
||||
],
|
||||
"repaired_connections": [
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
152.19,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
153.16,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.73,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
155.7,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SCL",
|
||||
"start": [
|
||||
157.27,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
158.24,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SDA",
|
||||
"start": [
|
||||
159.81,
|
||||
137.5
|
||||
],
|
||||
"end": [
|
||||
160.78,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
143.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
142.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
150.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
149.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
153.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
160.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
165.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
164.16,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
172.62,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
171.78,
|
||||
132.6685
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
143.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
141.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
149.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
148.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
143.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
141.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
149.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
148.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
155.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
153.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
160.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
155.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
153.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
161.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
160.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
167.0,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
165.72,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
173.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
172.22,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
167.0,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
165.72,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
173.5,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
172.22,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SDA",
|
||||
"start": [
|
||||
135.75,
|
||||
161.55
|
||||
],
|
||||
"end": [
|
||||
159.8,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/OLED_SCL",
|
||||
"start": [
|
||||
134.9,
|
||||
159.85
|
||||
],
|
||||
"end": [
|
||||
157.25,
|
||||
137.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_SELECT",
|
||||
"start": [
|
||||
161.5,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
161.5,
|
||||
134.1
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_DCD",
|
||||
"start": [
|
||||
133.1,
|
||||
131.9
|
||||
],
|
||||
"end": [
|
||||
148.08,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_RI",
|
||||
"start": [
|
||||
155.25,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
165.86,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_DTR",
|
||||
"start": [
|
||||
148.6,
|
||||
133.1
|
||||
],
|
||||
"end": [
|
||||
155.7,
|
||||
126.67
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_CTS",
|
||||
"start": [
|
||||
153.45,
|
||||
138.75
|
||||
],
|
||||
"end": [
|
||||
154.75,
|
||||
135.65
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_RX",
|
||||
"start": [
|
||||
135.75,
|
||||
139.5
|
||||
],
|
||||
"end": [
|
||||
144.45,
|
||||
132.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_VALID",
|
||||
"start": [
|
||||
157.75,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
165.9,
|
||||
127.95
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/RS_OFF_N",
|
||||
"start": [
|
||||
160.3,
|
||||
138.7
|
||||
],
|
||||
"end": [
|
||||
162.4,
|
||||
136.75
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_PREVIOUS",
|
||||
"start": [
|
||||
148.22,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
149.5,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/BTN_NEXT",
|
||||
"start": [
|
||||
172.22,
|
||||
168.0
|
||||
],
|
||||
"end": [
|
||||
173.5,
|
||||
168.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
148.22,
|
||||
172.5
|
||||
],
|
||||
"end": [
|
||||
149.5,
|
||||
172.5
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
145.54,
|
||||
126.67
|
||||
],
|
||||
"end": [
|
||||
150.25,
|
||||
135.8
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/GND",
|
||||
"start": [
|
||||
149.5,
|
||||
170.0
|
||||
],
|
||||
"end": [
|
||||
148.1,
|
||||
156.55
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
143.0,
|
||||
133.5
|
||||
],
|
||||
"end": [
|
||||
143.0,
|
||||
134.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
154.0,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
156.35,
|
||||
136.4
|
||||
]
|
||||
},
|
||||
{
|
||||
"net": "/+3V3",
|
||||
"start": [
|
||||
148.7,
|
||||
134.0
|
||||
],
|
||||
"end": [
|
||||
152.65,
|
||||
132.65
|
||||
]
|
||||
}
|
||||
],
|
||||
"retained_tracks": 476,
|
||||
"protected_sha256": {
|
||||
"serial-carrier.kicad_pro": "9ad5b992785ebfe01fa7ea4e1aa391a2c7d7f1f2c3b1c13136173c301ecb6516",
|
||||
"serial-carrier.kicad_prl": "f7c452b3be0ad43b4557f4d9c24a38124cd88268edfc2c9bbd59e3f2429e3772",
|
||||
"serial-carrier.kicad_sch": "621acfd9ed8e025a90442e5f82f73d7d5e3228d7a48459a0fa4b0c46e763912a"
|
||||
}
|
||||
}
|
||||