From 5c20d3172a2ac79f2d99f916d43c1b043a17f5ba Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Tue, 12 Aug 2025 13:26:52 +0800 Subject: [PATCH 1/9] add image viewer webview --- docs_espressif/en/debugproject.rst | 42 ++ docs_espressif/zh_CN/debugproject.rst | 42 ++ package.json | 6 + package.nls.es.json | 1 + package.nls.json | 1 + package.nls.pt.json | 1 + package.nls.ru.json | 1 + package.nls.zh-CN.json | 1 + src/cdtDebugAdapter/imageViewPanel.ts | 240 +++++++ src/extension.ts | 12 + src/views/image-view/ImageView.vue | 981 ++++++++++++++++++++++++++ src/views/image-view/main.ts | 5 + webpack.config.js | 7 + 13 files changed, 1340 insertions(+) create mode 100644 src/cdtDebugAdapter/imageViewPanel.ts create mode 100644 src/views/image-view/ImageView.vue create mode 100644 src/views/image-view/main.ts diff --git a/docs_espressif/en/debugproject.rst b/docs_espressif/en/debugproject.rst index 6fdfc57ae..72cd3399c 100644 --- a/docs_espressif/en/debugproject.rst +++ b/docs_espressif/en/debugproject.rst @@ -343,6 +343,48 @@ 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, or any raw image data. + +To use the Image Viewer: + +1. Start a debug session and pause at a breakpoint where your image data variable is in scope +2. Go to ``View`` > ``Command Palette`` and enter ``ESP-IDF: Open Image Viewer`` +3. In the Image Viewer panel, enter the name of your image data variable and its size +4. Select the appropriate image format and dimensions +5. Click ``Load Image`` to visualize the data + +**Supported Image Formats:** +- RGB565 (16-bit per pixel) +- RGB888 (24-bit per pixel) +- Grayscale (8-bit per pixel) +- YUV420, YUV422, YUV444 (various YUV formats) + +**Example Usage:** + +Consider a camera application with the following variable: + +.. code-block:: C + + uint8_t image_buffer[320 * 240 * 3]; // RGB888 format, 320x240 pixels + size_t image_size = sizeof(image_buffer); + +During debugging, you can: +- 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`` + +**Important Notes:** +- The Image Viewer only supports raw pixel formats. Compressed formats (JPEG, PNG, etc.) are not supported +- You must specify the correct size of the image data array +- The size can be provided as a number (bytes) or as the name of another variable containing the size +- For pointer variables, make sure to provide the actual data size, not the pointer size +- The Image Viewer automatically estimates dimensions based on the data size and selected format, but you can manually adjust them for better results + + Other extensions debug configuration ------------------------------------ diff --git a/docs_espressif/zh_CN/debugproject.rst b/docs_espressif/zh_CN/debugproject.rst index 7056da4f0..fb6d99c0b 100644 --- a/docs_espressif/zh_CN/debugproject.rst +++ b/docs_espressif/zh_CN/debugproject.rst @@ -213,3 +213,45 @@ 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:图像查看器** 功能,允许你在调试会话期间可视化来自调试变量的二进制图像数据。这对于处理摄像头传感器、显示缓冲区或任何原始图像数据的应用程序特别有用。 + +使用方法: + +1. 启动调试会话并在图像数据变量作用域内的断点处暂停 +2. 点击 ``查看`` > ``命令面板``,输入 ``ESP-IDF:打开图像查看器`` +3. 在图像查看器面板中,输入图像数据变量的名称和大小 +4. 选择适当的图像格式和尺寸 +5. 点击 ``加载图像`` 来可视化数据 + +**支持的图像格式:** +- RGB565(每像素 16 位) +- RGB888(每像素 24 位) +- 灰度图(每像素 8 位) +- YUV420、YUV422、YUV444(各种 YUV 格式) + +**使用示例:** + +考虑一个具有以下变量的摄像头应用程序: + +.. 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`` + +**重要说明:** +- 图像查看器仅支持原始像素格式。不支持压缩格式(JPEG、PNG 等) +- 必须指定图像数据数组的正确大小 +- 大小可以作为数字(字节)提供,或作为包含大小的另一个变量的名称 +- 对于指针变量,请确保提供实际数据大小,而不是指针大小 +- 图像查看器会根据数据大小和所选格式自动估算尺寸,但你可以手动调整以获得更好的结果 diff --git a/package.json b/package.json index 44b3b7319..552937d30 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", @@ -1816,6 +1817,11 @@ "title": "%espIdf.viewAsHex.title%", "category": "ESP-IDF" }, + { + "command": "espIdf.openImageViewer", + "title": "%espIdf.openImageViewer.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..ce898bf59 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -98,6 +98,7 @@ "espIdf.webview.nvsPartitionEditor.title": "Abrir Editor de Partición NVS", "espIdf.welcome.title": "Bienvenido", "espIdf.viewAsHex.title": "Ver como Hexadecimal", + "espIdf.openImageViewer.title": "Abrir Visor de Imágenes", "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)", diff --git a/package.nls.json b/package.nls.json index 9cb08f006..7be65e6ab 100644 --- a/package.nls.json +++ b/package.nls.json @@ -96,6 +96,7 @@ "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.openImageViewer.title": "Open Image Viewer", "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", diff --git a/package.nls.pt.json b/package.nls.pt.json index 7c4d82d29..8c554c98e 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -98,6 +98,7 @@ "espIdf.webview.nvsPartitionEditor.title": "Abra o Editor de Partição NVS", "espIdf.welcome.title": "Bem-vindo", "espIdf.viewAsHex.title": "Ver como Hexadecimal", + "espIdf.openImageViewer.title": "Abrir Visor de Imagens", "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)", diff --git a/package.nls.ru.json b/package.nls.ru.json index eacf7e65f..403733136 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -96,6 +96,7 @@ "espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Прошивка Unit Test App", "espIdf.unitTest.installPyTest.title": "Unit Test: Установка требований ESP-IDF PyTest.", "espIdf.viewAsHex.title": "Просмотреть как шестнадцатеричное", + "espIdf.openImageViewer.title": "Открыть просмотрщик изображений", "espIdf.hexView.copyValue.title": "Скопировать значение в буфер обмена", "espIdf.hexView.deleteElement.title": "Удалить шестнадцатеричное значение из списка", "espIdf.webview.nvsPartitionEditor.title": "Открыть редактор разделов NVS", diff --git a/package.nls.zh-CN.json b/package.nls.zh-CN.json index c7da2ed3e..2869501a0 100644 --- a/package.nls.zh-CN.json +++ b/package.nls.zh-CN.json @@ -96,6 +96,7 @@ "espIdf.unitTest.flashUnitTestApp.title": "单元测试:烧录单元测试应用程序", "espIdf.unitTest.installPyTest.title": "单元测试:安装 ESP-IDF PyTest 依赖项", "espIdf.viewAsHex.title": "以十六进制查看", + "espIdf.openImageViewer.title": "打开图像查看器", "espIdf.hexView.copyValue.title": "复制值到剪贴板", "espIdf.hexView.deleteElement.title": "从列表中删除十六进制值", "espIdf.webview.nvsPartitionEditor.title": "打开 NVS 分区编辑器", diff --git a/src/cdtDebugAdapter/imageViewPanel.ts b/src/cdtDebugAdapter/imageViewPanel.ts new file mode 100644 index 000000000..43db8435c --- /dev/null +++ b/src/cdtDebugAdapter/imageViewPanel.ts @@ -0,0 +1,240 @@ +/* + * 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"; + +export interface ImageElement { + name: string; + data: Uint8Array; +} + +export class ImageViewPanel { + private static instance: ImageViewPanel; + private readonly panel: vscode.WebviewPanel; + private readonly extensionPath: string; + private disposables: vscode.Disposable[] = []; + + 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); + } + + 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 + ); + } + + + private sendImageData(imageElement: ImageElement) { + const base64Data = Buffer.from(imageElement.data).toString("base64"); + this.panel.webview.postMessage({ + command: "updateImage", + data: base64Data, + name: imageElement.name, + }); + } + + 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 = { + 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 dispose() { + ImageViewPanel.instance = undefined; + this.disposables.forEach((d) => d.dispose()); + } +} diff --git a/src/extension.ts b/src/extension.ts index 685196d89..24bbb3b90 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"; @@ -1526,6 +1528,16 @@ export async function activate(context: vscode.ExtensionContext) { }); } ); + + registerIDFCommand( + "espIdf.openImageViewer", + () => { + return PreCheck.perform([openFolderCheck], () => { + // Show the ImageViewPanel without an image + ImageViewPanel.show(context.extensionPath); + }); + } + ); registerIDFCommand("espIdf.genCoverage", () => { return PreCheck.perform([openFolderCheck], async () => { diff --git a/src/views/image-view/ImageView.vue b/src/views/image-view/ImageView.vue new file mode 100644 index 000000000..3be4399de --- /dev/null +++ b/src/views/image-view/ImageView.vue @@ -0,0 +1,981 @@ + + + + + \ No newline at end of file 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"), From 3201febbd79184a2d1dcfd66f32558732d815144 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Tue, 12 Aug 2025 13:27:21 +0800 Subject: [PATCH 2/9] fix lint --- src/extension.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 24bbb3b90..97feab90b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1528,16 +1528,13 @@ export async function activate(context: vscode.ExtensionContext) { }); } ); - - registerIDFCommand( - "espIdf.openImageViewer", - () => { - return PreCheck.perform([openFolderCheck], () => { - // Show the ImageViewPanel without an image - ImageViewPanel.show(context.extensionPath); - }); - } - ); + + registerIDFCommand("espIdf.openImageViewer", () => { + return PreCheck.perform([openFolderCheck], () => { + // Show the ImageViewPanel without an image + ImageViewPanel.show(context.extensionPath); + }); + }); registerIDFCommand("espIdf.genCoverage", () => { return PreCheck.perform([openFolderCheck], async () => { From 2e305822b79d7a6aabc1901bdd6d031ac3755223 Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Tue, 12 Aug 2025 14:07:59 +0800 Subject: [PATCH 3/9] fix zh CN doc issue --- docs_espressif/zh_CN/debugproject.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_espressif/zh_CN/debugproject.rst b/docs_espressif/zh_CN/debugproject.rst index fb6d99c0b..1c5783d85 100644 --- a/docs_espressif/zh_CN/debugproject.rst +++ b/docs_espressif/zh_CN/debugproject.rst @@ -216,7 +216,7 @@ ESP-IDF 扩展在 ``运行和调试`` 视图中提供了 ``ESP-IDF:外设视 ESP-IDF:图像查看器 ------------------- +-------------------- ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调试会话期间可视化来自调试变量的二进制图像数据。这对于处理摄像头传感器、显示缓冲区或任何原始图像数据的应用程序特别有用。 From 3d40d22d3b9edead56c3e743d5be9332904419ae Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Tue, 9 Sep 2025 19:48:49 +0800 Subject: [PATCH 4/9] add lvgl lv_image_dsc_t support --- package.json | 10 + package.nls.es.json | 1 + package.nls.json | 1 + package.nls.pt.json | 1 + package.nls.ru.json | 1 + package.nls.zh-CN.json | 1 + src/cdtDebugAdapter/imageViewPanel.ts | 306 +++++++- src/extension.ts | 44 ++ src/views/image-view/ImageView.vue | 989 ++++++++++++++++++++++---- 9 files changed, 1220 insertions(+), 134 deletions(-) diff --git a/package.json b/package.json index 552937d30..9a5d3b6c1 100644 --- a/package.json +++ b/package.json @@ -543,6 +543,11 @@ "command": "espIdf.viewAsHex", "when": "inDebugMode && debugType == 'gdbtarget' && debugState == stopped", "group": "navigation" + }, + { + "command": "espIdf.viewAsLVGLImage", + "when": "inDebugMode && debugType == 'gdbtarget' && debugState == stopped", + "group": "navigation" } ] }, @@ -1817,6 +1822,11 @@ "title": "%espIdf.viewAsHex.title%", "category": "ESP-IDF" }, + { + "command": "espIdf.viewAsLVGLImage", + "title": "%espIdf.viewAsLVGLImage.title%", + "category": "ESP-IDF" + }, { "command": "espIdf.openImageViewer", "title": "%espIdf.openImageViewer.title%", diff --git a/package.nls.es.json b/package.nls.es.json index ce898bf59..e83d92310 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -98,6 +98,7 @@ "espIdf.webview.nvsPartitionEditor.title": "Abrir Editor de Partición NVS", "espIdf.welcome.title": "Bienvenido", "espIdf.viewAsHex.title": "Ver como Hexadecimal", + "espIdf.viewAsLVGLImage.title": "Ver como Imagen LVGL", "espIdf.openImageViewer.title": "Abrir Visor de Imágenes", "espIdf.hexView.copyValue.title": "Copiar valor al portapapeles", "espIdf.hexView.deleteElement.title": "Eliminar valor hexadecimal de la lista", diff --git a/package.nls.json b/package.nls.json index 7be65e6ab..9ee6aeb67 100644 --- a/package.nls.json +++ b/package.nls.json @@ -96,6 +96,7 @@ "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.viewAsLVGLImage.title": "View as LVGL Image", "espIdf.openImageViewer.title": "Open Image Viewer", "espIdf.hexView.copyValue.title": "Copy value to clipboard", "espIdf.hexView.deleteElement.title": "Delete hex value from list", diff --git a/package.nls.pt.json b/package.nls.pt.json index 8c554c98e..a3b35e759 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -98,6 +98,7 @@ "espIdf.webview.nvsPartitionEditor.title": "Abra o Editor de Partição NVS", "espIdf.welcome.title": "Bem-vindo", "espIdf.viewAsHex.title": "Ver como Hexadecimal", + "espIdf.viewAsLVGLImage.title": "Ver como Imagem LVGL", "espIdf.openImageViewer.title": "Abrir Visor de Imagens", "espIdf.hexView.copyValue.title": "Copiar valor para a área de transferência", "espIdf.hexView.deleteElement.title": "Excluir valor hexadecimal da lista", diff --git a/package.nls.ru.json b/package.nls.ru.json index 403733136..4c27727f8 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -96,6 +96,7 @@ "espIdf.unitTest.flashUnitTestApp.title": "Unit Test: Прошивка Unit Test App", "espIdf.unitTest.installPyTest.title": "Unit Test: Установка требований ESP-IDF PyTest.", "espIdf.viewAsHex.title": "Просмотреть как шестнадцатеричное", + "espIdf.viewAsLVGLImage.title": "Просмотреть как изображение LVGL", "espIdf.openImageViewer.title": "Открыть просмотрщик изображений", "espIdf.hexView.copyValue.title": "Скопировать значение в буфер обмена", "espIdf.hexView.deleteElement.title": "Удалить шестнадцатеричное значение из списка", diff --git a/package.nls.zh-CN.json b/package.nls.zh-CN.json index 2869501a0..03081accf 100644 --- a/package.nls.zh-CN.json +++ b/package.nls.zh-CN.json @@ -96,6 +96,7 @@ "espIdf.unitTest.flashUnitTestApp.title": "单元测试:烧录单元测试应用程序", "espIdf.unitTest.installPyTest.title": "单元测试:安装 ESP-IDF PyTest 依赖项", "espIdf.viewAsHex.title": "以十六进制查看", + "espIdf.viewAsLVGLImage.title": "以 LVGL 图像查看", "espIdf.openImageViewer.title": "打开图像查看器", "espIdf.hexView.copyValue.title": "复制值到剪贴板", "espIdf.hexView.deleteElement.title": "从列表中删除十六进制值", diff --git a/src/cdtDebugAdapter/imageViewPanel.ts b/src/cdtDebugAdapter/imageViewPanel.ts index 43db8435c..e73607122 100644 --- a/src/cdtDebugAdapter/imageViewPanel.ts +++ b/src/cdtDebugAdapter/imageViewPanel.ts @@ -24,6 +24,16 @@ export interface ImageElement { data: Uint8Array; } +export interface ImageWithDimensionsElement { + name: string; + data: Uint8Array; + dataSize?: number; + dataAddress?: string; + width: number; + height: number; + format: number; +} + export class ImageViewPanel { private static instance: ImageViewPanel; private readonly panel: vscode.WebviewPanel; @@ -56,10 +66,30 @@ export class ImageViewPanel { ImageViewPanel.instance = new ImageViewPanel(panel, extensionPath); } - private constructor( - panel: vscode.WebviewPanel, - extensionPath: string - ) { + public static handleLVGLVariableFromContext(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) { + ImageViewPanel.instance.handleExtractLVGLImageProperties( + debugContext.variable + ); + } + } + + private constructor(panel: vscode.WebviewPanel, extensionPath: string) { this.panel = panel; this.extensionPath = extensionPath; @@ -80,6 +110,11 @@ export class ImageViewPanel { message.size ); break; + case "extractLVGLImageProperties": + this.handleExtractLVGLImagePropertiesFromString( + message.variableName + ); + break; default: break; } @@ -89,7 +124,6 @@ export class ImageViewPanel { ); } - private sendImageData(imageElement: ImageElement) { const base64Data = Buffer.from(imageElement.data).toString("base64"); this.panel.webview.postMessage({ @@ -99,6 +133,22 @@ export class ImageViewPanel { }); } + private sendImageWithDimensionsData( + imageElement: ImageWithDimensionsElement + ) { + const base64Data = Buffer.from(imageElement.data).toString("base64"); + this.panel.webview.postMessage({ + command: "updateLVGLImage", + data: base64Data, + dataAddress: imageElement.dataAddress, + dataSize: imageElement.dataSize, + name: imageElement.name, + width: imageElement.width, + height: imageElement.height, + format: imageElement.format, + }); + } + private async handleLoadImageFromVariable( variableName: string, size: string | number @@ -183,7 +233,7 @@ export class ImageViewPanel { if (readResponse && readResponse.data) { const binaryData = Buffer.from(readResponse.data, "base64"); - const imageElement = { + const imageElement: ImageElement = { name: variableName, data: new Uint8Array(binaryData), }; @@ -212,6 +262,250 @@ export class ImageViewPanel { } } + private async processLVGLImageProperties( + variableName: string, + variablesResponse: any, + lvHeaderChildren: any, + session: vscode.DebugSession + ) { + // Extract properties from the image descriptor + const imageProperties: ImageWithDimensionsElement = { + name: variableName, + data: new Uint8Array(), + width: 0, + height: 0, + format: 0, + }; + + // Extract data_size and data from the main structure + variablesResponse.variables.forEach((child: any) => { + if (child.name === "data_size") { + imageProperties.dataSize = parseInt(child.value, 10); + } else if (child.name === "data") { + const match = child.value.match(/0x[0-9a-fA-F]+/); + if (match) { + imageProperties.dataAddress = match[0]; + } + } + }); + + // Extract width, height, and format from header + lvHeaderChildren.variables.forEach((child: any) => { + if (child.name === "w") { + imageProperties.width = parseInt(child.value, 10); + } else if (child.name === "h") { + imageProperties.height = parseInt(child.value, 10); + } else if (child.name === "cf") { + imageProperties.format = parseInt(child.value, 10); + } + }); + + // Validate that we have the required data + 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); + + // Update the panel title and send the data + this.panel.title = `Image Viewer: ${variableName}`; + this.sendImageWithDimensionsData(imageProperties); + return imageProperties; + } else { + this.panel.webview.postMessage({ + command: "showError", + error: `Could not read memory data for variable ${variableName}`, + }); + return null; + } + } + + private async handleExtractLVGLImagePropertiesFromString( + variableName: string + ) { + try { + const session = vscode.debug.activeDebugSession; + if (!session) { + this.panel.webview.postMessage({ + command: "showError", + error: "No active debug session found", + }); + 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; + + // Evaluate the variable to get its properties + const evaluateResponse = await session.customRequest("evaluate", { + expression: variableName, + frameId, + }); + + if (!evaluateResponse || !evaluateResponse.result) { + this.panel.webview.postMessage({ + command: "showError", + error: `Could not evaluate variable ${variableName}`, + }); + return; + } + + // Check if the variable is of type lv_image_dsc_t + if ( + !evaluateResponse.type || + evaluateResponse.type.indexOf("lv_image_dsc_t") === -1 + ) { + this.panel.webview.postMessage({ + command: "showError", + error: `Variable ${variableName} is not of type lv_image_dsc_t`, + }); + return; + } + + // Get the variable's children to access its structure + const variablesResponse = await session.customRequest("variables", { + variablesReference: evaluateResponse.variablesReference, + }); + + if (!variablesResponse || !variablesResponse.variables) { + this.panel.webview.postMessage({ + command: "showError", + error: `No children found for variable ${variableName}`, + }); + return; + } + + // Find the header child + const headerObj = variablesResponse.variables.find( + (child: any) => child.name === "header" + ); + + if (!headerObj) { + this.panel.webview.postMessage({ + command: "showError", + error: `No header found in variable ${variableName}`, + }); + return; + } + + // Get header children + const lvHeaderChildren = await session.customRequest("variables", { + variablesReference: headerObj.variablesReference, + }); + + if (!lvHeaderChildren || !lvHeaderChildren.variables) { + this.panel.webview.postMessage({ + command: "showError", + error: `No children found for header of variable ${variableName}`, + }); + return; + } + + // Process the LVGL image properties using shared function + await this.processLVGLImageProperties( + variableName, + variablesResponse, + lvHeaderChildren, + session + ); + } catch (error) { + this.panel.webview.postMessage({ + command: "showError", + error: `Error extracting LVGL image properties from string: ${error}`, + }); + } + } + + private async handleExtractLVGLImageProperties(variableName: { + 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; + } + + if (variableName.type.indexOf("lv_image_dsc_t") === -1) { + this.panel.webview.postMessage({ + command: "showError", + error: `Variable ${variableName.name} is not of type lv_image_dsc_t`, + }); + return; + } + + const lvImageDscChildren = await session.customRequest("variables", { + variablesReference: variableName.variablesReference, + }); + + if (!lvImageDscChildren || lvImageDscChildren.length === 0) { + this.panel.webview.postMessage({ + command: "showError", + error: `No children found for variable ${variableName.name}`, + }); + return; + } + + // Try to find the 'header' child to determine if it's new or legacy format + const headerObj = lvImageDscChildren.variables.find( + (child: any) => child.name === "header" + ); + + const lvHeaderChildren = await session.customRequest("variables", { + variablesReference: headerObj.variablesReference, + }); + + if (!lvHeaderChildren || lvHeaderChildren.length === 0) { + this.panel.webview.postMessage({ + command: "showError", + error: `No children found for variable ${headerObj.name}`, + }); + return; + } + + // Process the LVGL image properties using shared function + await this.processLVGLImageProperties( + variableName.name, + lvImageDscChildren, + lvHeaderChildren, + session + ); + } catch (error) { + this.panel.webview.postMessage({ + command: "showError", + error: `Error extracting LVGL image properties: ${error}`, + }); + } + } + private getHtmlContent(webview: vscode.Webview): string { const scriptPath = webview.asWebviewUri( vscode.Uri.file( diff --git a/src/extension.ts b/src/extension.ts index 97feab90b..b6a562f5c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1529,6 +1529,50 @@ export async function activate(context: vscode.ExtensionContext) { } ); + registerIDFCommand( + "espIdf.viewAsLVGLImage", + (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 + ImageViewPanel.handleLVGLVariableFromContext(debugContext); + } catch (e) { + const msg = e && e.message ? e.message : e; + Logger.errorNotify(msg, e, "extension espIdf.viewAsLVGLImage"); + } + }); + } + ); + registerIDFCommand("espIdf.openImageViewer", () => { return PreCheck.perform([openFolderCheck], () => { // Show the ImageViewPanel without an image diff --git a/src/views/image-view/ImageView.vue b/src/views/image-view/ImageView.vue index 3be4399de..33e73bab1 100644 --- a/src/views/image-view/ImageView.vue +++ b/src/views/image-view/ImageView.vue @@ -3,30 +3,60 @@

