docs: update README with ADR-045–048, Observatory, adaptive classifier, AMOLED display

- Update ADR count from 44 to 48
- Add adaptive classifier (ADR-048) to Intelligence features
- Add Observatory visualization (ADR-047) and AMOLED display (ADR-045) to Deployment features
- Update screenshot to v2-screen.png
- Add ADR-045 (AMOLED), ADR-046 (Android TV), ADR-047 (Observatory), DDD deployment model
- Add AMOLED display firmware (display_hal, display_task, display_ui, LVGL config)
- Add Observatory UI (13 Three.js modules, CSS, HTML entry point)
- Add trained adaptive model JSON
- Update .gitignore for managed_components, recordings, .swarm

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-03-05 10:20:48 -05:00
parent 5fa61ba7ea
commit 8b57a6f64c
35 changed files with 8674 additions and 7 deletions

View file

@ -1,6 +1,19 @@
idf_component_register(
SRCS "main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c"
"edge_processing.c" "ota_update.c" "power_mgmt.c"
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
INCLUDE_DIRS "."
set(SRCS
"main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c"
"edge_processing.c" "ota_update.c" "power_mgmt.c"
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
)
set(REQUIRES "")
# ADR-045: AMOLED display support (compile-time optional)
if(CONFIG_DISPLAY_ENABLE)
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
set(REQUIRES esp_lcd esp_lcd_touch lvgl)
endif()
idf_component_register(
SRCS ${SRCS}
INCLUDE_DIRS "."
REQUIRES ${REQUIRES}
)

View file

@ -85,6 +85,87 @@ menu "Edge Intelligence (ADR-039)"
endmenu
menu "AMOLED Display (ADR-045)"
config DISPLAY_ENABLE
bool "Enable AMOLED display support"
default y
help
Enable RM67162 QSPI AMOLED display and LVGL UI.
Auto-detects at boot; gracefully skips if no display hardware.
Requires SPIRAM for frame buffers.
config DISPLAY_FPS_LIMIT
int "Display refresh rate limit (FPS)"
default 30
range 10 60
depends on DISPLAY_ENABLE
help
Maximum display refresh rate. Lower values save CPU.
config DISPLAY_BRIGHTNESS
int "Default backlight brightness (%)"
default 80
range 0 100
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_CS
int "QSPI CS GPIO"
default 6
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_CLK
int "QSPI CLK GPIO"
default 47
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_D0
int "QSPI D0 GPIO"
default 18
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_D1
int "QSPI D1 GPIO"
default 7
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_D2
int "QSPI D2 GPIO"
default 48
depends on DISPLAY_ENABLE
config DISPLAY_QSPI_D3
int "QSPI D3 GPIO"
default 5
depends on DISPLAY_ENABLE
config DISPLAY_TOUCH_SDA
int "Touch I2C SDA GPIO"
default 3
depends on DISPLAY_ENABLE
config DISPLAY_TOUCH_SCL
int "Touch I2C SCL GPIO"
default 2
depends on DISPLAY_ENABLE
config DISPLAY_TOUCH_INT
int "Touch INT GPIO"
default 21
depends on DISPLAY_ENABLE
config DISPLAY_TOUCH_RST
int "Touch RST GPIO"
default 17
depends on DISPLAY_ENABLE
config DISPLAY_BL_PIN
int "Backlight PWM GPIO"
default 38
depends on DISPLAY_ENABLE
endmenu
menu "WASM Programmable Sensing (ADR-040)"
config WASM_ENABLE

View file

@ -0,0 +1,382 @@
/**
* @file display_hal.c
* @brief ADR-045: SH8601 QSPI AMOLED HAL for Waveshare ESP32-S3-Touch-AMOLED-1.8.
*
* Uses ESP-IDF esp_lcd_panel_io_spi in QSPI mode (quad_mode=true, lcd_cmd_bits=32).
* The panel_io layer handles the 0x02/0x32 QSPI command encoding.
*
* Hardware: SH8601 368x448, FT3168 touch, TCA9554 I/O expander for power/reset.
*
* Pin assignments (Waveshare ESP32-S3-Touch-AMOLED-1.8):
* QSPI: CS=12, CLK=11, D0=4, D1=5, D2=6, D3=7
* I2C: SDA=15, SCL=14 (shared: touch FT3168 + TCA9554 expander)
* Touch INT=21
*/
#include "display_hal.h"
#include "sdkconfig.h"
#if CONFIG_DISPLAY_ENABLE
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_lcd_panel_io.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "esp_heap_caps.h"
static const char *TAG = "disp_hal";
/* ---- QSPI Pin Definitions (Waveshare board) ---- */
#define DISP_QSPI_CS 12
#define DISP_QSPI_CLK 11
#define DISP_QSPI_D0 4
#define DISP_QSPI_D1 5
#define DISP_QSPI_D2 6
#define DISP_QSPI_D3 7
/* ---- I2C (shared: touch + TCA9554 expander) ---- */
#define I2C_SDA 15
#define I2C_SCL 14
#define TOUCH_INT_PIN 21
#define I2C_MASTER_NUM I2C_NUM_0
#define I2C_MASTER_FREQ_HZ 400000
/* ---- TCA9554 I/O expander ---- */
#define TCA9554_ADDR 0x20
#define TCA9554_REG_OUTPUT 0x01
#define TCA9554_REG_CONFIG 0x03
/* ---- FT3168 touch controller ---- */
#define FT3168_ADDR 0x38
/* ---- Display dimensions ---- */
#define DISP_H_RES 368
#define DISP_V_RES 448
/* ---- QSPI opcodes (packed into lcd_cmd bits [31:24]) ---- */
#define LCD_OPCODE_WRITE_CMD 0x02
#define LCD_OPCODE_WRITE_COLOR 0x32
/* ---- State ---- */
static esp_lcd_panel_io_handle_t s_io_handle = NULL;
static bool s_i2c_initialized = false;
static bool s_touch_initialized = false;
/* ---- I2C helpers ---- */
static esp_err_t i2c_write_reg(uint8_t dev_addr, uint8_t reg, const uint8_t *data, size_t len)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
if (data && len > 0) {
i2c_master_write(cmd, data, len, true);
}
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}
static esp_err_t i2c_read_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, size_t len)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);
i2c_master_read(cmd, data, len, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}
static esp_err_t init_i2c_bus(void)
{
if (s_i2c_initialized) return ESP_OK;
i2c_config_t i2c_cfg = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_SDA,
.scl_io_num = I2C_SCL,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t ret = i2c_param_config(I2C_MASTER_NUM, &i2c_cfg);
if (ret != ESP_OK) return ret;
ret = i2c_driver_install(I2C_MASTER_NUM, I2C_MODE_MASTER, 0, 0, 0);
if (ret != ESP_OK) return ret;
s_i2c_initialized = true;
ESP_LOGI(TAG, "I2C bus init OK (SDA=%d, SCL=%d)", I2C_SDA, I2C_SCL);
return ESP_OK;
}
/* ---- TCA9554 I/O expander: toggle pins for display power/reset ---- */
static esp_err_t tca9554_init_display_power(void)
{
/* Set pins 0, 1, 2 as outputs */
uint8_t cfg = 0xF8;
esp_err_t ret = i2c_write_reg(TCA9554_ADDR, TCA9554_REG_CONFIG, &cfg, 1);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "TCA9554 not found at 0x%02X: %s", TCA9554_ADDR, esp_err_to_name(ret));
return ret;
}
/* Set pins 0,1,2 LOW (reset state) */
uint8_t out = 0x00;
i2c_write_reg(TCA9554_ADDR, TCA9554_REG_OUTPUT, &out, 1);
vTaskDelay(pdMS_TO_TICKS(200));
/* Set pins 0,1,2 HIGH (power on + release reset) */
out = 0x07;
i2c_write_reg(TCA9554_ADDR, TCA9554_REG_OUTPUT, &out, 1);
vTaskDelay(pdMS_TO_TICKS(200));
ESP_LOGI(TAG, "TCA9554 display power/reset toggled");
return ESP_OK;
}
/* ---- Panel IO helpers: send commands via esp_lcd QSPI panel IO ---- */
static esp_err_t panel_write_cmd(uint8_t dcs_cmd, const void *data, size_t data_len)
{
/* Pack as 32-bit lcd_cmd: [31:24]=opcode, [23:8]=dcs_cmd, [7:0]=0 */
uint32_t lcd_cmd = ((uint32_t)LCD_OPCODE_WRITE_CMD << 24) | ((uint32_t)dcs_cmd << 8);
return esp_lcd_panel_io_tx_param(s_io_handle, (int)lcd_cmd, data, data_len);
}
static esp_err_t panel_write_color(const void *color_data, size_t data_len)
{
/* RAMWR (0x2C) packed as 32-bit lcd_cmd with quad opcode */
uint32_t lcd_cmd = ((uint32_t)LCD_OPCODE_WRITE_COLOR << 24) | (0x2C << 8);
return esp_lcd_panel_io_tx_color(s_io_handle, (int)lcd_cmd, color_data, data_len);
}
/* ---- SH8601 init sequence (from Waveshare reference) ---- */
typedef struct {
uint8_t cmd;
uint8_t data[4];
uint8_t data_len;
uint16_t delay_ms;
} sh8601_init_cmd_t;
static const sh8601_init_cmd_t sh8601_init_cmds[] = {
{0x11, {0x00}, 0, 120}, /* Sleep Out + 120ms */
{0x44, {0x01, 0xD1}, 2, 0}, /* Partial area */
{0x35, {0x00}, 1, 0}, /* Tearing Effect ON */
{0x53, {0x20}, 1, 10}, /* Write CTRL Display */
{0x2A, {0x00, 0x00, 0x01, 0x6F}, 4, 0}, /* CASET: 0-367 */
{0x2B, {0x00, 0x00, 0x01, 0xBF}, 4, 0}, /* RASET: 0-447 */
{0x51, {0x00}, 1, 10}, /* Brightness: 0 */
{0x29, {0x00}, 0, 10}, /* Display ON */
{0x51, {0xFF}, 1, 0}, /* Brightness: max */
{0x00, {0x00}, 0xFF, 0}, /* End sentinel */
};
static esp_err_t send_init_sequence(void)
{
for (int i = 0; sh8601_init_cmds[i].data_len != 0xFF; i++) {
const sh8601_init_cmd_t *cmd = &sh8601_init_cmds[i];
esp_err_t ret = panel_write_cmd(
cmd->cmd,
cmd->data_len > 0 ? cmd->data : NULL,
cmd->data_len);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "CMD 0x%02X failed: %s", cmd->cmd, esp_err_to_name(ret));
return ret;
}
if (cmd->delay_ms > 0) {
vTaskDelay(pdMS_TO_TICKS(cmd->delay_ms));
}
}
return ESP_OK;
}
/* ---- Public API ---- */
esp_err_t display_hal_init_panel(void)
{
ESP_LOGI(TAG, "Initializing Waveshare AMOLED 1.8\" (SH8601 368x448)...");
/* Step 1: Init I2C bus */
esp_err_t ret = init_i2c_bus();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "I2C bus init failed");
return ESP_ERR_NOT_FOUND;
}
/* Step 2: TCA9554 display power/reset (optional — only present on Waveshare board) */
ret = tca9554_init_display_power();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "TCA9554 not found — assuming display power is always-on (direct wiring)");
/* Continue without TCA9554 — the display may be powered directly */
}
/* Step 3: Initialize SPI bus */
spi_bus_config_t bus_cfg = {
.sclk_io_num = DISP_QSPI_CLK,
.data0_io_num = DISP_QSPI_D0,
.data1_io_num = DISP_QSPI_D1,
.data2_io_num = DISP_QSPI_D2,
.data3_io_num = DISP_QSPI_D3,
.max_transfer_sz = DISP_H_RES * DISP_V_RES * 2,
};
ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
return ESP_ERR_NOT_FOUND;
}
/* Step 4: Create panel IO with QSPI mode */
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = -1, /* No DC pin in QSPI mode */
.cs_gpio_num = DISP_QSPI_CS,
.pclk_hz = 40 * 1000 * 1000,
.lcd_cmd_bits = 32, /* 32-bit command: [opcode|dcs_cmd|0x00] */
.lcd_param_bits = 8,
.spi_mode = 0,
.trans_queue_depth = 10,
.flags = {
.quad_mode = true,
},
};
ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &s_io_handle);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Panel IO init failed: %s", esp_err_to_name(ret));
spi_bus_free(SPI2_HOST);
return ESP_ERR_NOT_FOUND;
}
ESP_LOGI(TAG, "QSPI panel IO created (40MHz, quad mode)");
/* Step 5: Send SH8601 init sequence */
ret = send_init_sequence();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "SH8601 init sequence failed");
esp_lcd_panel_io_del(s_io_handle);
spi_bus_free(SPI2_HOST);
s_io_handle = NULL;
return ESP_ERR_NOT_FOUND;
}
/* Step 6: Draw test pattern — cyan bar at top */
ESP_LOGI(TAG, "Drawing test pattern...");
uint16_t *line_buf = heap_caps_malloc(DISP_H_RES * 2, MALLOC_CAP_DMA);
if (line_buf) {
uint8_t caset[4] = {0, 0, (DISP_H_RES - 1) >> 8, (DISP_H_RES - 1) & 0xFF};
uint8_t raset[4] = {0, 0, (DISP_V_RES - 1) >> 8, (DISP_V_RES - 1) & 0xFF};
panel_write_cmd(0x2A, caset, 4);
panel_write_cmd(0x2B, raset, 4);
for (int y = 0; y < DISP_V_RES; y++) {
uint16_t color = (y < 30) ? 0x07FF : 0x0841;
for (int x = 0; x < DISP_H_RES; x++) {
line_buf[x] = color;
}
panel_write_color(line_buf, DISP_H_RES * 2);
}
free(line_buf);
ESP_LOGI(TAG, "Test pattern drawn");
}
ESP_LOGI(TAG, "SH8601 panel init OK (%dx%d)", DISP_H_RES, DISP_V_RES);
return ESP_OK;
}
void display_hal_draw(int x_start, int y_start, int x_end, int y_end,
const void *color_data)
{
if (!s_io_handle) return;
/* SH8601 requires coordinates divisible by 2 */
x_start &= ~1;
y_start &= ~1;
if (x_end & 1) x_end++;
if (y_end & 1) y_end++;
if (x_end > DISP_H_RES) x_end = DISP_H_RES;
if (y_end > DISP_V_RES) y_end = DISP_V_RES;
uint8_t caset[4] = {
(x_start >> 8) & 0xFF, x_start & 0xFF,
((x_end - 1) >> 8) & 0xFF, (x_end - 1) & 0xFF,
};
panel_write_cmd(0x2A, caset, 4);
uint8_t raset[4] = {
(y_start >> 8) & 0xFF, y_start & 0xFF,
((y_end - 1) >> 8) & 0xFF, (y_end - 1) & 0xFF,
};
panel_write_cmd(0x2B, raset, 4);
size_t len = (x_end - x_start) * (y_end - y_start) * 2;
panel_write_color(color_data, len);
}
esp_err_t display_hal_init_touch(void)
{
ESP_LOGI(TAG, "Probing FT3168 touch controller...");
if (!s_i2c_initialized) {
esp_err_t ret = init_i2c_bus();
if (ret != ESP_OK) return ESP_ERR_NOT_FOUND;
}
gpio_config_t int_cfg = {
.pin_bit_mask = (1ULL << TOUCH_INT_PIN),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&int_cfg);
uint8_t chip_id = 0;
esp_err_t ret = i2c_read_reg(FT3168_ADDR, 0xA8, &chip_id, 1);
if (ret != ESP_OK || chip_id == 0x00 || chip_id == 0xFF) {
ESP_LOGW(TAG, "FT3168 not found (ret=%s, id=0x%02X)", esp_err_to_name(ret), chip_id);
return ESP_ERR_NOT_FOUND;
}
s_touch_initialized = true;
ESP_LOGI(TAG, "FT3168 touch init OK (chip_id=0x%02X)", chip_id);
return ESP_OK;
}
bool display_hal_touch_read(uint16_t *x, uint16_t *y)
{
if (!s_touch_initialized) return false;
uint8_t buf[7] = {0};
esp_err_t ret = i2c_read_reg(FT3168_ADDR, 0x01, buf, 7);
if (ret != ESP_OK) return false;
uint8_t num_points = buf[1];
if (num_points == 0 || num_points > 2) return false;
*x = ((buf[2] & 0x0F) << 8) | buf[3];
*y = ((buf[4] & 0x0F) << 8) | buf[5];
return true;
}
void display_hal_set_brightness(uint8_t percent)
{
if (!s_io_handle) return;
if (percent > 100) percent = 100;
uint8_t val = (uint8_t)((uint32_t)percent * 255 / 100);
panel_write_cmd(0x51, &val, 1);
}
#endif /* CONFIG_DISPLAY_ENABLE */

