From 8fa1c297733bb396a5e2d3e570b543a32a2b43a2 Mon Sep 17 00:00:00 2001 From: Benign X <1341398182@qq.com> Date: Tue, 17 Mar 2026 10:54:30 +0800 Subject: [PATCH] chore(gdb): add snapshot/data_utils/formatter layers with declarative _DISPLAY_SPEC (#9866) --- docs/src/debugging/gdb_plugin.rst | 166 ++++++++++- scripts/gdb/lvglgdb/__init__.py | 2 - scripts/gdb/lvglgdb/cmds/core/lv_group.py | 8 +- scripts/gdb/lvglgdb/cmds/core/lv_indev.py | 8 +- scripts/gdb/lvglgdb/cmds/core/lv_obj_class.py | 9 +- scripts/gdb/lvglgdb/cmds/core/lv_observer.py | 3 +- scripts/gdb/lvglgdb/cmds/draw/lv_draw_task.py | 5 +- scripts/gdb/lvglgdb/cmds/misc/lv_anim.py | 5 +- scripts/gdb/lvglgdb/cmds/misc/lv_cache.py | 24 +- scripts/gdb/lvglgdb/cmds/misc/lv_fs.py | 8 +- .../gdb/lvglgdb/cmds/misc/lv_image_decoder.py | 8 +- scripts/gdb/lvglgdb/cmds/misc/lv_style.py | 21 +- scripts/gdb/lvglgdb/cmds/misc/lv_timer.py | 8 +- scripts/gdb/lvglgdb/lvgl/__init__.py | 17 +- scripts/gdb/lvglgdb/lvgl/core/lv_group.py | 74 +++-- scripts/gdb/lvglgdb/lvgl/core/lv_indev.py | 76 ++--- scripts/gdb/lvglgdb/lvgl/core/lv_obj.py | 102 ++++++- scripts/gdb/lvglgdb/lvgl/core/lv_obj_class.py | 117 ++++---- scripts/gdb/lvglgdb/lvgl/core/lv_observer.py | 59 +++- scripts/gdb/lvglgdb/lvgl/data_utils.py | 18 ++ .../gdb/lvglgdb/lvgl/display/lv_display.py | 22 ++ scripts/gdb/lvglgdb/lvgl/draw/lv_draw_task.py | 59 ++-- scripts/gdb/lvglgdb/lvgl/draw/lv_draw_unit.py | 36 ++- scripts/gdb/lvglgdb/lvgl/formatter.py | 281 ++++++++++++++++++ scripts/gdb/lvglgdb/lvgl/misc/__init__.py | 14 +- scripts/gdb/lvglgdb/lvgl/misc/lv_anim.py | 138 +++++---- scripts/gdb/lvglgdb/lvgl/misc/lv_cache.py | 87 +++--- .../gdb/lvglgdb/lvgl/misc/lv_cache_entry.py | 20 -- .../gdb/lvglgdb/lvgl/misc/lv_cache_lru_rb.py | 26 +- scripts/gdb/lvglgdb/lvgl/misc/lv_event.py | 59 ++-- scripts/gdb/lvglgdb/lvgl/misc/lv_fs.py | 73 ++--- .../gdb/lvglgdb/lvgl/misc/lv_image_cache.py | 73 ++--- .../gdb/lvglgdb/lvgl/misc/lv_image_decoder.py | 49 +-- .../lvgl/misc/lv_image_header_cache.py | 65 ++-- scripts/gdb/lvglgdb/lvgl/misc/lv_ll.py | 24 ++ scripts/gdb/lvglgdb/lvgl/misc/lv_rb.py | 73 ++--- scripts/gdb/lvglgdb/lvgl/misc/lv_style.py | 54 ++-- scripts/gdb/lvglgdb/lvgl/misc/lv_timer.py | 72 ++--- scripts/gdb/lvglgdb/lvgl/snapshot.py | 54 ++++ 39 files changed, 1357 insertions(+), 660 deletions(-) create mode 100644 scripts/gdb/lvglgdb/lvgl/data_utils.py create mode 100644 scripts/gdb/lvglgdb/lvgl/formatter.py create mode 100644 scripts/gdb/lvglgdb/lvgl/snapshot.py diff --git a/docs/src/debugging/gdb_plugin.rst b/docs/src/debugging/gdb_plugin.rst index 9025775241..060e734d65 100644 --- a/docs/src/debugging/gdb_plugin.rst +++ b/docs/src/debugging/gdb_plugin.rst @@ -25,15 +25,15 @@ Example of usage: (gdb) source lvgl/scripts/gdb/gdbinit.py (gdb) dump obj -L 2 - obj@0x60700000dd10 (0,0,799,599) - tabview@0x608000204ca0 (0,0,799,599) - obj@0x607000025da0 (0,0,799,69) - obj@0x607000025e80 (0,70,799,599) - obj@0x60700002bd70 (743,543,791,591) - btn@0x60700002c7f0 (747,547,787,587) - keyboard@0x60d0000f7040 (0,300,799,599) - dropdown-list@0x608000205420 (0,0,129,129) - label@0x60d0000f7ba0 (22,22,56,39) + obj@0x60700000dd10 0,0,799,599 + tabview@0x608000204ca0 0,0,799,599 + obj@0x607000025da0 0,0,799,69 + obj@0x607000025e80 0,70,799,599 + obj@0x60700002bd70 743,543,791,591 + btn@0x60700002c7f0 747,547,787,587 + keyboard@0x60d0000f7040 0,300,799,599 + dropdown-list@0x608000205420 0,0,129,129 + label@0x60d0000f7ba0 22,22,56,39 (gdb) The plugin provides the following commands. @@ -50,9 +50,10 @@ The plugin provides the following commands. - ``dump fs_drv``: List all registered filesystem drivers. - ``dump draw_task ``: List draw tasks from a layer. - ``info style``: Inspect style properties of an ``lv_style_t`` or an ``lv_obj_t``. -- ``info draw_unit``: Display all current drawing unit information. +- ``info draw_unit``: Print raw struct details for each drawing unit. - ``info obj_class ``: Show object class hierarchy. - ``info subject ``: Show subject and its observers. +- ``lvglobal``: (NuttX only) Set which LVGL instance to inspect. .. note:: @@ -204,8 +205,13 @@ Example: (gdb) info obj_class lv_button_class ObjClass: lv_button -> lv_obj -> lv_obj - size=... editable=0 group_def=2 - default_size=(CONTENT, CONTENT) theme_inheritable=True + name = lv_button + base = lv_obj + size = 48 editable=0 group_def=2 + editable = 0 + group_def = 2 + default_size = (CONTENT, CONTENT) theme_inheritable=True + theme_inh = True Inspect Subject *************** @@ -218,4 +224,138 @@ Example: (gdb) info subject &my_subject Subject: type=INT subscribers=2 - Observer: cb=0x... target=0x... for_obj=True + Observer: cb= target=0x... for_obj=True + + +Set LVGL Instance (NuttX) +************************* + +``lvglobal``: Set which LVGL instance to inspect by finding the ``lv_global`` +pointer. On single-instance systems, it auto-detects the global. On NuttX +multi-process systems, use ``--pid`` to specify the target process. + +.. code:: bash + + (gdb) lvglobal + (gdb) lvglobal --pid 3 + + +Data Export API +*************** + +Each wrapper class provides a ``snapshot()`` method that returns a ``Snapshot`` +object containing a pure Python dict (JSON-serializable) plus an optional +reference to the original wrapper via ``_source``. + +.. code:: python + + from lvglgdb import LVTimer, curr_inst + + timers = list(curr_inst().timers()) + snap = timers[0].snapshot() + + # Dict-like access + print(snap["timer_cb"], snap["period"]) + + # JSON serialization + import json + print(json.dumps(snap.as_dict(), indent=2)) + + # Bulk export + snapshots = LVTimer.snapshots(timers) + data = [s.as_dict() for s in snapshots] + +The ``Snapshot`` class supports dict-like read access (``[]``, ``keys()``, +``len()``, ``in``, iteration) and ``as_dict()`` for JSON serialization. +All values in ``as_dict()`` are pure Python types (``str``, ``int``, ``float``, +``bool``, ``None``, ``dict``, ``list``) with no ``gdb.Value`` references. + +Wrapper classes with ``snapshot()`` support: ``LVAnim``, ``LVTimer``, +``LVIndev``, ``LVGroup``, ``LVObject``, ``LVObjClass``, ``LVObserver``, +``LVSubject``, ``LVDrawTask``, ``LVDrawUnit``, ``LVFsDrv``, +``LVImageDecoder``, ``LVCache``, ``LVDisplay``, +``LVRedBlackTree``, ``LVEventDsc``, ``LVList``. + +``LVStyle`` provides ``snapshots()`` (plural) which returns a list of +``Snapshot`` objects for each style property, but does not have a singular +``snapshot()`` method. + +Most wrapper classes also provide a static ``snapshots(items)`` method for +bulk export (e.g. ``LVAnim.snapshots(anims)``). Additionally, +``LVImageCache`` and ``LVImageHeaderCache`` provide instance-level +``snapshots()`` methods that export all cache entries. + + +Architecture +************ + +The GDB plugin decouples data extraction from display through a snapshot +abstraction. Each wrapper class (``LVAnim``, ``LVTimer``, ``LVObject``, etc.) +declares a ``_DISPLAY_SPEC`` describing its fields and exports a ``snapshot()`` +method that returns a self-describing ``Snapshot`` object carrying both the +data dict and the display spec. The ``cmds/`` layer simply passes snapshots to +generic formatters (``print_info``, ``print_spec_table``) which read the +embedded spec to render output — no command needs to know the internal +structure of any wrapper. + +.. mermaid:: + :zoom: + + graph RL + subgraph "cmds/ layer" + CMD["GDB Commands
(dump obj, info style, ...)"] + end + + subgraph "formatter.py" + PI["print_info(snapshot)"] + PT["print_table()"] + PST["print_spec_table(snapshots)"] + RTC["resolve_table_columns(spec)"] + PST --> RTC + PST --> PT + end + + subgraph "snapshot.py" + SNAP["Snapshot
._data (pure dict)
._display_spec
._source"] + end + + subgraph "data_utils.py" + DU["ptr_or_none()
fmt_cb()
..."] + end + + subgraph "Wrapper classes" + direction TB + DS["_DISPLAY_SPEC
{info, table, empty_msg}"] + SN["snapshot() → Snapshot"] + SNS["snapshots() → list[Snapshot]"] + SN --> SNAP + SN -. "display_spec=" .-> DS + SNS --> SN + end + + subgraph "Wrappers" + direction LR + W1["LVAnim"] + W2["LVTimer"] + W3["LVCache"] + W4["LVObject"] + W5["LVGroup"] + W6["LVIndev"] + W7["...others"] + end + + CMD -- "wrapper.snapshot()" --> SN + CMD -- "print_info(snap)" --> PI + CMD -- "print_spec_table(snaps)" --> PST + PI -- "reads _display_spec" --> SNAP + PST -- "reads _display_spec" --> SNAP + SN -- "uses" --> DU + W1 & W2 & W3 & W4 & W5 & W6 & W7 -. "each defines" .-> DS + + style PI fill:#4CAF50,color:#fff + style PT fill:#4CAF50,color:#fff + style PST fill:#4CAF50,color:#fff + style RTC fill:#4CAF50,color:#fff + style SNAP fill:#2196F3,color:#fff + style DU fill:#FF9800,color:#fff + style DS fill:#9C27B0,color:#fff diff --git a/scripts/gdb/lvglgdb/__init__.py b/scripts/gdb/lvglgdb/__init__.py index f95912e09d..d050a1c2c1 100644 --- a/scripts/gdb/lvglgdb/__init__.py +++ b/scripts/gdb/lvglgdb/__init__.py @@ -13,7 +13,6 @@ from .lvgl import ( ObjStyle, LVStyle, StyleEntry, - dump_style_info, dump_obj_styles, dump_obj_info, style_prop_name, @@ -62,7 +61,6 @@ __all__ = [ "ObjStyle", "LVStyle", "StyleEntry", - "dump_style_info", "dump_obj_styles", "dump_obj_info", "style_prop_name", diff --git a/scripts/gdb/lvglgdb/cmds/core/lv_group.py b/scripts/gdb/lvglgdb/cmds/core/lv_group.py index 825910e3b2..da426d5a7b 100644 --- a/scripts/gdb/lvglgdb/cmds/core/lv_group.py +++ b/scripts/gdb/lvglgdb/cmds/core/lv_group.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.core.lv_group import LVGroup +from lvglgdb.lvgl.formatter import print_spec_table class DumpGroup(gdb.Command): @@ -13,4 +14,9 @@ class DumpGroup(gdb.Command): ) def invoke(self, args, from_tty): - LVGroup.print_entries(curr_inst().groups()) + groups = curr_inst().groups() + snaps = LVGroup.snapshots(groups) + if snaps: + print_spec_table(snaps) + else: + print(LVGroup._DISPLAY_SPEC["empty_msg"]) diff --git a/scripts/gdb/lvglgdb/cmds/core/lv_indev.py b/scripts/gdb/lvglgdb/cmds/core/lv_indev.py index a5d5a575c0..f3d6fe6171 100644 --- a/scripts/gdb/lvglgdb/cmds/core/lv_indev.py +++ b/scripts/gdb/lvglgdb/cmds/core/lv_indev.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.core.lv_indev import LVIndev +from lvglgdb.lvgl.formatter import print_spec_table class DumpIndev(gdb.Command): @@ -13,4 +14,9 @@ class DumpIndev(gdb.Command): ) def invoke(self, args, from_tty): - LVIndev.print_entries(curr_inst().indevs()) + indevs = curr_inst().indevs() + snaps = LVIndev.snapshots(indevs) + if snaps: + print_spec_table(snaps) + else: + print(LVIndev._DISPLAY_SPEC["empty_msg"]) diff --git a/scripts/gdb/lvglgdb/cmds/core/lv_obj_class.py b/scripts/gdb/lvglgdb/cmds/core/lv_obj_class.py index ae38aa7677..0390122c26 100644 --- a/scripts/gdb/lvglgdb/cmds/core/lv_obj_class.py +++ b/scripts/gdb/lvglgdb/cmds/core/lv_obj_class.py @@ -3,6 +3,7 @@ import argparse import gdb from lvglgdb.lvgl.core.lv_obj_class import LVObjClass +from lvglgdb.lvgl.formatter import print_info, print_spec_table class InfoObjClass(gdb.Command): @@ -35,7 +36,11 @@ class InfoObjClass(gdb.Command): if args.all or not args.expr: classes = LVObjClass.collect_all() - LVObjClass.print_entries(classes) + snaps = LVObjClass.snapshots(classes) + if snaps: + print_spec_table(snaps) + else: + print(LVObjClass._DISPLAY_SPEC["empty_msg"]) return try: @@ -43,4 +48,4 @@ class InfoObjClass(gdb.Command): except gdb.error as e: print(f"Error: {e}") return - cls.print_info() + print_info(cls.snapshot()) diff --git a/scripts/gdb/lvglgdb/cmds/core/lv_observer.py b/scripts/gdb/lvglgdb/cmds/core/lv_observer.py index 14fd84ec0c..42b0b6bb1c 100644 --- a/scripts/gdb/lvglgdb/cmds/core/lv_observer.py +++ b/scripts/gdb/lvglgdb/cmds/core/lv_observer.py @@ -1,6 +1,7 @@ import gdb from lvglgdb.lvgl.core.lv_observer import LVSubject +from lvglgdb.lvgl.formatter import print_info class InfoSubject(gdb.Command): @@ -20,4 +21,4 @@ class InfoSubject(gdb.Command): except gdb.error as e: print(f"Error: {e}") return - subject.print_info() + print_info(subject.snapshot()) diff --git a/scripts/gdb/lvglgdb/cmds/draw/lv_draw_task.py b/scripts/gdb/lvglgdb/cmds/draw/lv_draw_task.py index 44fc74a739..39dee8a1a0 100644 --- a/scripts/gdb/lvglgdb/cmds/draw/lv_draw_task.py +++ b/scripts/gdb/lvglgdb/cmds/draw/lv_draw_task.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.value import Value from lvglgdb.lvgl.draw.lv_draw_task import LVDrawTask +from lvglgdb.lvgl.formatter import print_spec_table class DumpDrawTask(gdb.Command): @@ -22,4 +23,6 @@ class DumpDrawTask(gdb.Command): if not int(task_head): print("No draw tasks on this layer.") return - LVDrawTask.print_entries(LVDrawTask(task_head)) + print_spec_table( + LVDrawTask.snapshots(LVDrawTask(task_head)), + ) diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_anim.py b/scripts/gdb/lvglgdb/cmds/misc/lv_anim.py index 018e071089..c0a05a7860 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_anim.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_anim.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.misc.lv_anim import LVAnim +from lvglgdb.lvgl.formatter import print_info, print_spec_table class DumpAnim(gdb.Command): @@ -20,7 +21,7 @@ class DumpAnim(gdb.Command): if args.strip() == "--detail": for anim in anims: - anim.print_info() + print_info(anim.snapshot()) print() else: - LVAnim.print_entries(anims) + print_spec_table(LVAnim.snapshots(anims)) diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_cache.py b/scripts/gdb/lvglgdb/cmds/misc/lv_cache.py index 338abfade8..37b3e62c0e 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_cache.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_cache.py @@ -37,7 +37,29 @@ class DumpCache(gdb.Command): print("Invalid cache: ", args.cache) return - cache.print_entries() + from lvglgdb.lvgl.formatter import print_info, print_spec_table + + # Print cache-level info + snap = cache.snapshot() + print_info(snap) + + # Print cache entries + snaps = cache.snapshots() + extra_fields = getattr(cache, "_last_extra_fields", []) + + col_align = {"src": "l", "type": "c"} + + extra_columns = ["entry"] + extra_fields if extra_fields else ["entry"] + + def _extra_row(d): + extras = d.get("extra_fields", {}) + extra_vals = [extras.get(f, "") for f in extra_fields] + return [d["entry_addr"]] + extra_vals + + print_spec_table(snaps, + align="r", numbered=False, col_align=col_align, + extra_columns=extra_columns, + extra_row_fn=_extra_row) class CheckPrefix(gdb.Command): diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_fs.py b/scripts/gdb/lvglgdb/cmds/misc/lv_fs.py index 8369ef15bb..30e89af37d 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_fs.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_fs.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.misc.lv_fs import LVFsDrv +from lvglgdb.lvgl.formatter import print_spec_table class DumpFsDrv(gdb.Command): @@ -13,4 +14,9 @@ class DumpFsDrv(gdb.Command): ) def invoke(self, args, from_tty): - LVFsDrv.print_entries(curr_inst().fs_drivers()) + drivers = curr_inst().fs_drivers() + snaps = LVFsDrv.snapshots(drivers) + if snaps: + print_spec_table(snaps) + else: + print(LVFsDrv._DISPLAY_SPEC["empty_msg"]) diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_image_decoder.py b/scripts/gdb/lvglgdb/cmds/misc/lv_image_decoder.py index 5bbd46f474..65fb9e9098 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_image_decoder.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_image_decoder.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.misc.lv_image_decoder import LVImageDecoder +from lvglgdb.lvgl.formatter import print_spec_table class DumpImageDecoder(gdb.Command): @@ -13,4 +14,9 @@ class DumpImageDecoder(gdb.Command): ) def invoke(self, args, from_tty): - LVImageDecoder.print_entries(curr_inst().image_decoders()) + decoders = curr_inst().image_decoders() + snaps = LVImageDecoder.snapshots(decoders) + if snaps: + print_spec_table(snaps) + else: + print(LVImageDecoder._DISPLAY_SPEC["empty_msg"]) diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_style.py b/scripts/gdb/lvglgdb/cmds/misc/lv_style.py index 61c6f65b36..1cb85330cb 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_style.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_style.py @@ -2,6 +2,25 @@ import argparse import gdb from lvglgdb.lvgl import LVStyle, dump_obj_styles +from lvglgdb.lvgl.formatter import print_table + + +def _style_row_fn(_i, d): + """Format a style property row with optional ANSI color block.""" + value_str = d["value_str"] + color_rgb = d.get("color_rgb") + if color_rgb: + r, g, b = color_rgb["r"], color_rgb["g"], color_rgb["b"] + value_str = f"{value_str} \033[48;2;{r};{g};{b}m \033[0m" + return [d["prop_name"], value_str] + + +def print_style_props(entries): + """Print style properties as a 2-column table with optional ANSI color.""" + print_table( + entries, ["prop", "value"], _style_row_fn, + "Empty style.", align="l", numbered=False, + ) class InfoStyle(gdb.Command): @@ -43,6 +62,6 @@ class InfoStyle(gdb.Command): if not style: print("Invalid style:", args.style) return - LVStyle(style).print_entries() + print_style_props(LVStyle(style).snapshots()) else: print("Usage: info style or info style --obj ") diff --git a/scripts/gdb/lvglgdb/cmds/misc/lv_timer.py b/scripts/gdb/lvglgdb/cmds/misc/lv_timer.py index b820f44526..f69c043e82 100644 --- a/scripts/gdb/lvglgdb/cmds/misc/lv_timer.py +++ b/scripts/gdb/lvglgdb/cmds/misc/lv_timer.py @@ -2,6 +2,7 @@ import gdb from lvglgdb.lvgl import curr_inst from lvglgdb.lvgl.misc.lv_timer import LVTimer +from lvglgdb.lvgl.formatter import print_spec_table class DumpTimer(gdb.Command): @@ -13,4 +14,9 @@ class DumpTimer(gdb.Command): ) def invoke(self, args, from_tty): - LVTimer.print_entries(curr_inst().timers()) + timers = curr_inst().timers() + snaps = LVTimer.snapshots(timers) + if snaps: + print_spec_table(snaps) + else: + print(LVTimer._DISPLAY_SPEC["empty_msg"]) diff --git a/scripts/gdb/lvglgdb/lvgl/__init__.py b/scripts/gdb/lvglgdb/lvgl/__init__.py index 3a66ec94b7..c89d71b3b6 100644 --- a/scripts/gdb/lvglgdb/lvgl/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/__init__.py @@ -25,18 +25,13 @@ from .misc import ( LVList, LVStyle, StyleEntry, - dump_style_info, style_prop_name, decode_selector, format_style_value, LVRedBlackTree, - dump_rb_info, LVCache, - dump_cache_info, LVCacheEntry, - dump_cache_entry_info, LVCacheLRURB, - dump_lru_rb_cache_info, LVCacheLRURBIterator, LVCacheIteratorBase, LVImageCache, @@ -54,6 +49,9 @@ from .misc import ( LVFsDrv, format_coord, ) +from .snapshot import Snapshot +from .data_utils import fmt_cb, ptr_or_none +from . import formatter __all__ = [ "LVObject", @@ -69,20 +67,15 @@ __all__ = [ "LVList", "LVStyle", "StyleEntry", - "dump_style_info", "dump_obj_styles", "dump_obj_info", "style_prop_name", "decode_selector", "format_style_value", "LVRedBlackTree", - "dump_rb_info", "LVCache", - "dump_cache_info", "LVCacheEntry", - "dump_cache_entry_info", "LVCacheLRURB", - "dump_lru_rb_cache_info", "LVCacheLRURBIterator", "LVCacheIteratorBase", "LVImageCache", @@ -106,4 +99,8 @@ __all__ = [ "LVSubject", "LVObserver", "SUBJECT_TYPE_NAMES", + "Snapshot", + "fmt_cb", + "ptr_or_none", + "formatter", ] diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_group.py b/scripts/gdb/lvglgdb/lvgl/core/lv_group.py index a0345f91c6..19c9f77dab 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_group.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_group.py @@ -1,5 +1,3 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput from ..misc.lv_ll import LVList @@ -7,6 +5,18 @@ from ..misc.lv_ll import LVList class LVGroup(Value): """LVGL focus group wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("objects", "obj_count"), + ("frozen", "frozen"), + ("editing", "editing"), + ("wrap", "wrap"), + ("focused", "focused"), + ], + "table": [], + "empty_msg": "No focus groups.", + } + def __init__(self, group: ValueInput): super().__init__(Value.normalize(group, "lv_group_t")) @@ -55,34 +65,38 @@ class LVGroup(Value): for obj_ptr in LVList(self.obj_ll, "lv_obj_t"): yield LVObject(obj_ptr) - @staticmethod - def print_entries(groups): - """Print focus groups as a PrettyTable.""" - table = PrettyTable() - table.field_names = ["#", "objects", "frozen", "editing", "wrap", "focused"] - table.align = "l" + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot - for i, group in enumerate(groups): - focus = group.obj_focus - if focus: - from .lv_obj import LVObject + focus = self.obj_focus + if focus: + from .lv_obj import LVObject - focus_obj = LVObject(focus) - focus_str = f"{focus_obj.class_name}@{int(focus):x}" - else: - focus_str = "(none)" - table.add_row( - [ - i, - group.obj_count, - group.frozen, - group.editing, - group.wrap, - focus_str, - ] - ) - - if not table.rows: - print("No focus groups.") + focus_obj = LVObject(focus) + focus_str = f"{focus_obj.class_name}@{int(focus):x}" + focused_addr = hex(int(focus)) else: - print(table) + focus_str = "(none)" + focused_addr = None + + member_addrs = [] + for obj_ptr in LVList(self.obj_ll, "lv_obj_t"): + addr = int(obj_ptr) + if addr: + member_addrs.append(hex(addr)) + + d = { + "addr": hex(int(self)), + "obj_count": self.obj_count, + "frozen": self.frozen, + "editing": self.editing, + "wrap": self.wrap, + "focused": focus_str, + "focused_addr": focused_addr, + "member_addrs": member_addrs, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + + @staticmethod + def snapshots(groups): + return [group.snapshot() for group in groups] diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_indev.py b/scripts/gdb/lvglgdb/lvgl/core/lv_indev.py index 600410f4f8..33a465ceba 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_indev.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_indev.py @@ -1,5 +1,3 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput from .lv_indev_consts import INDEV_TYPE_NAMES @@ -7,6 +5,20 @@ from .lv_indev_consts import INDEV_TYPE_NAMES class LVIndev(Value): """LVGL input device wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("type", "type_name"), + ("enabled", "enabled"), + ("state", "state"), + ("read_cb", "read_cb"), + ("long_press_time", "long_press_time"), + ("scroll_limit", "scroll_limit"), + ("group", "group"), + ], + "table": [], + "empty_msg": "No input devices.", + } + def __init__(self, indev: ValueInput): super().__init__(Value.normalize(indev, "lv_indev_t")) @@ -70,40 +82,28 @@ class LVIndev(Value): def driver_data(self) -> Value: return self.super_value("driver_data") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb, ptr_or_none + + grp = int(self.group) + grp_str = f"0x{grp:x}" if grp else "-" + d = { + "addr": hex(int(self)), + "type": self.type, + "type_name": self.type_name, + "enabled": self.enabled, + "state": self.state, + "read_cb": fmt_cb(self.read_cb), + "long_press_time": self.long_press_time, + "scroll_limit": self.scroll_limit, + "group": grp_str, + "display_addr": ptr_or_none(self.disp), + "group_addr": ptr_or_none(self.group), + "read_timer_addr": ptr_or_none(self.read_timer), + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(indevs): - """Print input devices as a PrettyTable.""" - table = PrettyTable() - table.field_names = [ - "#", - "type", - "enabled", - "state", - "read_cb", - "long_press_time", - "scroll_limit", - "group", - ] - table.align = "l" - - for i, indev in enumerate(indevs): - cb_str = indev.read_cb.format_string(symbols=True) - grp = int(indev.group) - grp_str = f"0x{grp:x}" if grp else "-" - table.add_row( - [ - i, - indev.type_name, - indev.enabled, - indev.state, - cb_str, - indev.long_press_time, - indev.scroll_limit, - grp_str, - ] - ) - - if not table.rows: - print("No input devices.") - else: - print(table) + def snapshots(indevs): + return [indev.snapshot() for indev in indevs] diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py b/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py index c08304a796..8ab3e051ab 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj.py @@ -1,5 +1,5 @@ from lvglgdb.value import Value, ValueInput -from lvglgdb.lvgl.misc.lv_style import LVStyle, StyleEntry, decode_selector +from lvglgdb.lvgl.misc.lv_style import LVStyle, decode_selector class ObjStyle: @@ -25,10 +25,35 @@ class ObjStyle: def __len__(self): return len(list(self.style)) + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + props = [s.as_dict() for s in self.style.snapshots()] + d = { + "index": self.index, + "selector": self.selector, + "selector_str": self.selector_str, + "flags_str": self.flags_str, + "properties": props, + } + return Snapshot(d, source=self) + class LVObject(Value): """LVGL object""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: ( + f"{d['class_name']}@{d['addr']}" + f" {d['coords']['x1']},{d['coords']['y1']}," + f"{d['coords']['x2']},{d['coords']['y2']}" + )), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, obj: ValueInput): super().__init__(Value.normalize(obj, "lv_obj_t")) @@ -105,22 +130,73 @@ class LVObject(Value): def get_child(self, index: int): return self.spec_attr.children[index] if self.spec_attr else None + def snapshot(self, include_children=False, include_styles=False): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import ptr_or_none + + d = { + "addr": hex(int(self)), + "class_name": self.class_name, + "coords": { + "x1": self.x1, + "y1": self.y1, + "x2": self.x2, + "y2": self.y2, + }, + "child_count": int(self.child_count), + "style_count": int(self.style_cnt), + "parent_addr": ptr_or_none(self.super_value("parent")), + "group_addr": self._get_group_addr(), + } + if include_children: + d["children"] = [ + c.snapshot(include_children=True, include_styles=include_styles).as_dict() + for c in self.children + ] + if include_styles: + d["styles"] = [s.snapshot().as_dict() for s in self.obj_styles] + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + + def _get_group_addr(self): + """Get group address from spec_attr, or None.""" + spec = self.spec_attr + if not spec or not int(spec): + return None + try: + grp = spec.group + addr = int(grp) + return hex(addr) if addr else None + except Exception: + return None + def dump_obj_info(obj: LVObject): - clzname = obj.class_name - coords = f"{obj.x1},{obj.y1},{obj.x2},{obj.y2}" - print(f"{clzname}@{hex(obj)} {coords}") + from lvglgdb.lvgl.formatter import print_info + print_info(obj.snapshot()) def dump_obj_styles(obj: ValueInput): - """Print all styles of an object, reusing LVStyle.print_entries().""" + """Print all styles of an object.""" + from lvglgdb.lvgl.formatter import print_table + lv_obj = LVObject(obj) - - has_any = False - for obj_style in lv_obj.obj_styles: - has_any = True - print(f"[{obj_style.index}] {obj_style.selector_str} {obj_style.flags_str}") - obj_style.style.print_entries() - - if not has_any: + d = lv_obj.snapshot(include_styles=True) + styles = d.get("styles") + if not styles: print("No styles applied.") + return + + def _style_row(_i, entry): + value_str = entry["value_str"] + color_rgb = entry.get("color_rgb") + if color_rgb: + r, g, b = color_rgb["r"], color_rgb["g"], color_rgb["b"] + value_str = f"{value_str} \033[48;2;{r};{g};{b}m \033[0m" + return [entry["prop_name"], value_str] + + for s in styles: + print(f"[{s['index']}] {s['selector_str']} {s['flags_str']}") + print_table( + s.get("properties", []), ["prop", "value"], _style_row, + "Empty style.", align="l", numbered=False, + ) diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_obj_class.py b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_class.py index cbab387109..fb97b40ecd 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_obj_class.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_obj_class.py @@ -1,5 +1,4 @@ import gdb -from prettytable import PrettyTable from lvglgdb.value import Value, ValueInput from ..misc.lv_utils import format_coord @@ -8,6 +7,37 @@ from ..misc.lv_utils import format_coord class LVObjClass(Value): """LVGL object class wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: ( + f"ObjClass: {' -> '.join(d.get('class_chain', [d['name']]))}" + )), + ("name", "name"), + ("base", "base_class"), + ("size", lambda d: ( + f"{d['instance_size']} editable={d['editable']}" + f" group_def={d['group_def']}" + )), + ("editable", "editable"), + ("group_def", "group_def"), + ("default_size", lambda d: ( + f"({d['width_def_str']}, {d['height_def_str']})" + f" theme_inheritable={d['theme_inheritable']}" + )), + ("theme_inh", "theme_inheritable"), + ("_skip_if", "constructor_cb", "-", "constructor_cb"), + ("_skip_if", "destructor_cb", "-", "destructor_cb"), + ("_skip_if", "event_cb", "-", "event_cb"), + ], + "table": [ + ("size", "instance_size"), + ("default_size", lambda d: ( + f"({d['width_def_str']}, {d['height_def_str']})" + )), + ], + "empty_msg": "No object classes found.", + } + def __init__(self, cls: ValueInput): super().__init__(Value.normalize(cls, "lv_obj_class_t")) @@ -87,63 +117,30 @@ class LVObjClass(Value): pass return classes + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb + + base = self.base_class + d = { + "addr": hex(int(self)), + "name": self.name, + "base_class": base.name if base else "-", + "instance_size": self.instance_size, + "editable": self.editable, + "group_def": self.group_def, + "width_def": self.width_def, + "height_def": self.height_def, + "width_def_str": format_coord(self.width_def), + "height_def_str": format_coord(self.height_def), + "theme_inheritable": self.theme_inheritable, + "constructor_cb": fmt_cb(self.constructor_cb), + "destructor_cb": fmt_cb(self.destructor_cb), + "event_cb": fmt_cb(self.event_cb), + "class_chain": [c.name for c in self.__iter__()], + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(classes): - """Print object classes as a PrettyTable.""" - table = PrettyTable() - table.field_names = [ - "#", - "name", - "base", - "size", - "editable", - "group_def", - "default_size", - "theme_inh", - ] - table.align = "l" - - for i, cls in enumerate(classes): - base = cls.base_class - base_name = base.name if base else "-" - table.add_row( - [ - i, - cls.name, - base_name, - cls.instance_size, - cls.editable, - cls.group_def, - f"({format_coord(cls.width_def)}, {format_coord(cls.height_def)})", - cls.theme_inheritable, - ] - ) - - if not table.rows: - print("No object classes found.") - else: - print(table) - - def print_info(self): - chain = list(self.__iter__()) - names = [c.name for c in chain] - print(f"ObjClass: {' -> '.join(names)}") - print( - f" size={self.instance_size} editable={self.editable} group_def={self.group_def}" - ) - w = format_coord(self.width_def) - h = format_coord(self.height_def) - print(f" default_size=({w}, {h}) theme_inheritable={self.theme_inheritable}") - ctor = int(self.constructor_cb) - dtor = int(self.destructor_cb) - evt = int(self.event_cb) - if ctor: - print( - f" constructor_cb = {self.constructor_cb.format_string(symbols=True)}" - ) - if dtor: - print( - f" destructor_cb = {self.destructor_cb.format_string(symbols=True)}" - ) - if evt: - print(f" event_cb = {self.event_cb.format_string(symbols=True)}") + def snapshots(classes): + return [cls.snapshot() for cls in classes] diff --git a/scripts/gdb/lvglgdb/lvgl/core/lv_observer.py b/scripts/gdb/lvglgdb/lvgl/core/lv_observer.py index d07292315d..6e7ca4e6db 100644 --- a/scripts/gdb/lvglgdb/lvgl/core/lv_observer.py +++ b/scripts/gdb/lvglgdb/lvgl/core/lv_observer.py @@ -37,16 +37,44 @@ class LVObserver(Value): def for_obj(self) -> bool: return bool(int(self.super_value("for_obj"))) - def print_info(self): - cb_str = self.cb.format_string(symbols=True, address=True) - print( - f" Observer: cb={cb_str} target={self.target}" f" for_obj={self.for_obj}" - ) + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb, ptr_or_none + + d = { + "addr": hex(int(self)), + "cb": fmt_cb(self.cb), + "target": str(self.target), + "for_obj": self.for_obj, + "user_data": str(self.user_data), + "subject_addr": ptr_or_none(self.subject), + "target_addr": ptr_or_none(self.target), + } + return Snapshot(d, source=self) class LVSubject(Value): """LVGL subject wrapper""" + _OBSERVER_FIELDS = [ + ("_title", lambda d: ( + f" Observer: cb={d['cb']} target={d['target']}" + f" for_obj={d['for_obj']}" + )), + ] + + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: ( + f"Subject: type={d['type_name']}" + f" subscribers={len(d.get('observers', []))}" + )), + ("_children", "observers", _OBSERVER_FIELDS), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, subject: ValueInput): super().__init__(Value.normalize(subject, "lv_subject_t")) @@ -70,8 +98,21 @@ class LVSubject(Value): for obs in LVList(self.subs_ll, "lv_observer_t"): yield LVObserver(obs) - def print_info(self): - ll = LVList(self.subs_ll, "lv_observer_t") - print(f"Subject: type={self.type_name} subscribers={ll.len}") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + observers = [] + observer_addrs = [] for obs in self.__iter__(): - obs.print_info() + observers.append(obs.snapshot().as_dict()) + observer_addrs.append(hex(int(obs))) + + d = { + "addr": hex(int(self)), + "type": self.type, + "type_name": self.type_name, + "size": self.size, + "observers": observers, + "observer_addrs": observer_addrs, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) diff --git a/scripts/gdb/lvglgdb/lvgl/data_utils.py b/scripts/gdb/lvglgdb/lvgl/data_utils.py new file mode 100644 index 0000000000..8763f7c404 --- /dev/null +++ b/scripts/gdb/lvglgdb/lvgl/data_utils.py @@ -0,0 +1,18 @@ +from typing import Optional + +from lvglgdb.value import Value + + +def fmt_cb(cb: Value) -> str: + """Format callback pointer as resolved symbol string or '-' for NULL. + Strips null bytes that may appear in some GDB output.""" + addr = int(cb) + if not addr: + return "-" + return cb.format_string(symbols=True, address=True).replace("\x00", "") + + +def ptr_or_none(val: Value) -> Optional[str]: + """Convert pointer to hex string or None if NULL.""" + addr = int(val) + return hex(addr) if addr else None diff --git a/scripts/gdb/lvglgdb/lvgl/display/lv_display.py b/scripts/gdb/lvglgdb/lvgl/display/lv_display.py index f27f466e09..11566c4893 100644 --- a/scripts/gdb/lvglgdb/lvgl/display/lv_display.py +++ b/scripts/gdb/lvglgdb/lvgl/display/lv_display.py @@ -6,6 +6,17 @@ from lvglgdb.value import Value, ValueInput class LVDisplay(Value): """LVGL display""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: f"Display @{d['addr']}"), + ("hor_res", "hor_res"), + ("ver_res", "ver_res"), + ("screen_count", "screen_count"), + ], + "table": [], + "empty_msg": "No displays.", + } + def __init__(self, disp: ValueInput): super().__init__(Value.normalize(disp, "lv_display_t")) @@ -43,3 +54,14 @@ class LVDisplay(Value): """Get currently active draw buffer (may be None)""" buf_ptr = self.super_value("buf_act") return LVDrawBuf(buf_ptr) if buf_ptr else None + + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + d = { + "addr": hex(int(self)), + "hor_res": self.hor_res, + "ver_res": self.ver_res, + "screen_count": int(self.screen_cnt), + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) diff --git a/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_task.py b/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_task.py index 79b35b04db..c615bd6d3e 100644 --- a/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_task.py +++ b/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_task.py @@ -1,5 +1,3 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput from .lv_draw_consts import DRAW_TASK_TYPE_NAMES, DRAW_TASK_STATE_NAMES @@ -7,6 +5,21 @@ from .lv_draw_consts import DRAW_TASK_TYPE_NAMES, DRAW_TASK_STATE_NAMES class LVDrawTask(Value): """LVGL draw task wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("type", "type_name"), + ("state", "state_name"), + ("area", lambda d: ( + f"({d['area']['x1']}, {d['area']['y1']}, " + f"{d['area']['x2']}, {d['area']['y2']})" + )), + ("opa", "opa"), + ("unit_id", "preferred_draw_unit_id"), + ], + "table": [], + "empty_msg": "No draw tasks.", + } + def __init__(self, task: ValueInput): super().__init__(Value.normalize(task, "lv_draw_task_t")) @@ -29,7 +42,7 @@ class LVDrawTask(Value): @property def area(self) -> tuple: a = self.super_value("area") - return (int(a["x1"]), int(a["y1"]), int(a["x2"]), int(a["y2"])) + return (int(a.x1), int(a.y1), int(a.x2), int(a.y2)) @property def opa(self) -> int: @@ -50,26 +63,22 @@ class LVDrawTask(Value): def preferred_draw_unit_id(self) -> int: return int(self.super_value("preferred_draw_unit_id")) + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + x1, y1, x2, y2 = self.area + d = { + "addr": hex(int(self)), + "type": self.type, + "type_name": self.type_name, + "state": self.state, + "state_name": self.state_name, + "area": {"x1": x1, "y1": y1, "x2": x2, "y2": y2}, + "opa": self.opa, + "preferred_draw_unit_id": self.preferred_draw_unit_id, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(tasks): - """Print draw tasks as a PrettyTable.""" - table = PrettyTable() - table.field_names = ["#", "type", "state", "area", "opa", "unit_id"] - table.align = "l" - - for i, t in enumerate(tasks): - table.add_row( - [ - i, - t.type_name, - t.state_name, - t.area, - t.opa, - t.preferred_draw_unit_id, - ] - ) - - if not table.rows: - print("No draw tasks.") - else: - print(table) + def snapshots(tasks): + return [t.snapshot() for t in tasks] diff --git a/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_unit.py b/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_unit.py index 41f38ef6a3..6eb706bfd4 100644 --- a/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_unit.py +++ b/scripts/gdb/lvglgdb/lvgl/draw/lv_draw_unit.py @@ -1,11 +1,18 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput class LVDrawUnit(Value): """LVGL draw unit wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("name", "name"), + ("idx", "idx"), + ], + "table": [], + "empty_msg": "No draw units.", + } + def __init__(self, unit: ValueInput): super().__init__(Value.normalize(unit, "lv_draw_unit_t")) @@ -29,17 +36,16 @@ class LVDrawUnit(Value): yield node node = node.next + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + d = { + "addr": hex(int(self)), + "name": self.name, + "idx": self.idx, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(units): - """Print draw units as a PrettyTable.""" - table = PrettyTable() - table.field_names = ["#", "name", "idx"] - table.align = "l" - - for i, unit in enumerate(units): - table.add_row([i, unit.name, unit.idx]) - - if not table.rows: - print("No draw units.") - else: - print(table) + def snapshots(units): + return [unit.snapshot() for unit in units] diff --git a/scripts/gdb/lvglgdb/lvgl/formatter.py b/scripts/gdb/lvglgdb/lvgl/formatter.py new file mode 100644 index 0000000000..b6426e7891 --- /dev/null +++ b/scripts/gdb/lvglgdb/lvgl/formatter.py @@ -0,0 +1,281 @@ +"""Generic formatting helpers for snapshot data. + +This module provides data-driven formatters that work with any snapshot dict. +No module-specific logic lives here — all customization is done via field specs +and format callbacks passed by callers. +""" +from typing import Callable, List, Optional, Sequence, Tuple, Union + +from prettytable import PrettyTable + + +# --------------------------------------------------------------------------- +# Type aliases for field specs +# --------------------------------------------------------------------------- + +# A field spec is a tuple describing one line of print_info output: +# (label, key_or_fmt) +# +# - label: the left-hand label string (e.g. "var") +# - key_or_fmt: either a dict key string, or a callable(d) -> str +# +# Special forms: +# ("_title", callable(d) -> str) — title line (no indent) +# ("_children", key, child_fields) — nested list of dicts +# ("_skip_if", key, sentinel, (label, key_or_fmt)) +# — only print if d[key] != sentinel + +FieldSpec = Union[ + Tuple[str, Union[str, Callable]], + Tuple[str, str, Callable], + Tuple[str, str, str, Tuple], + Tuple[str, str, list], +] + + +def print_info(d, fields: Sequence = None, indent: int = 0) -> None: + """Print a snapshot dict using a declarative field spec list. + + Fields resolution order: + 1. Explicit *fields* parameter (highest priority) + 2. d._display_spec["info"] (Snapshot self-describing) + 3. None → default rendering (iterate dict keys as "key = value") + """ + if fields is None: + spec = getattr(d, "_display_spec", None) + if spec: + fields = spec["info"] + + # Default rendering: iterate dict keys + if fields is None: + prefix = " " * indent + items = d.items() if hasattr(d, "items") else vars(d).items() + for key, value in items: + print(f"{prefix} {key} = {value}") + return + + prefix = " " * indent + for spec in fields: + # Bare string shorthand: "key" → ("key", "key") + if isinstance(spec, str): + spec = (spec, spec) + + tag = spec[0] + + if tag == "_title": + fmt_fn = spec[1] + print(f"{prefix}{fmt_fn(d)}") + + elif tag == "_children": + key, child_fields = spec[1], spec[2] + children = d.get(key, []) + for child in children: + print_info(child, fields=child_fields, indent=indent + 1) + + elif tag == "_skip_if": + key, sentinel, inner = spec[1], spec[2], spec[3] + # Bare string shorthand for inner: "key" → ("key", "key") + if isinstance(inner, str): + inner = (inner, inner) + if d.get(key) != sentinel: + _print_field(prefix, inner, d) + + else: + _print_field(prefix, spec, d) + + +def _print_field(prefix: str, spec: tuple, d) -> None: + """Print a single (label, key_or_fmt) field.""" + label, key_or_fmt = spec[0], spec[1] + if callable(key_or_fmt): + value = key_or_fmt(d) + else: + value = d.get(key_or_fmt, "") + print(f"{prefix} {label:14s} = {value}") + + +# --------------------------------------------------------------------------- +# Table helpers +# --------------------------------------------------------------------------- + + +def print_table( + entries: List, + columns: List[str], + row_fn: Callable, + empty_msg: str, + align: str = "l", + numbered: bool = True, + col_align: dict = None, +) -> None: + """Generic helper for printing a PrettyTable from snapshot data. + + Args: + entries: list of dict-like objects (Snapshot or plain dict). + columns: column header names (excluding '#' if numbered=True). + row_fn: callable(index, entry) -> list of cell values. + empty_msg: message to print when entries is empty. + align: default column alignment. + numbered: if True, prepend a '#' column with row index. + col_align: optional per-column alignment overrides, e.g. {"src": "l"}. + """ + table = PrettyTable() + table.field_names = (["#"] + columns) if numbered else columns + table.align = align + if col_align: + for col, a in col_align.items(): + if col in table.field_names: + table.align[col] = a + + for i, d in enumerate(entries): + row = row_fn(i, d) + if numbered: + row = [i] + row + table.add_row(row) + + if not table.rows: + print(empty_msg) + else: + print(table) + +def resolve_table_columns(spec: dict) -> tuple: + """Resolve Display_Spec into (column_headers, auto_row_fn). + + Merges the ``info`` field list with ``table`` overrides to produce a flat + list of column headers and a row-extraction function with ``(i, d)`` + signature suitable for :func:`print_table`. + + Algorithm: + 1. Walk ``spec["info"]``, skip ``_title`` / ``_children``, normalise + bare strings, and collect ``(label, key_or_fmt, skip_info)`` triples. + 2. Walk ``spec["table"]``, build an override map (keyed by label) and + an append list for labels not present in the base columns. + 3. Apply overrides in-place, then append new columns. + 4. Return ``(headers, auto_row_fn)``. + """ + # -- 1. base columns from info ---------------------------------------- + base_columns = [] + base_labels = set() + for entry in spec["info"]: + if isinstance(entry, str): + entry = (entry, entry) + + tag = entry[0] + if tag in ("_title", "_children"): + continue + + if tag == "_skip_if": + skip_key, sentinel, inner = entry[1], entry[2], entry[3] + if isinstance(inner, str): + inner = (inner, inner) + label, key_or_fmt = inner[0], inner[1] + base_columns.append((label, key_or_fmt, (skip_key, sentinel))) + else: + label, key_or_fmt = entry[0], entry[1] + base_columns.append((label, key_or_fmt, None)) + + base_labels.add(base_columns[-1][0]) + + # -- 2. table overrides and appends ----------------------------------- + override_map = {} + append_list = [] + for entry in spec.get("table", []): + if isinstance(entry, str): + entry = (entry, entry) + + tag = entry[0] + if tag == "_skip_if": + skip_key, sentinel, inner = entry[1], entry[2], entry[3] + if isinstance(inner, str): + inner = (inner, inner) + label, key_or_fmt = inner[0], inner[1] + item = (label, key_or_fmt, (skip_key, sentinel)) + else: + label, key_or_fmt = entry[0], entry[1] + item = (label, key_or_fmt, None) + + if label in base_labels: + override_map[label] = item + else: + append_list.append(item) + + # -- 3. merge --------------------------------------------------------- + resolved = [] + for label, key_or_fmt, skip in base_columns: + if label in override_map: + resolved.append(override_map[label]) + else: + resolved.append((label, key_or_fmt, skip)) + resolved.extend(append_list) + + # -- 4. build outputs ------------------------------------------------- + headers = [label for label, _, _ in resolved] + + def auto_row_fn(i, d): + row = [] + for _label, key_or_fmt, skip in resolved: + if skip is not None: + sk, sentinel = skip + if d.get(sk) == sentinel: + row.append("") + continue + if callable(key_or_fmt): + row.append(key_or_fmt(d)) + else: + row.append(d.get(key_or_fmt, "")) + return row + + return (headers, auto_row_fn) + +def print_spec_table( + entries: list, + spec: dict = None, + align: str = "l", + numbered: bool = True, + col_align: dict = None, + extra_columns: list = None, + extra_row_fn: Callable = None, +) -> None: + """Render a PrettyTable from a Display_Spec. + + Resolves columns from *spec* via :func:`resolve_table_columns`, optionally + prepends *extra_columns* (used by DumpCache for dynamic fields), then + delegates to :func:`print_table`. + + If *spec* is not provided, it is read from the first entry's + ``_display_spec`` attribute. + """ + if spec is None and entries: + spec = getattr(entries[0], "_display_spec", None) + if spec is None: + print("") + return + columns, auto_row_fn = resolve_table_columns(spec) + + if extra_columns and extra_row_fn: + full_columns = extra_columns + columns + + def combined_row_fn(i, d): + return extra_row_fn(d) + auto_row_fn(i, d) + + row_fn = combined_row_fn + else: + full_columns = columns + row_fn = auto_row_fn + + print_table( + entries, + full_columns, + row_fn, + spec["empty_msg"], + align=align, + numbered=numbered, + col_align=col_align, + ) + + + + + + + diff --git a/scripts/gdb/lvglgdb/lvgl/misc/__init__.py b/scripts/gdb/lvglgdb/lvgl/misc/__init__.py index 1adc7b0aad..60428166e7 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/__init__.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/__init__.py @@ -2,15 +2,14 @@ from .lv_ll import LVList from .lv_style import ( LVStyle, StyleEntry, - dump_style_info, style_prop_name, decode_selector, format_style_value, ) -from .lv_rb import LVRedBlackTree, dump_rb_info -from .lv_cache import LVCache, dump_cache_info -from .lv_cache_entry import LVCacheEntry, dump_cache_entry_info -from .lv_cache_lru_rb import LVCacheLRURB, dump_lru_rb_cache_info, LVCacheLRURBIterator +from .lv_rb import LVRedBlackTree +from .lv_cache import LVCache +from .lv_cache_entry import LVCacheEntry +from .lv_cache_lru_rb import LVCacheLRURB, LVCacheLRURBIterator from .lv_cache_iter_base import LVCacheIteratorBase from .lv_cache_iter_factory import create_cache_iterator from .lv_image_cache import LVImageCache @@ -33,18 +32,13 @@ __all__ = [ "LVList", "LVStyle", "StyleEntry", - "dump_style_info", "style_prop_name", "decode_selector", "format_style_value", "LVRedBlackTree", - "dump_rb_info", "LVCache", - "dump_cache_info", "LVCacheEntry", - "dump_cache_entry_info", "LVCacheLRURB", - "dump_lru_rb_cache_info", "LVCacheIteratorBase", "LVCacheLRURBIterator", "LVImageCache", diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_anim.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_anim.py index 2973cde040..8f096b4fe4 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_anim.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_anim.py @@ -1,19 +1,52 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput -def _fmt_cb(cb: Value) -> str: - """Format a callback pointer as symbol or hex.""" - addr = int(cb) - if not addr: - return "-" - return cb.format_string(symbols=True, address=True) - - class LVAnim(Value): """LVGL animation wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: f"Animation @{d['addr']}"), + "var", + "exec_cb", + "path_cb", + "start_cb", + "completed_cb", + "deleted_cb", + "user_data", + ("value", lambda d: ( + f"{d['start_value']} -> {d['current_value']}" + f" -> {d['end_value']}" + )), + ("duration", lambda d: ( + f"{d['duration']}ms act_time={d['act_time']}ms" + )), + ("repeat", lambda d: ( + f"{'inf' if d['repeat_cnt'] == 0xFFFFFFFF else d['repeat_cnt']}" + f" repeat_delay={d['repeat_delay']}ms" + )), + ("reverse", lambda d: ( + f"dur={d['reverse_duration']}ms" + f" delay={d['reverse_delay']}ms" + )), + ("status", lambda d: ( + f"{d['status']} early_apply={d['early_apply']}" + )), + ], + "table": [ + ("value(start/cur/end)", lambda d: ( + f"{d['start_value']}/{d['current_value']}/{d['end_value']}" + )), + ("duration", lambda d: f"{d['duration']}ms"), + ("act_time", lambda d: f"{d['act_time']}ms"), + ("repeat", lambda d: ( + "inf" if d["repeat_cnt"] == 0xFFFFFFFF + else str(d["repeat_cnt"]) + )), + ], + "empty_msg": "No active animations.", + } + def __init__(self, anim: ValueInput): super().__init__(Value.normalize(anim, "lv_anim_t")) @@ -101,61 +134,34 @@ class LVAnim(Value): return "reverse" return "running" - def print_info(self): - """Print detailed info for a single animation.""" - print(f"Animation @{hex(int(self))}") - print(f" var = {self.var}") - print(f" exec_cb = {_fmt_cb(self.exec_cb)}") - print(f" path_cb = {_fmt_cb(self.path_cb)}") - print(f" start_cb = {_fmt_cb(self.start_cb)}") - print(f" completed_cb = {_fmt_cb(self.completed_cb)}") - print(f" deleted_cb = {_fmt_cb(self.deleted_cb)}") - print(f" user_data = {self.user_data}") - print( - f" value = {self.start_value} -> {self.current_value} -> {self.end_value}" - ) - print(f" duration = {self.duration}ms act_time={self.act_time}ms") - repeat = "inf" if self.repeat_cnt == 0xFFFFFFFF else str(self.repeat_cnt) - print(f" repeat = {repeat} repeat_delay={self.repeat_delay}ms") - print( - f" reverse = dur={self.reverse_duration}ms delay={self.reverse_delay}ms" - ) - print(f" status = {self._status_str()} early_apply={self.early_apply}") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb, ptr_or_none + + d = { + "addr": hex(int(self)), + "var": str(self.var), + "var_addr": ptr_or_none(self.var), + "exec_cb": fmt_cb(self.exec_cb), + "path_cb": fmt_cb(self.path_cb), + "start_cb": fmt_cb(self.start_cb), + "completed_cb": fmt_cb(self.completed_cb), + "deleted_cb": fmt_cb(self.deleted_cb), + "user_data": str(self.user_data), + "start_value": self.start_value, + "current_value": self.current_value, + "end_value": self.end_value, + "duration": self.duration, + "act_time": self.act_time, + "repeat_cnt": self.repeat_cnt, + "repeat_delay": self.repeat_delay, + "reverse_duration": self.reverse_duration, + "reverse_delay": self.reverse_delay, + "status": self._status_str(), + "early_apply": self.early_apply, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) @staticmethod - def print_entries(anims): - """Print animations as a PrettyTable.""" - table = PrettyTable() - table.field_names = [ - "#", - "var", - "exec_cb", - "value(start/cur/end)", - "duration", - "act_time", - "repeat", - "status", - ] - table.align = "l" - - for i, anim in enumerate(anims): - cb_str = _fmt_cb(anim.exec_cb) - repeat = "inf" if anim.repeat_cnt == 0xFFFFFFFF else str(anim.repeat_cnt) - value_str = f"{anim.start_value}/{anim.current_value}/{anim.end_value}" - table.add_row( - [ - i, - anim.var, - cb_str, - value_str, - f"{anim.duration}ms", - f"{anim.act_time}ms", - repeat, - anim._status_str(), - ] - ) - - if not table.rows: - print("No active animations.") - else: - print(table) + def snapshots(anims): + return [anim.snapshot() for anim in anims] diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache.py index b548e37515..27fa74c754 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache.py @@ -1,4 +1,4 @@ -from typing import Union, List, Optional, Dict +from typing import Union import gdb from lvglgdb.value import Value, ValueInput @@ -8,6 +8,21 @@ from .lv_cache_iter_factory import create_cache_iterator class LVCache(Value): """LVGL cache wrapper - focuses on cache-level operations""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: "Cache Info:"), + ("Name", "name"), + ("Node Size", "node_size"), + ("Max Size", "max_size"), + ("Current Size", "current_size"), + ("Free Size", lambda d: d["max_size"] - d["current_size"]), + ("Enabled", "enabled"), + ("_skip_if", "iterator_type", None, ("Iterator Type", "iterator_type")), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, cache: ValueInput, datatype: Union[gdb.Type, str]): super().__init__(Value.normalize(cache, "lv_cache_t")) self.datatype = ( @@ -16,24 +31,27 @@ class LVCache(Value): else datatype ) - def print_info(self): - """Dump cache information""" - print(f"Cache Info:") - print(f" Name: {self.name.as_string()}") - print(f" Node Size: {int(self.node_size)}") - print(f" Max Size: {int(self.max_size)}") - print(f" Current Size: {int(self.size)}") - print(f" Free Size: {int(self.max_size) - int(self.size)}") - print(f" Enabled: {bool(int(self.max_size) > 0)}") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot - # Try to identify cache type + iter_type = None try: iterator = create_cache_iterator(self) - print(f" Iterator Type: {iterator.__class__.__name__}") - iterator.cache.print_info() - except gdb.error: + iter_type = iterator.__class__.__name__ + except Exception: pass + d = { + "addr": hex(int(self)), + "name": self.name.as_string(), + "node_size": int(self.node_size), + "max_size": int(self.max_size), + "current_size": int(self.size), + "enabled": bool(int(self.max_size) > 0), + "iterator_type": iter_type, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + def is_enabled(self): """Check if cache is enabled""" return int(self.max_size) > 0 @@ -55,31 +73,9 @@ class LVCache(Value): entries.append(entry) return entries - def print_entries(self, max_entries=10): - """Print cache entries in readable format""" - cache_entries = self.items() - cache_entries_cnt = len(cache_entries) - print(f"Cache Entries ({cache_entries_cnt} total):") - - count = 0 - for i, entry in enumerate(cache_entries): - if count >= max_entries: - print( - f" ... showing first {max_entries} of {cache_entries_cnt} entries" - ) - break - - print(f" [{i}] {entry}") - count += 1 - - if count == 0: - print(" (empty)") - elif count < int(cache_entries_cnt): - print(f" ... {cache_entries_cnt - count} more entries not shown") - def sanity_check(self, entry_checker=None): """Run sanity check and print results as a table""" - from prettytable import PrettyTable + from lvglgdb.lvgl.formatter import print_table iterator = iter(self) if iterator is None: @@ -87,21 +83,14 @@ class LVCache(Value): else: errors = iterator.sanity_check(entry_checker) - table = PrettyTable() - table.field_names = ["#", "status", "detail"] - table.align["detail"] = "l" - if errors: - for i, err in enumerate(errors): - table.add_row([i, "FAIL", err]) + rows = [{"status": "FAIL", "detail": e} for e in errors] else: - table.add_row([0, "PASS", f"all {len(iterator)} entries OK"]) + rows = [{"status": "PASS", "detail": f"all {len(iterator)} entries OK"}] - print(table) + print_table(rows, ["status", "detail"], + lambda i, d: [d["status"], d["detail"]], "", + col_align={"detail": "l"}) return errors -def dump_cache_info(cache: ValueInput, datatype: Union[gdb.Type, str]): - """Dump cache information""" - cache_obj = LVCache(cache, datatype) - cache_obj.print_info() diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_entry.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_entry.py index a29b8e79d8..9d849de739 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_entry.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_entry.py @@ -27,22 +27,6 @@ class LVCacheEntry(Value): entry_ptr = int(data_ptr) + data_ptr.type.target().sizeof return cls(entry_ptr, datatype) - def print_info(self): - """Dump cache entry information""" - print(f"Cache Entry Info:") - print(f" Reference Count: {int(self.ref_cnt)}") - print(f" Node Size: {int(self.node_size)}") - print(f" Flags: {int(self.flags)}") - print(f" Invalid: {self.is_invalid()}") - print(f" Disable Delete: {self.is_disabled_delete()}") - - # Try to get cache info if available - try: - cache = self.cache - if cache: - print(f" Cache: {cache}") - except: - pass def get_data(self): """Get entry data pointer""" @@ -80,7 +64,3 @@ class LVCacheEntry(Value): return super().__str__() -def dump_cache_entry_info(entry: ValueInput, datatype: Union[gdb.Type, str]): - """Dump cache entry information""" - entry_obj = LVCacheEntry(entry, datatype) - entry_obj.print_info() diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_lru_rb.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_lru_rb.py index f1505372da..ca7fca2625 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_lru_rb.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_cache_lru_rb.py @@ -144,20 +144,14 @@ class LVCacheLRURB(LVCache): super().__init__(cache, datatype) self.cache_base = Value(self) - def print_info(self): - """Dump LRU RB cache information""" - print(f"LRU RB Cache Info:") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot - # Try to get cache class info - try: - clz = self.clz - if clz: - print(f" Cache Class: {clz}") - # Check if it's LRU RB based - if "lru_rb" in str(clz).lower(): - print(f" Type: LRU with Red-Black Tree") - except: - pass + base = super().snapshot() + d = base.as_dict() + d["type"] = "lru_rb" + return Snapshot(d, source=self, + display_spec=getattr(base, "_display_spec", None)) def is_count_based(self): """Check if this is count-based LRU cache""" @@ -185,9 +179,3 @@ class LVCacheLRURB(LVCache): for entry in self: entries.append(entry) return entries - - -def dump_lru_rb_cache_info(cache: ValueInput): - """Dump LRU RB cache information""" - cache_obj = LVCacheLRURB(cache) - cache_obj.print_info() diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_event.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_event.py index be2be2d651..f1a8f6eecc 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_event.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_event.py @@ -49,11 +49,6 @@ class LVEvent(Value): def stop_processing(self) -> bool: return bool(int(self.super_value("stop_processing"))) - def print_info(self): - print(f"Event: code={self.code_name}({self.code})") - print(f" current_target={self.current_target}") - print(f" original_target={self.original_target}") - print(f" deleted={self.deleted} stop_processing={self.stop_processing}") class LVEventDsc(Value): @@ -91,20 +86,41 @@ class LVEventDsc(Value): def is_marked_deleting(self) -> bool: return bool(self.filter & 0x10000) - def print_info(self): - cb_str = self.cb.format_string(symbols=True, address=True) + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb + flags = [] if self.is_preprocess: flags.append("PRE") if self.is_marked_deleting: flags.append("DEL") - flag_str = f" [{','.join(flags)}]" if flags else "" - print(f" cb={cb_str} filter={self.filter_name}({self.filter_code}){flag_str}") + d = { + "cb": fmt_cb(self.cb), + "filter": self.filter_code, + "filter_name": self.filter_name, + "is_preprocess": self.is_preprocess, + "is_marked_deleting": self.is_marked_deleting, + "flags_str": ",".join(flags) or "-", + "user_data": str(self.user_data), + } + return Snapshot(d, source=self, display_spec=LVEventList._DISPLAY_SPEC) class LVEventList(Value): """LVGL event list wrapper (lv_event_list_t contains lv_array_t)""" + _DISPLAY_SPEC = { + "info": [ + ("callback", "cb"), + ("filter", "filter_name"), + ("flags", lambda d: d.get("flags_str", "-")), + ("user_data", "user_data"), + ], + "table": [], + "empty_msg": "No event descriptors.", + } + def __init__(self, event_list: ValueInput): super().__init__(Value.normalize(event_list, "lv_event_list_t")) @@ -131,26 +147,5 @@ class LVEventList(Value): return len(self.array) @staticmethod - def print_entries(event_dscs): - """Print event descriptors as a PrettyTable.""" - from prettytable import PrettyTable - - table = PrettyTable() - table.field_names = ["#", "callback", "filter", "flags", "user_data"] - table.align = "l" - - for i, dsc in enumerate(event_dscs): - cb_str = dsc.cb.format_string(symbols=True, address=True) - flags = [] - if dsc.is_preprocess: - flags.append("PRE") - if dsc.is_marked_deleting: - flags.append("DEL") - table.add_row( - [i, cb_str, dsc.filter_name, ",".join(flags) or "-", dsc.user_data] - ) - - if not table.rows: - print("No event descriptors.") - else: - print(table) + def snapshots(event_dscs): + return [dsc.snapshot() for dsc in event_dscs] diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_fs.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_fs.py index 5d27f257b7..5586762b2a 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_fs.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_fs.py @@ -1,5 +1,3 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput from .lv_utils import resolve_source_name, build_global_field_map @@ -7,6 +5,20 @@ from .lv_utils import resolve_source_name, build_global_field_map class LVFsDrv(Value): """LVGL filesystem driver wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("letter", lambda d: f"{d['letter']}:"), + ("type", "driver_name"), + ("cache_size", "cache_size"), + ("open_cb", "open_cb"), + ("read_cb", "read_cb"), + ("write_cb", "write_cb"), + ("close_cb", "close_cb"), + ], + "table": [], + "empty_msg": "No registered filesystem drivers.", + } + def __init__(self, drv: ValueInput): super().__init__(Value.normalize(drv, "lv_fs_drv_t")) @@ -80,45 +92,22 @@ class LVFsDrv(Value): def user_data(self) -> Value: return self.super_value("user_data") - @staticmethod - def _fmt_cb(cb: Value) -> str: - addr = int(cb) - if not addr: - return "-" - return cb.format_string(symbols=True).replace("\x00", "") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb + + d = { + "addr": hex(int(self)), + "letter": self.letter, + "driver_name": self.driver_name, + "cache_size": self.cache_size, + "open_cb": fmt_cb(self.open_cb), + "read_cb": fmt_cb(self.read_cb), + "write_cb": fmt_cb(self.write_cb), + "close_cb": fmt_cb(self.close_cb), + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) @staticmethod - def print_entries(drivers): - """Print filesystem drivers as a PrettyTable.""" - table = PrettyTable() - table.field_names = [ - "#", - "letter", - "type", - "cache_size", - "open_cb", - "read_cb", - "write_cb", - "close_cb", - ] - table.align = "l" - - fmt = LVFsDrv._fmt_cb - for i, drv in enumerate(drivers): - table.add_row( - [ - i, - f"{drv.letter}:", - drv.driver_name, - drv.cache_size, - fmt(drv.open_cb), - fmt(drv.read_cb), - fmt(drv.write_cb), - fmt(drv.close_cb), - ] - ) - - if not table.rows: - print("No registered filesystem drivers.") - else: - print(str(table).replace("\x00", "")) + def snapshots(drivers): + return [drv.snapshot() for drv in drivers] diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_cache.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_cache.py index 7d64633dc2..32e0035e16 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_cache.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_cache.py @@ -1,5 +1,4 @@ import gdb -from prettytable import PrettyTable from lvglgdb.value import Value from .lv_cache import LVCache from .lv_cache_entry import LVCacheEntry @@ -43,31 +42,34 @@ class LVImageCacheData(Value): class LVImageCache(object): + _DISPLAY_SPEC = { + "info": [ + ("size", "size"), + ("data_size", lambda d: str(d["data_size"])), + ("cf", lambda d: str(d["cf"])), + ("rc", lambda d: str(d["ref_count"])), + ("type", "src_type"), + ("decoder", "decoder_name"), + ("decoded", "decoded_addr"), + ("src", "src"), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, cache: Value): self._cache = LVCache(cache, "lv_image_cache_data_t") - def print_info(self): - self._cache.print_info() + def snapshot(self): + return self._cache.snapshot() + + def snapshots(self): + from lvglgdb.lvgl.snapshot import Snapshot - def print_entries(self): - """Print image cache entries using prettytable format""" iterator = iter(self._cache) - extra_fields = iterator.extra_fields - - table = PrettyTable() - fields = ( - ["entry"] - + extra_fields - + ["size", "data_size", "cf", "rc", "type", "decoder", "decoded", "src"] - ) - table.field_names = fields - table.align = "r" - table.align["src"] = "l" - table.align["type"] = "c" + result = [] for entry in iterator: - entry: LVCacheEntry - data_ptr = entry.get_data() if not data_ptr: continue @@ -113,23 +115,24 @@ class LVImageCache(object): except gdb.error as e: src_str = src_str or str(e) - row = ( - [f"{int(entry):#x}"] - + iterator.get_extra(entry) - + [ - size_str, - f"{data_size}", - f"{cf}", - f"{ref_cnt}", - type_str, - decoder_name, - f"{decoded_ptr:#x}", - src_str, - ] - ) - table.add_row(row) + extras = dict(zip(iterator.extra_fields, iterator.get_extra(entry))) + d = { + "entry_addr": f"{int(entry):#x}", + "extra_fields": extras, + "size": size_str, + "data_size": data_size, + "cf": cf, + "ref_count": ref_cnt, + "src_type": type_str, + "decoder_name": decoder_name, + "decoded_addr": f"{decoded_ptr:#x}", + "src": src_str, + } + result.append(Snapshot(d, source=entry, + display_spec=self._DISPLAY_SPEC)) - print(table) + self._last_extra_fields = iterator.extra_fields + return result @staticmethod def _check_image_entry(entry): diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_decoder.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_decoder.py index 4e0ea30182..373b07690c 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_decoder.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_decoder.py @@ -1,11 +1,20 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput class LVImageDecoder(Value): """LVGL image decoder wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("name", "name"), + ("info_cb", "info_cb"), + ("open_cb", "open_cb"), + ("close_cb", "close_cb"), + ], + "table": [], + "empty_msg": "No registered image decoders.", + } + def __init__(self, decoder: ValueInput): super().__init__(Value.normalize(decoder, "lv_image_decoder_t")) @@ -38,25 +47,19 @@ class LVImageDecoder(Value): def user_data(self) -> Value: return self.super_value("user_data") + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb + + d = { + "addr": hex(int(self)), + "name": self.name, + "info_cb": fmt_cb(self.info_cb), + "open_cb": fmt_cb(self.open_cb), + "close_cb": fmt_cb(self.close_cb), + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(decoders): - """Print image decoders as a PrettyTable.""" - table = PrettyTable() - table.field_names = ["#", "name", "info_cb", "open_cb", "close_cb"] - table.align = "l" - - for i, dec in enumerate(decoders): - table.add_row( - [ - i, - dec.name, - dec.info_cb.format_string(symbols=True), - dec.open_cb.format_string(symbols=True), - dec.close_cb.format_string(symbols=True), - ] - ) - - if not table.rows: - print("No registered image decoders.") - else: - print(table) + def snapshots(decoders): + return [dec.snapshot() for dec in decoders] diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_header_cache.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_header_cache.py index 60eb2f8fde..73f4cd1bd3 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_image_header_cache.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_image_header_cache.py @@ -1,5 +1,4 @@ import gdb -from prettytable import PrettyTable from lvglgdb.value import Value from .lv_cache import LVCache from .lv_cache_entry import LVCacheEntry @@ -34,29 +33,32 @@ class LVImageHeaderCacheData(Value): class LVImageHeaderCache(object): + _DISPLAY_SPEC = { + "info": [ + ("size", "size"), + ("cf", lambda d: str(d["cf"])), + ("rc", lambda d: str(d["ref_count"])), + ("type", "src_type"), + ("decoder", "decoder_name"), + ("src", "src"), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, cache: Value): self._cache = LVCache(cache, "lv_image_header_cache_data_t") - def print_info(self): - self._cache.print_info() + def snapshot(self): + return self._cache.snapshot() + + def snapshots(self): + from lvglgdb.lvgl.snapshot import Snapshot - def print_entries(self): - """Print image header cache entries using prettytable format""" iterator = iter(self._cache) - extra_fields = iterator.extra_fields - - table = PrettyTable() - fields = ( - ["entry"] + extra_fields + ["size", "cf", "rc", "type", "decoder", "src"] - ) - table.field_names = fields - table.align = "r" - table.align["src"] = "l" - table.align["type"] = "c" + result = [] for entry in iterator: - entry: LVCacheEntry - data_ptr = entry.get_data() if not data_ptr: continue @@ -97,21 +99,22 @@ class LVImageHeaderCache(object): except gdb.error as e: src_str = src_str or str(e) - row = ( - [f"{int(entry):#x}"] - + iterator.get_extra(entry) - + [ - size_str, - f"{cf}", - f"{ref_cnt}", - type_str, - decoder_name, - src_str, - ] - ) - table.add_row(row) + extras = dict(zip(iterator.extra_fields, iterator.get_extra(entry))) + d = { + "entry_addr": f"{int(entry):#x}", + "extra_fields": extras, + "size": size_str, + "cf": cf, + "ref_count": ref_cnt, + "src_type": type_str, + "decoder_name": decoder_name, + "src": src_str, + } + result.append(Snapshot(d, source=entry, + display_spec=self._DISPLAY_SPEC)) - print(table) + self._last_extra_fields = iterator.extra_fields + return result @staticmethod def _check_header_entry(entry): diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_ll.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_ll.py index 4e5c369532..32f658b56a 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_ll.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_ll.py @@ -7,6 +7,18 @@ from lvglgdb.value import Value, ValueInput class LVList(Value): """LVGL linked list iterator""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: "Linked List Info:"), + ("Address", "addr"), + ("Node Size", "n_size"), + ("Node Count", "node_count"), + ("_skip_if", "nodetype", None, ("Node Type", "nodetype")), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, ll: ValueInput, nodetype: Union[gdb.Type, str] = None): super().__init__(Value.normalize(ll, "lv_ll_t")) @@ -49,3 +61,15 @@ class LVList(Value): len += 1 node = self._next(node) return len + + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + + d = { + "addr": hex(int(self)), + "n_size": int(self.n_size), + "node_count": self.len, + "nodetype": str(self.nodetype) if self.nodetype else None, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_rb.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_rb.py index b3f0b02a67..0a5b917ec8 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_rb.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_rb.py @@ -7,6 +7,18 @@ from lvglgdb.value import Value, ValueInput class LVRedBlackTree(Value): """LVGL red-black tree iterator""" + _DISPLAY_SPEC = { + "info": [ + ("_title", lambda d: "Red-Black Tree Info:"), + ("Address", "addr"), + ("Node Size", "size"), + ("Node Count", "node_count"), + ("_skip_if", "datatype", None, ("Data Type", "datatype")), + ], + "table": [], + "empty_msg": "", + } + def __init__(self, rb: ValueInput, datatype: Union[gdb.Type, str] = None): super().__init__(Value.normalize(rb, "lv_rb_t")) self.lv_rb_node_t = gdb.lookup_type("lv_rb_node_t").pointer() @@ -67,55 +79,16 @@ class LVRedBlackTree(Value): return data.cast(self.datatype) return data - def format_data(self, data): - """Format data for display - simple GDB style""" - if data is None: - return "None" + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot - try: - ptr_addr = f"0x{int(data):x}" - except: - return str(data) - - if self.datatype and data: - try: - struct_data = data.dereference() - return f"{ptr_addr} -> {struct_data}" - except: - pass - - return ptr_addr - - def print_info(self): - """Dump basic tree information""" - print(f"Red-Black Tree Info:") - print(f" Size: {int(self.size)}") - print(f" Node Count: {len(self)}") - print(f" Root: {self.root}") - if self.root: - root_color = "Red" if int(self.root.color) == 0 else "Black" - print(f" Root Color: {root_color}") - if self.datatype: - print(f" Data Type: {self.datatype}") - - def print_tree(self, max_items=10): - """Print tree data in a readable format""" - print(f"Red-Black Tree Contents ({len(self)} total items):") - - count = 0 - for i, data in enumerate(self): - if count >= max_items: - print(f" ... showing first {max_items} of {len(self)} items") - break - - formatted = self.format_data(data) - print(f" [{i}] {formatted}") - count += 1 - - if count == 0: - print(" (empty)") - elif count < len(self): - print(f" ... {len(self) - count} more items not shown") + d = { + "addr": hex(int(self)), + "size": int(self.size), + "node_count": len(self), + "datatype": str(self.datatype) if self.datatype else None, + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) class LVRedBlackTreeIterator: @@ -162,7 +135,3 @@ class LVRedBlackTreeIterator: return f"LVRedBlackTreeIterator(current=0x{int(current):x})" -def dump_rb_info(rb: ValueInput, datatype: Union[gdb.Type, str] = None): - """Dump red-black tree information""" - tree = LVRedBlackTree(rb, datatype=datatype) - tree.print_info() diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_style.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_style.py index eff7b1edc1..bfa5c745d6 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_style.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_style.py @@ -2,7 +2,6 @@ from dataclasses import dataclass from typing import Iterator import gdb -from prettytable import PrettyTable from lvglgdb.value import Value, ValueInput from .lv_style_consts import ( STYLE_PROP_NAMES, @@ -37,7 +36,7 @@ def decode_selector(selector: int) -> str: def format_style_value(prop_id: int, value: Value) -> str: - """Format a style value based on property type.""" + """Format a style value based on property type (with ANSI color block).""" try: if prop_id in COLOR_PROPS: color = value.color @@ -55,6 +54,30 @@ def format_style_value(prop_id: int, value: Value) -> str: return str(value) +def _style_value_data(prop_id: int, value: Value) -> dict: + """Extract style value as pure data dict (no ANSI codes). + + Returns dict with 'value_str' and optional 'color_rgb'. + """ + try: + if prop_id in COLOR_PROPS: + color = value.color + r = int(color.red) & 0xFF + g = int(color.green) & 0xFF + b = int(color.blue) & 0xFF + return { + "value_str": f"#{r:02x}{g:02x}{b:02x}", + "color_rgb": {"r": r, "g": g, "b": b}, + } + elif prop_id in POINTER_PROPS: + ptr = int(value.ptr) + return {"value_str": f"{ptr:#x}" if ptr else "NULL"} + else: + return {"value_str": str(int(value.num))} + except gdb.error: + return {"value_str": str(value)} + + @dataclass class StyleEntry: """A single resolved style property.""" @@ -103,21 +126,18 @@ class LVStyle(Value): continue yield StyleEntry(prop_id, values_ptr[j]) - def print_entries(self): - """Print style properties as a table.""" - entries = list(self.__iter__()) - if not entries: - print("Empty style.") - return + def snapshots(self): + from lvglgdb.lvgl.snapshot import Snapshot - table = PrettyTable() - table.field_names = ["prop", "value"] - table.align = "l" - for e in entries: - table.add_row([e.prop_name, e.value_str]) - print(table) + result = [] + for entry in self.__iter__(): + vdata = _style_value_data(entry.prop_id, entry.value) + d = { + "prop_id": entry.prop_id, + "prop_name": entry.prop_name, + **vdata, + } + result.append(Snapshot(d, source=entry)) + return result -def dump_style_info(entry: StyleEntry): - """Print a single style property.""" - print(f"{entry.prop_name}({entry.prop_id}) = {entry.value_str}") diff --git a/scripts/gdb/lvglgdb/lvgl/misc/lv_timer.py b/scripts/gdb/lvglgdb/lvgl/misc/lv_timer.py index b5303ee51b..2e17892b6a 100644 --- a/scripts/gdb/lvglgdb/lvgl/misc/lv_timer.py +++ b/scripts/gdb/lvglgdb/lvgl/misc/lv_timer.py @@ -1,11 +1,25 @@ -from prettytable import PrettyTable - from lvglgdb.value import Value, ValueInput class LVTimer(Value): """LVGL timer wrapper""" + _DISPLAY_SPEC = { + "info": [ + ("callback", "timer_cb"), + ("period", "period"), + ("freq", "frequency"), + ("last_run", "last_run"), + ("repeat", lambda d: ( + "inf" if d["repeat_count"] == -1 + else str(d["repeat_count"]) + )), + ("paused", "paused"), + ], + "table": [], + "empty_msg": "No active timers.", + } + def __init__(self, timer: ValueInput): super().__init__(Value.normalize(timer, "lv_timer_t")) @@ -37,38 +51,24 @@ class LVTimer(Value): def auto_delete(self) -> bool: return bool(int(self.super_value("auto_delete"))) + def snapshot(self): + from lvglgdb.lvgl.snapshot import Snapshot + from lvglgdb.lvgl.data_utils import fmt_cb, ptr_or_none + + freq = f"{1000 / self.period:.1f}Hz" if self.period > 0 else "-" + d = { + "addr": hex(int(self)), + "timer_cb": fmt_cb(self.timer_cb), + "period": self.period, + "frequency": freq, + "last_run": self.last_run, + "repeat_count": self.repeat_count, + "paused": self.paused, + "user_data": str(self.user_data), + "user_data_addr": ptr_or_none(self.user_data), + } + return Snapshot(d, source=self, display_spec=self._DISPLAY_SPEC) + @staticmethod - def print_entries(timers): - """Print timers as a PrettyTable.""" - table = PrettyTable() - table.field_names = [ - "#", - "callback", - "period", - "freq", - "last_run", - "repeat", - "paused", - ] - table.align = "l" - - for i, timer in enumerate(timers): - cb_str = timer.timer_cb.format_string(symbols=True, address=True) - repeat = "inf" if timer.repeat_count == -1 else str(timer.repeat_count) - freq = f"{1000 / timer.period:.1f}Hz" if timer.period > 0 else "-" - table.add_row( - [ - i, - cb_str, - timer.period, - freq, - timer.last_run, - repeat, - timer.paused, - ] - ) - - if not table.rows: - print("No active timers.") - else: - print(table) + def snapshots(timers): + return [timer.snapshot() for timer in timers] diff --git a/scripts/gdb/lvglgdb/lvgl/snapshot.py b/scripts/gdb/lvglgdb/lvgl/snapshot.py new file mode 100644 index 0000000000..b51a79d742 --- /dev/null +++ b/scripts/gdb/lvglgdb/lvgl/snapshot.py @@ -0,0 +1,54 @@ +from typing import Any, Dict, Iterator, Optional + + +class Snapshot: + """Self-describing data snapshot from a wrapper instance. + + Holds a pure Python dict (JSON-serializable), an optional reference + to the original wrapper (_source), and an optional display spec that + describes how to format the data for terminal output. + """ + + __slots__ = ("_data", "_source", "_display_spec") + + def __init__(self, data: Dict[str, Any], source: Any = None, + display_spec: Optional[Dict] = None): + self._data = data + self._source = source + self._display_spec = display_spec + + # --- dict-like read access --- + + def __getitem__(self, key: str) -> Any: + return self._data[key] + + def __contains__(self, key: str) -> bool: + return key in self._data + + def __len__(self) -> int: + return len(self._data) + + def __iter__(self) -> Iterator[str]: + return iter(self._data) + + def keys(self): + return self._data.keys() + + def values(self): + return self._data.values() + + def items(self): + return self._data.items() + + def get(self, key: str, default: Any = None) -> Any: + return self._data.get(key, default) + + # --- serialization --- + + def as_dict(self) -> Dict[str, Any]: + return dict(self._data) + + def __repr__(self) -> str: + addr = self._data.get("addr", "?") + src_type = type(self._source).__name__ if self._source else "None" + return f"Snapshot(addr={addr}, source={src_type})"