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