View file

@ -0,0 +1,71 @@
/**
* @file display_hal.h
* @brief ADR-045: RM67162 QSPI AMOLED + CST816S touch HAL.
*
* Hardware abstraction for the LilyGO T-Display-S3 AMOLED panel.
* Probes hardware at boot; returns ESP_ERR_NOT_FOUND if absent.
*/
#ifndef DISPLAY_HAL_H
#define DISPLAY_HAL_H
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Probe and initialize the RM67162 QSPI AMOLED panel.
*
* Configures QSPI bus, sends panel init sequence, and fills
* the screen with dark background to confirm it works.
* Returns ESP_ERR_NOT_FOUND if the panel does not respond.
*
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no display detected.
*/
esp_err_t display_hal_init_panel(void);
/**
* Draw a rectangle of pixels to the AMOLED.
* Sends CASET + RASET + RAMWR directly via QSPI.
*
* @param x_start Left column (inclusive).
* @param y_start Top row (inclusive).
* @param x_end Right column (exclusive).
* @param y_end Bottom row (exclusive).
* @param color_data RGB565 pixel data, (x_end-x_start)*(y_end-y_start) pixels.
*/
void display_hal_draw(int x_start, int y_start, int x_end, int y_end,
const void *color_data);
/**
* Probe and initialize the CST816S capacitive touch controller.
*
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no touch IC detected.
*/
esp_err_t display_hal_init_touch(void);
/**
* Read touch point (non-blocking).
*
* @param[out] x Touch X coordinate (0..535).
* @param[out] y Touch Y coordinate (0..239).
* @return true if touch is active, false if released.
*/
bool display_hal_touch_read(uint16_t *x, uint16_t *y);
/**
* Set AMOLED brightness via MIPI DCS command.
*
* @param percent Brightness 0-100.
*/
void display_hal_set_brightness(uint8_t percent);
#ifdef __cplusplus
}
#endif
#endif /* DISPLAY_HAL_H */

