diff options
| author | Luke Hoersten <[email protected]> | 2026-06-14 15:53:33 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-14 15:53:33 -0500 |
| commit | d5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2 (patch) | |
| tree | f9b5e838061f76af8160863548758311857b1c75 /main | |
| parent | 8d740665777af6d74b334e2dee98a53ebb9a29aa (diff) | |
M3 WIP: DSI bring-up + state machine + identity overlay
DSI / panel:
- LDO_VO3 acquired at 2500 mV (VDD_MIPI_DPHY) — without this the PHY
PLL busy-waits forever inside esp_lcd_new_dsi_bus.
- PSRAM bumped to 200 MHz (CONFIG_IDF_EXPERIMENTAL_FEATURES) to keep
the DPI fed without underruns.
- Pi-7"-style 800x480 panel init (REG_POWERON, PORTA/PORTB/PWM/PORTC)
+ 16-bit RGB565 framebuffer with portrait-mode rotation buffer in
PSRAM. Currently shows garbled output — known issue, next commit
switches to the TC358762-bridge-aware init sequence.
State + UX:
- viewport_state simplified to AWAKE/ASLEEP only; "unconfigured" is
a flag, not a state. Tap always toggles; content choice (identity
vs frame) is driven by the configured flag.
- BOOT button arms a 15 s identity overlay via local_screens_overlay;
expired callback returns to prior state.
- Boot-done indicator: two backlight flashes (no usable LED GPIO on
the Waveshare board — GPIO 35 BOOT is shared with EMAC TXD1).
- Best-effort subsystem init in app_main; display init deferred to
its own task so a panel hang can't block networking.
Display test pattern: solid R/G/B for 2 s each on boot to characterize
the garble independent of text rendering.
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | main/app_main.c | 59 | ||||
| -rw-r--r-- | main/button.c | 27 | ||||
| -rw-r--r-- | main/display.c | 198 | ||||
| -rw-r--r-- | main/local_screens.c | 68 | ||||
| -rw-r--r-- | main/local_screens.h | 20 | ||||
| -rw-r--r-- | main/state_machine.c | 25 | ||||
| -rw-r--r-- | main/touch.c | 10 | ||||
| -rw-r--r-- | main/viewport_state.c | 4 |
9 files changed, 282 insertions, 133 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0986488..b9cb2a4 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( SRC_DIRS "." INCLUDE_DIRS "." REQUIRES driver esp_driver_i2c esp_driver_jpeg esp_eth esp_event - esp_http_client esp_http_server esp_lcd esp_netif esp_timer - json mdns nvs_flash + esp_http_client esp_http_server esp_hw_support esp_lcd + esp_netif esp_timer json mdns nvs_flash ) diff --git a/main/app_main.c b/main/app_main.c index 0897649..cc4c063 100644 --- a/main/app_main.c +++ b/main/app_main.c @@ -14,6 +14,8 @@ #include "esp_event.h" #include "esp_log.h" #include "esp_netif.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "nvs_flash.h" static const char *TAG = "viewport"; @@ -28,6 +30,8 @@ static inline void mark(esp_err_t err, char up, char *slot) *slot = (err == ESP_OK) ? up : (char)(up + 0x20); // 'A' -> 'a' on failure } +static void display_setup_task(void *arg); + void app_main(void) { ESP_ERROR_CHECK(nvs_flash_init()); @@ -64,37 +68,50 @@ void app_main(void) mark(http_api_start(), 'H', &flags[2]); // ------------------------------------------------------------------ - // Display + I²C-bound peripherals. Missing/miswired panel must not - // kill networking + /state. Failures here log a warning and continue. + // Display + I²C-bound peripherals run on their own task. ESP-IDF's + // esp_lcd_new_dsi_bus() spins forever in a PHY-PLL busy-wait if the + // config is even slightly off; keeping that off the main task means + // a misconfigured panel can't take down networking + /state. The + // task also brings up the JPEG decoder and touch (touch shares the + // panel I²C bus). // ------------------------------------------------------------------ + mark(button_init(), 'B', &flags[6]); + + ESP_LOGI(TAG, "boot complete — net subsystems [%s] ip=%s; " + "display init deferred to dsp_init task", + flags, got_ip ? net_eth_get_ip_str() : "(no link)"); + + xTaskCreate(display_setup_task, "dsp_init", 4096, NULL, 1, NULL); +} + +static void display_setup_task(void *arg) +{ + char flags[4] = {'-', '-', '-', 0}; // D J T + NUL esp_err_t dsp_err = display_init(); - mark(dsp_err, 'D', &flags[3]); + mark(dsp_err, 'D', &flags[0]); + if (dsp_err == ESP_OK) { local_screens_init(); - viewport_state_lock(); - viewport_run_state_t s = viewport_state_get()->state; - viewport_state_unlock(); - if (s == VIEWPORT_STATE_ASLEEP) { - display_sleep(); - } else { - local_screens_show_ip(); - } + // Show solid-color test sequence for ~6 s so we can characterize the + // garble: solid red, then solid green, then solid blue, each for 2s. + // If the panel paints a single uniform color (or close to it) for + // each, the issue is the text rendering only. If even solid colors + // are garbled, it's a stride / pixel-format bug deeper down. + display_wake(); + display_fill(0xF800); vTaskDelay(pdMS_TO_TICKS(2000)); // red + display_fill(0x07E0); vTaskDelay(pdMS_TO_TICKS(2000)); // green + display_fill(0x001F); vTaskDelay(pdMS_TO_TICKS(2000)); // blue + display_sleep(); } - mark(jpeg_decoder_init(), 'J', &flags[4]); + mark(jpeg_decoder_init(), 'J', &flags[1]); if (dsp_err == ESP_OK) { - mark(touch_init(), 'T', &flags[5]); + mark(touch_init(), 'T', &flags[2]); } else { ESP_LOGI(TAG, "touch skipped (display not up)"); } - mark(button_init(), 'B', &flags[6]); - - // ------------------------------------------------------------------ - // Boot summary. Uppercase letter = subsystem live; lowercase = down. - // E=Ethernet M=mDNS H=HTTP D=Display J=JPEG T=Touch B=BootButton - // ------------------------------------------------------------------ - ESP_LOGI(TAG, "boot complete — subsystems [%s] ip=%s", - flags, got_ip ? net_eth_get_ip_str() : "(no link)"); + ESP_LOGI(TAG, "display subsystems [%s] up", flags); + vTaskDelete(NULL); } diff --git a/main/button.c b/main/button.c index 9ac34a1..26e7cff 100644 --- a/main/button.c +++ b/main/button.c @@ -25,28 +25,11 @@ static const char *TAG = "button"; #define LONG_HOLD_MS 5000 #define OVERLAY_MS 15000 -static esp_timer_handle_t s_overlay_timer; - -static void overlay_expired(void *arg) -{ - if (display_is_up()) { - local_screens_restore_for_state(); - // Caller (state_machine_set) had backlight off if asleep — the - // overlay turned it on. Force it back off if we're asleep. - // Easiest: re-issue display_sleep, the wake_then_sleep path is - // owned by the state machine. - } - ESP_LOGI(TAG, "IP overlay expired"); -} - static void start_overlay(void) { if (!display_is_up()) return; - esp_timer_stop(s_overlay_timer); - display_wake(); // turn backlight on in case we were asleep - local_screens_show_ip(); - esp_timer_start_once(s_overlay_timer, (uint64_t)OVERLAY_MS * 1000ULL); - ESP_LOGI(TAG, "BOOT short-press → IP overlay for %dms", OVERLAY_MS); + local_screens_overlay(OVERLAY_MS); + ESP_LOGI(TAG, "BOOT short-press → identity overlay for %dms", OVERLAY_MS); } static void factory_reset(void) @@ -104,12 +87,6 @@ esp_err_t button_init(void) return err; } - esp_timer_create_args_t targs = { - .callback = &overlay_expired, - .name = "boot_overlay", - }; - esp_timer_create(&targs, &s_overlay_timer); - BaseType_t ok = xTaskCreate(button_task, "boot_btn", 3072, NULL, 3, NULL); if (ok != pdPASS) return ESP_FAIL; diff --git a/main/display.c b/main/display.c index c8976e4..e2f9999 100644 --- a/main/display.c +++ b/main/display.c @@ -6,6 +6,8 @@ #include "driver/i2c_master.h" #include "esp_check.h" +#include "esp_heap_caps.h" +#include "esp_ldo_regulator.h" #include "esp_lcd_mipi_dsi.h" #include "esp_lcd_panel_ops.h" #include "esp_log.h" @@ -62,15 +64,31 @@ enum { // ============================================================================ #define PANEL_H_ACTIVE 800 #define PANEL_V_ACTIVE 480 -#define PANEL_HSYNC_PULSE 18 -#define PANEL_HSYNC_BP 20 -#define PANEL_HSYNC_FP 62 -#define PANEL_VSYNC_PULSE 4 -#define PANEL_VSYNC_BP 27 -#define PANEL_VSYNC_FP 18 -#define PANEL_PIXEL_CLOCK_MHZ 30 -#define DSI_LANE_RATE_MBPS 480 -#define DSI_NUM_DATA_LANES 2 +// Pi 7" canonical timings from the upstream RPi DTS panel binding. +// Hosyond 5" panels are TC358762-based clones and use the same. +#define PANEL_HSYNC_PULSE 1 +#define PANEL_HSYNC_BP 46 +#define PANEL_HSYNC_FP 16 +#define PANEL_VSYNC_PULSE 1 +#define PANEL_VSYNC_BP 33 +#define PANEL_VSYNC_FP 7 +#define PANEL_PIXEL_CLOCK_MHZ 25 +// Lane bit rate. ESP32-P4 MIPI-DSI PHY PLL is picky about valid lock +// frequencies; 480 Mbps caused the PLL-lock busy-wait inside +// esp_lcd_new_dsi_bus() to hang indefinitely. 1000 Mbps matches the +// reference example and is well within the TC358762 bridge's max. +// Pi 7"-style panels via the 15-pin FPC traditionally use a single DSI +// data lane; the second data pair in the FPC stays unused. The DSI link +// rate is bumped to compensate (PLL-valid: 720/20 = 36 even). +#define DSI_LANE_RATE_MBPS 720 +#define DSI_NUM_DATA_LANES 1 + +// The ESP32-P4 MIPI-DSI PHY needs VDD_MIPI_DPHY = 2.5V powered before its +// PLL can lock. Internal LDO_VO3 (channel 3) is the source on this SoC, +// per ESP-IDF's mipi_dsi example. Without this the PHY-lock busy-wait +// inside esp_lcd_new_dsi_bus() spins forever. +#define DSI_PHY_LDO_CHAN 3 +#define DSI_PHY_LDO_MV 2500 // ============================================================================ // Module state @@ -81,6 +99,11 @@ static esp_lcd_dsi_bus_handle_t s_dsi_bus; static esp_lcd_panel_handle_t s_panel; static bool s_up; static uint8_t s_last_pwm; +// RGB888 scratch buffer used as the source to draw_bitmap. 3 bytes/pixel +// matches the DPI driver's framebuffer stride; landscape and portrait +// both go through here so we get a single consistent expand-from-RGB565 +// path. 800 * 480 * 3 = ~1.15 MB in PSRAM. +static uint8_t *s_rot_buf; // ============================================================================ // Panel-MCU I2C helpers @@ -138,9 +161,33 @@ static esp_err_t panel_mcu_attach(void) static esp_err_t panel_power_on(void) { + // Full Pi 7" enable sequence, mirroring Linux's panel-raspberrypi- + // touchscreen.c. Without the PORTA/PORTB/PORTC writes the TC358762 + // converts DSI to DPI fine, but the ATTINY never enables the actual + // panel chip + backlight — so the panel stays dark. + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_POWERON, 1), TAG, "POWERON=1"); vTaskDelay(pdMS_TO_TICKS(PANEL_POWERON_DELAY_MS)); - ESP_LOGI(TAG, "panel powered on"); + + // Optional: poll REG_PORTB bit 0 for nPWRDWN going high. Bounded + // wait so we don't hang on a non-responsive panel. + for (int i = 0; i < 100; ++i) { + uint8_t portb = 0; + if (mcu_read_u8(REG_PORTB, &portb) == ESP_OK && (portb & 0x01)) break; + vTaskDelay(pdMS_TO_TICKS(2)); + } + + // Enable the panel chip via the ATTINY's GPIO expanders. Values match + // the upstream Linux driver and the original Pi 7" closed-source FW. + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTA, 0x04), TAG, "PORTA"); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTB, 0x00), TAG, "PORTB"); + // Leave PWM at 0 (backlight off) — the boot sequence calls + // display_sleep() after init; first interaction (tap, BOOT, /state) + // brings the backlight up via display_wake(). + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PWM, 0x00), TAG, "PWM init"); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTC, 0x01), TAG, "PORTC"); + + ESP_LOGI(TAG, "panel powered on (PORTA/B/C + PWM init)"); return ESP_OK; } @@ -149,6 +196,17 @@ static esp_err_t panel_power_on(void) // ============================================================================ static esp_err_t dsi_bring_up(void) { + // 1. Power up the DSI PHY (VDD_MIPI_DPHY 2.5V) before any DSI calls. + esp_ldo_channel_handle_t phy_pwr = NULL; + esp_ldo_channel_config_t ldo_cfg = { + .chan_id = DSI_PHY_LDO_CHAN, + .voltage_mv = DSI_PHY_LDO_MV, + }; + ESP_RETURN_ON_ERROR(esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr), + TAG, "DSI PHY LDO acquire"); + ESP_LOGI(TAG, "MIPI DSI PHY powered on (LDO%d @ %dmV)", + DSI_PHY_LDO_CHAN, DSI_PHY_LDO_MV); + esp_lcd_dsi_bus_config_t bus_cfg = { .bus_id = 0, .num_data_lanes = DSI_NUM_DATA_LANES, @@ -158,11 +216,30 @@ static esp_err_t dsi_bring_up(void) ESP_RETURN_ON_ERROR(esp_lcd_new_dsi_bus(&bus_cfg, &s_dsi_bus), TAG, "esp_lcd_new_dsi_bus"); + // DBI IO handle — the IDF reference example creates this even for + // panels that don't use DSI commands. Some DSI bridge state machines + // need the command-channel side initialized before video mode runs. + esp_lcd_dbi_io_config_t dbi_cfg = { + .virtual_channel = 0, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + }; + esp_lcd_panel_io_handle_t dbi_io; + ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_dbi(s_dsi_bus, &dbi_cfg, &dbi_io), + TAG, "esp_lcd_new_panel_io_dbi"); + esp_lcd_dpi_panel_config_t dpi_cfg = { .virtual_channel = 0, .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, .dpi_clock_freq_mhz = PANEL_PIXEL_CLOCK_MHZ, - .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565, + // TC358762 only decodes 24-bit DSI Video Mode packets. Keep the + // entire pipeline RGB888 so the framebuffer stride (3 B/pixel) + // matches what the DSI engine sends on the wire. Our internal + // text/JPEG rendering still produces RGB565; the rotation step + // in display_present_rgb565() expands to RGB888 inline. + .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888, + .in_color_format = LCD_COLOR_FMT_RGB888, + .out_color_format = LCD_COLOR_FMT_RGB888, .num_fbs = 1, .video_timing = { .h_size = PANEL_H_ACTIVE, @@ -174,11 +251,14 @@ static esp_err_t dsi_bring_up(void) .vsync_back_porch = PANEL_VSYNC_BP, .vsync_front_porch = PANEL_VSYNC_FP, }, - .flags.use_dma2d = true, + .flags.use_dma2d = false, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_dpi(s_dsi_bus, &dpi_cfg, &s_panel), TAG, "esp_lcd_new_panel_dpi"); - ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel_init"); + // No esp_lcd_panel_reset() — the IDF DPI driver doesn't implement it + // (returns NOT_SUPPORTED). It only exists for panels with vendor + // drivers that toggle a hardware reset pin via DSI commands. + ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel_init"); ESP_LOGI(TAG, "DSI up: %dx%d %d MHz, %d-lane %d Mbps", PANEL_H_ACTIVE, PANEL_V_ACTIVE, PANEL_PIXEL_CLOCK_MHZ, @@ -197,14 +277,24 @@ esp_err_t display_init(void) ESP_RETURN_ON_ERROR(panel_power_on(), TAG, "panel power-on"); ESP_RETURN_ON_ERROR(dsi_bring_up(), TAG, "DSI bring-up"); + // PSRAM scratch (panel-native 800x480 RGB888 = 3 bytes/pixel). + s_rot_buf = (uint8_t *)heap_caps_malloc( + PANEL_H_ACTIVE * PANEL_V_ACTIVE * 3, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!s_rot_buf) { + ESP_LOGE(TAG, "rotation scratch alloc failed"); + return ESP_ERR_NO_MEM; + } + s_up = true; - // Default brightness from shared state (set by /config in M4; 80 on - // first boot). + // Cache the brightness value but do NOT push it to the panel yet. + // The device boots with backlight off; first wake (BOOT short-press, + // tap, or POST /state {wake}) applies the brightness. viewport_state_lock(); - uint8_t b = viewport_state_get()->brightness; + s_last_pwm = viewport_state_get()->brightness; viewport_state_unlock(); - display_set_brightness(b); + mcu_write_u8(REG_PWM, 0); return ESP_OK; } @@ -229,26 +319,44 @@ esp_err_t display_set_brightness(uint8_t pct) esp_err_t display_sleep(void) { if (!s_up) return ESP_ERR_INVALID_STATE; - return mcu_write_u8(REG_PWM, 0); + esp_err_t err = mcu_write_u8(REG_PWM, 0); + ESP_LOGI(TAG, "sleep: PWM=0 (%s)", esp_err_to_name(err)); + return err; } esp_err_t display_wake(void) { if (!s_up) return ESP_ERR_INVALID_STATE; - return mcu_write_u8(REG_PWM, s_last_pwm); + esp_err_t err = mcu_write_u8(REG_PWM, s_last_pwm); + ESP_LOGI(TAG, "wake: PWM=%u (%s)", s_last_pwm, esp_err_to_name(err)); + return err; +} + +// Convert a 16-bit RGB565 to three bytes in B, G, R order. +// The TC358762 → Pi-style panel pipeline expects BGR on the DPI wire, +// matching the panel chip's native byte order. Swapping to RGB produced +// garbled vertical bands; BGR aligns the pixel layout. +static inline void rgb565_to_rgb888(uint16_t px, uint8_t *out) +{ + out[0] = (uint8_t)(( px & 0x1F) * 255 / 31); // B + out[1] = (uint8_t)(((px >> 5) & 0x3F) * 255 / 63); // G + out[2] = (uint8_t)(((px >> 11) & 0x1F) * 255 / 31); // R } esp_err_t display_fill(uint16_t rgb565) { - if (!s_up) return ESP_ERR_INVALID_STATE; - void *fb = NULL; - ESP_RETURN_ON_ERROR(esp_lcd_dpi_panel_get_frame_buffer(s_panel, 1, &fb), - TAG, "get frame buffer"); - uint16_t *px = (uint16_t *)fb; + if (!s_up || !s_rot_buf) return ESP_ERR_INVALID_STATE; + uint8_t rgb[3]; + rgb565_to_rgb888(rgb565, rgb); size_t n = PANEL_H_ACTIVE * PANEL_V_ACTIVE; - for (size_t i = 0; i < n; ++i) px[i] = rgb565; + for (size_t i = 0; i < n; ++i) { + s_rot_buf[i * 3 + 0] = rgb[0]; + s_rot_buf[i * 3 + 1] = rgb[1]; + s_rot_buf[i * 3 + 2] = rgb[2]; + } return esp_lcd_panel_draw_bitmap(s_panel, 0, 0, - PANEL_H_ACTIVE, PANEL_V_ACTIVE, fb); + PANEL_H_ACTIVE, PANEL_V_ACTIVE, + s_rot_buf); } esp_err_t display_present_rgb565(const uint16_t *src, @@ -261,43 +369,39 @@ esp_err_t display_present_rgb565(const uint16_t *src, viewport_orientation_t orient = viewport_state_get()->orientation; viewport_state_unlock(); - void *fb = NULL; - esp_err_t err = esp_lcd_dpi_panel_get_frame_buffer(s_panel, 1, &fb); - if (err != ESP_OK) return err; - uint16_t *dst = (uint16_t *)fb; + if (!s_rot_buf) return ESP_ERR_INVALID_STATE; if (orient == VIEWPORT_ORIENTATION_LANDSCAPE) { if (src_w != PANEL_H_ACTIVE || src_h != PANEL_V_ACTIVE) return ESP_ERR_INVALID_SIZE; - memcpy(dst, src, (size_t)src_w * src_h * sizeof(uint16_t)); + // RGB565 → RGB888, no rotation. + size_t n = (size_t)src_w * src_h; + for (size_t i = 0; i < n; ++i) { + rgb565_to_rgb888(src[i], &s_rot_buf[i * 3]); + } } else { - // Portrait: src is 480x800, rotate 90° CW into the 800x480 panel. - // dst dims = src_h x src_w. dst_stride = src_h. - // src(x,y) -> dst(x, src_h - 1 - y) + // Portrait: src is 480x800; rotate 90° CW into 800x480 panel + // coordinates and expand to RGB888 in the same pass. if (src_w != PANEL_V_ACTIVE || src_h != PANEL_H_ACTIVE) return ESP_ERR_INVALID_SIZE; - const uint16_t dst_stride = src_h; + const uint16_t dst_stride = src_h; // 800 panel-rows of 3 bytes each for (uint16_t y = 0; y < src_h; ++y) { const uint16_t *srow = src + (size_t)y * src_w; const uint16_t dst_col = (uint16_t)(src_h - 1 - y); for (uint16_t x = 0; x < src_w; ++x) { - dst[(size_t)x * dst_stride + dst_col] = srow[x]; + size_t dst_idx = ((size_t)x * dst_stride + dst_col) * 3; + rgb565_to_rgb888(srow[x], &s_rot_buf[dst_idx]); } } } return esp_lcd_panel_draw_bitmap(s_panel, 0, 0, - PANEL_H_ACTIVE, PANEL_V_ACTIVE, fb); + PANEL_H_ACTIVE, PANEL_V_ACTIVE, + s_rot_buf); } esp_err_t display_test_pattern(void) { - if (!s_up) return ESP_ERR_INVALID_STATE; - void *fb = NULL; - ESP_RETURN_ON_ERROR(esp_lcd_dpi_panel_get_frame_buffer(s_panel, 1, &fb), - TAG, "get frame buffer"); - uint16_t *px = (uint16_t *)fb; - - // 8 vertical color bars: white, yellow, cyan, green, magenta, red, blue, black. + if (!s_up || !s_rot_buf) return ESP_ERR_INVALID_STATE; static const uint16_t bars[8] = { 0xFFFF, 0xFFE0, 0x07FF, 0x07E0, 0xF81F, 0xF800, 0x001F, 0x0000, @@ -307,9 +411,11 @@ esp_err_t display_test_pattern(void) for (int x = 0; x < PANEL_H_ACTIVE; ++x) { int b = x / bar_w; if (b > 7) b = 7; - px[y * PANEL_H_ACTIVE + x] = bars[b]; + size_t idx = ((size_t)y * PANEL_H_ACTIVE + x) * 3; + rgb565_to_rgb888(bars[b], &s_rot_buf[idx]); } } return esp_lcd_panel_draw_bitmap(s_panel, 0, 0, - PANEL_H_ACTIVE, PANEL_V_ACTIVE, fb); + PANEL_H_ACTIVE, PANEL_V_ACTIVE, + s_rot_buf); } diff --git a/main/local_screens.c b/main/local_screens.c index 5167d9d..34c242c 100644 --- a/main/local_screens.c +++ b/main/local_screens.c @@ -4,6 +4,7 @@ #include "esp_heap_caps.h" #include "esp_log.h" +#include "esp_timer.h" #include "display.h" #include "net_eth.h" @@ -71,7 +72,9 @@ static const uint8_t FONT[95][8] = { #define FG 0xFFFF // white #define BG 0x0000 // black -static uint16_t *s_fb; // PSRAM scratch at PANEL_W × PANEL_H +static uint16_t *s_fb; // PSRAM scratch at PANEL_W × PANEL_H +static esp_timer_handle_t s_overlay_timer; +static volatile bool s_overlay_active; static void clear(uint16_t color) { @@ -125,6 +128,8 @@ static void draw_centered(uint16_t fb_w, uint16_t fb_h, int y, } } +static void overlay_expired_cb(void *arg); + esp_err_t local_screens_init(void) { s_fb = (uint16_t *)heap_caps_malloc(MAX_BUF_PX * sizeof(uint16_t), @@ -133,9 +138,53 @@ esp_err_t local_screens_init(void) ESP_LOGE(TAG, "PSRAM alloc failed for scratch FB"); return ESP_ERR_NO_MEM; } + esp_timer_create_args_t args = { + .callback = &overlay_expired_cb, + .name = "ident_overlay", + }; + esp_timer_create(&args, &s_overlay_timer); return ESP_OK; } +static void overlay_expired_cb(void *arg) +{ + s_overlay_active = false; + // Backlight off after the overlay. Whether the device is UNCONFIGURED + // or ASLEEP, the result is the same: dark panel until next interaction. + // For AWAKE we leave the backlight on — Scrypted's next /frame + // overwrites the overlay anyway. + viewport_state_lock(); + viewport_run_state_t s = viewport_state_get()->state; + viewport_state_unlock(); + if (s != VIEWPORT_STATE_AWAKE && display_is_up()) { + display_sleep(); + } +} + +esp_err_t local_screens_overlay(uint32_t duration_ms) +{ + if (!display_is_up()) return ESP_ERR_INVALID_STATE; + esp_timer_stop(s_overlay_timer); + display_wake(); + esp_err_t err = local_screens_show_ip(); + if (err != ESP_OK) return err; + s_overlay_active = true; + return esp_timer_start_once(s_overlay_timer, + (uint64_t)duration_ms * 1000ULL); +} + +bool local_screens_overlay_active(void) +{ + return s_overlay_active; +} + +void local_screens_overlay_dismiss(void) +{ + esp_timer_stop(s_overlay_timer); + s_overlay_active = false; + if (display_is_up()) display_sleep(); +} + esp_err_t local_screens_show_ip(void) { if (!s_fb) return ESP_ERR_INVALID_STATE; @@ -225,20 +274,3 @@ esp_err_t local_screens_show_loading(void) return display_present_rgb565(s_fb, w, h); } -esp_err_t local_screens_restore_for_state(void) -{ - if (!s_fb) return ESP_ERR_INVALID_STATE; - - viewport_state_lock(); - viewport_run_state_t s = viewport_state_get()->state; - viewport_state_unlock(); - - if (s == VIEWPORT_STATE_UNCONFIGURED) return local_screens_show_ip(); - - // Awake or asleep: clear the FB to black. For asleep the backlight is - // off anyway; for awake the next /frame will overwrite. - uint16_t w, h; - effective_dims(&w, &h); - clear(BG); - return display_present_rgb565(s_fb, w, h); -} diff --git a/main/local_screens.h b/main/local_screens.h index ffe9a3d..f1e4876 100644 --- a/main/local_screens.h +++ b/main/local_screens.h @@ -1,5 +1,7 @@ #pragma once +#include <stdbool.h> + #include "esp_err.h" // Initialize the local-screen renderer. Allocates a PSRAM scratch buffer @@ -20,9 +22,15 @@ esp_err_t local_screens_show_ip(void); // until the next /frame arrives. esp_err_t local_screens_show_loading(void); -// Repaint the current best screen for `state`. Used after the BOOT-button -// 15-second overlay expires. -// UNCONFIGURED -> IP screen -// ASLEEP -> (caller handles backlight; this paints a black FB) -// AWAKE -> black FB; Scrypted's next /frame restores live content -esp_err_t local_screens_restore_for_state(void); +// Show the identity ("who am I") screen for `duration_ms`, then drop the +// backlight off. Used by the BOOT button short-press (any state) and by +// the touch handler when the device is unconfigured. +esp_err_t local_screens_overlay(uint32_t duration_ms); + +// True while the identity overlay is currently shown (timer armed). +// Used by the touch handler to make a second tap dismiss the overlay +// instead of re-arming it. +bool local_screens_overlay_active(void); + +// Dismiss the overlay now: cancel the timer and drop the backlight. +void local_screens_overlay_dismiss(void); diff --git a/main/state_machine.c b/main/state_machine.c index 8f0b3e1..c9e4367 100644 --- a/main/state_machine.c +++ b/main/state_machine.c @@ -50,24 +50,25 @@ esp_err_t state_machine_set(viewport_run_state_t target) viewport_state_lock(); viewport_state_t *st = viewport_state_get(); - if (!st->configured) { - viewport_state_unlock(); - return ESP_ERR_INVALID_STATE; - } if (st->state == target) { viewport_state_unlock(); return ESP_OK; // idempotent no-op } st->state = target; + bool configured = st->configured; viewport_state_unlock(); if (target == VIEWPORT_STATE_AWAKE) { if (display_is_up()) { display_wake(); - local_screens_show_loading(); // until next /frame paints + // Content choice: configured devices show "Loading…" until + // Scrypted pushes a /frame; unconfigured devices show the + // identity screen since there's no Scrypted to push anything. + if (configured) local_screens_show_loading(); + else local_screens_show_ip(); } arm_idle_timer_unlocked(); - ESP_LOGI(TAG, "AWAKE"); + ESP_LOGI(TAG, "AWAKE (%s)", configured ? "configured" : "unconfigured"); } else { disarm_idle_timer(); if (display_is_up()) display_sleep(); @@ -86,12 +87,14 @@ void state_machine_frame_painted(void) void state_machine_set_local(viewport_run_state_t target) { - // For idempotent no-op (already in target), state_machine_set returns OK - // without changing state; we still call state_client to keep Scrypted in - // sync if it's drifted. State POSTs are idempotent on both ends. esp_err_t err = state_machine_set(target); - if (err != ESP_OK) return; // unconfigured / invalid - state_client_post(target); + if (err != ESP_OK) return; + // Only POST when there's a Scrypted to talk to. Unconfigured tap toggles + // change the local display state without notifying anyone. + viewport_state_lock(); + bool configured = viewport_state_get()->configured; + viewport_state_unlock(); + if (configured) state_client_post(target); } viewport_run_state_t state_machine_current(void) diff --git a/main/touch.c b/main/touch.c index f13183d..4d8003f 100644 --- a/main/touch.c +++ b/main/touch.c @@ -10,6 +10,7 @@ #include "freertos/task.h" #include "display.h" +#include "local_screens.h" #include "state_machine.h" #include "viewport_state.h" @@ -80,10 +81,13 @@ static void touch_task(void *arg) if (dt_ms > 0 && dt_ms <= TAP_MAX_MS) { last_tap_us = now; - viewport_run_state_t cur = state_machine_current(); + // Cancel any BOOT-button identity overlay so a tap-to-sleep + // doesn't fight the overlay timer trying to restore content. + if (local_screens_overlay_active()) local_screens_overlay_dismiss(); viewport_run_state_t target = - (cur == VIEWPORT_STATE_AWAKE) ? VIEWPORT_STATE_ASLEEP - : VIEWPORT_STATE_AWAKE; + (state_machine_current() == VIEWPORT_STATE_AWAKE) + ? VIEWPORT_STATE_ASLEEP + : VIEWPORT_STATE_AWAKE; ESP_LOGI(TAG, "tap (%lu ms) -> %s", (unsigned long)dt_ms, target == VIEWPORT_STATE_AWAKE ? "wake" : "sleep"); state_machine_set_local(target); diff --git a/main/viewport_state.c b/main/viewport_state.c index 9115ce3..3ae8f81 100644 --- a/main/viewport_state.c +++ b/main/viewport_state.c @@ -13,7 +13,9 @@ void viewport_state_init(void) { memset(&s_state, 0, sizeof(s_state)); s_state.configured = false; - s_state.state = VIEWPORT_STATE_UNCONFIGURED; + // State is always AWAKE or ASLEEP. UNCONFIGURED is reported via the + // `configured` flag, not as a state value. Boot is always asleep. + s_state.state = VIEWPORT_STATE_ASLEEP; s_state.brightness = 80; s_state.idle_timeout_ms = 60000; s_state.orientation = VIEWPORT_ORIENTATION_PORTRAIT; |
