mirror of
https://github.com/lvgl/lvgl.git
synced 2026-03-23 14:03:13 +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 fs_drv``: List all registered filesystem drivers.
|
||||
- ``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 draw_unit``: Print raw struct details for each drawing unit.
|
||||
- ``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.
|
||||
|
||||
|
||||
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
|
||||
********************
|
||||
|
||||
@@ -289,14 +326,58 @@ bulk export (e.g. ``LVAnim.snapshots(anims)``). Additionally,
|
||||
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.
|
||||
The GDB plugin is organized into four layers. The overview below shows how
|
||||
terminal commands and the HTML dashboard both flow through the same snapshot
|
||||
abstraction down to raw GDB memory access:
|
||||
|
||||
.. mermaid::
|
||||
:zoom:
|
||||
|
||||
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::
|
||||
:zoom:
|
||||
|
||||
@@ -1,61 +1,72 @@
|
||||
# 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
|
||||
pip install lvglgdb
|
||||
```
|
||||
|
||||
# Simple Usage
|
||||
## Usage
|
||||
|
||||
In your GDB session:
|
||||
|
||||
In your GDB session, run:
|
||||
```bash
|
||||
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
|
||||
graph TD
|
||||
lvgl["lvgl<br/>(mem→python object)"]
|
||||
gdb_cmds["gdb_cmds<br/>(gdb commands)"]
|
||||
lvglgdb["lvglgdb"]
|
||||
lvgl["lvgl<br/>(mem → python objects)"]
|
||||
cmds["cmds<br/>(GDB commands)"]
|
||||
formatter["formatter<br/>(display logic)"]
|
||||
dashboard["cmds/dashboard<br/>(HTML renderer)"]
|
||||
|
||||
lvglgdb --> lvgl
|
||||
lvglgdb --> gdb_cmds
|
||||
gdb_cmds --> lvgl
|
||||
cmds --> formatter
|
||||
cmds --> lvgl
|
||||
dashboard --> lvgl
|
||||
formatter --> lvgl
|
||||
|
||||
classDef pkg fill:white,stroke:gray
|
||||
classDef core fill:white,stroke:gray
|
||||
class lvglgdb,lvgl,gdb_cmds pkg
|
||||
class lvgl,cmds,formatter,dashboard pkg
|
||||
```
|
||||
|
||||
@@ -13,6 +13,7 @@ from .misc import (
|
||||
DumpImageDecoder,
|
||||
DumpFsDrv,
|
||||
)
|
||||
from .dashboard import DumpDashboard
|
||||
from .debugger import Debugger
|
||||
from .drivers import Lvglobal
|
||||
|
||||
@@ -50,3 +51,6 @@ InfoSubject()
|
||||
|
||||
# Drivers
|
||||
Lvglobal()
|
||||
|
||||
# Dashboard
|
||||
DumpDashboard()
|
||||
|
||||
3
scripts/gdb/lvglgdb/cmds/dashboard/__init__.py
Normal file
3
scripts/gdb/lvglgdb/cmds/dashboard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .lv_dashboard import DumpDashboard
|
||||
|
||||
__all__ = ["DumpDashboard"]
|
||||
232
scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py
Normal file
232
scripts/gdb/lvglgdb/cmds/dashboard/data_collector.py
Normal file
@@ -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)
|
||||
47
scripts/gdb/lvglgdb/cmds/dashboard/html_renderer.py
Normal file
47
scripts/gdb/lvglgdb/cmds/dashboard/html_renderer.py
Normal file
@@ -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)
|
||||
)
|
||||
58
scripts/gdb/lvglgdb/cmds/dashboard/lv_dashboard.py
Normal file
58
scripts/gdb/lvglgdb/cmds/dashboard/lv_dashboard.py
Normal file
@@ -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")
|
||||
1525
scripts/gdb/lvglgdb/cmds/dashboard/static/dashboard.js
Normal file
1525
scripts/gdb/lvglgdb/cmds/dashboard/static/dashboard.js
Normal file
File diff suppressed because it is too large
Load Diff
895
scripts/gdb/lvglgdb/cmds/dashboard/static/style.css
Normal file
895
scripts/gdb/lvglgdb/cmds/dashboard/static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
39
scripts/gdb/lvglgdb/cmds/dashboard/static/template.html
Normal file
39
scripts/gdb/lvglgdb/cmds/dashboard/static/template.html
Normal file
@@ -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"""
|
||||
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:
|
||||
filename: Output file path
|
||||
@@ -87,57 +128,19 @@ class LVDrawBuf(Value):
|
||||
bool: True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Validate input parameters
|
||||
if not filename:
|
||||
raise ValueError("Output filename cannot be empty")
|
||||
|
||||
# Get buffer metadata
|
||||
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"])
|
||||
img = self._read_image(strict=True)
|
||||
if img is None:
|
||||
return False
|
||||
|
||||
# Determine output format
|
||||
output_format = (
|
||||
format.upper() if format else Path(filename).suffix[1:].upper() or "BMP"
|
||||
)
|
||||
|
||||
# Save image
|
||||
img.save(filename, format=output_format)
|
||||
|
||||
cf_info = self.color_format_info()
|
||||
print(
|
||||
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)}")
|
||||
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(
|
||||
self, pixel_data: bytes, width: int, height: int, color_format: int
|
||||
) -> Optional[Image.Image]:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "lvglgdb"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "LVGL GDB scripts"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -11,6 +11,9 @@ dependencies = [
|
||||
"prettytable~=3.16.0",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
lvglgdb = ["cmds/dashboard/static/*"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://lvgl.io"
|
||||
Documentation = "https://docs.lvgl.io/master/debugging/gdb_plugin.html"
|
||||
|
||||
Reference in New Issue
Block a user