View file

@ -0,0 +1,169 @@
/**
* @file display_task.c
* @brief ADR-045: FreeRTOS display task LVGL pump on Core 0, priority 1.
*
* Gracefully skips if RM67162 panel or SPIRAM is absent.
* Reads from edge_get_vitals() / edge_get_multi_person() (thread-safe).
*/
#include "display_task.h"
#include "sdkconfig.h"
#if CONFIG_DISPLAY_ENABLE
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_heap_caps.h"
#include "lvgl.h"
#include "display_hal.h"
#include "display_ui.h"
#define DISP_H_RES 368
#define DISP_V_RES 448
static const char *TAG = "disp_task";
/* ---- Config ---- */
#ifdef CONFIG_DISPLAY_FPS_LIMIT
#define DISP_FPS_LIMIT CONFIG_DISPLAY_FPS_LIMIT
#else
#define DISP_FPS_LIMIT 30
#endif
#define DISP_TASK_STACK (8 * 1024)
#define DISP_TASK_PRIORITY 1
#define DISP_TASK_CORE 0
#define DISP_BUF_LINES 40
/* ---- LVGL flush callback — calls display_hal_draw directly ---- */
static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
{
display_hal_draw(area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_p);
lv_disp_flush_ready(drv);
}
/* ---- LVGL touch input callback ---- */
static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
uint16_t x, y;
if (display_hal_touch_read(&x, &y)) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PRESSED;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}
/* ---- Display task ---- */
static void display_task(void *arg)
{
const TickType_t frame_period = pdMS_TO_TICKS(1000 / DISP_FPS_LIMIT);
ESP_LOGI(TAG, "Display task running on Core %d, %d fps limit",
xPortGetCoreID(), DISP_FPS_LIMIT);
display_ui_create(lv_scr_act());
TickType_t last_wake = xTaskGetTickCount();
while (1) {
display_ui_update();
lv_timer_handler();
vTaskDelayUntil(&last_wake, frame_period);
}
}
/* ---- Public API ---- */
esp_err_t display_task_start(void)
{
ESP_LOGI(TAG, "Initializing display subsystem...");
bool use_psram = false;
#if CONFIG_SPIRAM
size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
if (psram_free >= 64 * 1024) {
use_psram = true;
ESP_LOGI(TAG, "PSRAM available: %u KB — using PSRAM buffers", (unsigned)(psram_free / 1024));
} else {
ESP_LOGW(TAG, "PSRAM too small (%u bytes) — falling back to internal DMA memory", (unsigned)psram_free);
}
#else
ESP_LOGW(TAG, "SPIRAM not enabled — using internal DMA memory (smaller buffers)");
#endif
/* Probe display hardware */
esp_err_t ret = display_hal_init_panel();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Display not available — running headless");
return ESP_OK;
}
/* Init touch (optional) */
esp_err_t touch_ret = display_hal_init_touch();
/* Initialize LVGL */
lv_init();
/* Double-buffered draw buffers — prefer PSRAM, fall back to internal DMA */
size_t buf_lines = use_psram ? DISP_BUF_LINES : 10; /* Smaller buffers without PSRAM */
size_t buf_size = DISP_H_RES * buf_lines * sizeof(lv_color_t);
uint32_t alloc_caps = use_psram ? MALLOC_CAP_SPIRAM : (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
lv_color_t *buf1 = heap_caps_malloc(buf_size, alloc_caps);
lv_color_t *buf2 = heap_caps_malloc(buf_size, alloc_caps);
if (!buf1 || !buf2) {
ESP_LOGE(TAG, "Failed to allocate LVGL buffers (%u bytes, caps=0x%lx)",
(unsigned)buf_size, (unsigned long)alloc_caps);
if (buf1) free(buf1);
if (buf2) free(buf2);
return ESP_OK;
}
ESP_LOGI(TAG, "LVGL buffers: 2x %u bytes (%u lines, %s)",
(unsigned)buf_size, (unsigned)buf_lines, use_psram ? "PSRAM" : "internal DMA");
static lv_disp_draw_buf_t draw_buf;
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_H_RES * buf_lines);
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.hor_res = DISP_H_RES;
disp_drv.ver_res = DISP_V_RES;
disp_drv.flush_cb = lvgl_flush_cb;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register(&disp_drv);
if (touch_ret == ESP_OK) {
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = lvgl_touch_cb;
lv_indev_drv_register(&indev_drv);
ESP_LOGI(TAG, "Touch input registered");
}
BaseType_t xret = xTaskCreatePinnedToCore(
display_task, "display", DISP_TASK_STACK,
NULL, DISP_TASK_PRIORITY, NULL, DISP_TASK_CORE);
if (xret != pdPASS) {
ESP_LOGE(TAG, "Failed to create display task");
return ESP_OK;
}
ESP_LOGI(TAG, "Display task started (Core %d, priority %d, %d fps)",
DISP_TASK_CORE, DISP_TASK_PRIORITY, DISP_FPS_LIMIT);
return ESP_OK;
}
#else /* !CONFIG_DISPLAY_ENABLE */
esp_err_t display_task_start(void)
{
return ESP_OK;
}
#endif /* CONFIG_DISPLAY_ENABLE */

View file

@ -0,0 +1,29 @@
/**
* @file display_task.h
* @brief ADR-045: FreeRTOS display task LVGL pump on Core 0.
*/
#ifndef DISPLAY_TASK_H
#define DISPLAY_TASK_H
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* Start the display task on Core 0, priority 1.
*
* Probes for RM67162 panel and SPIRAM. If either is absent,
* logs a warning and returns ESP_OK (graceful skip).
*
* @return ESP_OK always (display is optional).
*/
esp_err_t display_task_start(void);
#ifdef __cplusplus
}
#endif
#endif /* DISPLAY_TASK_H */

View file

@ -0,0 +1,387 @@
/**
* @file display_ui.c
* @brief ADR-045: LVGL 4-view swipeable UI Dashboard | Vitals | Presence | System.
*
* Dark theme (#0a0a0f background) with cyan (#00d4ff) accent.
* Glowing line effects via layered semi-transparent chart series.
*/
#include "display_ui.h"
#include "sdkconfig.h"
#if CONFIG_DISPLAY_ENABLE
#include <stdio.h>
#include <string.h>
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "edge_processing.h"
static const char *TAG = "disp_ui";
/* ---- Theme colors ---- */
#define COLOR_BG lv_color_make(0x0A, 0x0A, 0x0F)
#define COLOR_CYAN lv_color_make(0x00, 0xD4, 0xFF)
#define COLOR_AMBER lv_color_make(0xFF, 0xB0, 0x00)
#define COLOR_GREEN lv_color_make(0x00, 0xFF, 0x80)
#define COLOR_RED lv_color_make(0xFF, 0x40, 0x40)
#define COLOR_DIM lv_color_make(0x30, 0x30, 0x40)
#define COLOR_TEXT lv_color_make(0xCC, 0xCC, 0xDD)
#define COLOR_TEXT_DIM lv_color_make(0x66, 0x66, 0x77)
/* ---- Chart data points ---- */
#define CHART_POINTS 60
/* ---- View handles ---- */
static lv_obj_t *s_tileview = NULL;
/* Dashboard */
static lv_obj_t *s_dash_chart = NULL;
static lv_chart_series_t *s_csi_series = NULL;
static lv_obj_t *s_dash_persons = NULL;
static lv_obj_t *s_dash_rssi = NULL;
static lv_obj_t *s_dash_motion = NULL;
/* Vitals */
static lv_obj_t *s_vital_chart = NULL;
static lv_chart_series_t *s_breath_series = NULL;
static lv_chart_series_t *s_hr_series = NULL;
static lv_obj_t *s_vital_bpm_br = NULL;
static lv_obj_t *s_vital_bpm_hr = NULL;
/* Presence */
#define GRID_COLS 4
#define GRID_ROWS 4
static lv_obj_t *s_grid_cells[GRID_COLS * GRID_ROWS];
static lv_obj_t *s_presence_label = NULL;
/* System */
static lv_obj_t *s_sys_cpu = NULL;
static lv_obj_t *s_sys_heap = NULL;
static lv_obj_t *s_sys_psram = NULL;
static lv_obj_t *s_sys_rssi = NULL;
static lv_obj_t *s_sys_uptime = NULL;
static lv_obj_t *s_sys_fps = NULL;
static lv_obj_t *s_sys_node = NULL;
/* ---- Style helpers ---- */
static lv_style_t s_style_bg;
static lv_style_t s_style_label;
static lv_style_t s_style_label_big;
static bool s_styles_inited = false;
static void init_styles(void)
{
if (s_styles_inited) return;
s_styles_inited = true;
lv_style_init(&s_style_bg);
lv_style_set_bg_color(&s_style_bg, COLOR_BG);
lv_style_set_bg_opa(&s_style_bg, LV_OPA_COVER);
lv_style_set_border_width(&s_style_bg, 0);
lv_style_set_pad_all(&s_style_bg, 4);
lv_style_init(&s_style_label);
lv_style_set_text_color(&s_style_label, COLOR_TEXT);
lv_style_set_text_font(&s_style_label, &lv_font_montserrat_14);
lv_style_init(&s_style_label_big);
lv_style_set_text_color(&s_style_label_big, COLOR_CYAN);
lv_style_set_text_font(&s_style_label_big, &lv_font_montserrat_20);
}
static lv_obj_t *make_label(lv_obj_t *parent, const char *text, const lv_style_t *style)
{
lv_obj_t *lbl = lv_label_create(parent);
lv_label_set_text(lbl, text);
if (style) lv_obj_add_style(lbl, (lv_style_t *)style, 0);
return lbl;
}
static lv_obj_t *make_tile(lv_obj_t *tv, uint8_t col, uint8_t row)
{
lv_obj_t *tile = lv_tileview_add_tile(tv, col, row, LV_DIR_HOR);
lv_obj_add_style(tile, &s_style_bg, 0);
return tile;
}
/* ---- View 0: Dashboard ---- */
static void create_dashboard(lv_obj_t *tile)
{
make_label(tile, "CSI Dashboard", &s_style_label);
/* CSI amplitude chart */
s_dash_chart = lv_chart_create(tile);
lv_obj_set_size(s_dash_chart, 400, 130);
lv_obj_align(s_dash_chart, LV_ALIGN_TOP_LEFT, 0, 24);
lv_chart_set_type(s_dash_chart, LV_CHART_TYPE_LINE);
lv_chart_set_point_count(s_dash_chart, CHART_POINTS);
lv_chart_set_range(s_dash_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
lv_obj_set_style_bg_color(s_dash_chart, COLOR_BG, 0);
lv_obj_set_style_border_color(s_dash_chart, COLOR_DIM, 0);
lv_obj_set_style_line_width(s_dash_chart, 0, LV_PART_TICKS);
s_csi_series = lv_chart_add_series(s_dash_chart, COLOR_CYAN, LV_CHART_AXIS_PRIMARY_Y);
/* Stats panel on the right */
lv_obj_t *panel = lv_obj_create(tile);
lv_obj_set_size(panel, 120, 130);
lv_obj_align(panel, LV_ALIGN_TOP_RIGHT, 0, 24);
lv_obj_set_style_bg_color(panel, lv_color_make(0x12, 0x12, 0x1A), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_border_color(panel, COLOR_DIM, 0);
lv_obj_set_style_pad_all(panel, 8, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
make_label(panel, "Persons", &s_style_label);
s_dash_persons = make_label(panel, "0", &s_style_label_big);
s_dash_rssi = make_label(panel, "RSSI: --", &s_style_label);
s_dash_motion = make_label(panel, "Motion: 0.0", &s_style_label);
}
/* ---- View 1: Vitals ---- */
static void create_vitals(lv_obj_t *tile)
{
make_label(tile, "Vital Signs", &s_style_label);
s_vital_chart = lv_chart_create(tile);
lv_obj_set_size(s_vital_chart, 480, 150);
lv_obj_align(s_vital_chart, LV_ALIGN_TOP_LEFT, 0, 24);
lv_chart_set_type(s_vital_chart, LV_CHART_TYPE_LINE);
lv_chart_set_point_count(s_vital_chart, CHART_POINTS);
lv_chart_set_range(s_vital_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 120);
lv_obj_set_style_bg_color(s_vital_chart, COLOR_BG, 0);
lv_obj_set_style_border_color(s_vital_chart, COLOR_DIM, 0);
lv_obj_set_style_line_width(s_vital_chart, 0, LV_PART_TICKS);
/* Breathing series (cyan) */
s_breath_series = lv_chart_add_series(s_vital_chart, COLOR_CYAN, LV_CHART_AXIS_PRIMARY_Y);
/* Heart rate series (amber) */
s_hr_series = lv_chart_add_series(s_vital_chart, COLOR_AMBER, LV_CHART_AXIS_PRIMARY_Y);
/* BPM readouts */
s_vital_bpm_br = make_label(tile, "Breathing: -- BPM", &s_style_label);
lv_obj_align(s_vital_bpm_br, LV_ALIGN_BOTTOM_LEFT, 4, -8);
lv_obj_set_style_text_color(s_vital_bpm_br, COLOR_CYAN, 0);
s_vital_bpm_hr = make_label(tile, "Heart Rate: -- BPM", &s_style_label);
lv_obj_align(s_vital_bpm_hr, LV_ALIGN_BOTTOM_RIGHT, -4, -8);
lv_obj_set_style_text_color(s_vital_bpm_hr, COLOR_AMBER, 0);
}
/* ---- View 2: Presence Grid ---- */
static void create_presence(lv_obj_t *tile)
{
make_label(tile, "Occupancy Map", &s_style_label);
int cell_w = 50;
int cell_h = 45;
int x_off = (368 - GRID_COLS * (cell_w + 4)) / 2;
int y_off = 30;
for (int r = 0; r < GRID_ROWS; r++) {
for (int c = 0; c < GRID_COLS; c++) {
lv_obj_t *cell = lv_obj_create(tile);
lv_obj_set_size(cell, cell_w, cell_h);
lv_obj_set_pos(cell, x_off + c * (cell_w + 4), y_off + r * (cell_h + 4));
lv_obj_set_style_bg_color(cell, COLOR_DIM, 0);
lv_obj_set_style_bg_opa(cell, LV_OPA_COVER, 0);
lv_obj_set_style_border_color(cell, COLOR_DIM, 0);
lv_obj_set_style_border_width(cell, 1, 0);
lv_obj_set_style_radius(cell, 4, 0);
s_grid_cells[r * GRID_COLS + c] = cell;
}
}
s_presence_label = make_label(tile, "Persons: 0", &s_style_label);
lv_obj_align(s_presence_label, LV_ALIGN_BOTTOM_MID, 0, -8);
}
/* ---- View 3: System ---- */
static void create_system(lv_obj_t *tile)
{
make_label(tile, "System Info", &s_style_label);
lv_obj_t *panel = lv_obj_create(tile);
lv_obj_set_size(panel, 500, 180);
lv_obj_align(panel, LV_ALIGN_TOP_LEFT, 0, 24);
lv_obj_set_style_bg_color(panel, lv_color_make(0x12, 0x12, 0x1A), 0);
lv_obj_set_style_border_width(panel, 1, 0);
lv_obj_set_style_border_color(panel, COLOR_DIM, 0);
lv_obj_set_style_pad_all(panel, 10, 0);
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
s_sys_node = make_label(panel, "Node: --", &s_style_label);
s_sys_cpu = make_label(panel, "CPU: --%", &s_style_label);
s_sys_heap = make_label(panel, "Heap: -- KB free", &s_style_label);
s_sys_psram = make_label(panel, "PSRAM: -- KB free",&s_style_label);
s_sys_rssi = make_label(panel, "WiFi RSSI: --", &s_style_label);
s_sys_uptime = make_label(panel, "Uptime: --", &s_style_label);
s_sys_fps = make_label(panel, "FPS: --", &s_style_label);
}
/* ---- Public API ---- */
void display_ui_create(lv_obj_t *parent)
{
init_styles();
s_tileview = lv_tileview_create(parent);
lv_obj_add_style(s_tileview, &s_style_bg, 0);
lv_obj_set_style_bg_color(s_tileview, COLOR_BG, 0);
lv_obj_t *t0 = make_tile(s_tileview, 0, 0);
lv_obj_t *t1 = make_tile(s_tileview, 1, 0);
lv_obj_t *t2 = make_tile(s_tileview, 2, 0);
lv_obj_t *t3 = make_tile(s_tileview, 3, 0);
create_dashboard(t0);
create_vitals(t1);
create_presence(t2);
create_system(t3);
ESP_LOGI(TAG, "UI created: 4 views (Dashboard|Vitals|Presence|System)");
}
/* ---- FPS tracking ---- */
static uint32_t s_frame_count = 0;
static uint32_t s_last_fps_time = 0;
static uint32_t s_current_fps = 0;
void display_ui_update(void)
{
/* FPS counter */
s_frame_count++;
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
if (now_ms - s_last_fps_time >= 1000) {
s_current_fps = s_frame_count;
s_frame_count = 0;
s_last_fps_time = now_ms;
}
/* Read edge data (thread-safe) */
edge_vitals_pkt_t vitals;
bool has_vitals = edge_get_vitals(&vitals);
edge_person_vitals_t persons[EDGE_MAX_PERSONS];
uint8_t n_active = 0;
edge_get_multi_person(persons, &n_active);
/* ---- Dashboard update ---- */
if (s_dash_chart && has_vitals) {
/* Push motion energy as amplitude proxy (scaled 0-100) */
int val = (int)(vitals.motion_energy * 10.0f);
if (val > 100) val = 100;
if (val < 0) val = 0;
lv_chart_set_next_value(s_dash_chart, s_csi_series, val);
}
if (s_dash_persons) {
char buf[8];
snprintf(buf, sizeof(buf), "%u", has_vitals ? vitals.n_persons : 0);
lv_label_set_text(s_dash_persons, buf);
}
if (s_dash_rssi && has_vitals) {
char buf[16];
snprintf(buf, sizeof(buf), "RSSI: %d", vitals.rssi);
lv_label_set_text(s_dash_rssi, buf);
}
if (s_dash_motion && has_vitals) {
char buf[24];
snprintf(buf, sizeof(buf), "Motion: %.1f", (double)vitals.motion_energy);
lv_label_set_text(s_dash_motion, buf);
}
/* ---- Vitals update ---- */
if (s_vital_chart && has_vitals) {
int br = (int)(vitals.breathing_rate / 100); /* Fixed-point to int BPM */
int hr = (int)(vitals.heartrate / 10000);
if (br > 120) br = 120;
if (hr > 120) hr = 120;
lv_chart_set_next_value(s_vital_chart, s_breath_series, br);
lv_chart_set_next_value(s_vital_chart, s_hr_series, hr);
char buf[32];
snprintf(buf, sizeof(buf), "Breathing: %d BPM", br);
lv_label_set_text(s_vital_bpm_br, buf);
snprintf(buf, sizeof(buf), "Heart Rate: %d BPM", hr);
lv_label_set_text(s_vital_bpm_hr, buf);
}
/* ---- Presence grid update ---- */
if (has_vitals) {
/* Simple visualization: color cells based on motion energy distribution */
float energy = vitals.motion_energy;
uint8_t active_cells = (uint8_t)(energy * 2); /* Scale for visibility */
if (active_cells > GRID_COLS * GRID_ROWS) active_cells = GRID_COLS * GRID_ROWS;
for (int i = 0; i < GRID_COLS * GRID_ROWS; i++) {
if (i < active_cells) {
/* Color gradient: green → amber → red based on intensity */
if (energy > 5.0f) {
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_RED, 0);
} else if (energy > 2.0f) {
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_AMBER, 0);
} else {
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_GREEN, 0);
}
} else {
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_DIM, 0);
}
}
char buf[20];
snprintf(buf, sizeof(buf), "Persons: %u", vitals.n_persons);
lv_label_set_text(s_presence_label, buf);
}
/* ---- System info update ---- */
{
char buf[48];
#ifdef CONFIG_CSI_NODE_ID
snprintf(buf, sizeof(buf), "Node: %d", CONFIG_CSI_NODE_ID);
#else
snprintf(buf, sizeof(buf), "Node: --");
#endif
lv_label_set_text(s_sys_node, buf);
snprintf(buf, sizeof(buf), "Heap: %lu KB free",
(unsigned long)(esp_get_free_heap_size() / 1024));
lv_label_set_text(s_sys_heap, buf);
#if CONFIG_SPIRAM
snprintf(buf, sizeof(buf), "PSRAM: %lu KB free",
(unsigned long)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
#else
snprintf(buf, sizeof(buf), "PSRAM: N/A");
#endif
lv_label_set_text(s_sys_psram, buf);
if (has_vitals) {
snprintf(buf, sizeof(buf), "WiFi RSSI: %d dBm", vitals.rssi);
lv_label_set_text(s_sys_rssi, buf);
}
uint32_t uptime_s = (uint32_t)(esp_timer_get_time() / 1000000);
uint32_t h = uptime_s / 3600;
uint32_t m = (uptime_s % 3600) / 60;
uint32_t s = uptime_s % 60;
snprintf(buf, sizeof(buf), "Uptime: %luh %02lum %02lus",
(unsigned long)h, (unsigned long)m, (unsigned long)s);
lv_label_set_text(s_sys_uptime, buf);
snprintf(buf, sizeof(buf), "FPS: %lu", (unsigned long)s_current_fps);
lv_label_set_text(s_sys_fps, buf);
}
}
#endif /* CONFIG_DISPLAY_ENABLE */

