diff --git a/docs/command_reference.md b/docs/command_reference.md index 1fe7f54..db02f7b 100644 --- a/docs/command_reference.md +++ b/docs/command_reference.md @@ -117,7 +117,7 @@ SSH listens on port 22 and uses the same `admin` credentials as HTTPS, but a sep | `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 pattern ` | Draw a full-screen electrical and geometry test pattern; `layout` renders separate status- and content-panel text. | | `debug display row <0..63>` | Draw the selected one-pixel display row for addressing and color-boundary checks. | | `debug display contrast <0..255>` | Set the OLED contrast to the specified bounded value. | | `debug display invert ` | Enable or disable OLED pixel inversion. | diff --git a/docs/electrical_tests.md b/docs/electrical_tests.md index 1f77ef4..1d15489 100644 --- a/docs/electrical_tests.md +++ b/docs/electrical_tests.md @@ -49,7 +49,17 @@ 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 +### 4. Rendered status/content layout + +Run the Phase 7B panel-layout pattern: + +```text +debug display pattern layout +``` + +Confirm that the status text is entirely in the yellow 128×16 panel (rows 0–15), content text is entirely in the blue 128×48 panel (rows 16–63), and the physical black divider between them remains visible. Record any text crossing the divider, clipping, incorrect panel color, or divider obscuration. + +### 5. Row 15/16 color-boundary test Clear the display, illuminate row 15, and record its physical color and position: @@ -67,9 +77,9 @@ 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 +### 6. Contrast, inversion, and display-off checks -With a visible pattern loaded, exercise the bounded contrast range and confirm that brightness changes without bus errors: +With a visible pattern loaded, exercise the bounded contrast range and confirm that brightness changes without bus errors. On the validated module, the useful visible brightness range begins at `1` and extends through `255`: ```text debug display contrast 0 @@ -87,7 +97,7 @@ 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 +### 7. Button checks With all buttons released, run `debug buttons status`. Confirm previous/back GPIO10, select/confirm GPIO13, and next GPIO14 report released/high due to their internal pull-ups; each should report pressed/low while held to `GND`. diff --git a/docs/roadmap.md b/docs/roadmap.md index c571572..89309af 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -37,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 | **In progress (7B)** | +| 7 | Local display and button interface | **In progress (7C)** | | 8 | Security and production hardening | **Planned** | | 9 | Authenticated, rollback-capable OTA | **Planned** | | 10 | BLE serial transport and provisioning evaluation | **Planned** | @@ -184,7 +184,7 @@ The order below is the current plan. Phase 7 is in progress; later phases remain 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. +Phase 7A diagnostics and target-hardware electrical validation are complete. Phase 7B display-driver validation is complete; Phase 7 overall remains in progress (7C). Phases 7C through 7E are not complete. #### Hardware baseline @@ -206,10 +206,12 @@ The persistent yellow strip is reserved for serial-service state, Wi-Fi strength - 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. +2. **Phase 7B — Display driver — Complete** + - The `local_display` service owns the local I²C bus, SSD1315-compatible panel, and framebuffer. It uses a static 1 KiB 128×64 framebuffer, a compact 5×7 uppercase/digit/punctuation renderer, and an internal mutex; do not add LVGL for this fixed monochrome UI. + - Rendering treats the yellow 128×16 status panel (rows 0–15) and blue 128×48 content panel (rows 16–63) as separate panels so the physical divider remains clear. + - Frame commits refresh only dirty 8-pixel pages. I²C runs at 100 kHz with bounded transactions. + - The display bus initializes at boot; a known display is automatically initialized and cleared. A missing or unresponsive display is nonfatal and does not make the serial core dependent on it. + - Target-hardware validation passed for initialization, all display diagnostic patterns, the rendered panel layout and physical divider, row 15/16 boundary, contrast, inversion, display-off/reinitialization, and button diagnostics. Observed contrast control has a useful brightness range of 1–255. 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. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0bced70..30fe9cd 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_display.c" "local_ui_hw_test.c" "rs232_hw_test.c" "rs232_port_owner.c" diff --git a/src/console_completion.c b/src/console_completion.c index 94bd159..ff30cba 100644 --- a/src/console_completion.c +++ b/src/console_completion.c @@ -43,6 +43,7 @@ static const char *const s_completion_candidates[] = { "debug display pattern checker", "debug display pattern grid", "debug display pattern corners", + "debug display pattern layout", "debug display row", "debug display contrast", "debug display invert", diff --git a/src/local_display.c b/src/local_display.c new file mode 100644 index 0000000..bb9b23e --- /dev/null +++ b/src/local_display.c @@ -0,0 +1,623 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Bounded SSD1315-compatible OLED service with separate physical panels. */ + +#include "local_display.h" + +#include + +#include "board_pins.h" +#include "driver/i2c_master.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/task.h" + +#define LOCAL_DISPLAY_PAGE_COUNT (LOCAL_DISPLAY_HEIGHT / 8U) +#define LOCAL_DISPLAY_FRAMEBUFFER_SIZE (LOCAL_DISPLAY_WIDTH * LOCAL_DISPLAY_PAGE_COUNT) +#define LOCAL_DISPLAY_I2C_SPEED_HZ 100000U +#define LOCAL_DISPLAY_I2C_TIMEOUT_MS 250U +#define LOCAL_DISPLAY_PROBE_TIMEOUT_MS 50U +#define LOCAL_DISPLAY_SCAN_TIMEOUT_MS 10U +#define LOCAL_DISPLAY_LOCK_TIMEOUT_MS 3000U +#define LOCAL_DISPLAY_DEFAULT_CONTRAST 127U +#define LOCAL_DISPLAY_COMMAND_CAPACITY 32U +#define LOCAL_DISPLAY_DATA_CHUNK_SIZE 128U + +static const uint8_t s_expected_addresses[] = {0x3cU, 0x3dU}; + +typedef struct { + char character; + uint8_t columns[5]; +} glyph_t; + +/* Compact 5x7 ASCII subset: lower-case input maps to upper-case glyphs. */ +static const glyph_t s_glyphs[] = { + {' ', {0x00, 0x00, 0x00, 0x00, 0x00}}, + {'!', {0x00, 0x00, 0x5f, 0x00, 0x00}}, + {'-', {0x08, 0x08, 0x08, 0x08, 0x08}}, + {'.', {0x00, 0x60, 0x60, 0x00, 0x00}}, + {'/', {0x20, 0x10, 0x08, 0x04, 0x02}}, + {':', {0x00, 0x36, 0x36, 0x00, 0x00}}, + {'?', {0x02, 0x01, 0x51, 0x09, 0x06}}, + {'_', {0x40, 0x40, 0x40, 0x40, 0x40}}, + {'0', {0x3e, 0x51, 0x49, 0x45, 0x3e}}, + {'1', {0x00, 0x42, 0x7f, 0x40, 0x00}}, + {'2', {0x42, 0x61, 0x51, 0x49, 0x46}}, + {'3', {0x21, 0x41, 0x45, 0x4b, 0x31}}, + {'4', {0x18, 0x14, 0x12, 0x7f, 0x10}}, + {'5', {0x27, 0x45, 0x45, 0x45, 0x39}}, + {'6', {0x3c, 0x4a, 0x49, 0x49, 0x30}}, + {'7', {0x01, 0x71, 0x09, 0x05, 0x03}}, + {'8', {0x36, 0x49, 0x49, 0x49, 0x36}}, + {'9', {0x06, 0x49, 0x49, 0x29, 0x1e}}, + {'A', {0x7e, 0x11, 0x11, 0x11, 0x7e}}, + {'B', {0x7f, 0x49, 0x49, 0x49, 0x36}}, + {'C', {0x3e, 0x41, 0x41, 0x41, 0x22}}, + {'D', {0x7f, 0x41, 0x41, 0x22, 0x1c}}, + {'E', {0x7f, 0x49, 0x49, 0x49, 0x41}}, + {'F', {0x7f, 0x09, 0x09, 0x09, 0x01}}, + {'G', {0x3e, 0x41, 0x49, 0x49, 0x7a}}, + {'H', {0x7f, 0x08, 0x08, 0x08, 0x7f}}, + {'I', {0x00, 0x41, 0x7f, 0x41, 0x00}}, + {'J', {0x20, 0x40, 0x41, 0x3f, 0x01}}, + {'K', {0x7f, 0x08, 0x14, 0x22, 0x41}}, + {'L', {0x7f, 0x40, 0x40, 0x40, 0x40}}, + {'M', {0x7f, 0x02, 0x0c, 0x02, 0x7f}}, + {'N', {0x7f, 0x04, 0x08, 0x10, 0x7f}}, + {'O', {0x3e, 0x41, 0x41, 0x41, 0x3e}}, + {'P', {0x7f, 0x09, 0x09, 0x09, 0x06}}, + {'Q', {0x3e, 0x41, 0x51, 0x21, 0x5e}}, + {'R', {0x7f, 0x09, 0x19, 0x29, 0x46}}, + {'S', {0x46, 0x49, 0x49, 0x49, 0x31}}, + {'T', {0x01, 0x01, 0x7f, 0x01, 0x01}}, + {'U', {0x3f, 0x40, 0x40, 0x40, 0x3f}}, + {'V', {0x1f, 0x20, 0x40, 0x20, 0x1f}}, + {'W', {0x7f, 0x20, 0x18, 0x20, 0x7f}}, + {'X', {0x63, 0x14, 0x08, 0x14, 0x63}}, + {'Y', {0x03, 0x04, 0x78, 0x04, 0x03}}, + {'Z', {0x61, 0x51, 0x49, 0x45, 0x43}}, +}; + +static i2c_master_bus_handle_t s_bus; +static i2c_master_dev_handle_t s_device; +static StaticSemaphore_t s_mutex_storage; +static SemaphoreHandle_t s_mutex; +static uint8_t s_framebuffer[LOCAL_DISPLAY_FRAMEBUFFER_SIZE]; +static bool s_bus_ready; +static bool s_initialized; +static bool s_frame_active; +static TaskHandle_t s_frame_owner; +static uint8_t s_address; +static uint8_t s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST; +static bool s_inverted; +static uint8_t s_dirty_pages; +static esp_err_t s_last_error = ESP_ERR_INVALID_STATE; + +static TickType_t milliseconds_to_ticks(uint32_t milliseconds) +{ + TickType_t ticks = pdMS_TO_TICKS(milliseconds); + return (milliseconds > 0U && ticks == 0U) ? 1U : ticks; +} + +static esp_err_t take_lock(void) +{ + if (s_mutex == NULL) { + return ESP_ERR_INVALID_STATE; + } + return xSemaphoreTake(s_mutex, milliseconds_to_ticks(LOCAL_DISPLAY_LOCK_TIMEOUT_MS)) == pdTRUE + ? ESP_OK + : ESP_ERR_TIMEOUT; +} + +static void give_lock(void) +{ + if (s_mutex != NULL) { + (void)xSemaphoreGive(s_mutex); + } +} + +static void set_last_error(esp_err_t error) +{ + s_last_error = error; +} + +static esp_err_t send_commands_locked(const uint8_t *commands, size_t count) +{ + if (!s_initialized || s_device == NULL || commands == NULL || count == 0U || + count > LOCAL_DISPLAY_COMMAND_CAPACITY) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t transfer[LOCAL_DISPLAY_COMMAND_CAPACITY + 1U]; + transfer[0] = 0x00U; + memcpy(&transfer[1], commands, count); + return i2c_master_transmit(s_device, transfer, count + 1U, + LOCAL_DISPLAY_I2C_TIMEOUT_MS); +} + +static esp_err_t send_command_locked(uint8_t command) +{ + return send_commands_locked(&command, 1U); +} + +static esp_err_t flush_dirty_locked(void) +{ + if (!s_initialized || s_device == NULL) { + return ESP_ERR_INVALID_STATE; + } + + uint8_t transfer[LOCAL_DISPLAY_DATA_CHUNK_SIZE + 1U]; + transfer[0] = 0x40U; + for (uint8_t page = 0U; page < LOCAL_DISPLAY_PAGE_COUNT; ++page) { + uint8_t page_mask = (uint8_t)(1U << page); + if ((s_dirty_pages & page_mask) == 0U) { + continue; + } + + const uint8_t commands[] = { + 0x21U, 0x00U, (uint8_t)(LOCAL_DISPLAY_WIDTH - 1U), + 0x22U, page, page, + }; + esp_err_t error = send_commands_locked(commands, sizeof(commands)); + if (error != ESP_OK) { + return error; + } + memcpy(&transfer[1], &s_framebuffer[(size_t)page * LOCAL_DISPLAY_WIDTH], + LOCAL_DISPLAY_WIDTH); + error = i2c_master_transmit(s_device, transfer, sizeof(transfer), + LOCAL_DISPLAY_I2C_TIMEOUT_MS); + if (error != ESP_OK) { + return error; + } + s_dirty_pages &= (uint8_t)~page_mask; + } + return ESP_OK; +} + +static void set_pixel_raw(uint8_t x, uint8_t y, bool on) +{ + if (x >= LOCAL_DISPLAY_WIDTH || y >= LOCAL_DISPLAY_HEIGHT) { + return; + } + size_t index = (size_t)(y / 8U) * LOCAL_DISPLAY_WIDTH + x; + uint8_t mask = (uint8_t)(1U << (y & 7U)); + uint8_t before = s_framebuffer[index]; + if (on) { + s_framebuffer[index] |= mask; + } else { + s_framebuffer[index] &= (uint8_t)~mask; + } + if (before != s_framebuffer[index]) { + s_dirty_pages |= (uint8_t)(1U << (y / 8U)); + } +} + +static bool panel_geometry(local_display_panel_t panel, uint8_t *origin_y, uint8_t *height) +{ + if (origin_y == NULL || height == NULL) { + return false; + } + switch (panel) { + case LOCAL_DISPLAY_PANEL_STATUS: + *origin_y = 0U; + *height = LOCAL_DISPLAY_STATUS_HEIGHT; + return true; + case LOCAL_DISPLAY_PANEL_CONTENT: + *origin_y = LOCAL_DISPLAY_STATUS_HEIGHT; + *height = LOCAL_DISPLAY_CONTENT_HEIGHT; + return true; + default: + return false; + } +} + +static const glyph_t *find_glyph(char character) +{ + if (character >= 'a' && character <= 'z') { + character = (char)(character - ('a' - 'A')); + } + for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) { + if (s_glyphs[index].character == character) { + return &s_glyphs[index]; + } + } + for (size_t index = 0U; index < sizeof(s_glyphs) / sizeof(s_glyphs[0]); ++index) { + if (s_glyphs[index].character == '?') { + return &s_glyphs[index]; + } + } + return NULL; +} + +static esp_err_t select_device_locked(uint8_t address) +{ + if (address != 0x3cU && address != 0x3dU) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t error = i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS); + if (error != ESP_OK) { + return error; + } + if (s_device != NULL && s_address == address) { + return ESP_OK; + } + if (s_device != NULL) { + error = i2c_master_bus_rm_device(s_device); + if (error != ESP_OK) { + return error; + } + s_device = NULL; + s_initialized = false; + } + + const i2c_device_config_t config = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = address, + .scl_speed_hz = LOCAL_DISPLAY_I2C_SPEED_HZ, + }; + error = i2c_master_bus_add_device(s_bus, &config, &s_device); + if (error == ESP_OK) { + s_address = address; + } + return error; +} + +static esp_err_t initialize_locked(uint8_t address) +{ + s_initialized = false; + esp_err_t error = select_device_locked(address); + if (error != ESP_OK) { + return error; + } + + const uint8_t commands[] = { + 0xaeU, 0xd5U, 0x80U, 0xa8U, 0x3fU, 0xd3U, 0x00U, 0x40U, + 0x8dU, 0x14U, 0x20U, 0x00U, 0xa1U, 0xc8U, 0xdaU, 0x12U, + 0x81U, LOCAL_DISPLAY_DEFAULT_CONTRAST, 0xd9U, 0xf1U, 0xdbU, + 0x40U, 0xa4U, 0xa6U, 0x2eU, + }; + + /* send_commands_locked requires initialized, so issue the bootstrap directly. */ + uint8_t transfer[sizeof(commands) + 1U]; + transfer[0] = 0x00U; + memcpy(&transfer[1], commands, sizeof(commands)); + error = i2c_master_transmit(s_device, transfer, sizeof(transfer), + LOCAL_DISPLAY_I2C_TIMEOUT_MS); + if (error != ESP_OK) { + return error; + } + + s_initialized = true; + s_contrast = LOCAL_DISPLAY_DEFAULT_CONTRAST; + s_inverted = false; + memset(s_framebuffer, 0, sizeof(s_framebuffer)); + s_dirty_pages = (uint8_t)((1U << LOCAL_DISPLAY_PAGE_COUNT) - 1U); + error = flush_dirty_locked(); + if (error == ESP_OK) { + error = send_command_locked(0xafU); + } + if (error != ESP_OK) { + s_initialized = false; + } + return error; +} + +esp_err_t local_display_init(void) +{ + if (s_bus_ready || s_mutex != NULL) { + return ESP_ERR_INVALID_STATE; + } + + s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_storage); + if (s_mutex == NULL) { + return ESP_ERR_NO_MEM; + } + + const i2c_master_bus_config_t config = { + .i2c_port = LOCAL_UI_I2C_PORT, + .sda_io_num = LOCAL_UI_DISPLAY_SDA_GPIO, + .scl_io_num = LOCAL_UI_DISPLAY_SCL_GPIO, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags.enable_internal_pullup = false, + }; + esp_err_t error = i2c_new_master_bus(&config, &s_bus); + if (error != ESP_OK) { + vSemaphoreDelete(s_mutex); + s_mutex = NULL; + set_last_error(error); + return error; + } + s_bus_ready = true; + set_last_error(ESP_OK); + return ESP_OK; +} + +esp_err_t local_display_probe_expected(uint8_t *address_7bit) +{ + if (address_7bit == NULL) { + return ESP_ERR_INVALID_ARG; + } + if (!s_bus_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + + uint8_t found = 0U; + for (size_t index = 0U; index < sizeof(s_expected_addresses) / sizeof(s_expected_addresses[0]); ++index) { + uint8_t address = s_expected_addresses[index]; + if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_PROBE_TIMEOUT_MS) == ESP_OK) { + if (found == 0U) { + *address_7bit = address; + } + ++found; + } + } + error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK; + set_last_error(error); + give_lock(); + return error; +} + +esp_err_t local_display_start_at(uint8_t address_7bit) +{ + if (!s_bus_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + error = initialize_locked(address_7bit); + set_last_error(error); + give_lock(); + return error; +} + +esp_err_t local_display_start(void) +{ + uint8_t address = 0U; + esp_err_t error = local_display_probe_expected(&address); + if (error != ESP_OK) { + return error; + } + return local_display_start_at(address); +} + +esp_err_t local_display_stop(void) +{ + if (!s_bus_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + if (!s_initialized) { + error = ESP_ERR_INVALID_STATE; + } else { + error = send_command_locked(0xaeU); + /* A failed command leaves panel state unknown; force a clean reinit. */ + s_initialized = false; + } + set_last_error(error); + give_lock(); + return error; +} + +esp_err_t local_display_get_snapshot(local_display_snapshot_t *snapshot) +{ + if (snapshot == NULL) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + *snapshot = (local_display_snapshot_t){ + .bus_ready = s_bus_ready, + .initialized = s_initialized, + .address_7bit = s_address, + .contrast = s_contrast, + .inverted = s_inverted, + .dirty_page_mask = s_dirty_pages, + .last_error = s_last_error, + }; + give_lock(); + return ESP_OK; +} + +esp_err_t local_display_scan(local_display_scan_callback_t callback, + void *context, + size_t *responding_count) +{ + if (!s_bus_ready) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + + uint8_t responses[0x78U - 0x08U]; + size_t found = 0U; + for (uint16_t address = 0x08U; address <= 0x77U; ++address) { + if (i2c_master_probe(s_bus, address, LOCAL_DISPLAY_SCAN_TIMEOUT_MS) == ESP_OK) { + responses[found++] = (uint8_t)address; + } + vTaskDelay(1U); + } + if (responding_count != NULL) { + *responding_count = found; + } + error = found == 0U ? ESP_ERR_NOT_FOUND : ESP_OK; + set_last_error(error); + give_lock(); + + /* Callers may safely use the display service from the callback. */ + if (callback != NULL) { + for (size_t index = 0U; index < found; ++index) { + callback(responses[index], context); + } + } + return error; +} + +esp_err_t local_display_set_contrast(uint8_t contrast) +{ + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + const uint8_t commands[] = {0x81U, contrast}; + error = send_commands_locked(commands, sizeof(commands)); + if (error == ESP_OK) { + s_contrast = contrast; + } else { + s_initialized = false; + } + set_last_error(error); + give_lock(); + return error; +} + +esp_err_t local_display_set_inverted(bool inverted) +{ + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + error = send_command_locked(inverted ? 0xa7U : 0xa6U); + if (error == ESP_OK) { + s_inverted = inverted; + } else { + s_initialized = false; + } + set_last_error(error); + give_lock(); + return error; +} + +esp_err_t local_display_frame_begin(void) +{ + esp_err_t error = take_lock(); + if (error != ESP_OK) { + return error; + } + if (!s_initialized || s_frame_active) { + give_lock(); + return ESP_ERR_INVALID_STATE; + } + s_frame_active = true; + s_frame_owner = xTaskGetCurrentTaskHandle(); + return ESP_OK; +} + +esp_err_t local_display_frame_end(void) +{ + if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) { + return ESP_ERR_INVALID_STATE; + } + esp_err_t error = flush_dirty_locked(); + if (error != ESP_OK) { + s_initialized = false; + } + set_last_error(error); + s_frame_active = false; + s_frame_owner = NULL; + give_lock(); + return error; +} + +void local_display_frame_cancel(void) +{ + if (s_frame_active && s_frame_owner == xTaskGetCurrentTaskHandle()) { + s_frame_active = false; + s_frame_owner = NULL; + give_lock(); + } +} + +void local_display_frame_clear(local_display_panel_t panel) +{ + if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) { + return; + } + uint8_t origin_y = 0U; + uint8_t height = 0U; + if (!panel_geometry(panel, &origin_y, &height)) { + return; + } + for (uint8_t y = origin_y; y < origin_y + height; ++y) { + for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { + set_pixel_raw(x, y, false); + } + } +} + +void local_display_frame_clear_all(void) +{ + if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) { + return; + } + for (size_t index = 0U; index < sizeof(s_framebuffer); ++index) { + if (s_framebuffer[index] != 0U) { + s_framebuffer[index] = 0U; + s_dirty_pages |= (uint8_t)(1U << (index / LOCAL_DISPLAY_WIDTH)); + } + } +} + +void local_display_frame_set_pixel(local_display_panel_t panel, + uint8_t x, + uint8_t y, + bool on) +{ + if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle()) { + return; + } + uint8_t origin_y = 0U; + uint8_t height = 0U; + if (!panel_geometry(panel, &origin_y, &height) || x >= LOCAL_DISPLAY_WIDTH || y >= height) { + return; + } + set_pixel_raw(x, (uint8_t)(origin_y + y), on); +} + +void local_display_frame_draw_text(local_display_panel_t panel, + uint8_t x, + uint8_t y, + const char *text) +{ + if (!s_frame_active || s_frame_owner != xTaskGetCurrentTaskHandle() || text == NULL) { + return; + } + uint8_t origin_y = 0U; + uint8_t height = 0U; + if (!panel_geometry(panel, &origin_y, &height) || y >= height) { + return; + } + + uint16_t cursor_x = x; + for (const char *character = text; *character != '\0'; ++character) { + const glyph_t *glyph = find_glyph(*character); + if (glyph == NULL || cursor_x + 5U > LOCAL_DISPLAY_WIDTH) { + break; + } + for (uint8_t column = 0U; column < 5U; ++column) { + for (uint8_t row = 0U; row < 7U; ++row) { + if ((glyph->columns[column] & (uint8_t)(1U << row)) != 0U && + y + row < height) { + set_pixel_raw((uint8_t)(cursor_x + column), + (uint8_t)(origin_y + y + row), true); + } + } + } + cursor_x += 6U; + if (cursor_x >= LOCAL_DISPLAY_WIDTH) { + break; + } + } +} diff --git a/src/local_display.h b/src/local_display.h new file mode 100644 index 0000000..50e596d --- /dev/null +++ b/src/local_display.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: GPL-3.0-only */ +/* Bounded SSD1315-compatible local OLED service. */ + +#pragma once + +#include +#include +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define LOCAL_DISPLAY_WIDTH 128U +#define LOCAL_DISPLAY_HEIGHT 64U +#define LOCAL_DISPLAY_STATUS_HEIGHT 16U +#define LOCAL_DISPLAY_CONTENT_HEIGHT 48U + +/* The physical black divider lies between status row 15 and content row 16. */ +typedef enum { + LOCAL_DISPLAY_PANEL_STATUS = 0, + LOCAL_DISPLAY_PANEL_CONTENT, +} local_display_panel_t; + +typedef struct { + bool bus_ready; + bool initialized; + uint8_t address_7bit; + uint8_t contrast; + bool inverted; + uint8_t dirty_page_mask; + esp_err_t last_error; +} local_display_snapshot_t; + +typedef void (*local_display_scan_callback_t)(uint8_t address_7bit, void *context); + +/* Set up I2C0 on the board-profile pins. No display probe occurs here. */ +esp_err_t local_display_init(void); + +/* Probe standard OLED addresses and initialize the first responding display. */ +esp_err_t local_display_start(void); + +/* Select and initialize one supported 7-bit address (0x3c or 0x3d). */ +esp_err_t local_display_start_at(uint8_t address_7bit); + +/* Turn off the panel while preserving the I2C bus for later diagnostics/restart. */ +esp_err_t local_display_stop(void); + +esp_err_t local_display_get_snapshot(local_display_snapshot_t *snapshot); +esp_err_t local_display_probe_expected(uint8_t *address_7bit); + +/* Bounded scan of usable 7-bit addresses 0x08 through 0x77. */ +esp_err_t local_display_scan(local_display_scan_callback_t callback, + void *context, + size_t *responding_count); + +esp_err_t local_display_set_contrast(uint8_t contrast); +esp_err_t local_display_set_inverted(bool inverted); + +/* + * A frame holds only the display's own mutex and is owned by the task that + * begins it. Callers must never retain a service/broker mutex while beginning + * or ending a frame. Only the owning task may end or cancel it; end sends only + * modified 8-pixel pages and releases the display mutex on all outcomes. + */ +esp_err_t local_display_frame_begin(void); +esp_err_t local_display_frame_end(void); +void local_display_frame_cancel(void); + +/* Drawing coordinates are panel-local and are clipped to the selected panel. */ +void local_display_frame_clear(local_display_panel_t panel); +void local_display_frame_clear_all(void); +void local_display_frame_set_pixel(local_display_panel_t panel, + uint8_t x, + uint8_t y, + bool on); +void local_display_frame_draw_text(local_display_panel_t panel, + uint8_t x, + uint8_t y, + const char *text); + +#ifdef __cplusplus +} +#endif diff --git a/src/local_ui_hw_test.c b/src/local_ui_hw_test.c index a16feb9..bd2c7cb 100644 --- a/src/local_ui_hw_test.c +++ b/src/local_ui_hw_test.c @@ -1,5 +1,5 @@ /* SPDX-License-Identifier: GPL-3.0-only */ -/* Phase 7A bounded SSD1315-compatible OLED and button diagnostics. */ +/* Phase 7A diagnostics built on the Phase 7B local display service. */ #include "local_ui_hw_test.h" @@ -12,23 +12,11 @@ #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 +#include "local_display.h" #define BUTTON_POLL_MS 10U #define BUTTON_DEBOUNCE_MS 30U @@ -36,9 +24,6 @@ #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; @@ -61,16 +46,8 @@ static const button_definition_t s_buttons[] = { {.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 esp_err_t s_button_initialization_error = ESP_ERR_INVALID_STATE; static TickType_t milliseconds_to_ticks(uint32_t milliseconds) { @@ -94,7 +71,6 @@ static bool parse_unsigned(const char *text, parsed < minimum || parsed > maximum) { return false; } - *value = parsed; return true; } @@ -111,7 +87,6 @@ static bool parse_display_address(const char *text, uint8_t *address) if (errno != 0 || end == text || *end != '\0') { return false; } - if (parsed == 0x3cU || parsed == 0x78U || parsed == 0x79U) { *address = 0x3cU; return true; @@ -140,7 +115,6 @@ static esp_err_t configure_buttons(void) .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; @@ -148,264 +122,11 @@ static esp_err_t configure_buttons(void) return error; } -static esp_err_t configure_i2c_bus(void) +static void print_address(uint8_t address) { - 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); - } - } + printf("7-bit 0x%02x (8-bit 0x%02x write / 0x%02x read)", + address, (unsigned int)(address << 1U), + (unsigned int)((address << 1U) | 1U)); } static void print_display_usage(void) @@ -415,7 +136,7 @@ static void print_display_usage(void) 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 pattern \n"); printf(" debug display row <0..63>\n"); printf(" debug display contrast <0..255>\n"); printf(" debug display invert \n"); @@ -429,34 +150,31 @@ static int command_display_status(int argc, char **argv) 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"); + local_display_snapshot_t snapshot; + esp_err_t error = local_display_get_snapshot(&snapshot); + if (error != ESP_OK) { + return report_error("Display status", error); } + printf("Local display: bus=%s initialized=%s last-error=%s dirty-pages=0x%02x\n", + snapshot.bus_ready ? "ready" : "unavailable", + snapshot.initialized ? "yes" : "no", + esp_err_to_name(snapshot.last_error), + snapshot.dirty_page_mask); + if (snapshot.address_7bit != 0U) { + printf("OLED: "); + print_address(snapshot.address_7bit); + printf(" contrast=%u inverted=%s\n", (unsigned int)snapshot.contrast, + snapshot.inverted ? "yes" : "no"); + } else { + printf("OLED: no selected address\n"); + } + printf("Panels: status=128x16 rows 0..15; content=128x48 rows 16..63; physical black divider between them\n"); 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_PREVIOUS_GPIO, LOCAL_UI_BUTTON_SELECT_GPIO, LOCAL_UI_BUTTON_NEXT_GPIO); - return s_bus_ready && s_buttons_ready ? 0 : 1; + return snapshot.bus_ready && s_buttons_ready ? 0 : 1; } static int command_display_probe(int argc, char **argv) @@ -468,13 +186,25 @@ static int command_display_probe(int argc, char **argv) } uint8_t address = 0U; - esp_err_t error = probe_expected_addresses(&address); + esp_err_t error = local_display_probe_expected(&address); if (error != ESP_OK) { + printf("No OLED response at expected 7-bit addresses 0x3c or 0x3d.\n"); return report_error("Display probe", error); } + printf("OLED response at "); + print_address(address); + printf(".\n"); return 0; } +static void scan_print_callback(uint8_t address, void *context) +{ + (void)context; + printf(" response: "); + print_address(address); + printf("\n"); +} + static int command_display_scan(int argc, char **argv) { if (argc != 2 || strcmp(argv[1], "--force") != 0) { @@ -482,30 +212,13 @@ static int command_display_scan(int argc, char **argv) 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); - } + size_t found = 0U; + printf("Scanning usable 7-bit I2C addresses 0x08..0x77 at 100000 Hz.\n"); + esp_err_t error = local_display_scan(scan_print_callback, NULL, &found); printf("I2C scan complete: %u responding address%s.\n", (unsigned int)found, found == 1U ? "" : "es"); - return found == 0U ? 1 : 0; + return error == ESP_OK ? 0 : report_error("Display scan", error); } static int command_display_init(int argc, char **argv) @@ -516,26 +229,27 @@ static int command_display_init(int argc, char **argv) } uint8_t address = 0U; + esp_err_t error; 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; } + error = local_display_start_at(address); } else { - esp_err_t error = probe_expected_addresses(&address); - if (error != ESP_OK) { - return report_error("Display probe", error); + error = local_display_start(); + if (error == ESP_OK) { + local_display_snapshot_t snapshot; + error = local_display_get_snapshot(&snapshot); + address = snapshot.address_7bit; } } - - 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); + printf("SSD1315-compatible 128x64 display initialized at "); + print_address(address); + printf(".\n"); return 0; } @@ -546,15 +260,10 @@ static int command_display_off(int argc, char **argv) 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); + esp_err_t error = local_display_stop(); 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; } @@ -563,7 +272,66 @@ 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; + strcmp(name, "corners") == 0 || strcmp(name, "layout") == 0; +} + +static void set_global_pixel(uint8_t x, uint8_t y, bool on) +{ + if (y < LOCAL_DISPLAY_STATUS_HEIGHT) { + local_display_frame_set_pixel(LOCAL_DISPLAY_PANEL_STATUS, x, y, on); + } else { + local_display_frame_set_pixel(LOCAL_DISPLAY_PANEL_CONTENT, x, + (uint8_t)(y - LOCAL_DISPLAY_STATUS_HEIGHT), on); + } +} + +static void draw_pattern(const char *name) +{ + local_display_frame_clear_all(); + if (strcmp(name, "clear") == 0) { + return; + } + if (strcmp(name, "fill") == 0) { + for (uint8_t y = 0U; y < LOCAL_DISPLAY_HEIGHT; ++y) { + for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { + set_global_pixel(x, y, true); + } + } + return; + } + if (strcmp(name, "checker") == 0 || strcmp(name, "grid") == 0) { + bool checker = strcmp(name, "checker") == 0; + for (uint8_t y = 0U; y < LOCAL_DISPLAY_HEIGHT; ++y) { + for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { + bool on = checker ? (((x + y) & 1U) == 0U) + : ((x % 8U) == 0U || (y % 8U) == 0U); + if (on) { + set_global_pixel(x, y, true); + } + } + } + return; + } + if (strcmp(name, "corners") == 0) { + for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { + set_global_pixel(x, 0U, true); + set_global_pixel(x, LOCAL_DISPLAY_HEIGHT - 1U, true); + } + for (uint8_t y = 0U; y < LOCAL_DISPLAY_HEIGHT; ++y) { + set_global_pixel(0U, y, true); + set_global_pixel(LOCAL_DISPLAY_WIDTH - 1U, y, true); + } + return; + } + + local_display_frame_clear(LOCAL_DISPLAY_PANEL_STATUS); + local_display_frame_clear(LOCAL_DISPLAY_PANEL_CONTENT); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 0U, "SER OK"); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_STATUS, 0U, 8U, "WR NONE"); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 0U, "DISPLAY DRIVER"); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 8U, "STATUS 16 PX"); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 24U, "CONTENT 48 PX"); + local_display_frame_draw_text(LOCAL_DISPLAY_PANEL_CONTENT, 0U, 40U, "SEPARATE PANELS"); } static int command_display_pattern(int argc, char **argv) @@ -572,12 +340,12 @@ static int command_display_pattern(int argc, char **argv) print_display_usage(); return 1; } - if (!s_panel_initialized) { - return report_error("Display pattern", ESP_ERR_INVALID_STATE); + esp_err_t error = local_display_frame_begin(); + if (error != ESP_OK) { + return report_error("Display pattern", error); } - - framebuffer_draw_pattern(argv[1]); - esp_err_t error = display_refresh(); + draw_pattern(argv[1]); + error = local_display_frame_end(); if (error != ESP_OK) { return report_error("Display pattern", error); } @@ -588,19 +356,19 @@ static int command_display_pattern(int argc, char **argv) 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)) { + if (argc != 2 || !parse_unsigned(argv[1], 0U, LOCAL_DISPLAY_HEIGHT - 1U, &row)) { print_display_usage(); return 1; } - if (!s_panel_initialized) { - return report_error("Display row", ESP_ERR_INVALID_STATE); + esp_err_t error = local_display_frame_begin(); + if (error != ESP_OK) { + return report_error("Display row", error); } - - memset(s_framebuffer, 0, sizeof(s_framebuffer)); - for (uint32_t x = 0U; x < DISPLAY_WIDTH; ++x) { - framebuffer_set_pixel(x, (uint32_t)row); + local_display_frame_clear_all(); + for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { + set_global_pixel(x, (uint8_t)row, true); } - esp_err_t error = display_refresh(); + error = local_display_frame_end(); if (error != ESP_OK) { return report_error("Display row", error); } @@ -615,16 +383,10 @@ static int command_display_contrast(int argc, char **argv) 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)); + esp_err_t error = local_display_set_contrast((uint8_t)contrast); 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; } @@ -635,17 +397,12 @@ static int command_display_invert(int argc, char **argv) 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); + bool inverted = strcmp(argv[1], "on") == 0; + esp_err_t error = local_display_set_inverted(inverted); if (error != ESP_OK) { return report_error("Display inversion", error); } - s_inverted = invert; - printf("Display inversion %s.\n", invert ? "enabled" : "disabled"); + printf("Display inversion %s.\n", inverted ? "enabled" : "disabled"); return 0; } @@ -655,7 +412,6 @@ static int command_display(int argc, char **argv) print_display_usage(); return argc < 2 || argc == 2 ? 0 : 1; } - if (strcmp(argv[1], "status") == 0) { return command_display_status(argc - 1, argv + 1); } @@ -704,17 +460,14 @@ static int command_buttons_status(int argc, char **argv) return 1; } if (!s_buttons_ready) { - return report_error("Button status", s_initialization_error); + return report_error("Button status", s_button_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"); + printf(" %-14s GPIO%d level=%d %s\n", s_buttons[index].name, + s_buttons[index].gpio, level, level == 0 ? "pressed" : "released"); } return 0; } @@ -723,33 +476,27 @@ 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))) { + (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); + return report_error("Button test", s_button_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. */ + .raw_level = gpio_get_level(s_buttons[index].gpio), .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); - + 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]; @@ -758,7 +505,6 @@ static int command_buttons_test(int argc, char **argv) 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; @@ -768,23 +514,19 @@ static int command_buttons_test(int argc, char **argv) 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; + 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); + s_buttons[index].name, (long long)duration_ms); } else { printf("%-14s released after %lld ms\n", - s_buttons[index].name, - (long long)duration_ms); + 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) { @@ -801,8 +543,7 @@ static int command_buttons_test(int argc, char **argv) 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, + 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"); @@ -826,7 +567,6 @@ static int command_buttons(int argc, char **argv) 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; @@ -834,14 +574,11 @@ static int command_buttons(int argc, char **argv) esp_err_t local_ui_hw_test_init(void) { - if (s_bus_ready || s_buttons_ready) { + if (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; + s_button_initialization_error = configure_buttons(); + return s_button_initialization_error; } int local_ui_hw_test_execute(int argc, char **argv) diff --git a/src/local_ui_hw_test.h b/src/local_ui_hw_test.h index 08cf22f..cdd22c0 100644 --- a/src/local_ui_hw_test.h +++ b/src/local_ui_hw_test.h @@ -1,5 +1,5 @@ /* SPDX-License-Identifier: GPL-3.0-only */ -/* Phase 7A local display and button electrical diagnostics. */ +/* Phase 7A local display/button diagnostics retained for Phase 7B validation. */ #pragma once @@ -9,7 +9,7 @@ extern "C" { #endif -/* Configure the shared I2C bus and active-low button inputs without probing. */ +/* Configure active-low button inputs; the Phase 7B display service owns I2C. */ esp_err_t local_ui_hw_test_init(void); /* Handle argv beginning with either "display" or "buttons". */ diff --git a/src/main.c b/src/main.c index 32e1d13..bcdeb7f 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_display.h" #include "local_ui_hw_test.h" #include "rs232_hw_test.h" #include "rs232_port_owner.h" @@ -36,7 +37,7 @@ static const char *TAG = "firmware"; void app_main(void) { - ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7A diagnostics started"); + ESP_LOGI(TAG, "ESP32-S3 Serial Swiss Army Knife Phase 7B display driver started"); if (esp_psram_is_initialized()) { ESP_LOGI(TAG, "PSRAM initialized: %u bytes", (unsigned int)esp_psram_get_size()); @@ -56,10 +57,22 @@ 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. */ + /* The optional display can fail without affecting UART0 or serial transports. */ + esp_err_t local_display_error = local_display_init(); + if (local_display_error != ESP_OK) { + ESP_LOGW(TAG, "Local display bus unavailable: %s", + esp_err_to_name(local_display_error)); + } else { + local_display_error = local_display_start(); + if (local_display_error != ESP_OK) { + ESP_LOGW(TAG, "Local display unavailable: %s", + esp_err_to_name(local_display_error)); + } + } + 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_LOGW(TAG, "Local UI button diagnostics unavailable: %s", esp_err_to_name(local_ui_error)); }