From ec9ca5e6d2df3c340259fd0b178fb77c08a731b6 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 29 Aug 2026 14:13:34 +0200 Subject: [PATCH] Add OLED and button diagnostics --- docs/command_reference.md | 13 +- docs/electrical_tests.md | 108 +++- docs/phase7_display_mockup.svg | 6 +- docs/roadmap.md | 39 +- docs/wiring.md | 33 +- src/CMakeLists.txt | 2 + src/board_pins.h | 9 + src/console_completion.c | 27 + src/local_ui_hw_test.c | 865 +++++++++++++++++++++++++++++++++ src/local_ui_hw_test.h | 23 + src/main.c | 10 +- src/rs232_hw_test.c | 8 +- src/rs232_hw_test.h | 2 +- 13 files changed, 1117 insertions(+), 28 deletions(-) create mode 100644 src/local_ui_hw_test.c create mode 100644 src/local_ui_hw_test.h diff --git a/docs/command_reference.md b/docs/command_reference.md index a79a8f5..1fe7f54 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -112,5 +112,16 @@ SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a sep | `debug uart-loopback [8N1|8E1|8O1|8N2|7E1|7O1] [bytes]` | Run a parameterized UART loopback test. | | `debug uart-suite` | Test supported baud rates and frame formats. | | `debug cts-flow-test` / `debug rts-flow-test` | Verify hardware transmit gating or receive backpressure. | +| `debug display status` | Show the current display diagnostic state. | +| `debug display probe` | Probe the expected OLED addresses 7-bit `0x3c` and `0x3d`, initially using 100 kHz I²C. The tested module responds at `0x3c`. | +| `debug display scan --force` | Scan usable 7-bit addresses `0x08`–`0x77` at 100 kHz; use only on this dedicated local-UI bus. | +| `debug display init [address]` | Initialize the OLED at 7-bit `0x3c`/`0x3d`, or their 8-bit write/read aliases: `0x78`/`0x79` and `0x7a`/`0x7b`. | +| `debug display off` | Turn off the initialized OLED. | +| `debug display pattern ` | Draw a full-screen electrical and geometry test pattern. | +| `debug display row <0..63>` | Draw the selected one-pixel display row for addressing and color-boundary checks. | +| `debug display contrast <0..255>` | Set the OLED contrast to the specified bounded value. | +| `debug display invert ` | Enable or disable OLED pixel inversion. | +| `debug buttons status` | Show the current active-low state of previous/back GPIO10, select/confirm GPIO13, and next GPIO14. | +| `debug buttons test [seconds]` | Run the bounded button event test for 1–30 seconds; the default is 10 seconds. | -Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. Diagnostics refuse to use UART1 until `serial stop` releases it. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed. +Follow the exact wiring in [Electrical tests](electrical_tests.md) before invoking diagnostics. The OLED must be powered from 3.3 V because module I²C pull-ups may connect to `VCC`; verify that all external pull-ups also terminate at 3.3 V. Display diagnostics probe the standard SSD1315-compatible 7-bit `0x3c`/`0x3d` addresses. The currently tested module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78`/`0x79`; an explicit `scan --force` is available only for the dedicated local-UI bus. Diagnostics initially run at 100 kHz and treat an absent display as nonfatal. RS-232 diagnostics that require UART1 refuse to use it until `serial stop` releases it. The RGB LED shows test state: blue idle, yellow/orange running, green passed, red failed. diff --git a/docs/electrical_tests.md b/docs/electrical_tests.md index 31397b4..1f77ef4 100644 --- a/docs/electrical_tests.md +++ b/docs/electrical_tests.md @@ -1,8 +1,112 @@ # Electrical tests -These procedures verify the MAX3243 breakout, UART1 data path, hardware flow control, and session broker. They are manual tests: the firmware never starts one automatically. +These procedures verify the Phase 7A OLED and buttons, MAX3243 breakout, UART1 data path, hardware flow control, and session broker. They are manual tests: the firmware never starts one automatically. -> **Safety:** With power removed, install only the wiring required by the selected test. DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are driven outputs. Never connect one of these outputs to another driven output. Keep temporary Dupont wiring short and secure. +> **Safety:** With power removed, install only the wiring required by the selected test. DE-9 pins 3 (`TX`), 4 (`DTR`), and 7 (`RTS`) are driven outputs. Never connect one of these outputs to another driven output. Keep temporary Dupont wiring short and secure. Power the OLED only from 3.3 V because module-mounted I²C pull-ups may connect SDA and SCL to the OLED `VCC` rail. + +## Phase 7A OLED and button bring-up + +Use the exact OLED and button connections in [Hardware wiring](wiring.md). Display diagnostics initially operate I²C at 100 kHz and probe the standard 7-bit `0x3c` and `0x3d` addresses. The connected test module acknowledges at `0x3c`, whose 8-bit write/read forms are `0x78` and `0x79`. A missing or unresponsive display is nonfatal: diagnostics should report it without disrupting UART0 or the serial services. + +### 1. Power-off wiring checks + +Disconnect both USB connectors and every other power source before checking or changing wiring. + +1. Confirm OLED `VCC` goes only to `3V3`, OLED `GND` goes to `GND`, SDA goes to GPIO11, and SCL goes to GPIO12. +2. Check for an unintended short between `3V3` and `GND`, and verify ground continuity between the OLED and ESP32 board. +3. Determine whether the OLED module has SDA/SCL pull-ups and verify that any module-mounted or external pull-ups terminate at 3.3 V, never 5 V. Add suitable external pull-ups to `3V3` only if the module does not provide them; account for parallel resistance if more than one set is fitted. +4. Confirm each button is wired between its input and `GND`: previous/back GPIO10, select/confirm GPIO13, and next GPIO14. With a meter, each button should be open when released and near zero ohms to `GND` when pressed. +5. Check that no button shorts two GPIOs together and that SDA and SCL are not swapped or shorted. + +### 2. Powered idle checks and address probe + +Apply power, but do not initialize the OLED yet. + +1. Measure OLED `VCC` relative to `GND`; it should be approximately 3.3 V. +2. Measure idle SDA on GPIO11 and idle SCL on GPIO12. Both should be near 3.3 V. Power down immediately if either bus line rises toward 5 V; correct the OLED supply or pull-up wiring before continuing. +3. Run `debug display status` and record the diagnostic state. +4. Run `debug display probe`. Confirm that it tests only 7-bit `0x3c` and `0x3d` at the initial 100 kHz bus rate. The tested module should acknowledge at `0x3c` (8-bit `0x78` write / `0x79` read). + +If the expected address does not respond, treat the result as a nonfatal hardware finding. On this dedicated local-UI bus, `debug display scan --force` may identify an unexpected address before further investigation. Otherwise leave the serial core running, power down, and recheck 3.3 V power, common ground, SDA/SCL order, solder joints, and pull-ups. Do not scan a bus shared with unrelated I²C devices. + +### 3. Initialization and display patterns + +Initialize the address observed during the scan. The tested module uses 7-bit `0x3c`, equivalently 8-bit `0x78` (write) and `0x79` (read): + +```text +debug display init 0x3c +``` + +Then run: + +```text +debug display pattern clear +debug display pattern fill +debug display pattern checker +debug display pattern grid +debug display pattern corners +``` + +Confirm that clear and fill affect the full 128×64 area, checker and grid have regular spacing without shifted or wrapped columns, and all four corner markers are visible in the correct locations. Unexpected mirroring, rotation, clipping, or column offsets must be recorded before Phase 7B fixes the display-driver assumptions. + +### 4. Row 15/16 color-boundary test + +Clear the display, illuminate row 15, and record its physical color and position: + +```text +debug display pattern clear +debug display row 15 +``` + +Repeat for row 16: + +```text +debug display pattern clear +debug display row 16 +``` + +**Verified result:** row 15 is the last yellow addressable row and row 16 is the first blue addressable row. The two colored areas are separated by a narrow physical black divider, so later UI rendering must treat the 128×16 yellow and 128×48 blue regions as separate panels rather than one visually continuous canvas. Also test another endpoint row if needed with `debug display row <0..63>` to confirm row addressing and orientation. + +### 5. Contrast, inversion, and display-off checks + +With a visible pattern loaded, exercise the bounded contrast range and confirm that brightness changes without bus errors: + +```text +debug display contrast 0 +debug display contrast 64 +debug display contrast 128 +debug display contrast 255 +``` + +Then verify inversion toggles all displayed pixels and can be restored: + +```text +debug display invert on +debug display invert off +``` + +Finally run `debug display off` and confirm the panel turns off cleanly. Use `debug display status`, then `debug display init 0x3c` (or equivalently `0x78` or `0x79`) before further display tests. + +### 6. Button checks + +With all buttons released, run `debug buttons status`. Confirm previous/back GPIO10, select/confirm GPIO13, and next GPIO14 report released/high due to their internal pull-ups; each should report pressed/low while held to `GND`. + +Run `debug buttons test` for the default 10-second interval. During the test, press and release each button separately with a deliberate short press, then repeat with a sustained long press. Confirm that the correct button and short/long classification are reported exactly once per intended action. + +Repeat with an explicit duration, for example: + +```text +debug buttons test 30 +``` + +Use the longer run to check: + +- **Debounce:** press with normal switch bounce and make several deliberately quick taps; one physical press must not produce a burst of duplicate press/release or short/long events. +- **Long press:** hold each button long enough for the diagnostic to classify it as long, then release it; it must not also create an unintended short-press action. +- **Stuck button:** hold one button before starting the test and keep it held. The input must remain identified as pressed/stuck without blocking checks of the other buttons, and the bounded diagnostic must still exit after the selected duration. +- **Recovery:** release the held button and confirm `debug buttons status` returns to released/high without a reboot. + +`debug buttons test [seconds]` accepts 1 through 30 seconds and defaults to 10 seconds when omitted. Record unexpected event duplication, missed transitions, incorrect GPIO mapping, false long presses, or a test that fails to terminate. ## Configuration A: data and handshake pairs diff --git a/docs/phase7_display_mockup.svg b/docs/phase7_display_mockup.svg index 73f2003..69b159d 100644 --- a/docs/phase7_display_mockup.svg +++ b/docs/phase7_display_mockup.svg @@ -1,7 +1,7 @@ Phase 7 dual-color OLED overview-screen mockup - An enlarged mockup of the proposed 128 by 64 pixel local status display. The upper sixteen rows are yellow and show service, Wi-Fi, client, writer, and alert status. The lower forty-eight rows are blue and show IP address, RS-232 configuration, modem signals, traffic counters, and page navigation. Three buttons below are labeled previous, OK, and next. + An enlarged mockup of the verified 128 by 64 pixel local status display. The upper sixteen yellow rows and lower forty-eight blue rows are separate rendered panels, divided by a narrow physical black separator. The yellow panel shows service, Wi-Fi, client, writer, and alert status. The blue panel shows IP address, RS-232 configuration, modem signals, traffic counters, and page navigation. Three buttons below are labeled previous, OK, and next. @@ -64,7 +64,7 @@ row 16 row 63 128 px - Expected split: yellow rows 0–15 · blue rows 16–63 + Verified panels: yellow rows 0–15 · black divider · blue rows 16–63 @@ -83,5 +83,5 @@ - Mockup only — final glyph metrics and the physical color boundary will be verified on hardware. + Mockup only — glyph metrics remain to be finalized; the physical color boundary is hardware-verified. diff --git a/docs/roadmap.md b/docs/roadmap.md index 8368552..c571572 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,6 +5,7 @@ This document tracks the implementation and hardware-validation plan for the ESP ## 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. @@ -36,7 +37,7 @@ These constraints apply across all phases: | 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 | **Planned** | +| 7 | Local display and button interface | **In progress (7B)** | | 8 | Security and production hardening | **Planned** | | 9 | Authenticated, rollback-capable OTA | **Planned** | | 10 | BLE serial transport and provisioning evaluation | **Planned** | @@ -175,47 +176,49 @@ The final target-hardware retest covered: The software-crypto build no longer reproduces the HTTPD watchdog stall. This validates that the failure was a shared hardware-crypto/PSRAM DMA problem rather than heap exhaustion. Phase 6 is complete; these concurrent arrangements remain regression tests for future transport, TLS, memory-placement, and ESP-IDF changes. -## Planned phases +## Current and planned phases -The order below is the current plan. Detailed requirements should be finalized at the start of each phase, and optional features must not weaken the completed serial and recovery paths. +The order below is the current plan. Phase 7 is in progress; later phases remain planned or under evaluation. Detailed requirements should be finalized at the start of each phase, and optional features must not weaken the completed serial and recovery paths. ### Phase 7 — Local display and buttons -Add a standalone local status/control interface without making it a dependency of the serial core. The planning baseline uses a 128×64 dual-color monochrome I²C OLED sold with an SSD1315 controller. Its expected SSD1306-compatible command set, I²C address, orientation, column mapping, and physical color boundary must be confirmed on the actual modules before the UI layout becomes fixed. +Add a standalone local status/control interface without making it a dependency of the serial core. The planning baseline uses a 128×64 dual-color monochrome I²C OLED sold with an SSD1315 controller. Phase 7A confirmed SSD1306-compatible operation, 7-bit I²C address `0x3c`, orientation, column mapping, contrast/inversion behavior, button inputs, and the physical color geometry on the selected hardware. + +Phase 7A diagnostics and target-hardware electrical validation are complete. Phase 7 overall remains in progress with Phase 7B next; Phases 7B through 7E are not complete. #### Hardware baseline -- Power the OLED from 3.3 V so any module-mounted I²C pull-ups remain ESP32-safe. -- Use GPIO11 for SDA and GPIO12 for SCL. These pins are currently unused and sit in the available GPIO10–14 block on the DevKit header. -- Use three active-low buttons with pull-ups: GPIO10 for previous/back, GPIO13 for select/confirm, and GPIO14 for next. +- Wire OLED `VCC` to `3V3` and OLED `GND` to `GND`. The OLED must use 3.3 V because module-mounted I²C pull-ups may connect SDA and SCL to `VCC`. +- Wire OLED `SDA` to GPIO11 and OLED `SCL` to GPIO12. These pins are currently unused and sit in the available GPIO10–14 block on the DevKit header. +- Wire three active-low buttons between their GPIO and `GND`, using the ESP32 internal pull-ups: GPIO10 for previous/back, GPIO13 for select/confirm, and GPIO14 for next. - Use short left/right presses for page or item navigation, short select for entry, a long left press for back/home, and an explicit select hold for disruptive confirmation. -- Verify whether the module provides suitable SDA/SCL pull-ups; add external pull-ups to 3.3 V if needed. -- Expect yellow rows 0–15 and blue rows 16–63, but determine the exact split with a movable one-pixel row test instead of relying on seller descriptions. +- Verify whether the module provides suitable SDA/SCL pull-ups and that every external pull-up is tied to 3.3 V, not 5 V; add external pull-ups to 3.3 V if the module does not provide them. +- Hardware verification established yellow rows 0–15 and blue rows 16–63. A narrow physical black divider separates the two regions even though row 15 is the final yellow addressable row and row 16 the first blue addressable row. - Keep assignments centralized in the board profile rather than scattering display or button GPIO assumptions through UI code. ![Proposed Phase 7 overview display](phase7_display_mockup.svg) -The persistent yellow strip is reserved for serial-service state, Wi-Fi strength, active USB/Web/SSH counts, current writer, total clients, and an alert indicator. The blue area rotates through overview, RS-232, broker-client, and network/service pages. No password, Wi-Fi secret, private-key material, or routine credential data may appear on the display. +The persistent yellow strip is reserved for serial-service state, Wi-Fi strength, active USB/Web/SSH counts, current writer, total clients, and an alert indicator. Phase 7B must render it as a separate 128×16 status panel. The blue 128×48 content panel begins at row 16 and rotates through overview, RS-232, broker-client, and network/service pages; the physical black divider must remain visually clear. No password, Wi-Fi secret, private-key material, or routine credential data may appear on the display. #### Implementation sequence -1. **Phase 7A — Electrical bring-up and diagnostics** - - Add bounded low-level display and button diagnostics under the existing `debug` submenu. - - Probe only the expected `0x3C` and `0x3D` addresses, then validate geometry, orientation, row/column addressing, contrast, inversion, and the yellow/blue row boundary. - - Validate each active-low button, pull-up behavior, debounce interval, short press, long press, and stuck-button handling. -2. **Phase 7B — Display driver** +1. **Phase 7A — Electrical bring-up and diagnostics — Complete** + - Bounded low-level display and button diagnostics are available under the existing `debug` submenu. + - The selected module acknowledged at 7-bit `0x3c` (8-bit `0x78` write / `0x79` read). A guarded full scan is retained for the dedicated local-UI bus; an absent display remains nonfatal and does not make the serial core dependent on the OLED. + - Hardware validation passed for geometry, orientation, row/column addressing, contrast, inversion, button pull-ups/debounce/short-press/long-press/stuck behavior, and the color geometry: yellow rows 0–15, blue rows 16–63, with a physical black separator between the regions. +2. **Phase 7B — Display driver — Planned** - Place the SSD1315 behind a small local panel interface and use ESP-IDF's SSD1306-compatible support if hardware testing confirms compatibility. - Use a bounded 1 KiB 128×64 framebuffer, a compact 5×7 font, and a small project-owned status-icon set; do not add LVGL for this fixed monochrome UI. - Prefer dirty 8-pixel-page updates, bounded I²C transaction timeouts, and nonfatal recovery after a missing or unresponsive display. -3. **Phase 7C — Read-only status UI** +3. **Phase 7C — Read-only status UI — Planned** - Build display state from existing serial, Wi-Fi, broker, USB, WebSocket, HTTPS, and SSH snapshot APIs rather than parsing CLI output or reaching into transport internals. - Provide overview, RS-232/modem, broker-client/writer, and network/service pages. - Refresh at a bounded low rate, initially about 4 Hz, from a low-priority owner task. Never hold a service lock across an I²C transaction. -4. **Phase 7D — Local controls** +4. **Phase 7D — Local controls — Planned** - Add a shallow menu for safe serial, Wi-Fi, HTTPS, SSH, writer-release, display, and reboot actions through direct service APIs. - Require a visible confirmation screen and a timed select hold before stopping active services, revoking a writer, rebooting, or performing another disruptive action. - A local display is not a serial broker client and cannot silently acquire the writer lease. -5. **Phase 7E — Reliability, persistence, and documentation** +5. **Phase 7E — Reliability, persistence, and documentation — Planned** - Add contrast and optional dim/blank timeout settings to limit OLED burn-in without making the display necessary for recovery. - Validate display removal, I²C NACK/timeouts, stuck buttons, queue saturation, and repeated actions. - Re-run concurrent USB CDC, WebSocket, and SSH traffic while the UI refreshes and confirm UART0 remains responsive. diff --git a/docs/wiring.md b/docs/wiring.md index 7e5dc8c..9403652 100644 --- a/docs/wiring.md +++ b/docs/wiring.md @@ -100,9 +100,40 @@ The USB-to-UART bridge's DTR/RTS controls serve automatic boot/reset and do not Native USB CDC DTR controls the lifetime of the `usb-cdc` broker client but is not forwarded to physical DE-9 DTR. Physical DTR follows the `serial` configuration. CDC RTS is status information only; GPIO15/DE-9 RTS remains UART1 receive flow control when `flow=rts-cts` is enabled. +## Phase 7A OLED and button wiring + +Phase 7A hardware validation used the following connections for the 128×64 I²C OLED and three local buttons. The selected module acknowledges at 7-bit I²C address `0x3c` (8-bit `0x78` write / `0x79` read) and has separate yellow rows 0–15 and blue rows 16–63, divided by a narrow physical black separator: + +| Device connection | ESP32-S3 connection | Electrical behavior | Purpose | +|---|---:|---|---| +| OLED `VCC` | `3V3` | 3.3 V power only | OLED power and I²C pull-up rail | +| OLED `GND` | `GND` | Common ground | OLED return and I²C reference | +| OLED `SDA` | GPIO11 | I²C data | Display data | +| OLED `SCL` | GPIO12 | I²C clock | Display clock | +| Previous/back button | GPIO10 to `GND` | Active-low input with internal pull-up | Previous item or back | +| Select/confirm button | GPIO13 to `GND` | Active-low input with internal pull-up | Select or confirm | +| Next button | GPIO14 to `GND` | Active-low input with internal pull-up | Next item | + +```text +ESP32-S3-DevKitC-1 N16R8 128×64 I²C OLED +──────────────────────── ──────────────── +3V3 ────────────> VCC +GND ────────────> GND +GPIO11 / SDA <───────────> SDA +GPIO12 / SCL ────────────> SCL + +GPIO10 ───── previous/back button ───── GND +GPIO13 ───── select/confirm button ──── GND +GPIO14 ───── next button ────────────── GND +``` + +> **OLED voltage warning:** Power OLED `VCC` from `3V3`, not 5 V. Many OLED modules connect their SDA/SCL pull-up resistors directly to `VCC`; powering such a module from 5 V could expose the ESP32-S3 GPIOs to unsafe I²C levels. + +Before applying power, verify whether the module already includes SDA and SCL pull-ups and where they terminate. Any module-mounted or external I²C pull-ups must go to 3.3 V. If pull-ups are absent, add suitable external pull-ups from SDA and SCL to `3V3`; if they are present, account for their parallel resistance before adding more. The buttons normally need no external pull-ups because firmware enables the ESP32 internal pull-ups. + ## Electrical verification -See [Electrical tests](electrical_tests.md) for safe loopback wiring, polarity checks, UART flow-control verification, and session-broker loopback testing. +See [Electrical tests](electrical_tests.md) for Phase 7A OLED/button bring-up, safe loopback wiring, polarity checks, UART flow-control verification, and session-broker loopback testing. ## Future hardware profiles diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b95a1f7..0bced70 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -6,6 +6,7 @@ idf_component_register( "system_console.c" "secure_random.c" "status_led.c" + "local_ui_hw_test.c" "rs232_hw_test.c" "rs232_port_owner.c" "serial_config.c" @@ -32,6 +33,7 @@ idf_component_register( bootloader_support console esp_driver_gpio + esp_driver_i2c esp_driver_uart esp_event esp_http_server diff --git a/src/board_pins.h b/src/board_pins.h index ff3cb56..d9aa02c 100644 --- a/src/board_pins.h +++ b/src/board_pins.h @@ -1,6 +1,7 @@ #pragma once #include "driver/gpio.h" +#include "driver/i2c_types.h" #include "driver/uart.h" /* @@ -12,6 +13,14 @@ */ #define BOARD_RGB_LED_GPIO GPIO_NUM_48 +/* Phase 7 local OLED and active-low navigation buttons. */ +#define LOCAL_UI_I2C_PORT I2C_NUM_0 +#define LOCAL_UI_DISPLAY_SDA_GPIO GPIO_NUM_11 +#define LOCAL_UI_DISPLAY_SCL_GPIO GPIO_NUM_12 +#define LOCAL_UI_BUTTON_PREVIOUS_GPIO GPIO_NUM_10 +#define LOCAL_UI_BUTTON_SELECT_GPIO GPIO_NUM_13 +#define LOCAL_UI_BUTTON_NEXT_GPIO GPIO_NUM_14 + #define RS232_UART_PORT UART_NUM_1 /* UART2 is used only as an internal traffic generator during flow-control tests. */ diff --git a/src/console_completion.c b/src/console_completion.c index 01c69c8..94bd159 100644 --- a/src/console_completion.c +++ b/src/console_completion.c @@ -24,6 +24,33 @@ static const char *const s_completion_candidates[] = { "debug uart-suite", "debug cts-flow-test", "debug rts-flow-test", + "debug display", + "debug display status", + "debug display probe", + "debug display scan", + "debug display scan --force", + "debug display init", + "debug display init 0x3c", + "debug display init 0x78", + "debug display init 0x79", + "debug display init 0x3d", + "debug display init 0x7a", + "debug display init 0x7b", + "debug display off", + "debug display pattern", + "debug display pattern clear", + "debug display pattern fill", + "debug display pattern checker", + "debug display pattern grid", + "debug display pattern corners", + "debug display row", + "debug display contrast", + "debug display invert", + "debug display invert on", + "debug display invert off", + "debug buttons", + "debug buttons status", + "debug buttons test", /* Serial service lifecycle, persistence, counters, and settings. */ "serial status", diff --git a/src/local_ui_hw_test.c b/src/local_ui_hw_test.c new file mode 100644 index 0000000..a16feb9 --- /dev/null +++ b/src/local_ui_hw_test.c @@ -0,0 +1,865 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Phase 7A bounded SSD1315-compatible OLED and button diagnostics. */ + +#include "local_ui_hw_test.h" + +#include +#include +#include +#include +#include +#include + +#include "board_pins.h" +#include "driver/gpio.h" +#include "driver/i2c_master.h" +#include "esp_err.h" +#include "esp_timer.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define DISPLAY_WIDTH 128U +#define DISPLAY_HEIGHT 64U +#define DISPLAY_PAGE_COUNT (DISPLAY_HEIGHT / 8U) +#define DISPLAY_FRAMEBUFFER_SIZE (DISPLAY_WIDTH * DISPLAY_PAGE_COUNT) +#define DISPLAY_I2C_SPEED_HZ 100000U +#define DISPLAY_I2C_TIMEOUT_MS 250 +#define DISPLAY_PROBE_TIMEOUT_MS 50 +#define DISPLAY_SCAN_TIMEOUT_MS 10 +#define DISPLAY_DEFAULT_CONTRAST 127U +#define DISPLAY_COMMAND_CAPACITY 32U +#define DISPLAY_DATA_CHUNK_SIZE 128U + +#define BUTTON_POLL_MS 10U +#define BUTTON_DEBOUNCE_MS 30U +#define BUTTON_LONG_PRESS_MS 1000U +#define BUTTON_TEST_DEFAULT_SECONDS 10U +#define BUTTON_TEST_MAX_SECONDS 30U + +/* The tested module uses 7-bit 0x3c; 0x3d is the common alternate strap. */ +static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU}; + +typedef struct { + const char *name; + gpio_num_t gpio; +} button_definition_t; + +typedef struct { + int raw_level; + int stable_level; + int64_t raw_changed_us; + int64_t pressed_us; + bool long_reported; + uint32_t short_presses; + uint32_t long_presses; + uint32_t stable_transitions; +} button_test_state_t; + +static const button_definition_t s_buttons[] = { + {.name = "previous/back", .gpio = LOCAL_UI_BUTTON_PREVIOUS_GPIO}, + {.name = "select/confirm", .gpio = LOCAL_UI_BUTTON_SELECT_GPIO}, + {.name = "next", .gpio = LOCAL_UI_BUTTON_NEXT_GPIO}, +}; + +static i2c_master_bus_handle_t s_bus; +static i2c_master_dev_handle_t s_display; +static uint8_t s_framebuffer[DISPLAY_FRAMEBUFFER_SIZE]; +static esp_err_t s_initialization_error = ESP_ERR_INVALID_STATE; +static bool s_bus_ready; +static bool s_buttons_ready; +static bool s_panel_initialized; +static uint8_t s_display_address; +static uint8_t s_contrast = DISPLAY_DEFAULT_CONTRAST; +static bool s_inverted; + +static TickType_t milliseconds_to_ticks(uint32_t milliseconds) +{ + TickType_t ticks = pdMS_TO_TICKS(milliseconds); + return (milliseconds > 0U && ticks == 0U) ? 1U : ticks; +} + +static bool parse_unsigned(const char *text, + unsigned long minimum, + unsigned long maximum, + unsigned long *value) +{ + if (text == NULL || value == NULL) { + return false; + } + + char *end = NULL; + errno = 0; + unsigned long parsed = strtoul(text, &end, 10); + if (errno != 0 || end == text || *end != '\0' || + parsed < minimum || parsed > maximum) { + return false; + } + + *value = parsed; + return true; +} + +static bool parse_display_address(const char *text, uint8_t *address) +{ + if (text == NULL || address == NULL) { + return false; + } + + char *end = NULL; + errno = 0; + unsigned long parsed = strtoul(text, &end, 0); + if (errno != 0 || end == text || *end != '\0') { + return false; + } + + if (parsed == 0x3cU || parsed == 0x78U || parsed == 0x79U) { + *address = 0x3cU; + return true; + } + if (parsed == 0x3dU || parsed == 0x7aU || parsed == 0x7bU) { + *address = 0x3dU; + return true; + } + return false; +} + +static int report_error(const char *operation, esp_err_t error) +{ + printf("%s failed: %s\n", operation, esp_err_to_name(error)); + return 1; +} + +static esp_err_t configure_buttons(void) +{ + const gpio_config_t config = { + .pin_bit_mask = (1ULL << LOCAL_UI_BUTTON_PREVIOUS_GPIO) | + (1ULL << LOCAL_UI_BUTTON_SELECT_GPIO) | + (1ULL << LOCAL_UI_BUTTON_NEXT_GPIO), + .mode = GPIO_MODE_INPUT, + .pull_up_en = GPIO_PULLUP_ENABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + esp_err_t error = gpio_config(&config); + if (error == ESP_OK) { + s_buttons_ready = true; + } + return error; +} + +static esp_err_t configure_i2c_bus(void) +{ + const i2c_master_bus_config_t config = { + .i2c_port = LOCAL_UI_I2C_PORT, + .sda_io_num = LOCAL_UI_DISPLAY_SDA_GPIO, + .scl_io_num = LOCAL_UI_DISPLAY_SCL_GPIO, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = false, + }; + + esp_err_t error = i2c_new_master_bus(&config, &s_bus); + if (error == ESP_OK) { + s_bus_ready = true; + } + return error; +} + +static esp_err_t probe_address(uint8_t address) +{ + if (!s_bus_ready) { + return s_initialization_error == ESP_OK ? ESP_ERR_INVALID_STATE + : s_initialization_error; + } + return i2c_master_probe(s_bus, address, DISPLAY_PROBE_TIMEOUT_MS); +} + +static esp_err_t select_display(uint8_t address) +{ + esp_err_t error = probe_address(address); + if (error != ESP_OK) { + return error; + } + + if (s_display != NULL && s_display_address == address) { + return ESP_OK; + } + if (s_display != NULL) { + error = i2c_master_bus_rm_device(s_display); + if (error != ESP_OK) { + return error; + } + s_display = NULL; + s_panel_initialized = false; + } + + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = address, + .scl_speed_hz = DISPLAY_I2C_SPEED_HZ, + }; + error = i2c_master_bus_add_device(s_bus, &config, &s_display); + if (error == ESP_OK) { + s_display_address = address; + } + return error; +} + +static esp_err_t probe_expected_addresses(uint8_t *selected_address) +{ + if (!s_bus_ready) { + return s_initialization_error == ESP_OK ? ESP_ERR_INVALID_STATE + : s_initialization_error; + } + + uint32_t found = 0U; + uint8_t first = 0U; + for (size_t index = 0U; + index < sizeof(s_expected_addresses) / sizeof(s_expected_addresses[0]); + ++index) { + uint8_t address = s_expected_addresses[index]; + esp_err_t error = probe_address(address); + if (error == ESP_OK) { + printf("OLED response at 7-bit 0x%02x (8-bit 0x%02x write / 0x%02x read).\n", + address, (unsigned int)(address << 1U), + (unsigned int)((address << 1U) | 1U)); + if (found == 0U) { + first = address; + } + ++found; + } else { + printf("No OLED response at 7-bit 0x%02x (%s).\n", + address, esp_err_to_name(error)); + } + } + + if (found == 0U) { + printf("No display found at the expected 7-bit 0x3c/0x3d addresses. Check 3.3 V power, ground, SDA/SCL order, and pull-ups.\n"); + return ESP_ERR_NOT_FOUND; + } + if (found > 1U) { + printf("Multiple expected addresses responded; selecting 7-bit 0x%02x for diagnostics.\n", + first); + } else { + printf("Selected display address 7-bit 0x%02x.\n", first); + } + if (selected_address != NULL) { + *selected_address = first; + } + return ESP_OK; +} + +static esp_err_t display_send_commands(const uint8_t *commands, size_t count) +{ + if (s_display == NULL || commands == NULL || count == 0U || + count > DISPLAY_COMMAND_CAPACITY) { + return ESP_ERR_INVALID_ARG; + } + + uint8_t transfer[DISPLAY_COMMAND_CAPACITY + 1U]; + transfer[0] = 0x00U; + memcpy(&transfer[1], commands, count); + return i2c_master_transmit(s_display, transfer, count + 1U, + DISPLAY_I2C_TIMEOUT_MS); +} + +static esp_err_t display_send_command(uint8_t command) +{ + return display_send_commands(&command, 1U); +} + +static esp_err_t display_refresh(void) +{ + if (!s_panel_initialized || s_display == NULL) { + return ESP_ERR_INVALID_STATE; + } + + const uint8_t address_commands[] = { + 0x21U, 0x00U, (uint8_t)(DISPLAY_WIDTH - 1U), + 0x22U, 0x00U, (uint8_t)(DISPLAY_PAGE_COUNT - 1U), + }; + esp_err_t error = display_send_commands( + address_commands, sizeof(address_commands)); + if (error != ESP_OK) { + return error; + } + + uint8_t transfer[DISPLAY_DATA_CHUNK_SIZE + 1U]; + transfer[0] = 0x40U; + for (size_t offset = 0U; offset < DISPLAY_FRAMEBUFFER_SIZE; + offset += DISPLAY_DATA_CHUNK_SIZE) { + size_t chunk = DISPLAY_FRAMEBUFFER_SIZE - offset; + if (chunk > DISPLAY_DATA_CHUNK_SIZE) { + chunk = DISPLAY_DATA_CHUNK_SIZE; + } + memcpy(&transfer[1], &s_framebuffer[offset], chunk); + error = i2c_master_transmit(s_display, transfer, chunk + 1U, + DISPLAY_I2C_TIMEOUT_MS); + if (error != ESP_OK) { + return error; + } + } + return ESP_OK; +} + +static esp_err_t display_initialize(uint8_t address) +{ + /* A failed transaction must never leave a partially initialized state. */ + s_panel_initialized = false; + esp_err_t error = select_display(address); + if (error != ESP_OK) { + return error; + } + + const uint8_t initialization[] = { + 0xaeU, /* Display off while geometry is configured. */ + 0xd5U, 0x80U, /* Default oscillator and divide ratio. */ + 0xa8U, 0x3fU, /* 1/64 multiplex. */ + 0xd3U, 0x00U, /* No display offset. */ + 0x40U, /* Display start line zero. */ + 0x8dU, 0x14U, /* Enable the module's internal charge pump. */ + 0x20U, 0x00U, /* Horizontal addressing mode. */ + 0xa1U, /* Segment remap for common four-pin modules. */ + 0xc8U, /* Descending COM scan direction. */ + 0xdaU, 0x12U, /* Alternative COM pin configuration. */ + 0x81U, DISPLAY_DEFAULT_CONTRAST, + 0xd9U, 0xf1U, /* Charge-pump precharge period. */ + 0xdbU, 0x40U, /* VCOMH deselect level. */ + 0xa4U, /* Use display RAM, not all-pixels-on mode. */ + 0xa6U, /* Normal, non-inverted pixels. */ + 0x2eU, /* Disable any prior scrolling mode. */ + }; + error = display_send_commands(initialization, sizeof(initialization)); + if (error != ESP_OK) { + return error; + } + + s_panel_initialized = true; + s_contrast = DISPLAY_DEFAULT_CONTRAST; + s_inverted = false; + memset(s_framebuffer, 0, sizeof(s_framebuffer)); + error = display_refresh(); + if (error == ESP_OK) { + error = display_send_command(0xafU); + } + if (error != ESP_OK) { + s_panel_initialized = false; + return error; + } + + vTaskDelay(milliseconds_to_ticks(20U)); + return ESP_OK; +} + +static void framebuffer_set_pixel(uint32_t x, uint32_t y) +{ + if (x >= DISPLAY_WIDTH || y >= DISPLAY_HEIGHT) { + return; + } + size_t index = (size_t)(y / 8U) * DISPLAY_WIDTH + x; + s_framebuffer[index] |= (uint8_t)(1U << (y & 7U)); +} + +static void framebuffer_draw_pattern(const char *name) +{ + memset(s_framebuffer, 0, sizeof(s_framebuffer)); + + if (strcmp(name, "fill") == 0) { + memset(s_framebuffer, 0xff, sizeof(s_framebuffer)); + return; + } + if (strcmp(name, "checker") == 0) { + for (uint32_t y = 0U; y < DISPLAY_HEIGHT; ++y) { + for (uint32_t x = 0U; x < DISPLAY_WIDTH; ++x) { + if (((x + y) & 1U) == 0U) { + framebuffer_set_pixel(x, y); + } + } + } + return; + } + if (strcmp(name, "grid") == 0) { + for (uint32_t y = 0U; y < DISPLAY_HEIGHT; ++y) { + for (uint32_t x = 0U; x < DISPLAY_WIDTH; ++x) { + if ((x % 8U) == 0U || (y % 8U) == 0U) { + framebuffer_set_pixel(x, y); + } + } + } + return; + } + if (strcmp(name, "corners") == 0) { + for (uint32_t x = 0U; x < DISPLAY_WIDTH; ++x) { + framebuffer_set_pixel(x, 0U); + framebuffer_set_pixel(x, DISPLAY_HEIGHT - 1U); + } + for (uint32_t y = 0U; y < DISPLAY_HEIGHT; ++y) { + framebuffer_set_pixel(0U, y); + framebuffer_set_pixel(DISPLAY_WIDTH - 1U, y); + } + for (uint32_t offset = 0U; offset < 8U; ++offset) { + framebuffer_set_pixel(offset, offset); + framebuffer_set_pixel(DISPLAY_WIDTH - 1U - offset, offset); + framebuffer_set_pixel(offset, DISPLAY_HEIGHT - 1U - offset); + framebuffer_set_pixel(DISPLAY_WIDTH - 1U - offset, + DISPLAY_HEIGHT - 1U - offset); + } + } +} + +static void print_display_usage(void) +{ + printf("Usage:\n"); + printf(" debug display status|probe\n"); + printf(" debug display scan --force\n"); + printf(" debug display init [0x3c|0x3d|0x78|0x79|0x7a|0x7b]\n"); + printf(" debug display off\n"); + printf(" debug display pattern \n"); + printf(" debug display row <0..63>\n"); + printf(" debug display contrast <0..255>\n"); + printf(" debug display invert \n"); +} + +static int command_display_status(int argc, char **argv) +{ + (void)argv; + if (argc != 1) { + print_display_usage(); + return 1; + } + + printf("Local UI diagnostics: init=%s bus=%s buttons=%s last-init=%s\n", + s_initialization_error == ESP_OK ? "ok" : "failed", + s_bus_ready ? "ready" : "unavailable", + s_buttons_ready ? "ready" : "unavailable", + esp_err_to_name(s_initialization_error)); + if (s_display_address == 0U) { + printf("OLED: address=none initialized=%s speed=%u Hz contrast=%u inverted=%s\n", + s_panel_initialized ? "yes" : "no", + (unsigned int)DISPLAY_I2C_SPEED_HZ, + (unsigned int)s_contrast, + s_inverted ? "yes" : "no"); + } else { + printf("OLED: 7-bit=0x%02x 8-bit=0x%02x/0x%02x initialized=%s speed=%u Hz contrast=%u inverted=%s\n", + s_display_address, + (unsigned int)(s_display_address << 1U), + (unsigned int)((s_display_address << 1U) | 1U), + s_panel_initialized ? "yes" : "no", + (unsigned int)DISPLAY_I2C_SPEED_HZ, + (unsigned int)s_contrast, + s_inverted ? "yes" : "no"); + } + printf("Pins: SDA=%d level=%d SCL=%d level=%d; buttons previous=%d select=%d next=%d\n", + LOCAL_UI_DISPLAY_SDA_GPIO, gpio_get_level(LOCAL_UI_DISPLAY_SDA_GPIO), + LOCAL_UI_DISPLAY_SCL_GPIO, gpio_get_level(LOCAL_UI_DISPLAY_SCL_GPIO), + LOCAL_UI_BUTTON_PREVIOUS_GPIO, + LOCAL_UI_BUTTON_SELECT_GPIO, + LOCAL_UI_BUTTON_NEXT_GPIO); + return s_bus_ready && s_buttons_ready ? 0 : 1; +} + +static int command_display_probe(int argc, char **argv) +{ + (void)argv; + if (argc != 1) { + print_display_usage(); + return 1; + } + + uint8_t address = 0U; + esp_err_t error = probe_expected_addresses(&address); + if (error != ESP_OK) { + return report_error("Display probe", error); + } + return 0; +} + +static int command_display_scan(int argc, char **argv) +{ + if (argc != 2 || strcmp(argv[1], "--force") != 0) { + printf("A full usable-address scan sends an address probe to every 7-bit address from 0x08 through 0x77.\n"); + print_display_usage(); + return 1; + } + if (!s_bus_ready) { + return report_error("Display scan", s_initialization_error); + } + + printf("Scanning usable 7-bit I2C addresses 0x08..0x77 at %u Hz.\n", + (unsigned int)DISPLAY_I2C_SPEED_HZ); + uint32_t found = 0U; + for (uint16_t address = 0x08U; address <= 0x77U; ++address) { + esp_err_t error = i2c_master_probe(s_bus, address, DISPLAY_SCAN_TIMEOUT_MS); + if (error == ESP_OK) { + printf(" response: 7-bit 0x%02x (8-bit 0x%02x write / 0x%02x read)\n", + (unsigned int)address, + (unsigned int)(address << 1U), + (unsigned int)((address << 1U) | 1U)); + ++found; + } else if (error != ESP_ERR_NOT_FOUND && error != ESP_ERR_TIMEOUT) { + printf(" 7-bit 0x%02x: %s\n", (unsigned int)address, + esp_err_to_name(error)); + } + vTaskDelay(1U); + } + printf("I2C scan complete: %u responding address%s.\n", (unsigned int)found, + found == 1U ? "" : "es"); + return found == 0U ? 1 : 0; +} + +static int command_display_init(int argc, char **argv) +{ + if (argc > 2) { + print_display_usage(); + return 1; + } + + uint8_t address = 0U; + if (argc == 2) { + if (!parse_display_address(argv[1], &address)) { + printf("Display address must be 7-bit 0x3c/0x3d or their 8-bit write/read forms.\n"); + return 1; + } + } else { + esp_err_t error = probe_expected_addresses(&address); + if (error != ESP_OK) { + return report_error("Display probe", error); + } + } + + esp_err_t error = display_initialize(address); + if (error != ESP_OK) { + return report_error("Display initialization", error); + } + printf("SSD1315-compatible 128x64 display initialized at 7-bit 0x%02x (8-bit 0x%02x/0x%02x), %u Hz.\n", + address, (unsigned int)(address << 1U), + (unsigned int)((address << 1U) | 1U), + (unsigned int)DISPLAY_I2C_SPEED_HZ); + return 0; +} + +static int command_display_off(int argc, char **argv) +{ + (void)argv; + if (argc != 1) { + print_display_usage(); + return 1; + } + if (!s_panel_initialized) { + return report_error("Display off", ESP_ERR_INVALID_STATE); + } + + esp_err_t error = display_send_command(0xaeU); + if (error != ESP_OK) { + return report_error("Display off", error); + } + s_panel_initialized = false; + printf("Display switched off; run 'debug display init' to reinitialize it.\n"); + return 0; +} + +static bool pattern_name_valid(const char *name) +{ + return strcmp(name, "clear") == 0 || strcmp(name, "fill") == 0 || + strcmp(name, "checker") == 0 || strcmp(name, "grid") == 0 || + strcmp(name, "corners") == 0; +} + +static int command_display_pattern(int argc, char **argv) +{ + if (argc != 2 || !pattern_name_valid(argv[1])) { + print_display_usage(); + return 1; + } + if (!s_panel_initialized) { + return report_error("Display pattern", ESP_ERR_INVALID_STATE); + } + + framebuffer_draw_pattern(argv[1]); + esp_err_t error = display_refresh(); + if (error != ESP_OK) { + return report_error("Display pattern", error); + } + printf("Displayed '%s' test pattern.\n", argv[1]); + return 0; +} + +static int command_display_row(int argc, char **argv) +{ + unsigned long row = 0U; + if (argc != 2 || !parse_unsigned(argv[1], 0U, DISPLAY_HEIGHT - 1U, &row)) { + print_display_usage(); + return 1; + } + if (!s_panel_initialized) { + return report_error("Display row", ESP_ERR_INVALID_STATE); + } + + memset(s_framebuffer, 0, sizeof(s_framebuffer)); + for (uint32_t x = 0U; x < DISPLAY_WIDTH; ++x) { + framebuffer_set_pixel(x, (uint32_t)row); + } + esp_err_t error = display_refresh(); + if (error != ESP_OK) { + return report_error("Display row", error); + } + printf("Displayed one-pixel horizontal line at row %lu.\n", row); + return 0; +} + +static int command_display_contrast(int argc, char **argv) +{ + unsigned long contrast = 0U; + if (argc != 2 || !parse_unsigned(argv[1], 0U, 255U, &contrast)) { + print_display_usage(); + return 1; + } + if (!s_panel_initialized) { + return report_error("Display contrast", ESP_ERR_INVALID_STATE); + } + + const uint8_t commands[] = {0x81U, (uint8_t)contrast}; + esp_err_t error = display_send_commands(commands, sizeof(commands)); + if (error != ESP_OK) { + return report_error("Display contrast", error); + } + s_contrast = (uint8_t)contrast; + printf("Display contrast set to %lu.\n", contrast); + return 0; +} + +static int command_display_invert(int argc, char **argv) +{ + if (argc != 2 || (strcmp(argv[1], "on") != 0 && strcmp(argv[1], "off") != 0)) { + print_display_usage(); + return 1; + } + if (!s_panel_initialized) { + return report_error("Display inversion", ESP_ERR_INVALID_STATE); + } + + bool invert = strcmp(argv[1], "on") == 0; + esp_err_t error = display_send_command(invert ? 0xa7U : 0xa6U); + if (error != ESP_OK) { + return report_error("Display inversion", error); + } + s_inverted = invert; + printf("Display inversion %s.\n", invert ? "enabled" : "disabled"); + return 0; +} + +static int command_display(int argc, char **argv) +{ + if (argc < 2 || strcmp(argv[1], "help") == 0) { + print_display_usage(); + return argc < 2 || argc == 2 ? 0 : 1; + } + + if (strcmp(argv[1], "status") == 0) { + return command_display_status(argc - 1, argv + 1); + } + if (strcmp(argv[1], "probe") == 0) { + return command_display_probe(argc - 1, argv + 1); + } + if (strcmp(argv[1], "scan") == 0) { + return command_display_scan(argc - 1, argv + 1); + } + if (strcmp(argv[1], "init") == 0) { + return command_display_init(argc - 1, argv + 1); + } + if (strcmp(argv[1], "off") == 0) { + return command_display_off(argc - 1, argv + 1); + } + if (strcmp(argv[1], "pattern") == 0) { + return command_display_pattern(argc - 1, argv + 1); + } + if (strcmp(argv[1], "row") == 0) { + return command_display_row(argc - 1, argv + 1); + } + if (strcmp(argv[1], "contrast") == 0) { + return command_display_contrast(argc - 1, argv + 1); + } + if (strcmp(argv[1], "invert") == 0) { + return command_display_invert(argc - 1, argv + 1); + } + + printf("Unknown display diagnostic '%s'.\n", argv[1]); + print_display_usage(); + return 1; +} + +static void print_buttons_usage(void) +{ + printf("Usage:\n"); + printf(" debug buttons status\n"); + printf(" debug buttons test [seconds] (1..30, default 10)\n"); +} + +static int command_buttons_status(int argc, char **argv) +{ + (void)argv; + if (argc != 1) { + print_buttons_usage(); + return 1; + } + if (!s_buttons_ready) { + return report_error("Button status", s_initialization_error); + } + + printf("Buttons are active-low with internal pull-ups:\n"); + for (size_t index = 0U; index < sizeof(s_buttons) / sizeof(s_buttons[0]); ++index) { + int level = gpio_get_level(s_buttons[index].gpio); + printf(" %-14s GPIO%d level=%d %s\n", + s_buttons[index].name, + s_buttons[index].gpio, + level, + level == 0 ? "pressed" : "released"); + } + return 0; +} + +static int command_buttons_test(int argc, char **argv) +{ + unsigned long seconds = BUTTON_TEST_DEFAULT_SECONDS; + if (argc > 2 || + (argc == 2 && !parse_unsigned(argv[1], 1U, BUTTON_TEST_MAX_SECONDS, + &seconds))) { + print_buttons_usage(); + return 1; + } + if (!s_buttons_ready) { + return report_error("Button test", s_initialization_error); + } + + button_test_state_t states[sizeof(s_buttons) / sizeof(s_buttons[0])]; + int64_t now = esp_timer_get_time(); + for (size_t index = 0U; index < sizeof(s_buttons) / sizeof(s_buttons[0]); ++index) { + int level = gpio_get_level(s_buttons[index].gpio); + states[index] = (button_test_state_t){ + .raw_level = level, + /* Start released; a held-low input must pass through debounce. */ + .stable_level = 1, + .raw_changed_us = now, + .pressed_us = 0, + }; + } + + int64_t deadline = now + (int64_t)seconds * 1000000LL; + printf("Testing buttons for %lu second%s; short press each button and hold one for at least %u ms.\n", + seconds, seconds == 1U ? "" : "s", + (unsigned int)BUTTON_LONG_PRESS_MS); + + while ((now = esp_timer_get_time()) < deadline) { + for (size_t index = 0U; index < sizeof(s_buttons) / sizeof(s_buttons[0]); ++index) { + button_test_state_t *state = &states[index]; + int raw = gpio_get_level(s_buttons[index].gpio); + if (raw != state->raw_level) { + state->raw_level = raw; + state->raw_changed_us = now; + } + + if (raw != state->stable_level && + now - state->raw_changed_us >= (int64_t)BUTTON_DEBOUNCE_MS * 1000LL) { + state->stable_level = raw; + ++state->stable_transitions; + if (raw == 0) { + state->pressed_us = now; + state->long_reported = false; + printf("%-14s pressed\n", s_buttons[index].name); + } else { + int64_t duration_ms = state->pressed_us == 0 + ? 0 + : (now - state->pressed_us) / 1000LL; + if (!state->long_reported) { + ++state->short_presses; + printf("%-14s short release after %lld ms\n", + s_buttons[index].name, + (long long)duration_ms); + } else { + printf("%-14s released after %lld ms\n", + s_buttons[index].name, + (long long)duration_ms); + } + state->pressed_us = 0; + } + } + + if (state->stable_level == 0 && !state->long_reported && + state->pressed_us != 0 && + now - state->pressed_us >= (int64_t)BUTTON_LONG_PRESS_MS * 1000LL) { + state->long_reported = true; + ++state->long_presses; + printf("%-14s long press\n", s_buttons[index].name); + } + } + vTaskDelay(milliseconds_to_ticks(BUTTON_POLL_MS)); + } + + printf("Button test summary:\n"); + bool stuck = false; + for (size_t index = 0U; index < sizeof(s_buttons) / sizeof(s_buttons[0]); ++index) { + bool pressed = gpio_get_level(s_buttons[index].gpio) == 0; + printf(" %-14s short=%u long=%u transitions=%u final=%s\n", + s_buttons[index].name, + (unsigned int)states[index].short_presses, + (unsigned int)states[index].long_presses, + (unsigned int)states[index].stable_transitions, + pressed ? "PRESSED" : "released"); + stuck |= pressed; + } + if (stuck) { + printf("Warning: one or more buttons remained asserted; check for a held or stuck input.\n"); + } + return 0; +} + +static int command_buttons(int argc, char **argv) +{ + if (argc < 2 || strcmp(argv[1], "help") == 0) { + print_buttons_usage(); + return argc < 2 || argc == 2 ? 0 : 1; + } + if (strcmp(argv[1], "status") == 0) { + return command_buttons_status(argc - 1, argv + 1); + } + if (strcmp(argv[1], "test") == 0) { + return command_buttons_test(argc - 1, argv + 1); + } + + printf("Unknown button diagnostic '%s'.\n", argv[1]); + print_buttons_usage(); + return 1; +} + +esp_err_t local_ui_hw_test_init(void) +{ + if (s_bus_ready || s_buttons_ready) { + return ESP_ERR_INVALID_STATE; + } + + esp_err_t button_error = configure_buttons(); + esp_err_t bus_error = configure_i2c_bus(); + s_initialization_error = button_error != ESP_OK ? button_error : bus_error; + return s_initialization_error; +} + +int local_ui_hw_test_execute(int argc, char **argv) +{ + if (argc < 1 || argv == NULL || argv[0] == NULL) { + return 1; + } + if (strcmp(argv[0], "display") == 0) { + return command_display(argc, argv); + } + if (strcmp(argv[0], "buttons") == 0) { + return command_buttons(argc, argv); + } + return 1; +} + +void local_ui_hw_test_print_usage(void) +{ + printf(" debug display [status|probe|scan|init|off|pattern|row|contrast|invert]\n"); + printf(" debug buttons [status|test]\n"); +} diff --git a/src/local_ui_hw_test.h b/src/local_ui_hw_test.h new file mode 100644 index 0000000..08cf22f --- /dev/null +++ b/src/local_ui_hw_test.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Phase 7A local display and button electrical diagnostics. */ + +#pragma once + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Configure the shared I2C bus and active-low button inputs without probing. */ +esp_err_t local_ui_hw_test_init(void); + +/* Handle argv beginning with either "display" or "buttons". */ +int local_ui_hw_test_execute(int argc, char **argv); + +/* Append the Phase 7A command forms to the existing debug-group usage. */ +void local_ui_hw_test_print_usage(void); + +#ifdef __cplusplus +} +#endif diff --git a/src/main.c b/src/main.c index 8330f1c..32e1d13 100644 --- a/src/main.c +++ b/src/main.c @@ -5,6 +5,7 @@ #include "esp_log.h" #include "esp_psram.h" #include "network_console.h" +#include "local_ui_hw_test.h" #include "rs232_hw_test.h" #include "rs232_port_owner.h" #include "secure_random.h" @@ -35,7 +36,7 @@ static const char *TAG = "firmware"; void app_main(void) { - ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife SSH transport phase started"); + ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7A diagnostics started"); if (esp_psram_is_initialized()) { ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size()); @@ -55,6 +56,13 @@ void app_main(void) ESP_ERROR_CHECK(rs232_port_owner_init()); ESP_ERROR_CHECK(rs232_hw_test_init()); + /* A missing or miswired optional display must never remove recovery paths. */ + esp_err_t local_ui_error = local_ui_hw_test_init(); + if (local_ui_error != ESP_OK) { + ESP_LOGW(TAG, "Local UI diagnostics unavailable: %s", + esp_err_to_name(local_ui_error)); + } + serial_config_t serial_config; bool used_stored_config = false; esp_err_t config_error = serial_config_load(&serial_config, &used_stored_config); diff --git a/src/rs232_hw_test.c b/src/rs232_hw_test.c index fa8f9c7..c7325e7 100644 --- a/src/rs232_hw_test.c +++ b/src/rs232_hw_test.c @@ -17,6 +17,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/queue.h" #include "freertos/task.h" +#include "local_ui_hw_test.h" #include "rs232_port_owner.h" #include "status_led.h" @@ -1583,6 +1584,7 @@ static void print_debug_usage(void) printf(" debug loopback-a|loopback-b|valid-test\n"); printf(" debug uart-loopback [format] [bytes]\n"); printf(" debug uart-suite|cts-flow-test|rts-flow-test\n"); + local_ui_hw_test_print_usage(); } static int command_debug(int argc, char **argv) @@ -1620,6 +1622,10 @@ static int command_debug(int argc, char **argv) if (strcmp(argv[1], "rts-flow-test") == 0) { return command_rts_flow_test(argc - 1, argv + 1); } + if (strcmp(argv[1], "display") == 0 || + strcmp(argv[1], "buttons") == 0) { + return local_ui_hw_test_execute(argc - 1, argv + 1); + } printf("Unknown debug command '%s'.\n", argv[1]); print_debug_usage(); @@ -1650,7 +1656,7 @@ esp_err_t rs232_hw_test_register_console_commands(void) }, { .command = "debug", - .help = "Low-level RS-232 hardware diagnostics; run 'debug' for subcommands", + .help = "Low-level RS-232 and local-UI hardware diagnostics; run 'debug' for subcommands", .hint = NULL, .func = &command_debug, .argtable = NULL, diff --git a/src/rs232_hw_test.h b/src/rs232_hw_test.h index 4900498..d605852 100644 --- a/src/rs232_hw_test.h +++ b/src/rs232_hw_test.h @@ -5,5 +5,5 @@ /* Configure all MAX3243 logic-side signals in their safe static-test state. */ esp_err_t rs232_hw_test_init(void); -/* Register top-level status and the Phase 0 `debug` submenu. */ +/* Register top-level status and the shared low-level `debug` submenu. */ esp_err_t rs232_hw_test_register_console_commands(void);