dreamstack/devices/waveshare-p4-panel/main/ds_runtime.c

458 lines
14 KiB
C

/**
* DreamStack Panel IR Runtime — Implementation
*
* Parses Panel IR JSON (using cJSON) and creates LVGL 9 widgets.
* This is the C port of the browser-based panel previewer logic.
*
* Key features:
* - Signal table with text template expansion ({0} → value)
* - Reactive updates: signal change → refresh bound labels
* - Event dispatch: button click → action opcode → ESP-NOW
* - Timer execution from IR timers[] array
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "esp_log.h"
#include "cJSON.h"
#include "lvgl.h"
#include "ds_runtime.h"
static const char *TAG = "ds-runtime";
// ─── Signal Table ───
static ds_signal_t s_signals[DS_MAX_SIGNALS];
static uint16_t s_signal_count = 0;
// ─── Timer Table ───
static ds_timer_t s_timers[DS_MAX_TIMERS];
static uint8_t s_timer_count = 0;
// ─── Text Binding Table ───
// Maps LVGL label objects to their IR text templates
typedef struct {
lv_obj_t *label; // LVGL label widget
char template[128]; // Text template with {N} placeholders
bool used;
} ds_binding_t;
static ds_binding_t s_bindings[DS_MAX_BINDINGS];
static uint16_t s_binding_count = 0;
// ─── Parent and Callback ───
static lv_obj_t *s_parent = NULL;
static lv_obj_t *s_root = NULL;
static ds_action_cb_t s_action_cb = NULL;
// ─── Forward Declarations ───
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent);
static void expand_template(const char *tpl, char *out, size_t out_len);
static void execute_action(cJSON *action);
static void refresh_bindings(void);
// ─── Action opcodes (match IR spec) ───
#define OP_INC 1
#define OP_DEC 2
#define OP_ADD 3
#define OP_SUB 4
#define OP_SET 5
#define OP_TOGGLE 6
static uint8_t parse_op(const char *op_str) {
if (strcmp(op_str, "inc") == 0) return OP_INC;
if (strcmp(op_str, "dec") == 0) return OP_DEC;
if (strcmp(op_str, "add") == 0) return OP_ADD;
if (strcmp(op_str, "sub") == 0) return OP_SUB;
if (strcmp(op_str, "set") == 0) return OP_SET;
if (strcmp(op_str, "toggle") == 0) return OP_TOGGLE;
return 0;
}
// ─── Timer Callback ───
static void timer_cb(lv_timer_t *timer) {
ds_timer_t *t = (ds_timer_t *)lv_timer_get_user_data(timer);
if (!t) return;
int32_t val = s_signals[t->action_sig].i;
switch (t->action_op) {
case OP_INC: val++; break;
case OP_DEC: val--; break;
case OP_ADD: val += t->action_val; break;
case OP_SUB: val -= t->action_val; break;
case OP_SET: val = t->action_val; break;
case OP_TOGGLE: val = val ? 0 : 1; break;
}
ds_signal_update(t->action_sig, val);
}
// ─── Button Event Handler ───
typedef struct {
cJSON *action; // JSON action object (kept alive while UI exists)
uint8_t node_id;
} btn_user_data_t;
static void btn_click_cb(lv_event_t *e) {
btn_user_data_t *ud = (btn_user_data_t *)lv_event_get_user_data(e);
if (ud && ud->action) {
execute_action(ud->action);
if (s_action_cb) {
s_action_cb(ud->node_id, 0); // notify ESP-NOW
}
}
}
// ─── Build Helpers ───
static lv_obj_t *build_container(cJSON *node, lv_obj_t *parent, bool is_row) {
lv_obj_t *cont = lv_obj_create(parent);
lv_obj_set_size(cont, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(cont, is_row ? LV_FLEX_FLOW_ROW : LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_bg_opa(cont, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(cont, 0, 0);
lv_obj_set_style_pad_all(cont, 4, 0);
cJSON *gap = cJSON_GetObjectItem(node, "gap");
if (gap) {
lv_obj_set_style_pad_gap(cont, gap->valueint, 0);
}
cJSON *children = cJSON_GetObjectItem(node, "c");
if (children && cJSON_IsArray(children)) {
cJSON *child;
cJSON_ArrayForEach(child, children) {
build_node(child, cont);
}
}
return cont;
}
static lv_obj_t *build_label(cJSON *node, lv_obj_t *parent) {
lv_obj_t *lbl = lv_label_create(parent);
cJSON *text = cJSON_GetObjectItem(node, "text");
if (text && text->valuestring) {
char expanded[256];
expand_template(text->valuestring, expanded, sizeof(expanded));
lv_label_set_text(lbl, expanded);
// Register binding if template contains {N}
if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
ds_binding_t *b = &s_bindings[s_binding_count++];
b->label = lbl;
strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
b->used = true;
}
}
cJSON *size = cJSON_GetObjectItem(node, "size");
if (size) {
lv_obj_set_style_text_font(lbl,
size->valueint >= 24 ? &lv_font_montserrat_24 :
size->valueint >= 18 ? &lv_font_montserrat_18 :
size->valueint >= 14 ? &lv_font_montserrat_14 :
&lv_font_montserrat_12, 0);
}
return lbl;
}
static lv_obj_t *build_button(cJSON *node, lv_obj_t *parent) {
lv_obj_t *btn = lv_btn_create(parent);
lv_obj_t *lbl = lv_label_create(btn);
cJSON *text = cJSON_GetObjectItem(node, "text");
if (text && text->valuestring) {
char expanded[256];
expand_template(text->valuestring, expanded, sizeof(expanded));
lv_label_set_text(lbl, expanded);
// Register binding for button text too
if (strchr(text->valuestring, '{') && s_binding_count < DS_MAX_BINDINGS) {
ds_binding_t *b = &s_bindings[s_binding_count++];
b->label = lbl;
strncpy(b->template, text->valuestring, sizeof(b->template) - 1);
b->used = true;
}
}
cJSON *on = cJSON_GetObjectItem(node, "on");
if (on) {
cJSON *click = cJSON_GetObjectItem(on, "click");
if (click) {
btn_user_data_t *ud = malloc(sizeof(btn_user_data_t));
ud->action = click; // keep reference (IR JSON stays in memory)
cJSON *id_obj = cJSON_GetObjectItem(node, "id");
ud->node_id = id_obj ? id_obj->valueint : 0;
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, ud);
}
}
return btn;
}
static lv_obj_t *build_slider(cJSON *node, lv_obj_t *parent) {
lv_obj_t *slider = lv_slider_create(parent);
lv_obj_set_width(slider, LV_PCT(80));
cJSON *min = cJSON_GetObjectItem(node, "min");
cJSON *max = cJSON_GetObjectItem(node, "max");
if (min) lv_slider_set_range(slider, min->valueint, max ? max->valueint : 100);
cJSON *bind = cJSON_GetObjectItem(node, "bind");
if (bind) {
int sid = bind->valueint;
lv_slider_set_value(slider, s_signals[sid].i, LV_ANIM_OFF);
// TODO: add slider change event → ds_signal_update
}
return slider;
}
static lv_obj_t *build_panel(cJSON *node, lv_obj_t *parent) {
lv_obj_t *pnl = lv_obj_create(parent);
lv_obj_set_size(pnl, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_flex_flow(pnl, LV_FLEX_FLOW_COLUMN);
lv_obj_set_style_pad_all(pnl, 12, 0);
lv_obj_set_style_radius(pnl, 8, 0);
cJSON *title = cJSON_GetObjectItem(node, "text");
if (title && title->valuestring) {
lv_obj_t *t = lv_label_create(pnl);
lv_label_set_text(t, title->valuestring);
lv_obj_set_style_text_font(t, &lv_font_montserrat_14, 0);
}
cJSON *children = cJSON_GetObjectItem(node, "c");
if (children && cJSON_IsArray(children)) {
cJSON *child;
cJSON_ArrayForEach(child, children) {
build_node(child, pnl);
}
}
return pnl;
}
// ─── Node Dispatcher ───
static lv_obj_t *build_node(cJSON *node, lv_obj_t *parent) {
if (!node) return NULL;
cJSON *type = cJSON_GetObjectItem(node, "t");
if (!type || !type->valuestring) return NULL;
const char *t = type->valuestring;
if (strcmp(t, "col") == 0) return build_container(node, parent, false);
if (strcmp(t, "row") == 0) return build_container(node, parent, true);
if (strcmp(t, "lbl") == 0) return build_label(node, parent);
if (strcmp(t, "btn") == 0) return build_button(node, parent);
if (strcmp(t, "sld") == 0) return build_slider(node, parent);
if (strcmp(t, "pnl") == 0) return build_panel(node, parent);
// stk, inp, sw, bar, img — add as needed
ESP_LOGW(TAG, "Unknown node type: %s", t);
return NULL;
}
// ─── Template Expansion ───
// Replace {N} with signal values: "{2}: {0}°F" → "Kitchen: 72°F"
static void expand_template(const char *tpl, char *out, size_t out_len) {
size_t pos = 0;
while (*tpl && pos < out_len - 1) {
if (*tpl == '{') {
tpl++;
int sig_id = 0;
while (*tpl >= '0' && *tpl <= '9') {
sig_id = sig_id * 10 + (*tpl - '0');
tpl++;
}
if (*tpl == '}') tpl++;
if (sig_id < DS_MAX_SIGNALS && s_signals[sig_id].used) {
int written = snprintf(out + pos, out_len - pos, "%d",
(int)s_signals[sig_id].i);
if (written > 0) pos += written;
}
} else {
out[pos++] = *tpl++;
}
}
out[pos] = '\0';
}
// ─── Action Executor ───
static void execute_action(cJSON *action) {
if (!action) return;
// Handle arrays of actions
if (cJSON_IsArray(action)) {
cJSON *item;
cJSON_ArrayForEach(item, action) {
execute_action(item);
}
return;
}
cJSON *op_json = cJSON_GetObjectItem(action, "op");
cJSON *s_json = cJSON_GetObjectItem(action, "s");
if (!op_json || !s_json) return;
uint8_t op = parse_op(op_json->valuestring);
uint16_t sid = s_json->valueint;
if (sid >= DS_MAX_SIGNALS) return;
int32_t val = s_signals[sid].i;
cJSON *v_json = cJSON_GetObjectItem(action, "v");
int32_t v = v_json ? v_json->valueint : 0;
switch (op) {
case OP_INC: val++; break;
case OP_DEC: val--; break;
case OP_ADD: val += v; break;
case OP_SUB: val -= v; break;
case OP_SET: val = v; break;
case OP_TOGGLE: val = val ? 0 : 1; break;
}
ds_signal_update(sid, val);
}
// ─── Refresh Bindings ───
// Called after any signal change to update bound labels
static void refresh_bindings(void) {
for (int i = 0; i < s_binding_count; i++) {
if (!s_bindings[i].used) continue;
char expanded[256];
expand_template(s_bindings[i].template, expanded, sizeof(expanded));
lv_label_set_text(s_bindings[i].label, expanded);
}
}
// ─── Public API ───
esp_err_t ds_runtime_init(void *parent, ds_action_cb_t action_cb) {
s_parent = (lv_obj_t *)parent;
s_action_cb = action_cb;
s_root = NULL;
s_signal_count = 0;
s_binding_count = 0;
s_timer_count = 0;
memset(s_signals, 0, sizeof(s_signals));
memset(s_bindings, 0, sizeof(s_bindings));
memset(s_timers, 0, sizeof(s_timers));
ESP_LOGI(TAG, "Runtime initialized");
return ESP_OK;
}
esp_err_t ds_ui_build(const char *ir_json, size_t length) {
// Destroy previous UI
ds_ui_destroy();
cJSON *ir = cJSON_ParseWithLength(ir_json, length);
if (!ir) {
ESP_LOGE(TAG, "Failed to parse IR JSON");
return ESP_ERR_INVALID_ARG;
}
// Load signals
cJSON *signals = cJSON_GetObjectItem(ir, "signals");
if (signals && cJSON_IsArray(signals)) {
cJSON *sig;
cJSON_ArrayForEach(sig, signals) {
cJSON *id = cJSON_GetObjectItem(sig, "id");
cJSON *v = cJSON_GetObjectItem(sig, "v");
if (id && id->valueint < DS_MAX_SIGNALS) {
int sid = id->valueint;
s_signals[sid].i = v ? v->valueint : 0;
s_signals[sid].type = DS_SIG_INT;
s_signals[sid].used = true;
s_signal_count++;
}
}
}
// Build UI tree
cJSON *root = cJSON_GetObjectItem(ir, "root");
if (root) {
s_root = build_node(root, s_parent);
}
// Load timers
cJSON *timers = cJSON_GetObjectItem(ir, "timers");
if (timers && cJSON_IsArray(timers)) {
cJSON *t;
cJSON_ArrayForEach(t, timers) {
if (s_timer_count >= DS_MAX_TIMERS) break;
cJSON *ms = cJSON_GetObjectItem(t, "ms");
cJSON *action = cJSON_GetObjectItem(t, "action");
if (ms && action) {
ds_timer_t *timer = &s_timers[s_timer_count];
timer->ms = ms->valueint;
cJSON *op = cJSON_GetObjectItem(action, "op");
cJSON *s = cJSON_GetObjectItem(action, "s");
cJSON *v = cJSON_GetObjectItem(action, "v");
timer->action_op = op ? parse_op(op->valuestring) : 0;
timer->action_sig = s ? s->valueint : 0;
timer->action_val = v ? v->valueint : 0;
timer->timer = lv_timer_create(timer_cb, timer->ms, timer);
s_timer_count++;
ESP_LOGI(TAG, "Timer: every %dms → op %d on s%d",
(int)timer->ms, timer->action_op, timer->action_sig);
}
}
}
ESP_LOGI(TAG, "UI built: %d signals, %d bindings, %d timers",
s_signal_count, s_binding_count, s_timer_count);
// Keep IR JSON alive for button action references
// (cJSON_Delete would invalidate action pointers)
// TODO: deep-copy actions to avoid this leak
return ESP_OK;
}
void ds_ui_destroy(void) {
// Stop timers
for (int i = 0; i < s_timer_count; i++) {
if (s_timers[i].timer) {
lv_timer_delete((lv_timer_t *)s_timers[i].timer);
s_timers[i].timer = NULL;
}
}
s_timer_count = 0;
// Clear bindings
s_binding_count = 0;
memset(s_bindings, 0, sizeof(s_bindings));
// Destroy LVGL tree
if (s_root) {
lv_obj_del(s_root);
s_root = NULL;
}
ESP_LOGI(TAG, "UI destroyed");
}
void ds_signal_update(uint16_t signal_id, int32_t value) {
if (signal_id >= DS_MAX_SIGNALS) return;
s_signals[signal_id].i = value;
s_signals[signal_id].used = true;
// Refresh all bound labels
refresh_bindings();
}
uint16_t ds_signal_count(void) {
return s_signal_count;
}
int32_t ds_signal_get(uint16_t signal_id) {
if (signal_id >= DS_MAX_SIGNALS) return 0;
return s_signals[signal_id].i;
}