View file

@ -0,0 +1,31 @@
/**
* @file display_ui.h
* @brief ADR-045: LVGL 4-view swipeable UI for CSI node stats.
*
* Views: Dashboard | Vitals | Presence | System
* Dark theme with cyan (#00d4ff) accent.
*/
#ifndef DISPLAY_UI_H
#define DISPLAY_UI_H
#include "lvgl.h"
#ifdef __cplusplus
extern "C" {
#endif
/** Create all LVGL views on the given tileview parent. */
void display_ui_create(lv_obj_t *parent);
/**
* Update all views with latest data. Called every display refresh cycle.
* Reads from edge_get_vitals() and edge_get_multi_person() internally.
*/
void display_ui_update(void);
#ifdef __cplusplus
}
#endif
#endif /* DISPLAY_UI_H */

View file

@ -0,0 +1,10 @@
## ESP-IDF Managed Component Dependencies (ADR-045)
dependencies:
## LVGL graphics library
lvgl/lvgl: "~8.3"
## CST816S capacitive touch driver
espressif/esp_lcd_touch_cst816s: "^1.0"
## LCD touch abstraction
espressif/esp_lcd_touch: "^1.0"

View file

@ -0,0 +1,94 @@
/**
* @file lv_conf.h
* @brief LVGL compile-time configuration for ESP32-S3 AMOLED display (ADR-045).
*
* Tuned for RM67162 536x240 QSPI AMOLED with 8MB PSRAM.
* Color depth: RGB565 (16-bit) for QSPI bandwidth.
* Double-buffered in SPIRAM, 30fps target.
*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include <stdint.h>
/* ---- Core ---- */
#define LV_COLOR_DEPTH 16
#define LV_COLOR_16_SWAP 1 /* Byte-swap for SPI/QSPI displays */
#define LV_MEM_CUSTOM 1 /* Use ESP-IDF heap instead of LVGL's internal allocator */
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h>
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
/* ---- Display ---- */
#define LV_HOR_RES_MAX 368
#define LV_VER_RES_MAX 448
#define LV_DPI_DEF 200
/* ---- Tick (provided by esp_timer in display_task.c) ---- */
#define LV_TICK_CUSTOM 1
#define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
#define LV_TICK_CUSTOM_SYS_TIME_EXPR ((uint32_t)(esp_timer_get_time() / 1000))
/* ---- Drawing ---- */
#define LV_DRAW_COMPLEX 1
#define LV_SHADOW_CACHE_SIZE 0
#define LV_CIRCLE_CACHE_SIZE 4
#define LV_IMG_CACHE_DEF_SIZE 0
/* ---- Fonts ---- */
#define LV_FONT_MONTSERRAT_14 1
#define LV_FONT_MONTSERRAT_20 1
#define LV_FONT_DEFAULT &lv_font_montserrat_14
/* ---- Widgets ---- */
#define LV_USE_ARC 1
#define LV_USE_BAR 1
#define LV_USE_BTN 0
#define LV_USE_BTNMATRIX 0
#define LV_USE_CANVAS 0
#define LV_USE_CHECKBOX 0
#define LV_USE_DROPDOWN 0
#define LV_USE_IMG 0
#define LV_USE_LABEL 1
#define LV_USE_LINE 1
#define LV_USE_ROLLER 0
#define LV_USE_SLIDER 0
#define LV_USE_SWITCH 0
#define LV_USE_TEXTAREA 0
#define LV_USE_TABLE 0
/* ---- Extra widgets ---- */
#define LV_USE_CHART 1
#define LV_CHART_AXIS_TICK_LABEL_MAX_LEN 32
#define LV_USE_METER 0
#define LV_USE_SPINBOX 0
#define LV_USE_SPAN 0
#define LV_USE_TILEVIEW 1 /* Used for swipeable page navigation */
#define LV_USE_TABVIEW 0
#define LV_USE_WIN 0
/* ---- Themes ---- */
#define LV_USE_THEME_DEFAULT 1
#define LV_THEME_DEFAULT_DARK 1
/* ---- Logging ---- */
#define LV_USE_LOG 0
#define LV_USE_ASSERT_NULL 1
#define LV_USE_ASSERT_MALLOC 1
/* ---- GPU / render ---- */
#define LV_USE_GPU_ESP32_S3 0 /* No parallel LCD interface — we use QSPI */
/* ---- Animation ---- */
#define LV_USE_ANIM 1
#define LV_ANIM_DEF_TIME 200
/* ---- Misc ---- */
#define LV_USE_GROUP 1 /* For touch/input device routing */
#define LV_USE_PERF_MONITOR 0
#define LV_USE_MEM_MONITOR 0
#define LV_SPRINTF_CUSTOM 0
#endif /* LV_CONF_H */

View file

@ -26,6 +26,7 @@
#include "power_mgmt.h"
#include "wasm_runtime.h"
#include "wasm_upload.h"
#include "display_task.h"
#include "esp_timer.h"
@ -203,6 +204,12 @@ void app_main(void)
/* Initialize power management. */
power_mgmt_init(g_nvs_config.power_duty);
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
esp_err_t disp_ret = display_task_start();
if (disp_ret != ESP_OK) {
ESP_LOGW(TAG, "Display init returned: %s", esp_err_to_name(disp_ret));
}
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
g_nvs_config.target_ip, g_nvs_config.target_port,
g_nvs_config.edge_tier,

View file

@ -0,0 +1,8 @@
# ESP32-S3 CSI Node — 8MB flash partition table (ADR-045)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x200000,
ota_1, app, ota_1, 0x220000, 0x200000,
spiffs, data, spiffs, 0x420000, 0x1E0000,
1 # ESP32-S3 CSI Node — 8MB flash partition table (ADR-045)
2 # Name, Type, SubType, Offset, Size, Flags
3 nvs, data, nvs, 0x9000, 0x6000,
4 otadata, data, ota, 0xf000, 0x2000,
5 phy_init, data, phy, 0x11000, 0x1000,
6 ota_0, app, ota_0, 0x20000, 0x200000,
7 ota_1, app, ota_1, 0x220000, 0x200000,
8 spiffs, data, spiffs, 0x420000, 0x1E0000,