mirror of
https://github.com/lvgl/lvgl.git
synced 2026-05-30 15:17:33 +08:00
chore(gdb): add dump dashboard command with HTML renderer and data collector (#9870)
This commit is contained in:
@@ -49,6 +49,7 @@ The plugin provides the following commands.
|
|||||||
- ``dump image_decoder``: List all registered image decoders.
|
- ``dump image_decoder``: List all registered image decoders.
|
||||||
- ``dump fs_drv``: List all registered filesystem drivers.
|
- ``dump fs_drv``: List all registered filesystem drivers.
|
||||||
- ``dump draw_task <expr>``: List draw tasks from a layer.
|
- ``dump draw_task <expr>``: List draw tasks from a layer.
|
||||||
|
- ``dump dashboard``: Generate an HTML dashboard of all LVGL runtime state.
|
||||||
- ``info style``: Inspect style properties of an ``lv_style_t`` or an ``lv_obj_t``.
|
- ``info style``: Inspect style properties of an ``lv_style_t`` or an ``lv_obj_t``.
|
||||||
- ``info draw_unit``: Print raw struct details for each drawing unit.
|
- ``info draw_unit``: Print raw struct details for each drawing unit.
|
||||||
- ``info obj_class <expr>``: Show object class hierarchy.
|
- ``info obj_class <expr>``: Show object class hierarchy.
|
||||||
@@ -192,6 +193,42 @@ Dump Draw Tasks
|
|||||||
and display each task's type, state, area, opacity, and preferred draw unit id.
|
and display each task's type, state, area, opacity, and preferred draw unit id.
|
||||||
|
|
||||||
|
|
||||||
|
Dump Dashboard
|
||||||
|
**************
|
||||||
|
|
||||||
|
``dump dashboard``: Collect all LVGL runtime state (displays, object trees,
|
||||||
|
animations, timers, caches, input devices, groups, draw units/tasks,
|
||||||
|
subjects/observers, image decoders, filesystem drivers) and generate a
|
||||||
|
self-contained HTML file for offline browsing.
|
||||||
|
|
||||||
|
The dashboard supports three output modes:
|
||||||
|
|
||||||
|
- ``dump dashboard``: Generate ``lvgl_dashboard.html`` with all data embedded.
|
||||||
|
- ``dump dashboard --json``: Export raw JSON data to ``lvgl_dashboard.json``.
|
||||||
|
- ``dump dashboard --viewer``: Generate an empty HTML viewer (``lvgl_viewer.html``)
|
||||||
|
that can load JSON files via drag-and-drop.
|
||||||
|
|
||||||
|
Use ``-o <path>`` to specify a custom output path.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
(gdb) dump dashboard
|
||||||
|
Dashboard written to lvgl_dashboard.html (1.23s)
|
||||||
|
|
||||||
|
(gdb) dump dashboard --json -o /tmp/state.json
|
||||||
|
Dashboard written to /tmp/state.json (0.98s)
|
||||||
|
|
||||||
|
(gdb) dump dashboard --viewer
|
||||||
|
Viewer written to lvgl_viewer.html
|
||||||
|
|
||||||
|
The generated HTML is fully self-contained (no external dependencies) and
|
||||||
|
includes a sidebar for navigation, a search box for filtering, collapsible
|
||||||
|
object trees with style details, framebuffer image previews, and cross-reference
|
||||||
|
links between related objects.
|
||||||
|
|
||||||
|
|
||||||
Inspect Object Class
|
Inspect Object Class
|
||||||
********************
|
********************
|
||||||
|
|
||||||
@@ -289,14 +326,58 @@ bulk export (e.g. ``LVAnim.snapshots(anims)``). Additionally,
|
|||||||
Architecture
|
Architecture
|
||||||
************
|
************
|
||||||
|
|
||||||
The GDB plugin decouples data extraction from display through a snapshot
|
The GDB plugin is organized into four layers. The overview below shows how
|
||||||
abstraction. Each wrapper class (``LVAnim``, ``LVTimer``, ``LVObject``, etc.)
|
terminal commands and the HTML dashboard both flow through the same snapshot
|
||||||
declares a ``_DISPLAY_SPEC`` describing its fields and exports a ``snapshot()``
|
abstraction down to raw GDB memory access:
|
||||||
method that returns a self-describing ``Snapshot`` object carrying both the
|
|
||||||
data dict and the display spec. The ``cmds/`` layer simply passes snapshots to
|
.. mermaid::
|
||||||
generic formatters (``print_info``, ``print_spec_table``) which read the
|
:zoom:
|
||||||
embedded spec to render output — no command needs to know the internal
|
|
||||||
structure of any wrapper.
|
graph TD
|
||||||
|
subgraph "Rendering Layer"
|
||||||
|
CLI["GDB Terminal<br/>dump obj, info style, ..."]
|
||||||
|
DASH["HTML Dashboard<br/>dump dashboard"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Formatter / Renderer"
|
||||||
|
FMT["formatter.py<br/>print_info · print_spec_table"]
|
||||||
|
HR["html_renderer.py<br/>template + CSS + JS"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Data Collection"
|
||||||
|
DC["data_collector.py<br/>collect_all() → JSON dict"]
|
||||||
|
SNAP["Snapshot<br/>_data + _display_spec"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Value Wrappers (lvgl/)"
|
||||||
|
W["LVObject · LVDisplay · LVAnim<br/>LVCache · LVTimer · LVDrawBuf<br/>LVIndev · LVGroup · ..."]
|
||||||
|
GDB["gdb.Value (C struct memory)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
CLI --> FMT
|
||||||
|
FMT --> SNAP
|
||||||
|
DASH --> HR
|
||||||
|
HR --> DC
|
||||||
|
DC --> SNAP
|
||||||
|
SNAP --> W
|
||||||
|
W --> GDB
|
||||||
|
|
||||||
|
style CLI fill:#4CAF50,color:#fff
|
||||||
|
style DASH fill:#4CAF50,color:#fff
|
||||||
|
style FMT fill:#FF9800,color:#fff
|
||||||
|
style HR fill:#FF9800,color:#fff
|
||||||
|
style DC fill:#2196F3,color:#fff
|
||||||
|
style SNAP fill:#2196F3,color:#fff
|
||||||
|
style W fill:#9C27B0,color:#fff
|
||||||
|
style GDB fill:#616161,color:#fff
|
||||||
|
|
||||||
|
Each wrapper class 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. The detailed
|
||||||
|
snapshot flow is shown below:
|
||||||
|
|
||||||
.. mermaid::
|
.. mermaid::
|
||||||
:zoom:
|
:zoom:
|
||||||
|
|||||||
+52
-41
@@ -1,61 +1,72 @@
|
|||||||
# lvglgdb
|
# lvglgdb
|
||||||
|
|
||||||
lvglgdb is a GDB script for LVGL.
|
GDB Python extension for inspecting and debugging LVGL internals.
|
||||||
|
Works with live debugging sessions, core dumps, and other
|
||||||
|
GDB-compatible targets.
|
||||||
|
|
||||||
# Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install lvglgdb
|
pip install lvglgdb
|
||||||
```
|
```
|
||||||
|
|
||||||
# Simple Usage
|
## Usage
|
||||||
|
|
||||||
|
In your GDB session:
|
||||||
|
|
||||||
In your GDB session, run:
|
|
||||||
```bash
|
```bash
|
||||||
py import lvglgdb
|
py import lvglgdb
|
||||||
|
|
||||||
dump obj
|
|
||||||
dump display -f png
|
|
||||||
dump cache image
|
|
||||||
dump cache image_header
|
|
||||||
check cache image
|
|
||||||
dump anim
|
|
||||||
dump timer
|
|
||||||
dump indev
|
|
||||||
dump group
|
|
||||||
dump image_decoder
|
|
||||||
dump fs_drv
|
|
||||||
dump draw_task <layer_expr>
|
|
||||||
|
|
||||||
# Inspect a single lv_style_t variable
|
|
||||||
info style my_style
|
|
||||||
|
|
||||||
# Inspect all styles of an lv_obj_t
|
|
||||||
info style --obj my_obj
|
|
||||||
|
|
||||||
# Show draw unit information
|
|
||||||
info draw_unit
|
|
||||||
|
|
||||||
# Show object class hierarchy
|
|
||||||
info obj_class obj->class_p
|
|
||||||
|
|
||||||
# Show subject and its observers
|
|
||||||
info subject &my_subject
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# Structure
|
### Dump Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dump obj # Dump widget tree
|
||||||
|
dump display -f png # Dump display framebuffer as PNG
|
||||||
|
dump cache image # Dump image cache entries
|
||||||
|
dump cache image_header # Dump image header cache entries
|
||||||
|
check cache image # Validate image cache integrity
|
||||||
|
dump anim # Dump active animations
|
||||||
|
dump timer # Dump registered timers
|
||||||
|
dump indev # Dump input devices
|
||||||
|
dump group # Dump focus groups
|
||||||
|
dump image_decoder # Dump registered image decoders
|
||||||
|
dump fs_drv # Dump filesystem drivers
|
||||||
|
dump draw_task <layer_expr> # Dump draw tasks for a layer
|
||||||
|
dump dashboard # Generate interactive HTML dashboard
|
||||||
|
dump dashboard -o out.html # Save dashboard to file
|
||||||
|
```
|
||||||
|
|
||||||
|
### Info Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
info style my_style # Inspect a single lv_style_t
|
||||||
|
info style --obj my_obj # Inspect all styles of an lv_obj_t
|
||||||
|
info draw_unit # Show draw unit information
|
||||||
|
info obj_class obj->class_p # Show object class hierarchy
|
||||||
|
info subject &my_subject # Show subject and its observers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
`dump dashboard` generates a self-contained HTML file with an interactive 3D
|
||||||
|
layer view, widget tree, style inspector, cache stats, animation list, and
|
||||||
|
draw buffer previews (RGB565 / RGB888 / ARGB8888 / XRGB8888).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph TD
|
||||||
lvgl["lvgl<br/>(mem→python object)"]
|
lvgl["lvgl<br/>(mem → python objects)"]
|
||||||
gdb_cmds["gdb_cmds<br/>(gdb commands)"]
|
cmds["cmds<br/>(GDB commands)"]
|
||||||
lvglgdb["lvglgdb"]
|
formatter["formatter<br/>(display logic)"]
|
||||||
|
dashboard["cmds/dashboard<br/>(HTML renderer)"]
|
||||||
|
|
||||||
lvglgdb --> lvgl
|
cmds --> formatter
|
||||||
lvglgdb --> gdb_cmds
|
cmds --> lvgl
|
||||||
gdb_cmds --> lvgl
|
dashboard --> lvgl
|
||||||
|
formatter --> lvgl
|
||||||
|
|
||||||
classDef pkg fill:white,stroke:gray
|
classDef pkg fill:white,stroke:gray
|
||||||
classDef core fill:white,stroke:gray
|
class lvgl,cmds,formatter,dashboard pkg
|
||||||
class lvglgdb,lvgl,gdb_cmds pkg
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from .misc import (
|
|||||||
DumpImageDecoder,
|
DumpImageDecoder,
|
||||||
DumpFsDrv,
|
DumpFsDrv,
|
||||||
)
|
)
|
||||||
|
from .dashboard import DumpDashboard
|
||||||
from .debugger import Debugger
|
from .debugger import Debugger
|
||||||
from .drivers import Lvglobal
|
from .drivers import Lvglobal
|
||||||
|
|
||||||
@@ -50,3 +51,6 @@ InfoSubject()
|
|||||||
|
|
||||||
# Drivers
|
# Drivers
|
||||||
Lvglobal()
|
Lvglobal()
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
DumpDashboard()
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .lv_dashboard import DumpDashboard
|
||||||
|
|
||||||
|
__all__ = ["DumpDashboard"]
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import base64
|
||||||
|
import functools
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import gdb
|
||||||
|
|
||||||
|
|
||||||
|
def safe_collect(subsystem: str):
|
||||||
|
"""Decorator that wraps a collector function with try/except/warning."""
|
||||||
|
def decorator(fn):
|
||||||
|
@functools.wraps(fn)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
gdb.write(f"Warning: failed to collect {subsystem}: {e}\n")
|
||||||
|
return []
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# Registry of simple subsystems: (dict_key, lvgl_accessor_method, label)
|
||||||
|
# accessor=None means the subsystem uses lvgl.<dict_key>().snapshots() pattern
|
||||||
|
SIMPLE_REGISTRY: list[tuple[str, str | None, str]] = [
|
||||||
|
("animations", "anims", "animations"),
|
||||||
|
("timers", "timers", "timers"),
|
||||||
|
("indevs", "indevs", "indevs"),
|
||||||
|
("groups", "groups", "groups"),
|
||||||
|
("draw_units", "draw_units", "draw units"),
|
||||||
|
("image_decoders", "image_decoders", "image decoders"),
|
||||||
|
("fs_drivers", "fs_drivers", "fs drivers"),
|
||||||
|
("image_header_cache", None, "image header cache"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_simple(lvgl, dict_key: str, accessor: str | None, label: str) -> list:
|
||||||
|
"""Collect a simple subsystem using the registry entry.
|
||||||
|
|
||||||
|
For entries with accessor != None: [x.snapshot().as_dict() for x in lvgl.<accessor>()]
|
||||||
|
For entries with accessor == None: [s.as_dict() for s in lvgl.<dict_key>().snapshots()]
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if accessor is not None:
|
||||||
|
return [x.snapshot().as_dict() for x in getattr(lvgl, accessor)()]
|
||||||
|
else:
|
||||||
|
return [s.as_dict() for s in getattr(lvgl, dict_key)().snapshots()]
|
||||||
|
except Exception as e:
|
||||||
|
gdb.write(f"Warning: failed to collect {label}: {e}\n")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def collect_all() -> dict:
|
||||||
|
"""Collect all LVGL runtime data into a JSON-compatible dict."""
|
||||||
|
from lvglgdb.lvgl import curr_inst
|
||||||
|
|
||||||
|
lvgl = curr_inst()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"meta": {
|
||||||
|
"timestamp": datetime.now().astimezone().isoformat(),
|
||||||
|
"lvgl_version": _get_lvgl_version(),
|
||||||
|
},
|
||||||
|
# Specialized collectors (complex logic, not registry-driven)
|
||||||
|
"displays": _collect_displays(lvgl),
|
||||||
|
"object_trees": _collect_object_trees(lvgl),
|
||||||
|
"image_cache": _collect_image_cache(lvgl),
|
||||||
|
"draw_tasks": _collect_draw_tasks(lvgl),
|
||||||
|
"subjects": _collect_subjects(lvgl),
|
||||||
|
}
|
||||||
|
# Registry-driven simple collectors
|
||||||
|
for dict_key, accessor, label in SIMPLE_REGISTRY:
|
||||||
|
data[dict_key] = _collect_simple(lvgl, dict_key, accessor, label)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lvgl_version() -> str | None:
|
||||||
|
"""Try to read LVGL version from macros."""
|
||||||
|
try:
|
||||||
|
major = int(gdb.parse_and_eval("LVGL_VERSION_MAJOR"))
|
||||||
|
minor = int(gdb.parse_and_eval("LVGL_VERSION_MINOR"))
|
||||||
|
patch = int(gdb.parse_and_eval("LVGL_VERSION_PATCH"))
|
||||||
|
return f"{major}.{minor}.{patch}"
|
||||||
|
except gdb.error:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _buf_to_dict(draw_buf) -> dict | None:
|
||||||
|
"""Convert an LVDrawBuf to a dict with base64 PNG image."""
|
||||||
|
if draw_buf is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
cf_info = draw_buf.color_format_info()
|
||||||
|
header = draw_buf.super_value("header")
|
||||||
|
stride = int(header["stride"])
|
||||||
|
height = int(header["h"])
|
||||||
|
bpp = cf_info["bpp"]
|
||||||
|
width = (stride * 8) // bpp if bpp else 0
|
||||||
|
|
||||||
|
png_bytes = draw_buf.to_png_bytes()
|
||||||
|
image_b64 = base64.b64encode(png_bytes).decode("ascii") if png_bytes else None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"addr": hex(int(draw_buf)),
|
||||||
|
"width": width,
|
||||||
|
"height": height,
|
||||||
|
"color_format": cf_info["name"],
|
||||||
|
"data_size": int(draw_buf.super_value("data_size")),
|
||||||
|
"image_base64": image_b64,
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@safe_collect("displays")
|
||||||
|
def _collect_displays(lvgl) -> list:
|
||||||
|
"""Collect display info with framebuffer data."""
|
||||||
|
result = []
|
||||||
|
for disp in lvgl.displays():
|
||||||
|
d = disp.snapshot().as_dict()
|
||||||
|
d["buf_1"] = _buf_to_dict(disp.buf_1)
|
||||||
|
d["buf_2"] = _buf_to_dict(disp.buf_2)
|
||||||
|
result.append(d)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@safe_collect("object trees")
|
||||||
|
def _collect_object_trees(lvgl) -> list:
|
||||||
|
"""Collect object trees for all displays with layer name annotations."""
|
||||||
|
result = []
|
||||||
|
for disp in lvgl.displays():
|
||||||
|
# Read special layer pointers for name annotation
|
||||||
|
layer_addrs = {}
|
||||||
|
for name in ("bottom_layer", "act_scr", "top_layer", "sys_layer"):
|
||||||
|
try:
|
||||||
|
ptr = disp.super_value(name)
|
||||||
|
if int(ptr):
|
||||||
|
layer_addrs[int(ptr)] = name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
tree = {
|
||||||
|
"display_addr": hex(int(disp)),
|
||||||
|
"screens": [],
|
||||||
|
}
|
||||||
|
for screen in disp.screens:
|
||||||
|
snap = screen.snapshot(
|
||||||
|
include_children=True, include_styles=True
|
||||||
|
).as_dict()
|
||||||
|
# Annotate with layer name if this screen is a known layer
|
||||||
|
screen_addr = int(screen)
|
||||||
|
snap["layer_name"] = layer_addrs.get(screen_addr)
|
||||||
|
tree["screens"].append(snap)
|
||||||
|
result.append(tree)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@safe_collect("image cache")
|
||||||
|
def _collect_image_cache(lvgl) -> list:
|
||||||
|
"""Collect image cache entries with optional decoded buffer previews."""
|
||||||
|
cache = lvgl.image_cache()
|
||||||
|
entries = cache.snapshots()
|
||||||
|
result = []
|
||||||
|
for snap in entries:
|
||||||
|
d = snap.as_dict()
|
||||||
|
# Try to get preview of decoded buffer
|
||||||
|
d["preview_base64"] = None
|
||||||
|
decoded_addr = d.get("decoded_addr")
|
||||||
|
if decoded_addr and decoded_addr != "0x0":
|
||||||
|
try:
|
||||||
|
from lvglgdb.lvgl.draw.lv_draw_buf import LVDrawBuf
|
||||||
|
buf = LVDrawBuf(gdb.Value(int(decoded_addr, 16)))
|
||||||
|
png_bytes = buf.to_png_bytes()
|
||||||
|
if png_bytes:
|
||||||
|
d["preview_base64"] = base64.b64encode(
|
||||||
|
png_bytes
|
||||||
|
).decode("ascii")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
result.append(d)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@safe_collect("draw tasks")
|
||||||
|
def _collect_draw_tasks(lvgl) -> list:
|
||||||
|
"""Collect draw tasks from each display's layer chain."""
|
||||||
|
from lvglgdb.lvgl.draw.lv_draw_task import LVDrawTask
|
||||||
|
result = []
|
||||||
|
for disp in lvgl.displays():
|
||||||
|
layer = disp.super_value("layer_head")
|
||||||
|
while layer and int(layer):
|
||||||
|
task_head = layer["draw_task_head"]
|
||||||
|
if int(task_head):
|
||||||
|
for t in LVDrawTask(task_head):
|
||||||
|
result.append(t.snapshot().as_dict())
|
||||||
|
layer = layer["next"]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@safe_collect("subjects")
|
||||||
|
def _collect_subjects(lvgl) -> list:
|
||||||
|
"""Collect subjects from object event lists across all displays."""
|
||||||
|
seen = set()
|
||||||
|
result = []
|
||||||
|
from lvglgdb.lvgl.core.lv_observer import LVSubject
|
||||||
|
for disp in lvgl.displays():
|
||||||
|
for screen in disp.screens:
|
||||||
|
_collect_subjects_from_obj(screen, seen, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_subjects_from_obj(obj, seen, result):
|
||||||
|
"""Recursively collect subjects from an object's event list."""
|
||||||
|
from lvglgdb.lvgl.core.lv_observer import LVSubject
|
||||||
|
|
||||||
|
event_list = obj.event_list
|
||||||
|
if event_list:
|
||||||
|
for dsc in event_list:
|
||||||
|
try:
|
||||||
|
user_data = dsc.user_data
|
||||||
|
if not int(user_data):
|
||||||
|
continue
|
||||||
|
# Check if this looks like a subject (has subs_ll field)
|
||||||
|
subject = LVSubject(user_data)
|
||||||
|
addr = int(subject)
|
||||||
|
if addr not in seen:
|
||||||
|
seen.add(addr)
|
||||||
|
result.append(subject.snapshot().as_dict())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for child in obj.children:
|
||||||
|
_collect_subjects_from_obj(child, seen, result)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
_STATIC_DIR = Path(__file__).parent / "static"
|
||||||
|
|
||||||
|
|
||||||
|
def render(data: dict, output_path: str) -> None:
|
||||||
|
"""Generate self-contained HTML with JSON data embedded."""
|
||||||
|
json_str = _safe_json_encode(data)
|
||||||
|
html = _build_html(json_str)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
|
||||||
|
|
||||||
|
def render_viewer(output_path: str) -> None:
|
||||||
|
"""Generate empty shell HTML viewer (no embedded data)."""
|
||||||
|
html = _build_html("")
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_json_encode(data: dict) -> str:
|
||||||
|
"""Serialize dict to JSON with HTML-safe escaping."""
|
||||||
|
raw = json.dumps(data, ensure_ascii=False, indent=None)
|
||||||
|
# Escape HTML-special chars to prevent injection in <script> block
|
||||||
|
return raw.replace("&", "\\u0026").replace("<", "\\u003c").replace(">", "\\u003e")
|
||||||
|
|
||||||
|
|
||||||
|
def _read_static(filename: str) -> str:
|
||||||
|
"""Read a static asset file from the static/ directory."""
|
||||||
|
return (_STATIC_DIR / filename).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_html(json_content: str) -> str:
|
||||||
|
"""Build the complete HTML page with embedded or empty JSON."""
|
||||||
|
template = _read_static("template.html")
|
||||||
|
css = _read_static("style.css")
|
||||||
|
js = _read_static("dashboard.js")
|
||||||
|
# Deterministic single-replacement order: CSS → JS → JSON_DATA.
|
||||||
|
# Each placeholder is replaced exactly once (count=1) so that content
|
||||||
|
# injected in an earlier step cannot be mis-interpreted as a later placeholder.
|
||||||
|
return (
|
||||||
|
template
|
||||||
|
.replace("{{CSS}}", css, 1)
|
||||||
|
.replace("{{JS}}", js, 1)
|
||||||
|
.replace("{{JSON_DATA}}", json_content, 1)
|
||||||
|
)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
import gdb
|
||||||
|
|
||||||
|
from lvglgdb.lvgl import curr_inst
|
||||||
|
from .data_collector import collect_all
|
||||||
|
from .html_renderer import render, render_viewer
|
||||||
|
|
||||||
|
|
||||||
|
class DumpDashboard(gdb.Command):
|
||||||
|
"""Generate an HTML dashboard of all LVGL runtime state."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
"dump dashboard", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
|
||||||
|
)
|
||||||
|
|
||||||
|
def invoke(self, args, from_tty):
|
||||||
|
parser = argparse.ArgumentParser(prog="dump dashboard")
|
||||||
|
parser.add_argument("-o", "--output", help="output file path")
|
||||||
|
group = parser.add_mutually_exclusive_group()
|
||||||
|
group.add_argument(
|
||||||
|
"--json", action="store_true", help="output JSON instead of HTML",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--viewer", action="store_true",
|
||||||
|
help="output empty viewer HTML (no data collection)",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts = parser.parse_args(gdb.string_to_argv(args))
|
||||||
|
except SystemExit:
|
||||||
|
return
|
||||||
|
|
||||||
|
if opts.viewer:
|
||||||
|
out = opts.output or "lvgl_viewer.html"
|
||||||
|
render_viewer(out)
|
||||||
|
gdb.write(f"Viewer written to {out}\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not curr_inst().ensure_init():
|
||||||
|
return
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
data = collect_all()
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
|
||||||
|
if opts.json:
|
||||||
|
out = opts.output or "lvgl_dashboard.json"
|
||||||
|
with open(out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
else:
|
||||||
|
out = opts.output or "lvgl_dashboard.html"
|
||||||
|
render(data, out)
|
||||||
|
|
||||||
|
gdb.write(f"Dashboard written to {out} ({elapsed:.2f}s)\n")
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,39 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LVGL Dashboard</title>
|
||||||
|
<style>
|
||||||
|
{{CSS}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="topbar-brand">
|
||||||
|
<span class="logo">LV</span> LVGL Dashboard
|
||||||
|
</div>
|
||||||
|
<div class="topbar-sep"></div>
|
||||||
|
<div class="topbar-meta" id="header-meta"></div>
|
||||||
|
<nav class="topbar-nav" id="topbar-nav"></nav>
|
||||||
|
<button class="theme-toggle" id="theme-toggle" title="Toggle light/dark theme"
|
||||||
|
aria-label="Toggle theme">🌙</button>
|
||||||
|
<input type="text" class="topbar-search" id="search"
|
||||||
|
placeholder="Filter..." aria-label="Search">
|
||||||
|
</header>
|
||||||
|
<main class="main">
|
||||||
|
<div class="bento" id="bento-grid">
|
||||||
|
<div id="drop-zone" style="display:none">
|
||||||
|
<div class="drop-icon">📂</div>
|
||||||
|
<p>Drag & drop a JSON file here, or click to select</p>
|
||||||
|
<div class="drop-hint">Exported via <code>dump dashboard --json</code></div>
|
||||||
|
<input type="file" id="file-input" accept=".json" aria-label="Load JSON file">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script type="application/json" id="lvgl-data">{{JSON_DATA}}</script>
|
||||||
|
<script>
|
||||||
|
{{JS}}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -75,9 +75,50 @@ class LVDrawBuf(Value):
|
|||||||
"""Get the buffer data ptr"""
|
"""Get the buffer data ptr"""
|
||||||
return self.super_value("data")
|
return self.super_value("data")
|
||||||
|
|
||||||
def data_dump(self, filename: str, format: str = None) -> bool:
|
def _read_image(self, strict: bool = False) -> Optional[Image.Image]:
|
||||||
|
"""Read buffer data and convert to PIL Image.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
strict: If True, raise on any size mismatch instead of ignoring.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PIL.Image or None on failure.
|
||||||
"""
|
"""
|
||||||
Dump the buffer data to an image file.
|
header = self.super_value("header")
|
||||||
|
stride = int(header["stride"])
|
||||||
|
height = int(header["h"])
|
||||||
|
cf_info = self.color_format_info()
|
||||||
|
data_ptr = self.super_value("data")
|
||||||
|
data_size = int(self.super_value("data_size"))
|
||||||
|
width = (stride * 8) // cf_info["bpp"] if cf_info["bpp"] else 0
|
||||||
|
expected_data_size = stride * height
|
||||||
|
|
||||||
|
if not data_ptr or width <= 0 or height <= 0:
|
||||||
|
if strict:
|
||||||
|
raise ValueError(f"Invalid buffer: ptr={data_ptr}, {width}x{height}")
|
||||||
|
return None
|
||||||
|
if data_size < expected_data_size:
|
||||||
|
if strict:
|
||||||
|
raise ValueError(
|
||||||
|
f"Data too small: expected at least {expected_data_size},"
|
||||||
|
f" got {data_size}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
if data_size > expected_data_size and strict:
|
||||||
|
gdb.write(
|
||||||
|
f"Warning: data_size {data_size} exceeds expected"
|
||||||
|
f" {expected_data_size}, extra bytes will be ignored\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
pixel_data = (
|
||||||
|
gdb.selected_inferior()
|
||||||
|
.read_memory(int(data_ptr), expected_data_size)
|
||||||
|
.tobytes()
|
||||||
|
)
|
||||||
|
return self._convert_to_image(pixel_data, width, height, cf_info["value"])
|
||||||
|
|
||||||
|
def data_dump(self, filename: str, format: str = None) -> bool:
|
||||||
|
"""Dump the buffer data to an image file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: Output file path
|
filename: Output file path
|
||||||
@@ -87,57 +128,19 @@ class LVDrawBuf(Value):
|
|||||||
bool: True if successful, False otherwise
|
bool: True if successful, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Validate input parameters
|
|
||||||
if not filename:
|
if not filename:
|
||||||
raise ValueError("Output filename cannot be empty")
|
raise ValueError("Output filename cannot be empty")
|
||||||
|
|
||||||
# Get buffer metadata
|
img = self._read_image(strict=True)
|
||||||
header = self.super_value("header")
|
|
||||||
stride = int(header["stride"])
|
|
||||||
height = int(header["h"])
|
|
||||||
cf_info = self.color_format_info()
|
|
||||||
data_ptr = self.super_value("data")
|
|
||||||
data_size = int(self.super_value("data_size"))
|
|
||||||
width = (stride * 8) // cf_info["bpp"] if cf_info["bpp"] else 0
|
|
||||||
expected_data_size = stride * height
|
|
||||||
|
|
||||||
# Validate buffer data
|
|
||||||
if not data_ptr:
|
|
||||||
raise ValueError("Data pointer is NULL")
|
|
||||||
if width <= 0 or height <= 0:
|
|
||||||
raise ValueError(f"Invalid dimensions: {width}x{height}")
|
|
||||||
if data_size <= 0:
|
|
||||||
raise ValueError(f"Invalid data size: {data_size}")
|
|
||||||
if data_size < expected_data_size:
|
|
||||||
raise ValueError(
|
|
||||||
f"Data size mismatch: expected {expected_data_size}, got {data_size}"
|
|
||||||
)
|
|
||||||
elif data_size > expected_data_size:
|
|
||||||
gdb.write(
|
|
||||||
f"\033[93mData size mismatch: expected {expected_data_size}, got {data_size}\033[0m\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Read pixel data
|
|
||||||
pixel_data = (
|
|
||||||
gdb.selected_inferior()
|
|
||||||
.read_memory(int(data_ptr), expected_data_size)
|
|
||||||
.tobytes()
|
|
||||||
)
|
|
||||||
if not pixel_data:
|
|
||||||
raise ValueError("Failed to read pixel data")
|
|
||||||
|
|
||||||
# Process based on color format
|
|
||||||
img = self._convert_to_image(pixel_data, width, height, cf_info["value"])
|
|
||||||
if img is None:
|
if img is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Determine output format
|
|
||||||
output_format = (
|
output_format = (
|
||||||
format.upper() if format else Path(filename).suffix[1:].upper() or "BMP"
|
format.upper() if format else Path(filename).suffix[1:].upper() or "BMP"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save image
|
|
||||||
img.save(filename, format=output_format)
|
img.save(filename, format=output_format)
|
||||||
|
|
||||||
|
cf_info = self.color_format_info()
|
||||||
print(
|
print(
|
||||||
f"Successfully saved {cf_info['name']} buffer as {output_format} to {filename}"
|
f"Successfully saved {cf_info['name']} buffer as {output_format} to {filename}"
|
||||||
)
|
)
|
||||||
@@ -153,6 +156,20 @@ class LVDrawBuf(Value):
|
|||||||
print(f"Unexpected error: {str(e)}")
|
print(f"Unexpected error: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def to_png_bytes(self) -> Optional[bytes]:
|
||||||
|
"""Convert buffer to PNG bytes in memory. Returns None on failure."""
|
||||||
|
import io
|
||||||
|
|
||||||
|
try:
|
||||||
|
img = self._read_image(strict=False)
|
||||||
|
if img is None:
|
||||||
|
return None
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
except (gdb.MemoryError, Exception):
|
||||||
|
return None
|
||||||
|
|
||||||
def _convert_to_image(
|
def _convert_to_image(
|
||||||
self, pixel_data: bytes, width: int, height: int, color_format: int
|
self, pixel_data: bytes, width: int, height: int, color_format: int
|
||||||
) -> Optional[Image.Image]:
|
) -> Optional[Image.Image]:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "lvglgdb"
|
name = "lvglgdb"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "LVGL GDB scripts"
|
description = "LVGL GDB scripts"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -11,6 +11,9 @@ dependencies = [
|
|||||||
"prettytable~=3.16.0",
|
"prettytable~=3.16.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
lvglgdb = ["cmds/dashboard/static/*"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://lvgl.io"
|
Homepage = "https://lvgl.io"
|
||||||
Documentation = "https://docs.lvgl.io/master/debugging/gdb_plugin.html"
|
Documentation = "https://docs.lvgl.io/master/debugging/gdb_plugin.html"
|
||||||
|
|||||||
Reference in New Issue
Block a user