Load Image from Debug Variable

-
- - + + +
+

LVGL Image Object

+
+ + + Name of your lv_obj_t image object variable +
+ +
+ {{ lvglLoadError }} +
-
- - - Enter a number (bytes) or variable name containing the size + +
+ OR
- -
- {{ loadError }} + + +
+

Manual Variable Input

+
+ + +
+
+ + + Enter a number (bytes) or variable name containing the size +
+ +
+ {{ loadError }} +
@@ -40,19 +70,46 @@
+ + +
+
Extracted Properties:
+
    +
  • Dimensions: {{ lvglProperties.width }} × {{ lvglProperties.height }}
  • +
  • Format: {{ getFormatName(lvglProperties.format) }} ({{ lvglProperties.format }})
  • +
  • Data Size: {{ lvglProperties.dataSize }} bytes
  • +
  • Data Address: {{ lvglProperties.dataAddress }}
  • +
  • Source Type: {{ lvglProperties.sourceType }}
  • +
+
- +
@@ -106,13 +163,22 @@ \ No newline at end of file + From 285983e14ed785f5c1b8d52bd15f35649bbe743d Mon Sep 17 00:00:00 2001 From: Brian Ignacio Date: Mon, 15 Sep 2025 18:28:31 +0800 Subject: [PATCH 9/9] mv configs to json file, mv image format logic to panel --- DEFAULT_CONFIGS.json | 107 +++++++ docs_espressif/en/debugproject.rst | 115 +++++-- docs_espressif/en/settings.rst | 2 + docs_espressif/zh_CN/debugproject.rst | 115 +++++-- docs_espressif/zh_CN/settings.rst | 2 + package.json | 6 + package.nls.es.json | 1 + package.nls.json | 1 + package.nls.pt.json | 1 + package.nls.ru.json | 1 + package.nls.zh-CN.json | 1 + src/cdtDebugAdapter/imageViewPanel.ts | 445 +++++++++++++++----------- src/views/image-view/ImageView.vue | 146 +-------- 13 files changed, 587 insertions(+), 356 deletions(-) create mode 100644 DEFAULT_CONFIGS.json 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/docs_espressif/en/debugproject.rst b/docs_espressif/en/debugproject.rst index 789a1df52..9f3a1a46f 100644 --- a/docs_espressif/en/debugproject.rst +++ b/docs_espressif/en/debugproject.rst @@ -351,8 +351,8 @@ The ESP-IDF extension provides an **ESP-IDF: Image Viewer** feature that allows **Quick Access Methods:** 1. **Right-click on variables in the debug session:** - - Right-click on any ``lv_image_dsc_t`` variable and select ``View as LVGL Image`` - - Right-click on any ``cv::Mat`` variable and select ``View as OpenCV Image`` + - 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`` @@ -362,24 +362,28 @@ The ESP-IDF extension provides an **ESP-IDF: Image Viewer** feature that allows **Supported Image Formats:** -**LVGL Color Formats (lv_color_format_t):** +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 -- Grayscale, YUV420, YUV422, YUV444 -- And many more LVGL-specific formats - -**OpenCV Mat Formats:** -- BGR888 (3 channels) -- BGRA8888 (4 channels) -- Grayscale (1 channel) +- RGB101010, RGB121212, RGB161616 -**Raw Pixel Formats:** -- RGB565 (16-bit per pixel) -- RGB888 (24-bit per pixel) +**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:** @@ -397,7 +401,7 @@ The ESP-IDF extension provides an **ESP-IDF: Image Viewer** feature that allows .data = image_data // Pointer to image data }; -During debugging, right-click on ``my_image`` and select ``View as LVGL Image``. The Image Viewer will automatically extract the format, dimensions, and data from the LVGL structure. +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:** @@ -406,7 +410,7 @@ During debugging, right-click on ``my_image`` and select ``View as LVGL Image``. cv::Mat image(240, 320, CV_8UC3); // 320x240 BGR888 image // ... populate image data ... -During debugging, right-click on ``image`` and select ``View as OpenCV Image``. The Image Viewer will automatically extract the dimensions, format, and data from the OpenCV Mat structure. +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:** @@ -421,15 +425,90 @@ For manual usage: - 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:** -- **LVGL Support**: Automatically extracts image properties from ``lv_image_dsc_t`` structures -- **OpenCV Support**: Automatically extracts image properties from ``cv::Mat`` objects -- **Format Detection**: The Image Viewer automatically detects and maps LVGL and OpenCV formats to the appropriate display format +- **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/debugproject.rst b/docs_espressif/zh_CN/debugproject.rst index 8d7c3ca78..6178e20fc 100644 --- a/docs_espressif/zh_CN/debugproject.rst +++ b/docs_espressif/zh_CN/debugproject.rst @@ -223,8 +223,8 @@ ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调 **快速访问方法:** 1. **在调试会话中右键点击变量:** - - 右键点击任何 ``lv_image_dsc_t`` 变量并选择 ``以 LVGL 图像查看`` - - 右键点击任何 ``cv::Mat`` 变量并选择 ``以 OpenCV 图像查看`` + - 右键点击任何图像相关变量(``lv_image_dsc_t``、``cv::Mat``、``png_image`` 等)并选择 ``将变量作为图像查看`` + - 图像查看器会自动检测变量类型并提取相应的图像属性 2. **手动图像查看器:** - 点击 ``查看`` > ``命令面板``,输入 ``ESP-IDF:打开图像查看器`` @@ -234,24 +234,28 @@ ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调 **支持的图像格式:** -**LVGL 颜色格式 (lv_color_format_t):** +图像查看器支持全面的图像格式范围: + +**RGB 格式:** - RGB565、RGB888、RGBA8888、ARGB8888、XRGB8888 - BGR888、BGRA8888、ABGR8888、XBGR8888 - RGB332、RGB444、RGB555、RGB666、RGB777 -- 灰度图、YUV420、YUV422、YUV444 -- 以及更多 LVGL 特定格式 - -**OpenCV Mat 格式:** -- BGR888(3 通道) -- BGRA8888(4 通道) -- 灰度图(1 通道) +- RGB101010、RGB121212、RGB161616 -**原始像素格式:** -- RGB565(每像素 16 位) -- RGB888(每像素 24 位) +**其他格式:** - 灰度图(每像素 8 位) - YUV420、YUV422、YUV444(各种 YUV 格式) +**内置支持:** + +**LVGL 图像描述符 (lv_image_dsc_t):** +- 自动从 LVGL 结构中提取格式、尺寸和数据 +- 支持所有 LVGL 颜色格式,并自动映射到显示格式 + +**OpenCV Mat (cv::Mat):** +- 自动从 OpenCV Mat 对象中提取尺寸、格式和数据 +- 支持 BGR888、BGRA8888 和灰度格式 + **使用示例:** **LVGL 图像示例:** @@ -269,7 +273,7 @@ ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调 .data = image_data // 指向图像数据的指针 }; -在调试过程中,右键点击 ``my_image`` 并选择 ``以 LVGL 图像查看``。图像查看器将自动从 LVGL 结构中提取格式、尺寸和数据。 +在调试过程中,右键点击 ``my_image`` 并选择 ``将变量作为图像查看``。图像查看器会自动检测其为 LVGL 图像并提取格式、尺寸和数据。 **OpenCV Mat 示例:** @@ -278,7 +282,7 @@ ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调 cv::Mat image(240, 320, CV_8UC3); // 320x240 BGR888 图像 // ... 填充图像数据 ... -在调试过程中,右键点击 ``image`` 并选择 ``以 OpenCV 图像查看``。图像查看器将自动从 OpenCV Mat 结构中提取尺寸、格式和数据。 +在调试过程中,右键点击 ``image`` 并选择 ``将变量作为图像查看``。图像查看器会自动检测其为 OpenCV Mat 并提取尺寸、格式和数据。 **手动原始数据示例:** @@ -293,12 +297,87 @@ ESP-IDF 扩展提供了 **ESP-IDF:图像查看器** 功能,允许你在调 - 选择 ``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"``。 + **重要说明:** -- **LVGL 支持**:自动从 ``lv_image_dsc_t`` 结构中提取图像属性 -- **OpenCV 支持**:自动从 ``cv::Mat`` 对象中提取图像属性 -- **格式检测**:图像查看器自动检测并将 LVGL 和 OpenCV 格式映射到适当的显示格式 +- **自动检测**:图像查看器自动检测支持的图像类型并提取属性 +- **统一界面**:单个 ``将变量作为图像查看`` 命令适用于所有支持的格式 +- **格式验证**:所有格式都根据支持的显示格式进行验证 - **原始数据**:图像查看器支持原始像素格式。不支持压缩格式(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 be3cccf89..eda57ba2f 100644 --- a/package.json +++ b/package.json @@ -1301,6 +1301,12 @@ "default": 60, "scope": "resource", "description": "%param.serialPortDetectionTimeout%" + }, + "idf.imageViewerConfigs": { + "type": "string", + "description": "%param.imageViewerConfigs.title%", + "scope": "resource", + "default": "" } } } diff --git a/package.nls.es.json b/package.nls.es.json index f8a8baf1d..d4997b651 100644 --- a/package.nls.es.json +++ b/package.nls.es.json @@ -190,6 +190,7 @@ "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", diff --git a/package.nls.json b/package.nls.json index e602c907e..c6a0a88e3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -191,6 +191,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 9c2da61a1..cb378b276 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -189,6 +189,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 da27604a9..61a5e5a16 100644 --- a/package.nls.ru.json +++ b/package.nls.ru.json @@ -190,6 +190,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 6dceab7de..a8e4781c8 100644 --- a/package.nls.zh-CN.json +++ b/package.nls.zh-CN.json @@ -191,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/src/cdtDebugAdapter/imageViewPanel.ts b/src/cdtDebugAdapter/imageViewPanel.ts index 2be5efc07..1400b1267 100644 --- a/src/cdtDebugAdapter/imageViewPanel.ts +++ b/src/cdtDebugAdapter/imageViewPanel.ts @@ -18,6 +18,11 @@ 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; @@ -65,180 +70,29 @@ export class ImageViewPanel { private disposables: vscode.Disposable[] = []; private imageFormatConfigs: ImageFormatConfig[] = []; - // Default configurations for built-in formats - private static readonly DEFAULT_CONFIGS: ImageFormatConfig[] = [ - { - 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: { - // IMPORTANT: All format values must match the frontend dropdown options - // LVGL Color Format constants (lv_color_format_t) -> dropdown values - 0x00: "grayscale", // LV_COLOR_FORMAT_UNKNOWN -> fallback to grayscale - 0x01: "grayscale", // LV_COLOR_FORMAT_RAW -> not supported, fallback - 0x02: "grayscale", // LV_COLOR_FORMAT_RAW_ALPHA -> not supported, fallback - 0x03: "grayscale", // LV_COLOR_FORMAT_L8 -> not supported, fallback - 0x04: "grayscale", // LV_COLOR_FORMAT_I1 -> not supported, fallback - 0x05: "grayscale", // LV_COLOR_FORMAT_I2 -> not supported, fallback - 0x06: "grayscale", // LV_COLOR_FORMAT_I4 -> not supported, fallback - 0x07: "grayscale", // LV_COLOR_FORMAT_I8 -> not supported, fallback - 0x08: "grayscale", // LV_COLOR_FORMAT_A8 -> not supported, fallback - 0x09: "rgb565", // LV_COLOR_FORMAT_RGB565 - 0x0a: "rgb565", // LV_COLOR_FORMAT_ARGB8565 -> closest match - 0x0b: "rgb565", // LV_COLOR_FORMAT_RGB565A8 -> closest match - 0x0c: "grayscale", // LV_COLOR_FORMAT_AL88 -> not supported, fallback - 0x0d: "rgb565", // LV_COLOR_FORMAT_RGB565_SWAPPED -> closest match - 0x0e: "rgb888", // LV_COLOR_FORMAT_RGB888 - 0x0f: "argb8888", // LV_COLOR_FORMAT_ARGB8888 - 0x10: "bgra8888", // LV_COLOR_FORMAT_XRGB8888 -> mapped to bgra8888 - 0x11: "argb8888", // LV_COLOR_FORMAT_ARGB8888_PREMULTIPLIED -> closest match - 0x12: "grayscale", // LV_COLOR_FORMAT_A1 -> not supported, fallback - 0x13: "grayscale", // LV_COLOR_FORMAT_A2 -> not supported, fallback - 0x14: "grayscale", // LV_COLOR_FORMAT_A4 -> not supported, fallback - 0x15: "rgb555", // LV_COLOR_FORMAT_ARGB1555 -> closest match - 0x16: "rgb444", // LV_COLOR_FORMAT_ARGB4444 -> closest match - 0x17: "rgb332", // LV_COLOR_FORMAT_ARGB2222 -> closest match - 0x18: "yuv420", // LV_COLOR_FORMAT_YUV_START -> fallback to yuv420 - 0x19: "yuv420", // LV_COLOR_FORMAT_I420 - 0x1a: "yuv422", // LV_COLOR_FORMAT_I422 - 0x1b: "yuv444", // LV_COLOR_FORMAT_I444 - 0x1c: "grayscale", // LV_COLOR_FORMAT_I400 -> not supported, fallback - 0x1d: "yuv420", // LV_COLOR_FORMAT_NV21 -> closest match - 0x1e: "yuv420", // LV_COLOR_FORMAT_NV12 -> closest match - 0x1f: "yuv422", // LV_COLOR_FORMAT_YUY2 -> closest match - 0x20: "yuv422", // LV_COLOR_FORMAT_UYVY -> closest match - 0x21: "yuv420", // LV_COLOR_FORMAT_YUV_END -> fallback to yuv420 - 0x22: "rgb888", // LV_COLOR_FORMAT_PROPRIETARY_START -> fallback - 0x23: "rgb888", // LV_COLOR_FORMAT_NEMA_TSC_START -> fallback - 0x24: "rgb444", // LV_COLOR_FORMAT_NEMA_TSC4 -> closest match - 0x25: "rgb666", // LV_COLOR_FORMAT_NEMA_TSC6 -> closest match - 0x26: "rgb666", // LV_COLOR_FORMAT_NEMA_TSC6A -> closest match - 0x27: "rgb666", // LV_COLOR_FORMAT_NEMA_TSC6AP -> closest match - 0x28: "rgb888", // LV_COLOR_FORMAT_NEMA_TSC12 -> closest match - 0x29: "rgb888", // LV_COLOR_FORMAT_NEMA_TSC12A -> closest match - 0x2a: "rgb888", // LV_COLOR_FORMAT_NEMA_TSC_END -> fallback - 0x2b: "rgb888", // LV_COLOR_FORMAT_NATIVE -> fallback to rgb888 - 0x2c: "rgba8888", // LV_COLOR_FORMAT_NATIVE_WITH_ALPHA -> fallback to 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", // BGR888 equivalent - }, - dataAddress: { - type: "string", - isChild: true, - value: "data", - }, - dataSize: { - type: "formula", - isChild: false, - value: "$var.rows * $var.step.buf[0]", - }, - imageFormats: { - // IMPORTANT: All format values must match the frontend dropdown options - // OpenCV Mat format mapping - 0x00: "grayscale", // OpenCV Grayscale (1 channel) - 0x0e: "bgr888", // OpenCV BGR888 (3 channels) - 0x0f: "bgra8888", // OpenCV BGRA8888 (4 channels) - 0x10: "bgra8888", // OpenCV XRGB8888 (4 channels) - }, - }, - { - name: "libpng image", - typePattern: "png_image", - width: { - type: "string", - isChild: true, - value: "width", - }, - height: { - type: "string", - isChild: true, - value: "height", - }, - format: { - type: "string", - isChild: true, - value: "format", - }, - dataAddress: { - type: "string", - isChild: false, - value: "image_data", - }, - dataSize: { - type: "string", - isChild: false, - value: "buf_size", - }, - imageFormats: { - // IMPORTANT: All format values must match the frontend dropdown options - // libpng PNG_COLOR_TYPE constants -> dropdown values - 0x00: "grayscale", // PNG_COLOR_TYPE_GRAY (8-bit grayscale) - 0x02: "rgb888", // PNG_COLOR_TYPE_RGB (24-bit RGB) - 0x03: "rgb888", // PNG_COLOR_TYPE_PALETTE -> fallback to rgb888 (palette not directly supported) - 0x04: "rgba8888", // PNG_COLOR_TYPE_GRAY_ALPHA (8-bit grayscale + alpha) - 0x06: "rgba8888", // PNG_COLOR_TYPE_RGB_ALPHA (32-bit RGBA) - - // Extended bit depth combinations (format = color_type | (bit_depth << 8)) - // 8-bit formats - 0x0200: "rgb888", // PNG_COLOR_TYPE_RGB, 8-bit - 0x0300: "rgb888", // PNG_COLOR_TYPE_PALETTE, 8-bit -> fallback - 0x0400: "rgba8888", // PNG_COLOR_TYPE_GRAY_ALPHA, 8-bit - 0x0600: "rgba8888", // PNG_COLOR_TYPE_RGB_ALPHA, 8-bit - - // 16-bit formats (mapped to closest 8-bit equivalents) - 0x1000: "grayscale", // PNG_COLOR_TYPE_GRAY, 16-bit -> grayscale - 0x1200: "rgb888", // PNG_COLOR_TYPE_RGB, 16-bit -> rgb888 - 0x1300: "rgb888", // PNG_COLOR_TYPE_PALETTE, 16-bit -> fallback - 0x1400: "rgba8888", // PNG_COLOR_TYPE_GRAY_ALPHA, 16-bit -> rgba8888 - 0x1600: "rgba8888", // PNG_COLOR_TYPE_RGB_ALPHA, 16-bit -> rgba8888 - - // Other bit depths (1, 2, 4-bit) - mapped to grayscale - 0x0100: "grayscale", // PNG_COLOR_TYPE_GRAY, 1-bit - 0x0201: "grayscale", // PNG_COLOR_TYPE_GRAY, 2-bit - 0x0401: "grayscale", // PNG_COLOR_TYPE_GRAY, 4-bit - }, - }, + // 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) { @@ -325,7 +179,218 @@ export class ImageViewPanel { } private initializeImageFormatConfigs() { - this.imageFormatConfigs = [...ImageViewPanel.DEFAULT_CONFIGS]; + 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 { @@ -532,7 +597,7 @@ export class ImageViewPanel { if (imageProperties) { // Update the panel title and send the data this.panel.title = `Image Viewer: ${variable.name} (${config.name})`; - this.sendImageWithDimensionsData(imageProperties, config.name, config.imageFormats); + this.sendImageWithDimensionsData(imageProperties, config.name); } } catch (error) { this.panel.webview.postMessage({ @@ -573,20 +638,23 @@ export class ImageViewPanel { frameId )) as number; - const rawFormat = (await this.extractFieldValue( + const rawFormat = await this.extractFieldValue( session, variablesReference, config.format, frameId - )) as number; + ); - // Convert numeric format to string using imageFormats mapping if available - if (config.imageFormats && typeof rawFormat === 'number') { - imageProperties.format = rawFormat; // Keep raw format for display - // The frontend will use the imageFormats mapping to convert to display format - } else { - imageProperties.format = rawFormat; - } + // 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, @@ -651,12 +719,11 @@ export class ImageViewPanel { private sendImageWithDimensionsData( imageElement: ImageWithDimensionsElement, - configName?: string, - imageFormats?: { [key: number]: string } + configName?: string ) { const base64Data = Buffer.from(imageElement.data).toString("base64"); this.panel.webview.postMessage({ - command: "updateLVGLImage", + command: "updateImageWithProperties", data: base64Data, dataAddress: imageElement.dataAddress, dataSize: imageElement.dataSize, @@ -665,7 +732,7 @@ export class ImageViewPanel { height: imageElement.height, format: imageElement.format, configName: configName, // Pass the configuration name - imageFormats: imageFormats, // Pass the format mapping + validatedFormat: (imageElement as any).validatedFormat, // Pass the validated format string }); } diff --git a/src/views/image-view/ImageView.vue b/src/views/image-view/ImageView.vue index 5f639edc3..f88de9b00 100644 --- a/src/views/image-view/ImageView.vue +++ b/src/views/image-view/ImageView.vue @@ -237,8 +237,8 @@ onMounted(() => { case "updateImage": handleImageUpdate(message); break; - case "updateLVGLImage": - handleLVGLImageUpdate(message); + case "updateImageWithProperties": + handleImageWithPropertiesUpdate(message); break; case "showError": loadError.value = message.error; @@ -272,8 +272,8 @@ function loadImageFromVariable() { }); } -async function handleLVGLImageUpdate(message: any) { - // Handle LVGL image data with dimensions and format +async function handleImageWithPropertiesUpdate(message: any) { + // Handle image data with dimensions and format (LVGL, OpenCV, libpng, etc.) try { // Use configName to determine the format type, fallback to name-based detection const configName = message.configName; @@ -301,45 +301,19 @@ async function handleLVGLImageUpdate(message: any) { return; } - // Set format from backend format mapping AFTER image data is loaded - if (message.format !== undefined) { - let formatValue: string | null = null; - - // Use imageFormats mapping from backend if available - if (message.imageFormats && typeof message.format === 'number') { - const backendFormat = message.imageFormats[message.format]; - if (backendFormat && isValidFormat(backendFormat)) { - formatValue = backendFormat; - } else { - // Invalid format from backend mapping - error.value = `Invalid format '${backendFormat}' from backend mapping for format value ${message.format}. Please check the imageFormats configuration.`; - return; - } - } else if (configName) { - // Fallback to custom format detection for string formats - const customFormat = getFormatValueFromCustomFormat(message.format); - if (isValidFormat(customFormat)) { - formatValue = customFormat; - } else { - // This should not happen as getFormatValueFromCustomFormat always returns a valid format - error.value = `Internal error: getFormatValueFromCustomFormat returned invalid format '${customFormat}'`; - return; - } - } - - if (formatValue) { - selectedFormat.value = formatValue; - // Use nextTick to ensure the dropdown updates before rendering + // Set format from backend validated format AFTER image data is loaded + if (message.validatedFormat) { + selectedFormat.value = message.validatedFormat; + // Use nextTick to ensure the dropdown updates before rendering + await nextTick(); + + // Force immediate image update with new format (if canvas is ready) + if (canvas.value) { + updateImage(); + } else { await nextTick(); - - // Force immediate image update with new format (if canvas is ready) if (canvas.value) { updateImage(); - } else { - await nextTick(); - if (canvas.value) { - updateImage(); - } } } } @@ -386,7 +360,7 @@ async function handleLVGLImageUpdate(message: any) { updateImage(); } } catch (err) { - error.value = `Failed to load LVGL image: ${err}`; + error.value = `Failed to load image with properties: ${err}`; } } @@ -406,96 +380,6 @@ function getPropertiesTitle(): string { } } -// All available formats from the dropdown -const ALL_FORMATS = [ - "rgb565", - "rgb888", - "rgba8888", - "argb8888", - "xrgb8888", - "bgr888", - "bgra8888", - "abgr8888", - "xbgr8888", - "rgb332", - "rgb444", - "rgb555", - "rgb666", - "rgb777", - "rgb101010", - "rgb121212", - "rgb161616", - "grayscale", - "yuv420", - "yuv422", - "yuv444", -]; - -function isValidFormat(format: string): boolean { - return ALL_FORMATS.includes(format); -} - -function getFormatValueFromCustomFormat(format: string): string { - // Direct string match (case-insensitive) - const directMatch = ALL_FORMATS.find( - (f) => f.toLowerCase() === format.toLowerCase() - ); - if (directMatch) { - return directMatch; - } - - // Partial string matching for common variations - const formatLower = format.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"; - - // Safe fallback for unrecognized string formats - return "rgb888"; -} function handleImageUpdate(data: ImageData) { try {