mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
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:
parent
5fa61ba7ea
commit
8b57a6f64c
35 changed files with 8674 additions and 7 deletions
|
|
@ -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}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
382
firmware/esp32-csi-node/main/display_hal.c
Normal file
382
firmware/esp32-csi-node/main/display_hal.c
Normal 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 */
|
||||
71
firmware/esp32-csi-node/main/display_hal.h
Normal file
71
firmware/esp32-csi-node/main/display_hal.h
Normal 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 */
|
||||
169
firmware/esp32-csi-node/main/display_task.c
Normal file
169
firmware/esp32-csi-node/main/display_task.c
Normal 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 */
|
||||
29
firmware/esp32-csi-node/main/display_task.h
Normal file
29
firmware/esp32-csi-node/main/display_task.h
Normal 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 */
|
||||
387
firmware/esp32-csi-node/main/display_ui.c
Normal file
387
firmware/esp32-csi-node/main/display_ui.c
Normal 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 */
|
||||
31
firmware/esp32-csi-node/main/display_ui.h
Normal file
31
firmware/esp32-csi-node/main/display_ui.h
Normal 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 */
|
||||
10
firmware/esp32-csi-node/main/idf_component.yml
Normal file
10
firmware/esp32-csi-node/main/idf_component.yml
Normal 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"
|
||||
94
firmware/esp32-csi-node/main/lv_conf.h
Normal file
94
firmware/esp32-csi-node/main/lv_conf.h
Normal 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 */
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
8
firmware/esp32-csi-node/partitions_display.csv
Normal file
8
firmware/esp32-csi-node/partitions_display.csv
Normal 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,
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue