diff --git a/DEFAULT_CONFIGS.json b/DEFAULT_CONFIGS.json
new file mode 100644
index 000000000..4a63571f7
--- /dev/null
+++ b/DEFAULT_CONFIGS.json
@@ -0,0 +1,107 @@
+[
+ {
+ "name": "LVGL Image Descriptor",
+ "typePattern": "lv_image_dsc_t",
+ "width": {
+ "type": "string",
+ "isChild": true,
+ "value": "header.w"
+ },
+ "height": {
+ "type": "string",
+ "isChild": true,
+ "value": "header.h"
+ },
+ "format": {
+ "type": "string",
+ "isChild": true,
+ "value": "header.cf"
+ },
+ "dataAddress": {
+ "type": "string",
+ "isChild": true,
+ "value": "data"
+ },
+ "dataSize": {
+ "type": "string",
+ "isChild": true,
+ "value": "data_size"
+ },
+ "imageFormats": {
+ "0": "grayscale",
+ "1": "grayscale",
+ "2": "grayscale",
+ "3": "grayscale",
+ "4": "grayscale",
+ "5": "grayscale",
+ "6": "grayscale",
+ "7": "grayscale",
+ "8": "grayscale",
+ "9": "rgb565",
+ "10": "rgb565",
+ "11": "rgb565",
+ "12": "grayscale",
+ "13": "rgb565",
+ "14": "rgb888",
+ "15": "argb8888",
+ "16": "bgra8888",
+ "17": "argb8888",
+ "18": "yuv420",
+ "19": "yuv420",
+ "20": "yuv422",
+ "21": "yuv444",
+ "22": "grayscale",
+ "23": "yuv420",
+ "24": "yuv420",
+ "25": "yuv422",
+ "26": "yuv422",
+ "27": "yuv420",
+ "28": "rgb888",
+ "29": "rgb888",
+ "30": "rgb444",
+ "31": "rgb666",
+ "32": "rgb666",
+ "33": "rgb666",
+ "34": "rgb888",
+ "35": "rgb888",
+ "36": "rgb888",
+ "37": "rgb888",
+ "38": "rgba8888"
+ }
+ },
+ {
+ "name": "OpenCV Mat",
+ "typePattern": "cv::Mat|Mat",
+ "width": {
+ "type": "string",
+ "isChild": true,
+ "value": "cols"
+ },
+ "height": {
+ "type": "string",
+ "isChild": true,
+ "value": "rows"
+ },
+ "format": {
+ "type": "number",
+ "isChild": false,
+ "value": "0x0E"
+ },
+ "dataAddress": {
+ "type": "string",
+ "isChild": true,
+ "value": "data"
+ },
+ "dataSize": {
+ "type": "formula",
+ "isChild": false,
+ "value": "$var.rows * $var.step.buf[0]"
+ },
+ "imageFormats": {
+ "0": "grayscale",
+ "14": "bgr888",
+ "15": "bgra8888",
+ "16": "bgra8888"
+ }
+ }
+]
diff --git a/README.md b/README.md
index ce40c1ecc..218dd7ed5 100644
--- a/README.md
+++ b/README.md
@@ -463,7 +463,7 @@ Press F1 or click menu `View` -> `Command Palette...` to show Visual
- Scripts and Tools
+ Scripts and Tools
Run idf.py reconfigure Task
This command will execute idf.py reconfigure (CMake configure task), which is useful for generating compile_commands.json for the C/C++ language support.
@@ -523,6 +523,18 @@ Press F1 or click menu `View` -> `Command Palette...` to show Visual
+
+ Load Image from LVGL C File
+ Load and display an image from a LVGL C file containing lv_image_dsc_t structure. This command allows you to view LVGL images without requiring a debug session.
+
+
+
+
+ Open Image Viewer
+ Open the Image Viewer panel to display images from debug variables or LVGL C files. This panel provides tools for viewing and analyzing image data in various formats.
+
+
+
Cleanup
Clear ESP-IDF Search Results
diff --git a/README_CN.md b/README_CN.md
index fa2de0775..95674d4b1 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -455,7 +455,7 @@ ESP-IDF 扩展在 VS Code 底部蓝色窗口的状态栏中提供了一系列命
- 脚本和工具
+ 脚本和工具
运行 idf.py reconfigure 任务
此命令将执行 idf.py reconfigure (CMake 配置任务),能够帮助生成 compile_commands.json 文件以支持 C/C++ 语言特性。
@@ -516,7 +516,19 @@ ESP-IDF 扩展在 VS Code 底部蓝色窗口的状态栏中提供了一系列命
- 清理
+ 从 LVGL C 文件加载图像
+ 从包含 lv_image_dsc_t 结构的 LVGL C 文件中加载并显示图像。此命令允许您在不需要调试会话的情况下查看 LVGL 图像。
+
+
+
+
+ 打开图像查看器
+ 打开图像查看器面板,用于显示来自调试变量或 LVGL C 文件的图像。此面板提供查看和分析各种格式图像数据的工具。
+
+
+
+
+ 清理
清除 ESP-IDF 搜索结果
清除资源管理器文档搜索结果 选项卡中的所有搜索结果。
diff --git a/docs_espressif/en/commands.rst b/docs_espressif/en/commands.rst
index 6cf3a653b..06f8ecda6 100644
--- a/docs_espressif/en/commands.rst
+++ b/docs_espressif/en/commands.rst
@@ -136,3 +136,7 @@ All commands start with ``ESP-IDF:``.
- Copy the unit test app in the current project, build the current project and flash the unit test application to the connected device. More information can be found in :ref:`Unit Testing Documentation `.
* - Unit Test: Install ESP-IDF Pytest Requirements
- Install the ESP-IDF Pytest requirement packages to be able to execute ESP-IDF unit tests. More information can be found in :ref:`Unit Testing Documentation `.
+ * - Load Image from LVGL C File
+ - Load and display an image from a LVGL C file containing lv_image_dsc_t structure. This command allows you to view LVGL images without requiring a debug session.
+ * - Open Image Viewer
+ - Open the Image Viewer panel to display images from debug variables or LVGL C files. This panel provides tools for viewing and analyzing image data in various formats.
diff --git a/docs_espressif/en/debugproject.rst b/docs_espressif/en/debugproject.rst
index 6fdfc57ae..9f3a1a46f 100644
--- a/docs_espressif/en/debugproject.rst
+++ b/docs_espressif/en/debugproject.rst
@@ -343,6 +343,174 @@ You can start a monitor session to capture fatal error events with **ESP-IDF: La
- **GDB Stub** is configured when **Panic Handler Behaviour** is set to ``Invoke GDBStub`` using the ``ESP-IDF: SDK Configuration Editor`` extension command or ``idf.py menuconfig`` in a terminal.
+ESP-IDF: Image Viewer
+---------------------
+
+The ESP-IDF extension provides an **ESP-IDF: Image Viewer** feature that allows you to visualize binary image data from debug variables during a debugging session. This is particularly useful for applications that work with camera sensors, display buffers, LVGL graphics, OpenCV computer vision, or any raw image data.
+
+**Quick Access Methods:**
+
+1. **Right-click on variables in the debug session:**
+ - Right-click on any image-related variable (``lv_image_dsc_t``, ``cv::Mat``, ``png_image``, etc.) and select ``View Variable as Image``
+ - The Image Viewer automatically detects the variable type and extracts the appropriate image properties
+
+2. **Manual Image Viewer:**
+ - Go to ``View`` > ``Command Palette`` and enter ``ESP-IDF: Open Image Viewer``
+ - Enter the name of your image data variable and its size
+ - Select the appropriate image format and dimensions
+ - Click ``Load Image`` to visualize the data
+
+**Supported Image Formats:**
+
+The Image Viewer supports a comprehensive range of image formats:
+
+**RGB Formats:**
+- RGB565, RGB888, RGBA8888, ARGB8888, XRGB8888
+- BGR888, BGRA8888, ABGR8888, XBGR8888
+- RGB332, RGB444, RGB555, RGB666, RGB777
+- RGB101010, RGB121212, RGB161616
+
+**Other Formats:**
+- Grayscale (8-bit per pixel)
+- YUV420, YUV422, YUV444 (various YUV formats)
+
+**Built-in Support:**
+
+**LVGL Image Descriptor (lv_image_dsc_t):**
+- Automatically extracts format, dimensions, and data from LVGL structures
+- Supports all LVGL color formats with automatic mapping to display formats
+
+**OpenCV Mat (cv::Mat):**
+- Automatically extracts dimensions, format, and data from OpenCV Mat objects
+- Supports BGR888, BGRA8888, and Grayscale formats
+
+**Example Usage:**
+
+**LVGL Image Example:**
+
+.. code-block:: C
+
+ // LVGL image descriptor
+ lv_image_dsc_t my_image = {
+ .header = {
+ .cf = LV_COLOR_FORMAT_RGB888, // Color format
+ .w = 320, // Width
+ .h = 240 // Height
+ },
+ .data_size = 320 * 240 * 3, // Data size in bytes
+ .data = image_data // Pointer to image data
+ };
+
+During debugging, right-click on ``my_image`` and select ``View Variable as Image``. The Image Viewer will automatically detect it as an LVGL image and extract the format, dimensions, and data.
+
+**OpenCV Mat Example:**
+
+.. code-block:: C
+
+ cv::Mat image(240, 320, CV_8UC3); // 320x240 BGR888 image
+ // ... populate image data ...
+
+During debugging, right-click on ``image`` and select ``View Variable as Image``. The Image Viewer will automatically detect it as an OpenCV Mat and extract the dimensions, format, and data.
+
+**Manual Raw Data Example:**
+
+.. code-block:: C
+
+ uint8_t image_buffer[320 * 240 * 3]; // RGB888 format, 320x240 pixels
+ size_t image_size = sizeof(image_buffer);
+
+For manual usage:
+- Enter ``image_buffer`` as the variable name
+- Enter ``image_size`` or ``230400`` (320 * 240 * 3) as the size
+- Select ``RGB888`` format
+- Set width to ``320`` and height to ``240``
+
+**Custom Image Format Configuration:**
+
+You can extend the Image Viewer to support custom image formats by creating a JSON configuration file and setting the ``idf.imageViewerConfigs`` configuration option.
+
+**Example Custom Configuration:**
+
+.. code-block:: JSON
+
+ [
+ {
+ "name": "Custom Image Structure",
+ "typePattern": "my_image_t",
+ "width": {
+ "type": "string",
+ "isChild": true,
+ "value": "w"
+ },
+ "height": {
+ "type": "string",
+ "isChild": true,
+ "value": "h"
+ },
+ "format": {
+ "type": "number",
+ "isChild": false,
+ "value": "0x0E"
+ },
+ "dataAddress": {
+ "type": "string",
+ "isChild": true,
+ "value": "pixels"
+ },
+ "dataSize": {
+ "type": "formula",
+ "isChild": false,
+ "value": "$var.w * $var.h * 3"
+ },
+ "imageFormats": {
+ "14": "rgb888",
+ "15": "rgba8888"
+ }
+ }
+ ]
+
+**Configuration Options:**
+
+- **typePattern**: Regex pattern to match the GDB type of the selected variable when right-clicking "View Variable as Image" (e.g., ``"my_image_t"``, ``"lv_image_dsc_t"``, ``"cv::Mat|Mat"``)
+- **width/height**: Configuration for extracting image dimensions
+- **format**: Configuration for extracting image format
+- **dataAddress**: Configuration for extracting image data pointer
+- **dataSize**: Configuration for calculating image data size (supports formulas)
+- **imageFormats**: Mapping of numeric format values to display format strings
+
+**Field Configuration Details:**
+
+Each field (width, height, format, dataAddress, dataSize) has the following properties:
+
+- **type**: Specifies the data type of the field value:
+ - ``"string"``: The value is a string (field name or expression)
+ - ``"number"``: The value is a numeric constant (e.g., ``"0x0E"``, ``"14"``)
+ - ``"formula"``: The value is a mathematical formula (only for dataSize field)
+
+- **isChild**: Determines how the field value is interpreted:
+ - ``true``: The value represents a child field of the right-clicked variable (e.g., ``"header.w"``, ``"data"``)
+ - ``false``: The value is a direct expression or constant that can be evaluated by GDB
+
+- **value**: The actual field name, expression, or constant to use for extraction
+
+**Important Configuration Notes:**
+
+- **dataSize Formula**: When using formulas in the ``dataSize`` field, the string ``$var`` will be automatically replaced with the actual variable name when you right-click and select "View Variable as Image". For example, if your variable is named ``my_image`` and the formula is ``$var.w * $var.h * 3``, it will be evaluated as ``my_image.w * my_image.h * 3``. **Note**: The formula must be a valid GDB expression since it is calculated by GDB itself.
+
+- **Format Number Mapping**: The numeric keys in the ``imageFormats`` object must match the actual numeric values that the ``format`` field extracts from your image structure. For example, if your image structure's format field contains the value ``14``, then the ``imageFormats`` object should have a key ``"14"`` that maps to the appropriate display format string like ``"rgb888"``.
+
+**Important Notes:**
+- **Automatic Detection**: The Image Viewer automatically detects supported image types and extracts properties
+- **Unified Interface**: Single ``View Variable as Image`` command works for all supported formats
+- **Format Validation**: All formats are validated against supported display formats
+- **Raw Data**: The Image Viewer supports raw pixel formats. Compressed formats (JPEG, PNG, etc.) are not supported
+- **Size Specification**: For manual usage, you must specify the correct size of the image data array
+- **Variable Size**: The size can be provided as a number (bytes) or as the name of another variable containing the size
+- **Pointer Variables**: For pointer variables, make sure to provide the actual data size, not the pointer size
+- **Auto-Dimensioning**: The Image Viewer automatically estimates dimensions based on the data size and selected format, but you can manually adjust them for better results
+- **Extensible**: Custom image formats can be added through configuration files
+
+
Other extensions debug configuration
------------------------------------
diff --git a/docs_espressif/en/settings.rst b/docs_espressif/en/settings.rst
index f0c64d2b1..f3f4f3e9d 100644
--- a/docs_espressif/en/settings.rst
+++ b/docs_espressif/en/settings.rst
@@ -128,6 +128,8 @@ These settings are specific to the ESP32 Chip/Board.
- SVD file absolute path to resolve chip debug peripheral tree view
* - **idf.jtagFlashCommandExtraArgs**
- OpenOCD JTAG flash extra arguments. Default is ``["verify", "reset"]``.
+ * - **idf.imageViewerConfigs**
+ - Path to custom image format configurations JSON file for the Image Viewer feature. Can be relative to workspace folder or absolute path.
This is how the extension uses them:
diff --git a/docs_espressif/zh_CN/commands.rst b/docs_espressif/zh_CN/commands.rst
index 2dd9b03d3..1895a177c 100644
--- a/docs_espressif/zh_CN/commands.rst
+++ b/docs_espressif/zh_CN/commands.rst
@@ -136,3 +136,7 @@
- 复制当前项目中的单元测试应用程序,构建当前项目并将单元测试应用程序烧录到连接的设备上。详情请参阅 :ref:`单元测试 `。
* - 单元测试:安装 ESP-IDF PyTest 依赖项
- 安装 ESP-IDF Pytest 依赖项,以便能够执行 ESP-IDF 单元测试。详情请参阅 :ref:`单元测试 `。
+ * - 从 LVGL C 文件加载图像
+ - 从包含 lv_image_dsc_t 结构的 LVGL C 文件中加载并显示图像。此命令允许您在不需要调试会话的情况下查看 LVGL 图像。
+ * - 打开图像查看器
+ - 打开图像查看器面板,用于显示来自调试变量或 LVGL C 文件的图像。此面板提供查看和分析各种格式图像数据的工具。
diff --git a/docs_espressif/zh_CN/debugproject.rst b/docs_espressif/zh_CN/debugproject.rst
index 7056da4f0..6178e20fc 100644
--- a/docs_espressif/zh_CN/debugproject.rst
+++ b/docs_espressif/zh_CN/debugproject.rst
@@ -213,3 +213,171 @@ ESP-IDF 扩展在 ``运行和调试`` 视图中提供了 ``ESP-IDF:外设视
- 配置 **核心转储**:在扩展中使用命令 ``ESP-IDF:SDK 配置编辑器`` 或在终端中使用 ``idf.py menuconfig``,将 **核心转储的数据目标** 设置为 ``UART`` 或 ``FLASH``。
- 配置 **GDB Stub**:在扩展中使用命令 ``ESP-IDF:SDK 配置编辑器`` 或在终端中使用 ``idf.py menuconfig``,将 **紧急处理程序行为** 设置为 ``Invoke GDBStub``。
+
+
+ESP-IDF:图像查看器
+--------------------
+
+ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调试会话期间可视化来自调试变量的二进制图像数据。这对于处理摄像头传感器、显示缓冲区、LVGL 图形、OpenCV 计算机视觉或任何原始图像数据的应用程序特别有用。
+
+**快速访问方法:**
+
+1. **在调试会话中右键点击变量:**
+ - 右键点击任何图像相关变量(``lv_image_dsc_t``、``cv::Mat``、``png_image`` 等)并选择 ``将变量作为图像查看``
+ - 图像查看器会自动检测变量类型并提取相应的图像属性
+
+2. **手动图像查看器:**
+ - 点击 ``查看`` > ``命令面板``,输入 ``ESP-IDF:打开图像查看器``
+ - 输入图像数据变量的名称和大小
+ - 选择适当的图像格式和尺寸
+ - 点击 ``加载图像`` 来可视化数据
+
+**支持的图像格式:**
+
+图像查看器支持全面的图像格式范围:
+
+**RGB 格式:**
+- RGB565、RGB888、RGBA8888、ARGB8888、XRGB8888
+- BGR888、BGRA8888、ABGR8888、XBGR8888
+- RGB332、RGB444、RGB555、RGB666、RGB777
+- RGB101010、RGB121212、RGB161616
+
+**其他格式:**
+- 灰度图(每像素 8 位)
+- YUV420、YUV422、YUV444(各种 YUV 格式)
+
+**内置支持:**
+
+**LVGL 图像描述符 (lv_image_dsc_t):**
+- 自动从 LVGL 结构中提取格式、尺寸和数据
+- 支持所有 LVGL 颜色格式,并自动映射到显示格式
+
+**OpenCV Mat (cv::Mat):**
+- 自动从 OpenCV Mat 对象中提取尺寸、格式和数据
+- 支持 BGR888、BGRA8888 和灰度格式
+
+**使用示例:**
+
+**LVGL 图像示例:**
+
+.. code-block:: C
+
+ // LVGL 图像描述符
+ lv_image_dsc_t my_image = {
+ .header = {
+ .cf = LV_COLOR_FORMAT_RGB888, // 颜色格式
+ .w = 320, // 宽度
+ .h = 240 // 高度
+ },
+ .data_size = 320 * 240 * 3, // 数据大小(字节)
+ .data = image_data // 指向图像数据的指针
+ };
+
+在调试过程中,右键点击 ``my_image`` 并选择 ``将变量作为图像查看``。图像查看器会自动检测其为 LVGL 图像并提取格式、尺寸和数据。
+
+**OpenCV Mat 示例:**
+
+.. code-block:: C
+
+ cv::Mat image(240, 320, CV_8UC3); // 320x240 BGR888 图像
+ // ... 填充图像数据 ...
+
+在调试过程中,右键点击 ``image`` 并选择 ``将变量作为图像查看``。图像查看器会自动检测其为 OpenCV Mat 并提取尺寸、格式和数据。
+
+**手动原始数据示例:**
+
+.. code-block:: C
+
+ uint8_t image_buffer[320 * 240 * 3]; // RGB888 格式,320x240 像素
+ size_t image_size = sizeof(image_buffer);
+
+手动使用时:
+- 输入 ``image_buffer`` 作为变量名
+- 输入 ``image_size`` 或 ``230400``(320 * 240 * 3)作为大小
+- 选择 ``RGB888`` 格式
+- 将宽度设置为 ``320``,高度设置为 ``240``
+
+**自定义图像格式配置:**
+
+你可以通过创建 JSON 配置文件并设置 ``idf.imageViewerConfigs`` 配置选项来扩展图像查看器以支持自定义图像格式。
+
+**示例自定义配置:**
+
+.. code-block:: JSON
+
+ [
+ {
+ "name": "自定义图像结构",
+ "typePattern": "my_image_t",
+ "width": {
+ "type": "string",
+ "isChild": true,
+ "value": "w"
+ },
+ "height": {
+ "type": "string",
+ "isChild": true,
+ "value": "h"
+ },
+ "format": {
+ "type": "number",
+ "isChild": false,
+ "value": "0x0E"
+ },
+ "dataAddress": {
+ "type": "string",
+ "isChild": true,
+ "value": "pixels"
+ },
+ "dataSize": {
+ "type": "formula",
+ "isChild": false,
+ "value": "$var.w * $var.h * 3"
+ },
+ "imageFormats": {
+ "14": "rgb888",
+ "15": "rgba8888"
+ }
+ }
+ ]
+
+**配置选项:**
+
+- **typePattern**:匹配右键点击"将变量作为图像查看"时选中变量的 GDB 类型的正则表达式模式(例如 ``"my_image_t"``、``"lv_image_dsc_t"``、``"cv::Mat|Mat"``)
+- **width/height**:提取图像尺寸的配置
+- **format**:提取图像格式的配置
+- **dataAddress**:提取图像数据指针的配置
+- **dataSize**:计算图像数据大小的配置(支持公式)
+- **imageFormats**:数字格式值到显示格式字符串的映射
+
+**字段配置详情:**
+
+每个字段(width、height、format、dataAddress、dataSize)具有以下属性:
+
+- **type**:指定字段值的数据类型:
+ - ``"string"``:值是字符串(字段名或表达式)
+ - ``"number"``:值是数字常量(例如 ``"0x0E"``、``"14"``)
+ - ``"formula"``:值是数学公式(仅用于 dataSize 字段)
+
+- **isChild**:确定如何解释字段值:
+ - ``true``:值表示右键点击变量的子字段(例如 ``"header.w"``、``"data"``)
+ - ``false``:值是可由 GDB 直接评估的表达式或常量
+
+- **value**:用于提取的实际字段名、表达式或常量
+
+**重要配置说明:**
+
+- **dataSize 公式**:在 ``dataSize`` 字段中使用公式时,字符串 ``$var`` 会在你右键点击并选择"将变量作为图像查看"时自动替换为实际的变量名。例如,如果你的变量名为 ``my_image``,公式为 ``$var.w * $var.h * 3``,它将被计算为 ``my_image.w * my_image.h * 3``。**注意**:公式必须是有效的 GDB 表达式,因为它由 GDB 本身计算。
+
+- **格式数字映射**:``imageFormats`` 对象中的数字键必须与 ``format`` 字段从你的图像结构中提取的实际数字值匹配。例如,如果你的图像结构的格式字段包含值 ``14``,那么 ``imageFormats`` 对象应该有一个键 ``"14"``,它映射到适当的显示格式字符串,如 ``"rgb888"``。
+
+**重要说明:**
+- **自动检测**:图像查看器自动检测支持的图像类型并提取属性
+- **统一界面**:单个 ``将变量作为图像查看`` 命令适用于所有支持的格式
+- **格式验证**:所有格式都根据支持的显示格式进行验证
+- **原始数据**:图像查看器支持原始像素格式。不支持压缩格式(JPEG、PNG 等)
+- **大小指定**:手动使用时,必须指定图像数据数组的正确大小
+- **变量大小**:大小可以作为数字(字节)提供,或作为包含大小的另一个变量的名称
+- **指针变量**:对于指针变量,请确保提供实际数据大小,而不是指针大小
+- **自动尺寸估算**:图像查看器会根据数据大小和所选格式自动估算尺寸,但你可以手动调整以获得更好的结果
+- **可扩展性**:可以通过配置文件添加自定义图像格式
diff --git a/docs_espressif/zh_CN/settings.rst b/docs_espressif/zh_CN/settings.rst
index 989ea1bfb..48ea8eacb 100644
--- a/docs_espressif/zh_CN/settings.rst
+++ b/docs_espressif/zh_CN/settings.rst
@@ -116,6 +116,8 @@ ESP-IDF 相关设置
- SVD 文件的绝对路径,用于解析芯片在调试器中的外设树视图
* - **idf.jtagFlashCommandExtraArgs**
- OpenOCD JTAG 闪存额外参数。默认值为 ["verify", "reset"]
+ * - **idf.imageViewerConfigs**
+ - 图像查看器功能的自定义图像格式配置 JSON 文件路径。可以是相对于工作区文件夹的相对路径或绝对路径。
扩展将按照以下方式使用上述设置:
diff --git a/package.json b/package.json
index 44b3b7319..79092f5b5 100644
--- a/package.json
+++ b/package.json
@@ -109,6 +109,7 @@
"onView:idfPartitionExplorer",
"onView:espRainmaker",
"onView:idfComponents",
+ "onCommand:espIdf.openImageViewer",
"workspaceContains:**/CMakeLists.txt"
],
"main": "./dist/extension",
@@ -542,6 +543,11 @@
"command": "espIdf.viewAsHex",
"when": "inDebugMode && debugType == 'gdbtarget' && debugState == stopped",
"group": "navigation"
+ },
+ {
+ "command": "espIdf.viewVariableAsImage",
+ "when": "inDebugMode && debugType == 'gdbtarget' && debugState == stopped",
+ "group": "navigation"
}
]
},
@@ -1295,6 +1301,12 @@
"default": 60,
"scope": "resource",
"description": "%param.serialPortDetectionTimeout%"
+ },
+ "idf.imageViewerConfigs": {
+ "type": "string",
+ "description": "%param.imageViewerConfigs.title%",
+ "scope": "resource",
+ "default": ""
}
}
}
@@ -1816,6 +1828,21 @@
"title": "%espIdf.viewAsHex.title%",
"category": "ESP-IDF"
},
+ {
+ "command": "espIdf.viewVariableAsImage",
+ "title": "%espIdf.viewVariableAsImage.title%",
+ "category": "ESP-IDF"
+ },
+ {
+ "command": "espIdf.openImageViewer",
+ "title": "%espIdf.openImageViewer.title%",
+ "category": "ESP-IDF"
+ },
+ {
+ "command": "espIdf.loadImageFromFile",
+ "title": "%espIdf.loadImageFromFile.title%",
+ "category": "ESP-IDF"
+ },
{
"command": "espIdf.hexView.copyValue",
"title": "%espIdf.hexView.copyValue.title%",
diff --git a/package.nls.es.json b/package.nls.es.json
index 11ff94d0c..38b66745b 100644
--- a/package.nls.es.json
+++ b/package.nls.es.json
@@ -98,6 +98,9 @@
"espIdf.webview.nvsPartitionEditor.title": "Abrir Editor de Partición NVS",
"espIdf.welcome.title": "Bienvenido",
"espIdf.viewAsHex.title": "Ver como Hexadecimal",
+ "espIdf.viewVariableAsImage.title": "Ver variable como imagen",
+ "espIdf.openImageViewer.title": "Abrir Visualizador de Imágenes",
+ "espIdf.loadImageFromFile.title": "Cargar Imagen desde Archivo C LVGL",
"espIdf.hexView.copyValue.title": "Copiar valor al portapapeles",
"espIdf.hexView.deleteElement.title": "Eliminar valor hexadecimal de la lista",
"esp_idf.appOffset.description": "Anular la dirección de inicio del programa de compilación (ESP32_APP_FLASH_OFF)",
@@ -188,13 +191,14 @@
"param.unitTestFilePattern.title": "Patrón glob para descubrir archivos de prueba unitaria",
"param.pyTestEmbeddedServices.title": "Lista de servicios integrados para la ejecución de pytest",
"param.serialPortDetectionTimeout": "Tiempo de espera en segundos para la detección del puerto serie usando esptool.py",
+ "param.imageViewerConfigs.title": "Ruta al archivo JSON de configuraciones de formato de imagen personalizadas",
"trace.poll_period.description": "poll_period se establecerá para el rastreo de la aplicación",
"trace.skip_size.description": "skip_size se establecerá para el rastreo de la aplicación",
"trace.stop_tmo.description": "stop_tmo se establecerá para el rastreo de la aplicación",
"trace.trace_size.description": "trace_size se establecerá para el rastreo de la aplicación",
"trace.wait4halt.description": "wait4halt se establecerá para el rastreo de la aplicación",
"view.components.name": "Componentes del proyecto",
- "view.debug.peripheral": "Visor periférico ESP-IDF",
+ "view.debug.peripheral": "Visualizador periférico ESP-IDF",
"view.debug.hexView": "ESP-IDF: Vista Hexadecimal",
"view.idf.espEFuseExplorer": "Explorador de eFuse",
"view.idf.espRainmaker": "RainMaker",
diff --git a/package.nls.json b/package.nls.json
index 9cb08f006..4bc0feb7a 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -96,6 +96,9 @@
"espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Flash Unit Test App",
"espIdf.unitTest.installPyTest.title": "Unit Test: Install ESP-IDF PyTest Requirements",
"espIdf.viewAsHex.title": "View as Hex",
+ "espIdf.viewVariableAsImage.title": "View Variable as Image",
+ "espIdf.openImageViewer.title": "Open Image Viewer",
+ "espIdf.loadImageFromFile.title": "Load Image from LVGL C File",
"espIdf.hexView.copyValue.title": "Copy value to clipboard",
"espIdf.hexView.deleteElement.title": "Delete hex value from list",
"espIdf.webview.nvsPartitionEditor.title": "Open NVS Partition Editor",
@@ -189,6 +192,7 @@
"param.unitTestFilePattern.title": "Glob pattern for unit test files to discover",
"param.pyTestEmbeddedServices.title": "List of embedded services for pytest execution",
"param.serialPortDetectionTimeout": "Timeout in seconds for serial port detection using esptool.py",
+ "param.imageViewerConfigs.title": "Path to custom image format configurations JSON file",
"trace.poll_period.description": "poll_period will be set for the apptrace",
"trace.skip_size.description": "skip_size will be set for the apptrace",
"trace.stop_tmo.description": "stop_tmo will be set for the apptrace",
diff --git a/package.nls.pt.json b/package.nls.pt.json
index 7c4d82d29..22a3fb8b1 100644
--- a/package.nls.pt.json
+++ b/package.nls.pt.json
@@ -98,6 +98,9 @@
"espIdf.webview.nvsPartitionEditor.title": "Abra o Editor de Partição NVS",
"espIdf.welcome.title": "Bem-vindo",
"espIdf.viewAsHex.title": "Ver como Hexadecimal",
+ "espIdf.viewVariableAsImage.title": "Ver variável como imagem",
+ "espIdf.openImageViewer.title": "Abrir Visualizador de Imagens",
+ "espIdf.loadImageFromFile.title": "Carregar Imagem do Arquivo C LVGL",
"espIdf.hexView.copyValue.title": "Copiar valor para a área de transferência",
"espIdf.hexView.deleteElement.title": "Excluir valor hexadecimal da lista",
"esp_idf.appOffset.description": "Substituir o deslocamento do endereço inicial do programa de construção (ESP32_APP_FLASH_OFF)",
@@ -187,6 +190,7 @@
"param.unitTestFilePattern.title": "Padrão glob para descobrir arquivos de teste unitário",
"param.pyTestEmbeddedServices.title": "Lista de serviços incorporados para execução do pytest",
"param.serialPortDetectionTimeout": "Tempo limite em segundos para detecção de porta serial usando esptool.py",
+ "param.imageViewerConfigs.title": "Caminho para arquivo JSON de configurações de formato de imagem personalizadas",
"trace.poll_period.description": "poll_period será definido para o apptrace",
"trace.skip_size.description": "skip_size será definido para o apptrace",
"trace.stop_tmo.description": "stop_tmo será definido para o apptrace",
diff --git a/package.nls.ru.json b/package.nls.ru.json
index eacf7e65f..ccdf5e6e7 100644
--- a/package.nls.ru.json
+++ b/package.nls.ru.json
@@ -96,6 +96,9 @@
"espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Прошивка Unit Test App",
"espIdf.unitTest.installPyTest.title": "Unit Test: Установка требований ESP-IDF PyTest.",
"espIdf.viewAsHex.title": "Просмотреть как шестнадцатеричное",
+ "espIdf.viewVariableAsImage.title": "Просмотреть переменную как изображение",
+ "espIdf.openImageViewer.title": "Открыть просмотрщик изображений",
+ "espIdf.loadImageFromFile.title": "Загрузить изображение из LVGL C файла",
"espIdf.hexView.copyValue.title": "Скопировать значение в буфер обмена",
"espIdf.hexView.deleteElement.title": "Удалить шестнадцатеричное значение из списка",
"espIdf.webview.nvsPartitionEditor.title": "Открыть редактор разделов NVS",
@@ -188,6 +191,7 @@
"param.unitTestFilePattern.title": "Шаблон glob для обнаружения файлов модульных тестов",
"param.pyTestEmbeddedServices.title": "Список встроенных сервисов для выполнения pytest",
"param.serialPortDetectionTimeout": "Тайм-аут в секундах для обнаружения последовательного порта с помощью esptool.py",
+ "param.imageViewerConfigs.title": "Путь к JSON-файлу пользовательских конфигураций формата изображения",
"trace.poll_period.description": "для apptrace будет установлен параметр poll_ period",
"trace.skip_size.description": "для apptrace будет установлен параметр skip_size",
"trace.stop_tmo.description": "для apptrace будет установлен параметр stop_tmo",
diff --git a/package.nls.zh-CN.json b/package.nls.zh-CN.json
index c7da2ed3e..86a1f69a8 100644
--- a/package.nls.zh-CN.json
+++ b/package.nls.zh-CN.json
@@ -96,6 +96,9 @@
"espIdf.unitTest.flashUnitTestApp.title": "单元测试:烧录单元测试应用程序",
"espIdf.unitTest.installPyTest.title": "单元测试:安装 ESP-IDF PyTest 依赖项",
"espIdf.viewAsHex.title": "以十六进制查看",
+ "espIdf.viewVariableAsImage.title": "以图像查看变量",
+ "espIdf.openImageViewer.title": "打开图像查看器",
+ "espIdf.loadImageFromFile.title": "从 LVGL C 文件加载图像",
"espIdf.hexView.copyValue.title": "复制值到剪贴板",
"espIdf.hexView.deleteElement.title": "从列表中删除十六进制值",
"espIdf.webview.nvsPartitionEditor.title": "打开 NVS 分区编辑器",
@@ -189,6 +192,7 @@
"param.unitTestFilePattern.title": "用于发现单元测试文件的 glob 模式",
"param.pyTestEmbeddedServices.title": "pytest 执行的内嵌服务列表",
"param.serialPortDetectionTimeout": "使用 esptool.py 检测串口时的超时时间(秒)",
+ "param.imageViewerConfigs.title": "自定义图像格式配置 JSON 文件的路径",
"trace.poll_period.description": "设置 apptrace 的 poll_period 参数",
"trace.skip_size.description": "设置 apptrace 的 skip_size 参数",
"trace.stop_tmo.description": "设置 apptrace 的 stop_tmo 参数",
diff --git a/src/cdtDebugAdapter/imageViewPanel.ts b/src/cdtDebugAdapter/imageViewPanel.ts
new file mode 100644
index 000000000..344e3596b
--- /dev/null
+++ b/src/cdtDebugAdapter/imageViewPanel.ts
@@ -0,0 +1,1282 @@
+/*
+ * Project: ESP-IDF VSCode Extension
+ * File Created: Wednesday, 23rd April 2025 5:52:06 pm
+ * Copyright 2025 Espressif Systems (Shanghai) CO LTD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as vscode from "vscode";
+import * as path from "path";
+import * as fs from "fs";
+import { readParameter } from "../idfConfiguration";
+import { Logger } from "../logger/logger";
+import { ESP } from "../config";
+import { workspace } from "vscode";
+
+export interface ImageElement {
+ name: string;
+ data: Uint8Array;
+}
+
+export interface ImageWithDimensionsElement {
+ name: string;
+ data: Uint8Array;
+ dataSize?: number;
+ dataAddress?: string;
+ width: number;
+ height: number;
+ format: number;
+}
+
+// Simplified JSON Configuration interfaces
+export interface ImageFormatConfig {
+ name: string;
+ typePattern: string; // Regex pattern to match variable type
+ width: FieldConfig;
+ height: FieldConfig;
+ format: FieldConfig;
+ dataAddress: FieldConfig;
+ dataSize: DataSizeConfig;
+ imageFormats?: { [key: number]: string }; // Format number to dropdown string mapping
+}
+
+export interface FieldConfig {
+ type: "number" | "string";
+ isChild: boolean;
+ value: string; // Field name or direct value
+}
+
+export interface DataSizeConfig {
+ type: "number" | "string" | "formula";
+ isChild: boolean;
+ value: string; // Field name, direct value, or formula
+}
+
+export class ImageViewPanel {
+ private static instance: ImageViewPanel;
+ private readonly panel: vscode.WebviewPanel;
+ private readonly extensionPath: string;
+ private disposables: vscode.Disposable[] = [];
+ private imageFormatConfigs: ImageFormatConfig[] = [];
+
+ // Valid format strings that match the frontend dropdown
+ private static readonly VALID_FORMATS = [
+ "rgb565",
+ "rgb888",
+ "rgba8888",
+ "argb8888",
+ "xrgb8888",
+ "bgr888",
+ "bgra8888",
+ "abgr8888",
+ "xbgr8888",
+ "rgb332",
+ "rgb444",
+ "rgb555",
+ "rgb666",
+ "rgb777",
+ "rgb101010",
+ "rgb121212",
+ "rgb161616",
+ "grayscale",
+ "yuv420",
+ "yuv422",
+ "yuv444",
+ ];
+
+ public static show(extensionPath: string) {
+ const column = vscode.window.activeTextEditor
+ ? vscode.window.activeTextEditor.viewColumn
+ : undefined;
+
+ if (ImageViewPanel.instance) {
+ ImageViewPanel.instance.panel.reveal(column);
+ return;
+ }
+
+ const panel = vscode.window.createWebviewPanel(
+ "espIdf.imageView",
+ "Image Viewer",
+ column || vscode.ViewColumn.One,
+ {
+ enableScripts: true,
+ retainContextWhenHidden: true,
+ localResourceRoots: [
+ vscode.Uri.file(path.join(extensionPath, "dist", "views")),
+ ],
+ }
+ );
+
+ ImageViewPanel.instance = new ImageViewPanel(panel, extensionPath);
+ }
+
+ public static handleVariableAsImage(debugContext: {
+ container: {
+ expensive: boolean;
+ name: string;
+ variablesReference: number;
+ };
+ sessionId: string;
+ variable: {
+ evaluateName: string;
+ memoryReference: string;
+ name: string;
+ value: string;
+ variablesReference: number;
+ type: string;
+ };
+ }) {
+ if (ImageViewPanel.instance) {
+ // Use the new configuration-based extraction with automatic type detection
+ ImageViewPanel.instance.handleExtractImageWithConfig(
+ debugContext.variable
+ );
+ }
+ }
+
+ public static async loadImageFromFile(
+ extensionPath: string,
+ filePath: string
+ ) {
+ try {
+ // Show the ImageViewPanel
+ ImageViewPanel.show(extensionPath);
+
+ if (ImageViewPanel.instance) {
+ await ImageViewPanel.instance.parseLvglImageFromFile(filePath);
+ }
+ } catch (error) {
+ Logger.error(
+ "Failed to load image from file:",
+ error,
+ "ImageViewPanel loadImageFromFile"
+ );
+ if (ImageViewPanel.instance) {
+ ImageViewPanel.instance.panel.webview.postMessage({
+ command: "showError",
+ error: `Failed to load image from LVGL cfile: ${error}`,
+ });
+ }
+ }
+ }
+
+ private constructor(panel: vscode.WebviewPanel, extensionPath: string) {
+ this.panel = panel;
+ this.extensionPath = extensionPath;
+
+ this.panel.iconPath = vscode.Uri.file(
+ path.join(extensionPath, "media", "espressif_icon.png")
+ );
+
+ this.panel.webview.html = this.getHtmlContent(this.panel.webview);
+
+ this.panel.onDidDispose(() => this.dispose(), null, this.disposables);
+
+ this.panel.webview.onDidReceiveMessage(
+ (message) => {
+ switch (message.command) {
+ case "loadImageFromVariable":
+ this.handleLoadImageFromVariable(
+ message.variableName,
+ message.size
+ );
+ break;
+ default:
+ break;
+ }
+ },
+ null,
+ this.disposables
+ );
+
+ // Initialize with default configurations
+ this.initializeImageFormatConfigs();
+ }
+
+ private initializeImageFormatConfigs() {
+ this.imageFormatConfigs = this.loadImageFormatConfigs();
+ }
+
+ private loadImageFormatConfigs(): ImageFormatConfig[] {
+ // Load default configurations from JSON file
+ const defaultConfigs = this.loadDefaultConfigs();
+
+ // Load user configurations if specified
+ const userConfigs = this.loadUserConfigs();
+
+ // Merge configurations: user configs override default configs based on typePattern
+ return this.mergeConfigurations(defaultConfigs, userConfigs);
+ }
+
+ private loadDefaultConfigs(): ImageFormatConfig[] {
+ try {
+ const defaultConfigPath = path.join(
+ this.extensionPath,
+ "DEFAULT_CONFIGS.json"
+ );
+ const configData = fs.readFileSync(defaultConfigPath, "utf8");
+ const configs = JSON.parse(configData) as ImageFormatConfig[];
+
+ // Convert string keys in imageFormats to numbers (JSON doesn't support numeric keys)
+ return configs.map((config) => ({
+ ...config,
+ imageFormats: config.imageFormats
+ ? this.convertImageFormatsKeys(config.imageFormats)
+ : undefined,
+ }));
+ } catch (error) {
+ Logger.error(
+ "Failed to load default image format configurations:",
+ error,
+ "ImageViewPanel loadDefaultConfigs"
+ );
+ return [];
+ }
+ }
+
+ private loadUserConfigs(): ImageFormatConfig[] {
+ try {
+ const userConfigPath = readParameter("idf.imageViewerConfigs");
+ if (!userConfigPath) {
+ return [];
+ }
+
+ // Resolve relative paths relative to workspace folder
+ let workspaceFolderUri = ESP.GlobalConfiguration.store.get(
+ ESP.GlobalConfiguration.SELECTED_WORKSPACE_FOLDER
+ );
+ if (!workspaceFolderUri) {
+ workspaceFolderUri = vscode.workspace.workspaceFolders
+ ? workspace.workspaceFolders[0].uri
+ : undefined;
+ }
+ const resolvedPath = workspaceFolderUri
+ ? path.resolve(workspaceFolderUri.fsPath, userConfigPath)
+ : userConfigPath;
+
+ if (!fs.existsSync(resolvedPath)) {
+ Logger.warn(
+ `User image format configuration file not found: ${resolvedPath}`
+ );
+ return [];
+ }
+
+ const configData = fs.readFileSync(resolvedPath, "utf8");
+ const configs = JSON.parse(configData) as ImageFormatConfig[];
+
+ // Convert string keys in imageFormats to numbers
+ return configs.map((config) => ({
+ ...config,
+ imageFormats: config.imageFormats
+ ? this.convertImageFormatsKeys(config.imageFormats)
+ : undefined,
+ }));
+ } catch (error) {
+ Logger.error(
+ "Failed to load user image format configurations:",
+ error,
+ "ImageViewPanel loadUserConfigs"
+ );
+ return [];
+ }
+ }
+
+ private convertImageFormatsKeys(imageFormats: {
+ [key: string]: string;
+ }): { [key: number]: string } {
+ const converted: { [key: number]: string } = {};
+ for (const [key, value] of Object.entries(imageFormats)) {
+ const numericKey = parseInt(key, 10);
+ if (!isNaN(numericKey)) {
+ converted[numericKey] = value;
+ }
+ }
+ return converted;
+ }
+
+ private mergeConfigurations(
+ defaultConfigs: ImageFormatConfig[],
+ userConfigs: ImageFormatConfig[]
+ ): ImageFormatConfig[] {
+ const merged = [...defaultConfigs];
+
+ for (const userConfig of userConfigs) {
+ const existingIndex = merged.findIndex(
+ (config) => config.typePattern === userConfig.typePattern
+ );
+ if (existingIndex >= 0) {
+ // Override existing configuration
+ merged[existingIndex] = userConfig;
+ } else {
+ // Add new configuration
+ merged.push(userConfig);
+ }
+ }
+
+ return merged;
+ }
+
+ private static isValidFormat(format: string): boolean {
+ return ImageViewPanel.VALID_FORMATS.includes(format);
+ }
+
+ private static validateAndGetFormat(
+ rawFormat: number | string,
+ imageFormats?: { [key: number]: string },
+ configName?: string
+ ): string {
+ // Handle numeric formats using imageFormats mapping
+ if (typeof rawFormat === "number" && imageFormats) {
+ const mappedFormat = imageFormats[rawFormat];
+ if (mappedFormat && ImageViewPanel.isValidFormat(mappedFormat)) {
+ return mappedFormat;
+ } else {
+ throw new Error(
+ `Invalid format '${mappedFormat}' from backend mapping for format value ${rawFormat}. ` +
+ `Please check the imageFormats configuration for ${
+ configName || "unknown config"
+ }.`
+ );
+ }
+ }
+
+ // Handle string formats
+ if (typeof rawFormat === "string") {
+ // Direct validation for string formats
+ if (ImageViewPanel.isValidFormat(rawFormat)) {
+ return rawFormat;
+ }
+
+ // Try partial matching for common variations
+ const formatLower = rawFormat.toLowerCase();
+ if (formatLower.includes("rgb565") || formatLower.includes("565"))
+ return "rgb565";
+ if (formatLower.includes("rgb888") || formatLower.includes("888"))
+ return "rgb888";
+ if (formatLower.includes("rgba8888") || formatLower.includes("rgba"))
+ return "rgba8888";
+ if (formatLower.includes("argb8888") || formatLower.includes("argb"))
+ return "argb8888";
+ if (formatLower.includes("xrgb8888") || formatLower.includes("xrgb"))
+ return "xrgb8888";
+ if (formatLower.includes("bgr888") || formatLower.includes("bgr"))
+ return "bgr888";
+ if (formatLower.includes("bgra8888") || formatLower.includes("bgra"))
+ return "bgra8888";
+ if (formatLower.includes("abgr8888") || formatLower.includes("abgr"))
+ return "abgr8888";
+ if (formatLower.includes("xbgr8888") || formatLower.includes("xbgr"))
+ return "xbgr8888";
+ if (formatLower.includes("rgb332") || formatLower.includes("332"))
+ return "rgb332";
+ if (formatLower.includes("rgb444") || formatLower.includes("444"))
+ return "rgb444";
+ if (formatLower.includes("rgb555") || formatLower.includes("555"))
+ return "rgb555";
+ if (formatLower.includes("rgb666") || formatLower.includes("666"))
+ return "rgb666";
+ if (formatLower.includes("rgb777") || formatLower.includes("777"))
+ return "rgb777";
+ if (formatLower.includes("rgb101010") || formatLower.includes("101010"))
+ return "rgb101010";
+ if (formatLower.includes("rgb121212") || formatLower.includes("121212"))
+ return "rgb121212";
+ if (formatLower.includes("rgb161616") || formatLower.includes("161616"))
+ return "rgb161616";
+ if (
+ formatLower.includes("grayscale") ||
+ formatLower.includes("gray") ||
+ formatLower.includes("mono")
+ )
+ return "grayscale";
+ if (formatLower.includes("yuv420") || formatLower.includes("420"))
+ return "yuv420";
+ if (formatLower.includes("yuv422") || formatLower.includes("422"))
+ return "yuv422";
+ if (formatLower.includes("yuv444") || formatLower.includes("444"))
+ return "yuv444";
+
+ // If no match found, throw error
+ throw new Error(
+ `Invalid format string '${rawFormat}'. Valid formats are: ${ImageViewPanel.VALID_FORMATS.join(
+ ", "
+ )}`
+ );
+ }
+
+ // Fallback for unknown format types
+ return "rgb888";
+ }
+
+ private findMatchingConfig(variableType: string): ImageFormatConfig | null {
+ return (
+ this.imageFormatConfigs.find((config) => {
+ const regex = new RegExp(config.typePattern, "i");
+ return regex.test(variableType);
+ }) || null
+ );
+ }
+
+ private async extractFieldValue(
+ session: vscode.DebugSession,
+ variablesReference: number,
+ fieldConfig: FieldConfig,
+ frameId: number,
+ extractAsAddress: boolean = false
+ ): Promise {
+ if (fieldConfig.type === "number") {
+ // Direct number value - handle both decimal and hex
+ const value = fieldConfig.value;
+ if (value.startsWith("0x") || value.startsWith("0X")) {
+ // Hex number
+ return parseInt(value, 16);
+ } else {
+ // Decimal number
+ return parseInt(value, 10);
+ }
+ }
+
+ if (fieldConfig.type === "string") {
+ if (fieldConfig.isChild) {
+ // Navigate through child variables
+ return await this.extractChildValue(
+ session,
+ variablesReference,
+ fieldConfig.value,
+ extractAsAddress
+ );
+ } else {
+ // Evaluate as expression
+ const evaluateResponse = await session.customRequest("evaluate", {
+ expression: fieldConfig.value,
+ frameId,
+ });
+ if (evaluateResponse && evaluateResponse.result) {
+ if (extractAsAddress) {
+ const match = evaluateResponse.result.match(/0x[0-9a-fA-F]+/);
+ if (match) {
+ return match[0];
+ }
+ throw new Error(
+ `Could not extract address from: ${evaluateResponse.result}`
+ );
+ } else {
+ return parseInt(evaluateResponse.result, 10);
+ }
+ }
+ throw new Error(`Could not evaluate expression: ${fieldConfig.value}`);
+ }
+ }
+
+ throw new Error(`Unsupported field type: ${fieldConfig.type}`);
+ }
+
+ private async extractChildValue(
+ session: vscode.DebugSession,
+ variablesReference: number,
+ fieldPath: string,
+ extractAsAddress: boolean = false
+ ): Promise {
+ const pathParts = fieldPath.split(".");
+ let currentRef = variablesReference;
+
+ // Navigate through the path
+ for (let i = 0; i < pathParts.length; i++) {
+ const part = pathParts[i];
+
+ const response = await session.customRequest("variables", {
+ variablesReference: currentRef,
+ });
+
+ const variables = response.variables || [];
+ const variable = variables.find((v: any) => v.name === part);
+
+ if (!variable) {
+ throw new Error(`Property '${part}' not found in path '${fieldPath}'`);
+ }
+
+ if (i === pathParts.length - 1) {
+ // Last part - extract the value
+ if (extractAsAddress) {
+ const match = variable.value.match(/0x[0-9a-fA-F]+/);
+ if (match) {
+ return match[0];
+ }
+ throw new Error(`Could not extract address from: ${variable.value}`);
+ } else {
+ return parseInt(variable.value, 10);
+ }
+ } else {
+ // Navigate deeper
+ currentRef = variable.variablesReference;
+ }
+ }
+
+ throw new Error(`Could not extract value from path: ${fieldPath}`);
+ }
+
+ private async extractDataSize(
+ session: vscode.DebugSession,
+ variablesReference: number,
+ dataSizeConfig: DataSizeConfig,
+ frameId: number,
+ variableName: string
+ ): Promise {
+ // Handle formula type specially
+ if (dataSizeConfig.type === "formula") {
+ // For formulas, we need to evaluate them in the context of the variable
+ let formula = dataSizeConfig.value;
+
+ // Replace $var with the actual variable name
+ // Example: "$var.rows * $var.step.buf[0]" -> "opencv_image.rows * opencv_image.step.buf[0]"
+ formula = formula.replace(/\$var/g, `${variableName}`);
+
+ // Evaluate the final formula with GDB
+ const evaluateResponse = await session.customRequest("evaluate", {
+ expression: formula,
+ frameId,
+ });
+ if (evaluateResponse && evaluateResponse.result) {
+ return parseInt(evaluateResponse.result, 10);
+ }
+ throw new Error(`Could not evaluate formula: ${formula}`);
+ }
+
+ // For number and string types, use the unified extractFieldValue method
+ // Convert DataSizeConfig to FieldConfig for compatibility
+ const fieldConfig: FieldConfig = {
+ type: dataSizeConfig.type as "number" | "string",
+ isChild: dataSizeConfig.isChild,
+ value: dataSizeConfig.value,
+ };
+
+ return (await this.extractFieldValue(
+ session,
+ variablesReference,
+ fieldConfig,
+ frameId
+ )) as number;
+ }
+
+ private async handleExtractImageWithConfig(variable: {
+ evaluateName: string;
+ memoryReference: string;
+ name: string;
+ value: string;
+ variablesReference: number;
+ type: string;
+ }) {
+ try {
+ const session = vscode.debug.activeDebugSession;
+ if (!session) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: "No active debug session found",
+ });
+ return;
+ }
+
+ // Find matching configuration
+ let config: ImageFormatConfig = this.findMatchingConfig(
+ variable.type || ""
+ );
+
+ if (!config) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `No matching configuration found for variable type: ${variable.type}`,
+ });
+ return;
+ }
+
+ // Get current thread and frame
+ const threads = await session.customRequest("threads");
+ const threadId = threads.threads[0].id;
+
+ const stack = await session.customRequest("stackTrace", {
+ threadId,
+ startFrame: 0,
+ levels: 1,
+ });
+ const frameId = stack.stackFrames[0].id;
+
+ // Extract image properties using the configuration
+ const imageProperties = await this.extractImagePropertiesWithConfig(
+ session,
+ variable.name,
+ variable.variablesReference,
+ config,
+ frameId
+ );
+
+ if (imageProperties) {
+ // Update the panel title and send the data
+ this.panel.title = `Image Viewer: ${variable.name} (${config.name})`;
+ this.sendImageWithDimensionsData(imageProperties, config.name);
+ }
+ } catch (error) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Error extracting image with configuration: ${error}`,
+ });
+ }
+ }
+
+ private async extractImagePropertiesWithConfig(
+ session: vscode.DebugSession,
+ variableName: string,
+ variablesReference: number,
+ config: ImageFormatConfig,
+ frameId: number
+ ): Promise {
+ const imageProperties: ImageWithDimensionsElement = {
+ name: variableName,
+ data: new Uint8Array(),
+ width: 0,
+ height: 0,
+ format: 0,
+ };
+
+ try {
+ // Extract basic properties first
+ imageProperties.width = (await this.extractFieldValue(
+ session,
+ variablesReference,
+ config.width,
+ frameId
+ )) as number;
+
+ imageProperties.height = (await this.extractFieldValue(
+ session,
+ variablesReference,
+ config.height,
+ frameId
+ )) as number;
+
+ const rawFormat = await this.extractFieldValue(
+ session,
+ variablesReference,
+ config.format,
+ frameId
+ );
+
+ // Validate and convert format to final string
+ const validatedFormat = ImageViewPanel.validateAndGetFormat(
+ rawFormat,
+ config.imageFormats,
+ config.name
+ );
+
+ // Store both raw format (for display) and validated format (for processing)
+ imageProperties.format = rawFormat as number; // Keep raw format for display
+ (imageProperties as any).validatedFormat = validatedFormat; // Add validated format
+
+ imageProperties.dataAddress = (await this.extractFieldValue(
+ session,
+ variablesReference,
+ config.dataAddress,
+ frameId,
+ true // extractAsAddress = true
+ )) as string;
+
+ // Extract data size (may use formula)
+ imageProperties.dataSize = await this.extractDataSize(
+ session,
+ variablesReference,
+ config.dataSize,
+ frameId,
+ variableName
+ );
+
+ // Validate required properties
+ if (!imageProperties.dataAddress || !imageProperties.dataSize) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not extract data address or size from variable ${variableName}`,
+ });
+ return null;
+ }
+
+ // Read memory data
+ const readResponse = await session.customRequest("readMemory", {
+ memoryReference: imageProperties.dataAddress,
+ count: imageProperties.dataSize,
+ });
+
+ if (readResponse && readResponse.data) {
+ const binaryData = Buffer.from(readResponse.data, "base64");
+ imageProperties.data = new Uint8Array(binaryData);
+ return imageProperties;
+ } else {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not read memory data for variable ${variableName}`,
+ });
+ return null;
+ }
+ } catch (error) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Error extracting image properties: ${error}`,
+ });
+ return null;
+ }
+ }
+
+ private sendImageData(imageElement: ImageElement) {
+ const base64Data = Buffer.from(imageElement.data).toString("base64");
+ this.panel.webview.postMessage({
+ command: "updateImage",
+ data: base64Data,
+ name: imageElement.name,
+ });
+ }
+
+ private sendImageWithDimensionsData(
+ imageElement: ImageWithDimensionsElement,
+ configName?: string
+ ) {
+ const base64Data = Buffer.from(imageElement.data).toString("base64");
+ this.panel.webview.postMessage({
+ command: "updateImageWithProperties",
+ data: base64Data,
+ dataAddress: imageElement.dataAddress,
+ dataSize: imageElement.dataSize,
+ name: imageElement.name,
+ width: imageElement.width,
+ height: imageElement.height,
+ format: imageElement.format,
+ configName: configName, // Pass the configuration name
+ validatedFormat: (imageElement as any).validatedFormat, // Pass the validated format string
+ });
+ }
+
+ private async handleLoadImageFromVariable(
+ variableName: string,
+ size: string | number
+ ) {
+ try {
+ const session = vscode.debug.activeDebugSession;
+ if (!session) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: "No active debug session found",
+ });
+ return;
+ }
+
+ // Extract memory address from variable
+ let memoryAddress: string | null = null;
+
+ const threads = await session.customRequest("threads");
+ const threadId = threads.threads[0].id;
+
+ const stack = await session.customRequest("stackTrace", {
+ threadId,
+ startFrame: 0,
+ levels: 1,
+ });
+ const frameId = stack.stackFrames[0].id;
+
+ // Try to get the variable value to extract the address
+ const evaluateResponse = await session.customRequest("evaluate", {
+ expression: variableName,
+ frameId,
+ });
+
+ if (evaluateResponse && evaluateResponse.result) {
+ const match = evaluateResponse.result.match(/0x[0-9a-fA-F]+/);
+ if (match) {
+ memoryAddress = match[0];
+ }
+ }
+
+ if (!memoryAddress) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not extract memory address from variable ${variableName}`,
+ });
+ return;
+ }
+
+ // Determine read size
+ let readSize: number;
+ if (typeof size === "number") {
+ readSize = size;
+ } else {
+ // Try to evaluate the size variable
+ const sizeResponse = await session.customRequest("evaluate", {
+ expression: size,
+ frameId,
+ });
+ if (sizeResponse && sizeResponse.result) {
+ readSize = parseInt(sizeResponse.result, 10);
+ if (isNaN(readSize)) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not parse size from variable ${size}`,
+ });
+ return;
+ }
+ } else {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not evaluate size variable ${size}`,
+ });
+ return;
+ }
+ }
+
+ // Read memory data
+ const readResponse = await session.customRequest("readMemory", {
+ memoryReference: memoryAddress,
+ count: readSize,
+ });
+
+ if (readResponse && readResponse.data) {
+ const binaryData = Buffer.from(readResponse.data, "base64");
+ const imageElement: ImageElement = {
+ name: variableName,
+ data: new Uint8Array(binaryData),
+ };
+
+ // Update the panel title and send the data
+ this.panel.title = `Image Viewer: ${variableName}`;
+ this.sendImageData(imageElement);
+ } else {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Could not read memory data for variable ${variableName}`,
+ });
+ }
+ } catch (error) {
+ if (error && error.message && error.message.includes("-var-create")) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Variable ${variableName} not found in the current debug session.`,
+ });
+ return;
+ }
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Error loading image: ${error}`,
+ });
+ }
+ }
+
+ private getHtmlContent(webview: vscode.Webview): string {
+ const scriptPath = webview.asWebviewUri(
+ vscode.Uri.file(
+ path.join(this.extensionPath, "dist", "views", "imageView-bundle.js")
+ )
+ );
+
+ return `
+
+
+
+
+ Image Viewer
+
+
+
+
+
+ `;
+ }
+
+ private async parseLvglImageFromFile(filePath: string) {
+ try {
+ const fileContent = fs.readFileSync(filePath, "utf8");
+ if (!fileContent.includes("lv_image_dsc_t")) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `File does not contain LVGL image data (lv_image_dsc_t). Only LVGL C files are supported.`,
+ });
+ return;
+ }
+ const config = this.imageFormatConfigs.find(
+ (c) => c.typePattern === "lv_image_dsc_t"
+ );
+ if (!config) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `LVGL configuration not found.`,
+ });
+ return;
+ }
+
+ const imageData = this.parseImageDataFromCFile(fileContent, config);
+
+ if (imageData) {
+ this.panel.title = `Image Viewer: ${path.basename(filePath)} (LVGL)`;
+ this.sendImageWithDimensionsData(imageData, config.name);
+ }
+ } catch (error) {
+ this.panel.webview.postMessage({
+ command: "showError",
+ error: `Error parsing LVGL image from file: ${error}`,
+ });
+ }
+ }
+
+ private parseImageDataFromCFile(
+ fileContent: string,
+ config: ImageFormatConfig
+ ): ImageWithDimensionsElement | null {
+ try {
+ const imageData: ImageWithDimensionsElement = {
+ name: "parsed_image",
+ data: new Uint8Array(),
+ width: 0,
+ height: 0,
+ format: 0,
+ };
+
+ imageData.width = this.extractValueFromCFile(fileContent, config.width);
+ imageData.height = this.extractValueFromCFile(fileContent, config.height);
+ imageData.format = this.extractValueFromCFile(fileContent, config.format);
+
+ imageData.dataSize = this.extractValueFromCFile(
+ fileContent,
+ config.dataSize
+ );
+ const dataAddress = this.extractDataAddressFromCFile(fileContent, config);
+ if (!dataAddress) {
+ throw new Error("Could not extract data address from C file");
+ }
+
+ const dataArray = this.extractDataArrayFromCFile(
+ fileContent,
+ dataAddress
+ );
+ if (dataArray) {
+ imageData.data = new Uint8Array(dataArray);
+ } else {
+ throw new Error("Could not extract image data array from C file");
+ }
+
+ const validatedFormat = ImageViewPanel.validateAndGetFormat(
+ imageData.format,
+ config.imageFormats,
+ config.name
+ );
+
+ (imageData as any).validatedFormat = validatedFormat;
+
+ return imageData;
+ } catch (error) {
+ Logger.error(
+ "Error parsing image data from C file:",
+ error,
+ "ImageViewPanel parseImageDataFromCFile"
+ );
+ return null;
+ }
+ }
+
+ private extractValueFromCFile(
+ fileContent: string,
+ fieldConfig: FieldConfig | DataSizeConfig
+ ): number {
+ if (fieldConfig.type === "number") {
+ const value = fieldConfig.value;
+ if (value.startsWith("0x") || value.startsWith("0X")) {
+ return parseInt(value, 16);
+ } else {
+ return parseInt(value, 10);
+ }
+ }
+
+ if (fieldConfig.type === "string") {
+ if (fieldConfig.isChild) {
+ const pathParts = fieldConfig.value.split(".");
+ const structMatch = this.findLvImageStruct(fileContent);
+ if (!structMatch) {
+ throw new Error(
+ `Could not find lv_image_dsc_t struct definition for field: ${fieldConfig.value}`
+ );
+ }
+
+ let currentContent = structMatch[0];
+
+ for (const part of pathParts) {
+ const fieldRegex = new RegExp(`\\.${part}\\s*=\\s*([^,}]+)`, "i");
+ const match = currentContent.match(fieldRegex);
+ if (match) {
+ const valueStr = match[1].trim();
+ if (valueStr.startsWith("0x") || valueStr.startsWith("0X")) {
+ return parseInt(valueStr, 16);
+ } else if (valueStr.startsWith("LV_COLOR_FORMAT_")) {
+ return this.parseLvColorFormat(valueStr);
+ } else if (valueStr.startsWith("sizeof(")) {
+ const arrayMatch = valueStr.match(/sizeof\(([^)]+)\)/);
+ if (arrayMatch) {
+ return this.findArraySize(fileContent, arrayMatch[1]);
+ }
+ } else {
+ // Try to parse as number
+ const numValue = parseInt(valueStr, 10);
+ if (!isNaN(numValue)) {
+ return numValue;
+ }
+ }
+ }
+ }
+
+ throw new Error(`Could not find field: ${fieldConfig.value}`);
+ } else {
+ throw new Error(
+ "Direct expression evaluation not supported for file parsing"
+ );
+ }
+ }
+
+ if (fieldConfig.type === "formula") {
+ throw new Error(
+ "Formula-based data size supported for file parsing"
+ );
+ }
+
+ throw new Error(`Unsupported field type: ${fieldConfig.type}`);
+ }
+
+ private findLvImageStruct(fileContent: string): RegExpMatchArray | null {
+ // Look for lv_image_dsc_t struct definition with more specific pattern
+ const patterns = [
+ // Pattern 1: const lv_image_dsc_t name = { ... };
+ /const\s+lv_image_dsc_t\s+\w+\s*=\s*\{([^}]+)\}/s,
+ // Pattern 2: lv_image_dsc_t name = { ... };
+ /lv_image_dsc_t\s+\w+\s*=\s*\{([^}]+)\}/s,
+ // Pattern 3: const struct with lv_image_dsc_t
+ /const\s+.*lv_image_dsc_t.*=\s*\{([^}]+)\}/s,
+ ];
+
+ for (const pattern of patterns) {
+ const match = fileContent.match(pattern);
+ if (match) {
+ return match;
+ }
+ }
+
+ return null;
+ }
+
+ private parseLvColorFormat(formatStr: string): number {
+ const formatMap: { [key: string]: number } = {
+ LV_COLOR_FORMAT_NATIVE_WITH_ALPHA: 15, // Same as ARGB8888
+ LV_COLOR_FORMAT_NATIVE: 14, // Same as RGB888
+ LV_COLOR_FORMAT_RGB565: 9,
+ LV_COLOR_FORMAT_RGB888: 14,
+ LV_COLOR_FORMAT_ARGB8888: 15,
+ LV_COLOR_FORMAT_BGRA8888: 16,
+ LV_COLOR_FORMAT_YUV420: 18,
+ LV_COLOR_FORMAT_YUV422: 20,
+ LV_COLOR_FORMAT_YUV444: 21,
+ LV_COLOR_FORMAT_GRAYSCALE: 0,
+ };
+
+ const result = formatMap[formatStr] || 0;
+ Logger.info(
+ `Parsed LVGL color format: ${formatStr} -> ${result}`,
+ "ImageViewPanel parseLvColorFormat"
+ );
+ return result;
+ }
+
+ private findArraySize(fileContent: string, arrayName: string): number {
+ const arrayPattern = new RegExp(
+ `const\\s+.*\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ );
+ const match = fileContent.match(arrayPattern);
+
+ if (match) {
+ const arrayContent = match[1];
+ const items = arrayContent.split(",");
+ return items.filter((item) => item.trim()).length;
+ }
+
+ return 0;
+ }
+
+ private extractDataAddressFromCFile(
+ fileContent: string,
+ config: ImageFormatConfig
+ ): string | null {
+ try {
+ // Find the lv_image_dsc_t struct first
+ const structMatch = this.findLvImageStruct(fileContent);
+ if (!structMatch) {
+ return null;
+ }
+
+ const structContent = structMatch[0];
+
+ // Look for the data field assignment
+ const dataFieldMatch = structContent.match(/\.data\s*=\s*(\w+)/);
+ if (dataFieldMatch) {
+ return dataFieldMatch[1];
+ }
+
+ return null;
+ } catch (error) {
+ Logger.error(
+ "Error extracting data address from C file:",
+ error,
+ "ImageViewPanel extractDataAddressFromCFile"
+ );
+ return null;
+ }
+ }
+
+ private extractDataArrayFromCFile(
+ fileContent: string,
+ dataAddress: string
+ ): number[] | null {
+ try {
+ const rawData = this.findArrayByName(fileContent, dataAddress);
+ if (!rawData) {
+ return null;
+ }
+ return this.correctEndianness(rawData);
+ } catch (error) {
+ Logger.error(
+ "Error extracting data array from C file:",
+ error,
+ "ImageViewPanel extractDataArrayFromCFile"
+ );
+ return null;
+ }
+ }
+
+ private findArrayByName(
+ fileContent: string,
+ arrayName: string
+ ): number[] | null {
+ // Look for the specific array by name with various patterns
+ const arrayPatterns = [
+ // Pattern 1: const uint8_t arrayName[] = { ... };
+ new RegExp(
+ `const\\s+uint8_t\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ ),
+ // Pattern 2: const unsigned char arrayName[] = { ... };
+ new RegExp(
+ `const\\s+unsigned\\s+char\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ ),
+ // Pattern 3: const char arrayName[] = { ... };
+ new RegExp(
+ `const\\s+char\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ ),
+ // Pattern 4: uint8_t arrayName[] = { ... };
+ new RegExp(
+ `uint8_t\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ ),
+ // Pattern 5: unsigned char arrayName[] = { ... };
+ new RegExp(
+ `unsigned\\s+char\\s+${arrayName}\\s*\\[\\]\\s*=\\s*\\{([^}]+)\\}`,
+ "s"
+ ),
+ ];
+
+ for (const pattern of arrayPatterns) {
+ const match = fileContent.match(pattern);
+ if (match) {
+ const arrayContent = match[1];
+ return this.parseArrayContent(arrayContent);
+ }
+ }
+
+ return null;
+ }
+
+ private parseArrayContent(arrayContent: string): number[] | null {
+ try {
+ const values: number[] = [];
+ const cleanedContent = arrayContent.replace(/\s+/g, " ").trim();
+ const items = cleanedContent.split(",");
+ for (const item of items) {
+ const trimmed = item.trim();
+ if (trimmed && trimmed !== "") {
+ let value: number;
+ if (trimmed.startsWith("0x") || trimmed.startsWith("0X")) {
+ value = parseInt(trimmed, 16);
+ } else if (
+ trimmed.startsWith("0") &&
+ trimmed.length > 1 &&
+ !trimmed.startsWith("0x")
+ ) {
+ value = parseInt(trimmed, 8);
+ } else {
+ value = parseInt(trimmed, 10);
+ }
+
+ if (!isNaN(value) && value >= 0 && value <= 255) {
+ values.push(value);
+ }
+ }
+ }
+
+ return values.length > 0 ? values : null;
+ } catch (error) {
+ Logger.error(
+ "Error parsing array content:",
+ error,
+ "ImageViewPanel parseArrayContent"
+ );
+ return null;
+ }
+ }
+
+ private correctEndianness(rawData: number[]): number[] {
+ try {
+
+ if (rawData.length % 4 !== 0) {
+ return rawData;
+ }
+
+ const correctedData: number[] = [];
+
+ // Process data in groups of 4 bytes (one pixel)
+ for (let i = 0; i < rawData.length; i += 4) {
+ if (i + 3 < rawData.length) {
+ // Swap bytes within each 4-byte pixel
+ // Original: [B0, B1, B2, B3] (little-endian)
+ // Corrected: [B3, B2, B1, B0] (big-endian)
+ correctedData.push(rawData[i + 3]); // Alpha
+ correctedData.push(rawData[i + 2]); // Red
+ correctedData.push(rawData[i + 1]); // Green
+ correctedData.push(rawData[i]); // Blue
+ }
+ }
+ return correctedData;
+ } catch (error) {
+ Logger.error(
+ "Error correcting endianness:",
+ error,
+ "ImageViewPanel correctEndianness"
+ );
+ return rawData;
+ }
+ }
+
+ private dispose() {
+ ImageViewPanel.instance = undefined;
+ this.disposables.forEach((d) => d.dispose());
+ }
+}
diff --git a/src/extension.ts b/src/extension.ts
index 685196d89..602d62787 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -184,6 +184,8 @@ import {
HexTreeItem,
HexViewProvider,
} from "./cdtDebugAdapter/hexViewProvider";
+
+import { ImageViewPanel } from "./cdtDebugAdapter/imageViewPanel";
import { configureClangSettings } from "./clang";
import { OpenOCDErrorMonitor } from "./espIdf/hints/openocdhint";
import { updateHintsStatusBarItem } from "./statusBar";
@@ -1527,6 +1529,86 @@ export async function activate(context: vscode.ExtensionContext) {
}
);
+ registerIDFCommand(
+ "espIdf.viewVariableAsImage",
+ (debugContext: {
+ container: {
+ expensive: boolean;
+ name: string;
+ variablesReference: number;
+ };
+ sessionId: string;
+ variable: {
+ evaluateName: string;
+ memoryReference: string;
+ name: string;
+ value: string;
+ variablesReference: number;
+ type: string;
+ };
+ }) => {
+ return PreCheck.perform([openFolderCheck], async () => {
+ if (
+ !debugContext ||
+ !debugContext.variable ||
+ !debugContext.variable.evaluateName
+ ) {
+ return;
+ }
+ if (!vscode.debug.activeDebugSession) {
+ return;
+ }
+
+ try {
+ // Show the ImageViewPanel and pass the variable information
+ ImageViewPanel.show(context.extensionPath);
+
+ // Send the variable information to the ImageViewPanel with automatic type detection
+ ImageViewPanel.handleVariableAsImage(debugContext);
+ } catch (e) {
+ const msg = e && e.message ? e.message : e;
+ Logger.errorNotify(msg, e, "extension espIdf.viewVariableAsImage");
+ }
+ });
+ }
+ );
+
+ registerIDFCommand("espIdf.openImageViewer", () => {
+ return PreCheck.perform([openFolderCheck], () => {
+ // Show the ImageViewPanel without an image
+ ImageViewPanel.show(context.extensionPath);
+ });
+ });
+
+ registerIDFCommand("espIdf.loadImageFromFile", async () => {
+ return PreCheck.perform([openFolderCheck], async () => {
+ try {
+ // Show file picker to select LVGL C file
+ const fileUri = await vscode.window.showOpenDialog({
+ canSelectMany: false,
+ openLabel: "Select LVGL C file with image data",
+ filters: {
+ "C files": ["c", "h"],
+ "All files": ["*"],
+ },
+ });
+
+ if (fileUri && fileUri[0]) {
+ const filePath = fileUri[0].fsPath;
+
+ // Load LVGL image directly (no config selection needed)
+ await ImageViewPanel.loadImageFromFile(
+ context.extensionPath,
+ filePath
+ );
+ }
+ } catch (error) {
+ const msg = error && error.message ? error.message : error;
+ Logger.errorNotify(msg, error, "extension espIdf.loadImageFromFile");
+ }
+ });
+ });
+
registerIDFCommand("espIdf.genCoverage", () => {
return PreCheck.perform([openFolderCheck], async () => {
try {
diff --git a/src/views/image-view/ImageView.vue b/src/views/image-view/ImageView.vue
new file mode 100644
index 000000000..f88de9b00
--- /dev/null
+++ b/src/views/image-view/ImageView.vue
@@ -0,0 +1,1567 @@
+
+
+
+
+
+
+
+
+
{{ imageName }}
+
{{ imageInfo }}
+
+
+
+
+
+
+
+
+
{{ getPropertiesTitle() }}
+
+
+ Dimensions: {{ imageProperties.width }} ×
+ {{ imageProperties.height }}
+
+
+ Format:
+ {{ selectedFormat.toUpperCase() }} ({{ imageProperties.format }})
+
+
+ Data Size: {{ imageProperties.dataSize }} bytes
+
+
+ Data Address: {{ imageProperties.dataAddress }}
+
+
+ Configuration: {{ imageProperties.configName }}
+
+
+
+
+
+
+
+ Format:
+
+ RGB565
+ RGB888
+ RGBA8888
+ ARGB8888
+ XRGB8888
+ BGR888
+ BGRA8888
+ ABGR8888
+ XBGR8888
+ RGB332
+ RGB444
+ RGB555
+ RGB666
+ RGB777
+ RGB101010
+ RGB121212
+ RGB161616
+ Grayscale
+ YUV420
+ YUV422
+ YUV444
+
+
+
+
+
Width:
+
+
Height:
+
+
Update
+
+ 256×250
+ 320×200
+ 512×125
+ 640×100
+
+
+
+
+ {{ error }}
+
+
+
+
+ Debug Information
+
+
Data Preview (first 32 bytes):
+
{{ dataPreview }}
+
Data Statistics:
+
+ Total bytes: {{ imageData ? imageData.length : 0 }}
+ Min value: {{ dataStats.min }}
+ Max value: {{ dataStats.max }}
+ Average: {{ dataStats.avg.toFixed(2) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/image-view/main.ts b/src/views/image-view/main.ts
new file mode 100644
index 000000000..641298e46
--- /dev/null
+++ b/src/views/image-view/main.ts
@@ -0,0 +1,5 @@
+import { createApp } from 'vue';
+import ImageView from './ImageView.vue';
+
+const app = createApp(ImageView);
+app.mount('#app');
\ No newline at end of file
diff --git a/webpack.config.js b/webpack.config.js
index 739df821a..1c8d677b5 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -140,6 +140,13 @@ const webViewConfig = {
"troubleshoot",
"main.ts"
),
+ imageView: path.resolve(
+ __dirname,
+ "src",
+ "views",
+ "image-view",
+ "main.ts"
+ ),
},
output: {
path: path.resolve(__dirname, "dist", "views"),