chore(gdb): improve widget wrapper generator type handling

- Fix buttonmatrix map_p misparsed as 'const' (regex for const * const *)
- Fix _get_spec_int returning 0 for missing fields (now returns None)
- Fix struct parsing truncation on nested braces (brace-balanced matching)
- Scan LVGL headers to distinguish enum/int-alias types from structs
- Add safe_wrapper helper for known struct types (LVDrawBuf, LVList, LVAnim, LVArray)
- Skip unknown struct types with TODO for Value.to_dict()
This commit is contained in:
Benign X
2026-04-21 19:09:27 +08:00
committed by VIFEX
parent e748c63502
commit 2899149467
17 changed files with 189 additions and 50 deletions
+5 -2
View File
@@ -240,11 +240,14 @@ class LVObject(Value):
return hex(addr) if addr else None
def _get_spec_int(self, field_name):
"""Get an int field from spec_attr, or None."""
"""Get an int field from spec_attr, or None if unavailable."""
spec = self.spec_attr
if not spec or not int(spec):
return None
return int(spec.safe_field(field_name, 0))
val = spec.safe_field(field_name)
if val is None:
return None
return int(val)
def _get_scroll(self):
"""Get scroll offset from spec_attr, or None."""
@@ -45,3 +45,15 @@ def safe_point(obj, field_name):
"x": int(val.safe_field("x", 0)),
"y": int(val.safe_field("y", 0)),
}
def safe_wrapper(obj, field_name, module_path, class_name):
"""Read a struct field using its known Value wrapper, return snapshot dict."""
val = obj.safe_field(field_name)
if val is None or not getattr(val, 'is_ok', True):
return None
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
return wrapper.snapshot().as_dict()
@@ -6,6 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from .lv_image import LVImage
from ._helpers import ptr_or_none, safe_wrapper
class LVAnimimg(LVImage):
@@ -17,7 +18,11 @@ class LVAnimimg(LVImage):
@property
def anim(self):
return int(self._wv.safe_field("anim", 0))
return safe_wrapper(self._wv, "anim", "lvglgdb.lvgl.misc.lv_anim", "LVAnim")
@property
def dsc(self):
return ptr_or_none(self._wv.safe_field("dsc"))
@property
def pic_count(self):
@@ -28,6 +33,7 @@ class LVAnimimg(LVImage):
s = super().snapshot(include_children=include_children, include_styles=include_styles)
d = s.get('widget_data') or {}
d["anim"] = self.anim
d["dsc"] = self.dsc
d["pic_count"] = self.pic_count
s['widget_data'] = d
return s
@@ -46,14 +46,6 @@ class LVBar(LVObject):
"""Whether value been reversed"""
return int(self._wv.safe_field("val_reversed", 0))
@property
def cur_value_anim(self):
return int(self._wv.safe_field("cur_value_anim", 0))
@property
def start_value_anim(self):
return int(self._wv.safe_field("start_value_anim", 0))
@property
def mode(self):
"""Type of bar"""
@@ -74,8 +66,6 @@ class LVBar(LVObject):
d["start_value"] = self.start_value
d["indic_area"] = self.indic_area
d["val_reversed"] = self.val_reversed
d["cur_value_anim"] = self.cur_value_anim
d["start_value_anim"] = self.start_value_anim
d["mode"] = self.mode
d["orientation"] = self.orientation
s['widget_data'] = d
@@ -17,9 +17,9 @@ class LVButtonmatrix(LVObject):
self._wv = self.cast("lv_buttonmatrix_t", ptr=True)
@property
def const(self):
def map_p(self):
"""Pointer to the current map"""
return safe_string(self._wv, "const")
return safe_string(self._wv, "map_p")
@property
def button_areas(self):
@@ -60,7 +60,7 @@ class LVButtonmatrix(LVObject):
"""Snapshot with widget-specific fields in widget_data."""
s = super().snapshot(include_children=include_children, include_styles=include_styles)
d = s.get('widget_data') or {}
d["const"] = self.const
d["map_p"] = self.map_p
d["button_areas"] = self.button_areas
d["ctrl_bits"] = self.ctrl_bits
d["btn_cnt"] = self.btn_cnt
@@ -20,16 +20,6 @@ class LVCalendar(LVObject):
def btnm(self):
return ptr_or_none(self._wv.safe_field("btnm"))
@property
def today(self):
"""Date of today"""
return int(self._wv.safe_field("today", 0))
@property
def showed_date(self):
"""Currently visible month (day is ignored)"""
return int(self._wv.safe_field("showed_date", 0))
@property
def highlighted_dates(self):
"""Apply different style on these days (pointer to user-defined array)"""
@@ -49,8 +39,6 @@ class LVCalendar(LVObject):
s = super().snapshot(include_children=include_children, include_styles=include_styles)
d = s.get('widget_data') or {}
d["btnm"] = self.btnm
d["today"] = self.today
d["showed_date"] = self.showed_date
d["highlighted_dates"] = self.highlighted_dates
d["highlighted_dates_num"] = self.highlighted_dates_num
d["use_chinese_calendar"] = self.use_chinese_calendar
@@ -6,7 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from .lv_image import LVImage
from ._helpers import ptr_or_none
from ._helpers import ptr_or_none, safe_wrapper
class LVCanvas(LVImage):
@@ -22,7 +22,7 @@ class LVCanvas(LVImage):
@property
def static_buf(self):
return int(self._wv.safe_field("static_buf", 0))
return safe_wrapper(self._wv, "static_buf", "lvglgdb.lvgl.draw.lv_draw_buf", "LVDrawBuf")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+3 -2
View File
@@ -6,6 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from lvglgdb.lvgl.core.lv_obj import LVObject
from ._helpers import safe_wrapper
class LVChart(LVObject):
@@ -18,12 +19,12 @@ class LVChart(LVObject):
@property
def series_ll(self):
"""Linked list for series (stores lv_chart_series_t)"""
return int(self._wv.safe_field("series_ll", 0))
return safe_wrapper(self._wv, "series_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cursor_ll(self):
"""Linked list for cursors (stores lv_chart_cursor_t)"""
return int(self._wv.safe_field("cursor_ll", 0))
return safe_wrapper(self._wv, "cursor_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def pressed_point_id(self):
@@ -6,7 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from lvglgdb.lvgl.core.lv_obj import LVObject
from ._helpers import ptr_or_none, safe_string
from ._helpers import ptr_or_none, safe_string, safe_wrapper
class LVImePinyin(LVObject):
@@ -30,7 +30,7 @@ class LVImePinyin(LVObject):
@property
def k9_legal_py_ll(self):
return int(self._wv.safe_field("k9_legal_py_ll", 0))
return safe_wrapper(self._wv, "k9_legal_py_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cand_str(self):
@@ -29,10 +29,6 @@ class LVLabel(LVObject):
"""Offset where bytes have been replaced with dots"""
return int(self._wv.safe_field("dot_begin", 0))
@property
def hint(self):
return int(self._wv.safe_field("hint", 0))
@property
def sel_start(self):
return int(self._wv.safe_field("sel_start", 0))
@@ -92,7 +88,6 @@ class LVLabel(LVObject):
d["text"] = self.text
d["translation_tag"] = self.translation_tag
d["dot_begin"] = self.dot_begin
d["hint"] = self.hint
d["sel_start"] = self.sel_start
d["sel_end"] = self.sel_end
d["size_cache"] = self.size_cache
@@ -15,7 +15,27 @@ class LVLine(LVObject):
super().__init__(obj)
self._wv = self.cast("lv_line_t", ptr=True)
@property
def point_num(self):
"""Number of points in 'point_array'"""
return int(self._wv.safe_field("point_num", 0))
@property
def y_inv(self):
"""1: y == 0 will be on the bottom"""
return int(self._wv.safe_field("y_inv", 0))
@property
def point_array_is_mutable(self):
"""whether the point array is const or mutable"""
return int(self._wv.safe_field("point_array_is_mutable", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
s = super().snapshot(include_children=include_children, include_styles=include_styles)
d = s.get('widget_data') or {}
d["point_num"] = self.point_num
d["y_inv"] = self.y_inv
d["point_array_is_mutable"] = self.point_array_is_mutable
s['widget_data'] = d
return s
+2 -2
View File
@@ -6,7 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from lvglgdb.lvgl.core.lv_obj import LVObject
from ._helpers import ptr_or_none
from ._helpers import ptr_or_none, safe_wrapper
class LVMenu(LVObject):
@@ -59,7 +59,7 @@ class LVMenu(LVObject):
@property
def history_ll(self):
return int(self._wv.safe_field("history_ll", 0))
return safe_wrapper(self._wv, "history_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cur_depth(self):
+8 -2
View File
@@ -6,6 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from lvglgdb.lvgl.core.lv_obj import LVObject
from ._helpers import safe_string, safe_wrapper
class LVScale(LVObject):
@@ -18,7 +19,11 @@ class LVScale(LVObject):
@property
def section_ll(self):
"""Linked list for the sections (stores lv_scale_section_t)"""
return int(self._wv.safe_field("section_ll", 0))
return safe_wrapper(self._wv, "section_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def txt_src(self):
return safe_string(self._wv, "txt_src")
@property
def mode(self):
@@ -87,13 +92,14 @@ class LVScale(LVObject):
@property
def needles(self):
"""Needle list of this scale"""
return int(self._wv.safe_field("needles", 0))
return safe_wrapper(self._wv, "needles", "lvglgdb.lvgl.misc.lv_array", "LVArray")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
s = super().snapshot(include_children=include_children, include_styles=include_styles)
d = s.get('widget_data') or {}
d["section_ll"] = self.section_ll
d["txt_src"] = self.txt_src
d["mode"] = self.mode
d["range_min"] = self.range_min
d["range_max"] = self.range_max
@@ -6,6 +6,7 @@ Do not edit manually. Regenerate from the GDB script root with:
"""
from lvglgdb.lvgl.core.lv_obj import LVObject
from ._helpers import safe_wrapper
class LVSpangroup(LVObject):
@@ -36,7 +37,7 @@ class LVSpangroup(LVObject):
@property
def child_ll(self):
return int(self._wv.safe_field("child_ll", 0))
return safe_wrapper(self._wv, "child_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def overflow(self):
@@ -24,6 +24,10 @@ class LVTable(LVObject):
def row_cnt(self):
return int(self._wv.safe_field("row_cnt", 0))
@property
def cell_data(self):
return ptr_or_none(self._wv.safe_field("cell_data"))
@property
def row_h(self):
return ptr_or_none(self._wv.safe_field("row_h"))
@@ -46,6 +50,7 @@ class LVTable(LVObject):
d = s.get('widget_data') or {}
d["col_cnt"] = self.col_cnt
d["row_cnt"] = self.row_cnt
d["cell_data"] = self.cell_data
d["row_h"] = self.row_h
d["col_w"] = self.col_w
d["col_act"] = self.col_act
@@ -56,6 +56,40 @@ class LVTextarea(LVObject):
"""Time to show characters in password mode before change them to '*'"""
return int(self._wv.safe_field("pwd_show_time", 0))
@property
def sel_start(self):
"""Temporary values for text selection"""
return int(self._wv.safe_field("sel_start", 0))
@property
def sel_end(self):
return int(self._wv.safe_field("sel_end", 0))
@property
def text_sel_in_prog(self):
"""User is in process of selecting"""
return int(self._wv.safe_field("text_sel_in_prog", 0))
@property
def text_sel_en(self):
"""Text can be selected on this text area"""
return int(self._wv.safe_field("text_sel_en", 0))
@property
def pwd_mode(self):
"""Replace characters with '*'"""
return int(self._wv.safe_field("pwd_mode", 0))
@property
def one_line(self):
"""One line mode (ignore line breaks)"""
return int(self._wv.safe_field("one_line", 0))
@property
def static_accepted_chars(self):
"""1: Only a pointer is saved in `accepted_chars`"""
return int(self._wv.safe_field("static_accepted_chars", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
s = super().snapshot(include_children=include_children, include_styles=include_styles)
@@ -68,5 +102,12 @@ class LVTextarea(LVObject):
d["accepted_chars"] = self.accepted_chars
d["max_length"] = self.max_length
d["pwd_show_time"] = self.pwd_show_time
d["sel_start"] = self.sel_start
d["sel_end"] = self.sel_end
d["text_sel_in_prog"] = self.text_sel_in_prog
d["text_sel_en"] = self.text_sel_en
d["pwd_mode"] = self.pwd_mode
d["one_line"] = self.one_line
d["static_accepted_chars"] = self.static_accepted_chars
s['widget_data'] = d
return s
@@ -36,6 +36,35 @@ SIMPLE_INT_TYPES = {
}
def _scan_enum_types() -> set[str]:
"""Scan LVGL headers to find all typedef enum and int-like alias type names."""
result = set()
for h in LVGL_SRC.rglob("*.h"):
text = h.read_text(errors="ignore")
# typedef enum { ... } lv_xxx_t;
for m in re.finditer(
r"typedef\s+enum\s*\{[^}]*\}\s*(lv_\w+_t)", text, re.DOTALL
):
result.add(m.group(1))
# typedef <int-like> lv_xxx_t;
for m in re.finditer(
r"typedef\s+((?:unsigned\s+)?\w+)\s+(lv_\w+_t)\s*;", text
):
base = m.group(1).strip()
if base in (
"unsigned int", "unsigned char", "unsigned short",
"unsigned long", "int", "char", "short", "long",
"uint8_t", "uint16_t", "uint32_t", "uint64_t",
"int8_t", "int16_t", "int32_t", "int64_t", "size_t",
):
result.add(m.group(2))
return result
# Pre-scan: types safe to cast to int (enums + int-like typedefs)
_INT_SAFE_TYPES = _scan_enum_types()
@dataclass
class StructField:
name: str
@@ -127,11 +156,11 @@ def parse_struct_fields(body: str) -> list[StructField]:
))
continue
fm = re.match(r"((?:const\s+)?\w[\w\s]*?\s*\*?)\s+(\*?\w+)", line)
fm = re.match(r"((?:const\s+)?[\w][\w\s]*?(?:\s*\*\s*(?:const\s*)?)*\*?)\s+(\*?\w+)", line)
if fm:
c_type = fm.group(1).strip()
name = fm.group(2).strip().lstrip("*")
is_ptr = "*" in fm.group(1) or fm.group(2).startswith("*")
is_ptr = "*" in c_type or fm.group(2).startswith("*")
fields.append(StructField(
name=name, c_type=c_type,
is_pointer=is_ptr,
@@ -143,15 +172,31 @@ def parse_struct_fields(body: str) -> list[StructField]:
return fields
def _find_structs(text: str):
"""Find top-level struct _lv_*_t definitions with brace-balanced matching."""
for m in re.finditer(r"struct\s+_lv_(\w+)_t\s*\{", text):
name = m.group(1)
start = m.end()
depth = 1
i = start
while i < len(text) and depth > 0:
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
i += 1
if depth == 0:
yield name, text[start:i - 1]
def parse_widgets() -> dict[str, WidgetDef]:
widgets = {}
for private_h in sorted(WIDGETS_DIR.glob("*/lv_*_private.h")):
text = private_h.read_text()
widget_dir = private_h.parent.name
for m in re.finditer(r"struct\s+_lv_(\w+)_t\s*\{([^}]+)\}", text, re.DOTALL):
struct_name = f"lv_{m.group(1)}_t"
body = m.group(2)
for raw_name, body in _find_structs(text):
struct_name = f"lv_{raw_name}_t"
first_line = ""
for line in body.splitlines():
@@ -194,9 +239,21 @@ def _field_expr(f: StructField) -> str | None:
return f'safe_area(self._wv, "{f.name}")'
if f.c_type == "lv_point_t":
return f'safe_point(self._wv, "{f.name}")'
# Known wrapper types — use snapshot() for rich output
_WRAPPER_TYPES = {
"lv_draw_buf_t": ("lvglgdb.lvgl.draw.lv_draw_buf", "LVDrawBuf"),
"lv_ll_t": ("lvglgdb.lvgl.misc.lv_ll", "LVList"),
"lv_anim_t": ("lvglgdb.lvgl.misc.lv_anim", "LVAnim"),
"lv_array_t": ("lvglgdb.lvgl.misc.lv_array", "LVArray"),
}
if f.c_type in _WRAPPER_TYPES:
mod, cls = _WRAPPER_TYPES[f.c_type]
return f'safe_wrapper(self._wv, "{f.name}", "{mod}", "{cls}")'
if f.is_bitfield or f.c_type in SIMPLE_INT_TYPES or f.c_type.startswith(("uint", "int")):
return f'int(self._wv.safe_field("{f.name}", 0))'
if f.c_type.startswith("lv_") and f.c_type.endswith("_t"):
# TODO: implement generic struct expansion (Value.to_dict) for
# non-enum lv_*_t types like lv_calendar_date_t.
if f.c_type in _INT_SAFE_TYPES:
return f'int(self._wv.safe_field("{f.name}", 0))'
return None
@@ -264,6 +321,18 @@ def safe_point(obj, field_name):
"x": int(val.safe_field("x", 0)),
"y": int(val.safe_field("y", 0)),
}
def safe_wrapper(obj, field_name, module_path, class_name):
"""Read a struct field using its known Value wrapper, return snapshot dict."""
val = obj.safe_field(field_name)
if val is None or not getattr(val, 'is_ok', True):
return None
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
return wrapper.snapshot().as_dict()
'''
@@ -302,6 +371,8 @@ def gen_widget_file(wdef: WidgetDef, widgets: dict[str, WidgetDef]) -> str:
needs.add("safe_area")
if "safe_point" in expr:
needs.add("safe_point")
if "safe_wrapper" in expr:
needs.add("safe_wrapper")
if needs:
imports = ", ".join(sorted(needs))