src.nth.io/

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 15:53:33 -0500
committerLuke Hoersten <[email protected]>2026-06-14 15:53:33 -0500
commitd5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2 (patch)
treef9b5e838061f76af8160863548758311857b1c75 /main
parent8d740665777af6d74b334e2dee98a53ebb9a29aa (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.txt4
-rw-r--r--main/app_main.c59
-rw-r--r--main/button.c27
-rw-r--r--main/display.c198
-rw-r--r--main/local_screens.c68
-rw-r--r--main/local_screens.h20
-rw-r--r--main/state_machine.c25
-rw-r--r--main/touch.c10
-rw-r--r--main/viewport_state.c4
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;