From 6416db4dcfc3819d6355c2ce5469f89d4c2c2aec Mon Sep 17 00:00:00 2001
From: Benign X <1341398182@qq.com>
Date: Tue, 21 Apr 2026 19:44:32 +0800
Subject: [PATCH] chore(gdb): add widget_specs JSON and spec-driven dashboard
rendering
- Add widget_specs.json with auto-generated field types and hand-written
summary templates, primary fields, and enum name mappings
- Generator merges _auto section with hand-written overrides on each run
- data_collector outputs widget_specs to dashboard JSON top-level
- Update uinspy frontend with spec-driven detail panel and tree summary
---
.../lvglgdb/cmds/dashboard/data_collector.py | 55 ++
.../lvglgdb/cmds/dashboard/static/uinspy.html | 103 ++-
.../scripts/generators/gen_widget_wrappers.py | 91 ++-
.../gdb/scripts/generators/widget_specs.json | 751 ++++++++++++++++++
4 files changed, 961 insertions(+), 39 deletions(-)
create mode 100644 scripts/gdb/scripts/generators/widget_specs.json
diff --git a/scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py b/scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py
index 3ed25f969f..bc8c648e4b 100644
--- a/scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py
+++ b/scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py
@@ -58,6 +58,8 @@ def collect_all() -> dict:
# Registry-driven simple collectors
for dict_key, accessor, label in SIMPLE_REGISTRY:
data[dict_key] = _collect_simple(lvgl, dict_key, accessor, label)
+ # Widget specs (global lookup table for uinspy rendering)
+ data["widget_specs"] = _collect_widget_specs(data.get("object_trees", []))
return data
@@ -220,3 +222,56 @@ def _collect_subjects_from_obj(obj, seen, result):
except CorruptedError:
# Children pointer unreadable: stop traversal for this subtree
pass
+
+
+def _collect_widget_specs(object_trees: list) -> dict:
+ """Build widget_specs dict for all class_names found in object trees."""
+ import json as _json
+ from pathlib import Path
+
+ specs_path = Path(__file__).parent.parent.parent.parent / "scripts" / "generators" / "widget_specs.json"
+ if not specs_path.exists():
+ return {}
+
+ all_specs = _json.loads(specs_path.read_text())
+
+ # Collect all class_names from object trees
+ seen = set()
+
+ def _walk(node):
+ cn = node.get("class_name", "")
+ if cn:
+ seen.add(cn)
+ for child in node.get("children", []):
+ _walk(child)
+
+ for tree in object_trees:
+ for screen in tree.get("screens", []):
+ _walk(screen)
+
+ # Build output: merge _auto + enums into flat fields spec
+ result = {}
+ for cn in seen:
+ raw = all_specs.get(cn) or all_specs.get("lv_" + cn)
+ if not raw:
+ continue
+ auto_fields = raw.get("_auto", {}).get("fields", {})
+ enums = raw.get("enums", {})
+ fields = {}
+ for fname, ftype in auto_fields.items():
+ entry = {"type": ftype}
+ if fname in enums:
+ entry["type"] = "enum"
+ entry["names"] = enums[fname]
+ fields[fname] = entry
+
+ spec = {}
+ if raw.get("summary_tpl") is not None:
+ spec["summary_tpl"] = raw["summary_tpl"]
+ if raw.get("primary"):
+ spec["primary"] = raw["primary"]
+ if fields:
+ spec["fields"] = fields
+ if spec:
+ result[cn] = spec
+ return result
diff --git a/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html b/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html
index 8066e28135..20ea6b6749 100644
--- a/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html
+++ b/scripts/gdb/lvglgdb/cmds/dashboard/static/uinspy.html
@@ -1137,6 +1137,10 @@ ui-topbar .topbar-search::placeholder {
.obj-node > summary:hover {
background-color: var(--color-hover-summary);
}
+.obj-summary-hint {
+ margin-left: calc(var(--spacing) * 1);
+ color: var(--color-overlay0);
+}
.obj-node.obj-selected > summary {
background-color: var(--color-nav-active-bg);
outline: 1px solid var(--blue);
@@ -1815,7 +1819,27 @@ function selectObj(addr) {
document.getElementById("obj-" + prev)?.classList.remove("obj-selected");
selectedAddr.val = addr, document.getElementById("obj-" + addr)?.classList.add("obj-selected");
}
-var dashData = signal(null);
+var dashData = signal(null), widgetSpecs = {};
+function setWidgetSpecs(specs) {
+ widgetSpecs = specs;
+}
+function getWidgetSpec(className) {
+ return widgetSpecs[className] || widgetSpecs["lv_" + className];
+}
+function widgetSummary(className, wd) {
+ if (!wd)
+ return "";
+ let spec = getWidgetSpec(className);
+ if (!spec?.summary_tpl)
+ return "";
+ return spec.summary_tpl.replace(/\{(\w+)\}/g, (_, k) => {
+ let v = wd[k];
+ if (v == null)
+ return "";
+ let s = String(v);
+ return s.length > 24 ? s.slice(0, 24) + "…" : s;
+ });
+}
function countObjects(trees) {
let n = 0;
function walk(obj) {
@@ -2100,9 +2124,17 @@ function buildCard(item, config) {
}
// src/builders/obj-tree.ui.ts
-function formatWdVal(v) {
+function formatField(v, spec) {
if (v == null)
return "-";
+ if (spec) {
+ if (spec.type === "enum" && spec.names && typeof v === "number")
+ return spec.names[v] ?? String(v);
+ if (spec.type === "bool")
+ return v ? "true" : "false";
+ if (spec.type === "string" && typeof v === "string")
+ return v.length > 60 ? '"' + v.slice(0, 60) + '…"' : '"' + v + '"';
+ }
if (typeof v === "object")
return JSON.stringify(v);
return String(v);
@@ -2112,7 +2144,15 @@ function renderObjTree(obj, depth = 0) {
if (det.className = "obj-node", obj.addr)
det.id = "obj-" + obj.addr;
let sum = document.createElement("summary");
- if (sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]), sum.textContent = (obj.name ? obj.name + " " : "") + (obj.class_name || "obj"), obj.flags_list?.includes("HIDDEN"))
+ sum.style.setProperty("--depth-color", DEPTH_COLORS[depth % DEPTH_COLORS.length]);
+ let nameText = (obj.name ? obj.name + " " : "") + (obj.class_name || "obj");
+ sum.textContent = nameText;
+ let hint = widgetSummary(obj.class_name, obj.widget_data);
+ if (hint) {
+ let span = document.createElement("span");
+ span.className = "obj-summary-hint", span.textContent = hint, sum.appendChild(span);
+ }
+ if (obj.flags_list?.includes("HIDDEN"))
sum.textContent += " \uD83D\uDC41\uD83D\uDDE8";
if (det.appendChild(sum), obj.addr)
objDataMap[obj.addr] = obj, registerHL(obj.addr, det), sum.addEventListener("mouseenter", () => highlightObj(obj.addr)), sum.addEventListener("mouseleave", () => clearHighlight()), sum.addEventListener("click", (e) => {
@@ -2187,23 +2227,25 @@ function renderObjDetail(addr, panel) {
refSec.appendChild(kvPair("user_data", obj.user_data));
if (obj.name)
refSec.appendChild(kvPair("name", obj.name));
- if (refSec.appendChild(kvPair("children", String(obj.child_count || 0))), refSec.appendChild(kvPair("styles", String(obj.style_count || 0))), panel.appendChild(refSec), obj.scroll || obj.ext_click_pad || obj.ext_draw_size || obj.scrollbar_mode || obj.layer_type || obj.w_layout || obj.h_layout) {
+ refSec.appendChild(kvPair("children", String(obj.child_count || 0))), refSec.appendChild(kvPair("styles", String(obj.style_count || 0))), panel.appendChild(refSec);
+ let _n = (v) => v != null;
+ if (obj.scroll || _n(obj.ext_click_pad) || _n(obj.ext_draw_size) || _n(obj.scrollbar_mode) || _n(obj.layer_type) || obj.w_layout || obj.h_layout) {
let layoutSec = el("div", "detail-section");
if (layoutSec.appendChild(el("div", "detail-section-title", "Layout & Scroll")), obj.scroll)
layoutSec.appendChild(kvPair("scroll", `${obj.scroll.x}, ${obj.scroll.y}`));
- if (obj.ext_click_pad)
+ if (_n(obj.ext_click_pad))
layoutSec.appendChild(kvPair("ext_click_pad", String(obj.ext_click_pad)));
- if (obj.ext_draw_size)
+ if (_n(obj.ext_draw_size))
layoutSec.appendChild(kvPair("ext_draw_size", String(obj.ext_draw_size)));
- if (obj.scrollbar_mode != null)
+ if (_n(obj.scrollbar_mode))
layoutSec.appendChild(kvPair("scrollbar_mode", String(obj.scrollbar_mode)));
- if (obj.scroll_dir != null)
+ if (_n(obj.scroll_dir))
layoutSec.appendChild(kvPair("scroll_dir", String(obj.scroll_dir)));
- if (obj.scroll_snap_x)
+ if (_n(obj.scroll_snap_x))
layoutSec.appendChild(kvPair("scroll_snap_x", String(obj.scroll_snap_x)));
- if (obj.scroll_snap_y)
+ if (_n(obj.scroll_snap_y))
layoutSec.appendChild(kvPair("scroll_snap_y", String(obj.scroll_snap_y)));
- if (obj.layer_type)
+ if (_n(obj.layer_type))
layoutSec.appendChild(kvPair("layer_type", String(obj.layer_type)));
if (obj.w_layout)
layoutSec.appendChild(kvPair("w_layout", "true"));
@@ -2226,38 +2268,23 @@ function renderObjDetail(addr, panel) {
intSec.appendChild(wrap), panel.appendChild(intSec);
}
if (obj.widget_data && Object.keys(obj.widget_data).length) {
- let wd = obj.widget_data, primary = {
- lv_label: ["text", "long_mode", "recolor"],
- lv_image: ["src", "w", "h", "rotation", "scale_x", "scale_y", "align"],
- lv_bar: ["cur_value", "min_value", "max_value", "start_value", "mode"],
- lv_slider: ["cur_value", "min_value", "max_value", "start_value", "mode", "dragging"],
- lv_arc: ["value", "min_value", "max_value", "rotation", "type"],
- lv_switch: ["anim_state", "orientation"],
- lv_checkbox: ["txt"],
- lv_dropdown: ["options", "option_cnt", "sel_opt_id", "dir"],
- lv_textarea: ["placeholder_txt", "max_length", "pwd_show_time"],
- lv_tabview: ["tab_cur", "tab_pos", "tab_bar_size"],
- lv_roller: ["option_cnt", "sel_opt_id", "mode"],
- lv_chart: ["point_cnt", "hdiv_cnt", "vdiv_cnt", "type"],
- lv_scale: ["mode", "range_min", "range_max", "total_tick_count", "angle_range", "rotation"],
- lv_spinner: ["duration", "angle"],
- lv_keyboard: ["mode", "popovers"],
- lv_led: ["color", "bright"],
- lv_spinbox: ["value", "range_min", "range_max", "step", "digit_count", "dec_point_pos"],
- lv_calendar: ["today", "showed_date"],
- lv_table: ["col_cnt", "row_cnt"],
- lv_buttonmatrix: ["btn_cnt", "row_cnt", "btn_id_sel", "one_check"]
- }[obj.class_name] || [], allKeys = Object.keys(wd), priKeys = allKeys.filter((k) => primary.includes(k)), advKeys = allKeys.filter((k) => !primary.includes(k)), wdSec = el("div", "detail-section");
+ let wd = obj.widget_data, spec = getWidgetSpec(obj.class_name), fieldSpecs = spec?.fields || {}, primary = spec?.primary || [], allKeys = Object.keys(wd), priKeys = primary.filter((k) => (k in wd)), advKeys = allKeys.filter((k) => !priKeys.includes(k)), wdSec = el("div", "detail-section");
wdSec.appendChild(el("div", "detail-section-title", "Widget · " + obj.class_name));
+ let renderField = (k) => {
+ let fs = fieldSpecs[k], raw = wd[k];
+ wdSec.appendChild(kvPair(k, formatField(raw, fs)));
+ };
for (let k of priKeys.length ? priKeys : allKeys.slice(0, 6))
- wdSec.appendChild(kvPair(k, formatWdVal(wd[k])));
+ renderField(k);
let rest = priKeys.length ? advKeys : allKeys.slice(6);
if (rest.length) {
let toggle = el("details", "detail-adv-toggle");
toggle.appendChild(el("summary", "detail-adv-summary", `${rest.length} more fields`));
let inner = el("div", "");
- for (let k of rest)
- inner.appendChild(kvPair(k, formatWdVal(wd[k])));
+ for (let k of rest) {
+ let fs = fieldSpecs[k];
+ inner.appendChild(kvPair(k, formatField(wd[k], fs)));
+ }
toggle.appendChild(inner), wdSec.appendChild(toggle);
}
panel.appendChild(wdSec);
@@ -3223,7 +3250,7 @@ class UiDashboard extends BaseComponent {
let data = dashData.val;
if (!data)
return;
- grid.innerHTML = "";
+ setWidgetSpecs(data.widget_specs || {}), grid.innerHTML = "";
let bento = el("div", "bento"), objCount = countObjects(data.object_trees || []);
STAT_DEFS.forEach((s) => {
let val = s.key === "_objects" ? objCount : data[s.key]?.length || 0;
@@ -3235,7 +3262,7 @@ class UiDashboard extends BaseComponent {
customElements.define("ui-dashboard", UiDashboard);
// src/app.ts
-document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-04-02 13:52 GMT+8"}·${"07131c2"}·${"Canvas2D"}·GitHub·LVGL·MIT`;
+document.getElementById("about").innerHTML = `uinspy v${"0.4.6"}·Built ${"2026-04-21 19:26 GMT+8"}·${"1b5dc67"}·${"Canvas2D"}·GitHub·LVGL·MIT`;