/* SPDX-License-Identifier: GPL-3.0-only */ /* Phase 7A diagnostics built on the Phase 7B local display service. */ #include "local_ui_hw_test.h" #include #include #include #include #include #include #include "board_pins.h" #include "driver/gpio.h" #include "esp_err.h" #include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "local_display.h" #include "local_status_ui.h" #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 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 bool s_buttons_ready; static esp_err_t s_button_initialization_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 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 void print_address(uint8_t address) { 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) { 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; } 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_NEXT_GPIO); return snapshot.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 = 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) { 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; } 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 error == ESP_OK ? 0 : report_error("Display scan", error); } static int command_display_init(int argc, char **argv) { if (argc > 2) { print_display_usage(); return 1; } 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 { error = local_display_start(); if (error == ESP_OK) { local_display_snapshot_t snapshot; error = local_display_get_snapshot(&snapshot); address = snapshot.address_7bit; } } if (error != ESP_OK) { return report_error("Display initialization", error); } printf("SSD1315-compatible 128x64 display initialized at "); print_address(address); printf(".\n"); return 0; } static int command_display_off(int argc, char **argv) { (void)argv; if (argc != 1) { print_display_usage(); return 1; } esp_err_t error = local_display_stop(); if (error != ESP_OK) { return report_error("Display off", error); } 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 || 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) { if (argc != 2 || !pattern_name_valid(argv[1])) { print_display_usage(); return 1; } esp_err_t error = local_display_frame_begin(); if (error != ESP_OK) { return report_error("Display pattern", error); } draw_pattern(argv[1]); error = local_display_frame_end(); 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, LOCAL_DISPLAY_HEIGHT - 1U, &row)) { print_display_usage(); return 1; } esp_err_t error = local_display_frame_begin(); if (error != ESP_OK) { return report_error("Display row", error); } local_display_frame_clear_all(); for (uint8_t x = 0U; x < LOCAL_DISPLAY_WIDTH; ++x) { set_global_pixel(x, (uint8_t)row, true); } error = local_display_frame_end(); 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; } esp_err_t error = local_display_set_contrast((uint8_t)contrast); if (error != ESP_OK) { return report_error("Display contrast", error); } 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; } 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); } printf("Display inversion %s.\n", inverted ? "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; } /* Keep diagnostic output visible instead of immediately redrawing status pages. */ local_status_ui_hold_for_diagnostics(); 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_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"); } 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_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) { states[index] = (button_test_state_t){ .raw_level = gpio_get_level(s_buttons[index].gpio), .stable_level = 1, .raw_changed_us = now, }; } 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_buttons_ready) { return ESP_ERR_INVALID_STATE; } s_button_initialization_error = configure_buttons(); return s_button_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"); }