chore(gdb): add snapshot/data_utils/formatter layers with declarative _DISPLAY_SPEC (#9866)

This commit is contained in:
Benign X
2026-03-17 10:54:30 +08:00
committed by GitHub
parent ec02abe233
commit 8fa1c29773
39 changed files with 1357 additions and 660 deletions

View File

@@ -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 <expr>``: 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 <expr>``: Show object class hierarchy.
- ``info subject <expr>``: 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... <my_cb> target=0x... for_obj=True
Observer: cb=<my_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<br/>(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<br/>._data (pure dict)<br/>._display_spec<br/>._source"]
end
subgraph "data_utils.py"
DU["ptr_or_none()<br/>fmt_cb()<br/>..."]
end
subgraph "Wrapper classes"
direction TB
DS["_DISPLAY_SPEC<br/>{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

View File

@@ -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",

View File

@@ -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"])

View File

@@ -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"])

View File

@@ -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())

View File

@@ -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())

View File

@@ -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)),
)

View File

@@ -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))

View File

@@ -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):

View File

@@ -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"])

View File

@@ -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"])

View File

@@ -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 <style_var> or info style --obj <obj_var>")

View File

@@ -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"])

View File

@@ -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",
]

View File

@@ -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]

View File

@@ -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]

View File

@@ -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,
)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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]

View File

@@ -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]

View File

@@ -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,
)

View File

@@ -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",

View File

@@ -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]

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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]

View File

@@ -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]

View File

@@ -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):

View File

@@ -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]

View File

@@ -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):

View File

@@ -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)

View File

@@ -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()

View File

@@ -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}")

View File

@@ -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]

View File

@@ -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})"