From b0fc4b0e577e241339b1d3b436f680ef6a7faddf Mon Sep 17 00:00:00 2001 From: morris Date: Wed, 10 Jun 2026 19:35:46 +0800 Subject: [PATCH] feat(dma2d): added async color converter example illustrate how to use DMA2D convert a YUV422 image to RGB888 format --- examples/peripherals/.build-test-rules.yml | 6 + .../dma/async_color_convert/CMakeLists.txt | 5 + .../dma/async_color_convert/README.md | 69 +++++++ .../dma/async_color_convert/golden_result.ppm | Bin 0 -> 18445 bytes .../async_color_convert/main/CMakeLists.txt | 4 + .../main/assets/sample_96x64_uyvy.yuv | 1 + .../main/async_color_convert_example_main.c | 119 +++++++++++++ .../pytest_async_color_convert.py | 168 ++++++++++++++++++ 8 files changed, 372 insertions(+) create mode 100644 examples/peripherals/dma/async_color_convert/CMakeLists.txt create mode 100644 examples/peripherals/dma/async_color_convert/README.md create mode 100644 examples/peripherals/dma/async_color_convert/golden_result.ppm create mode 100644 examples/peripherals/dma/async_color_convert/main/CMakeLists.txt create mode 100644 examples/peripherals/dma/async_color_convert/main/assets/sample_96x64_uyvy.yuv create mode 100644 examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c create mode 100644 examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py diff --git a/examples/peripherals/.build-test-rules.yml b/examples/peripherals/.build-test-rules.yml index a96f9667059..06018ccc657 100644 --- a/examples/peripherals/.build-test-rules.yml +++ b/examples/peripherals/.build-test-rules.yml @@ -100,6 +100,12 @@ examples/peripherals/dac/dac_cosine_wave: - esp_driver_dac - soc +examples/peripherals/dma/async_color_convert: + disable: + - if: SOC_DMA2D_SUPPORTED != 1 + depends_components: + - esp_driver_dma + examples/peripherals/dma/async_crc: disable: - if: SOC_GDMA_SUPPORT_CRC != 1 diff --git a/examples/peripherals/dma/async_color_convert/CMakeLists.txt b/examples/peripherals/dma/async_color_convert/CMakeLists.txt new file mode 100644 index 00000000000..3192fb91ee2 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(async_color_convert_example) diff --git a/examples/peripherals/dma/async_color_convert/README.md b/examples/peripherals/dma/async_color_convert/README.md new file mode 100644 index 00000000000..77efb5f5dc2 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/README.md @@ -0,0 +1,69 @@ +| Supported Targets | ESP32-P4 | ESP32-S31 | +| ----------------- | -------- | --------- | + +# Async Color Convert Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +## Overview + +This example demonstrates how to use the Async Color Convert driver (`esp_async_color_convert.h`) with the DMA2D backend. + +The example performs: + +- Loading an embedded UYVY422 raw image from flash +- Letting DMA2D read the source image directly from mapped flash +- Performing a blocking UYVY422 -> RGB888 conversion with the Async Color Convert driver +- Base64-encoding the converted BGR24 image and printing it with machine-parseable markers +- Letting pytest decode the payload, save a PPM artifact, and compare it against a golden reference image + +## Hardware Required + +Any board with a supported ESP target that mentioned in the above table can be used. + +## Build and Flash + +Run `idf.py -p PORT flash monitor` to build and flash the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://docs.espressif.com/projects/esp-idf/en/latest/get-started/index.html) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +```text +Loading embedded UYVY image from flash... +Embedded image size: 12288 bytes +Converting UYVY422 -> RGB888... +Converted image size: 18432 bytes +IMAGE_META width=96 height=64 format=BGR24 encoding=base64 +IMAGE_BASE64_BEGIN +IMAGE_BASE64 ... +IMAGE_BASE64 ... +IMAGE_BASE64_END +Async color convert visual demo done. +``` + +## Visual Result In Pytest + +The accompanying pytest script captures the `IMAGE_META` and `IMAGE_BASE64` output, reconstructs the converted image, and saves it as: + +- `dut.logdir/async_color_convert_result.ppm` + +It also compares the generated result with `golden_result.ppm` by hashing the decoded RGB pixel content. This turns the example into a regression test as well as a visual demo: the image must both render correctly for a human and match the stored golden output for CI. + +## Replacing The Embedded UYVY Asset + +The example embeds `main/assets/sample_96x64_uyvy.yuv`. + +You can regenerate a compatible asset from any PNG with `ffmpeg`. One simple workflow is: + +```bash +ffmpeg -y -i input.png -vf scale=96:64 -pix_fmt uyvy422 -f rawvideo sample_96x64_uyvy.yuv +``` + +After replacing the `.yuv` file, rebuild and flash the example. The firmware will emit the converted image as base64, and pytest will save the resulting PPM artifact automatically. If you intend the new image to become the expected output, update `golden_result.ppm` as well so the regression check stays in sync. + +## Troubleshooting + +(For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you as soon as possible.) diff --git a/examples/peripherals/dma/async_color_convert/golden_result.ppm b/examples/peripherals/dma/async_color_convert/golden_result.ppm new file mode 100644 index 0000000000000000000000000000000000000000..0f987ed1191b06865fab00f0d33e05b7fa4aec1d GIT binary patch literal 18445 zcmeI1e@vBC7{?Dq26v=_42dyDOj$POlmXF-jDFh2EZc00Ss7z2NLX25TBK-%PzEx_ zuOSG63Zw><6*_fbGQ+fztT|vM1uKX43uUo5o$a}vJ-he5=Y8MH^IrbB?7Pdk?>X=H z`JT_7!{y%lbb{@^gvf+ATWoy1jq$BsDG2!fTjZ63fd9lUuM`9V{;KgxK_KvIqm&P0 zgksSw^~_!{C$tq-o^gicA-}gviPD_zhb{75+qWOgQyJSAy_gGs3Snm!&rm`{}7i(j^bIWZsPOw0#4L-O#;-BQGSA_La-90GG6awg`3oFRG4g+VD|KA{mWcb^1vVm`>3m=AJ>EN)hu}7?t_ODKIDIgPe)^ zAZJJ(e`Z{Ym``x@E1&j(IWZsPOw0#4L-NGa6H>%{7TlBF)eq*xe2_CSALIgPe)^AZJMacz3ZBF`r-BYDPRs{66Z1jN_TdVALI>Bn;L14p&qh2WpK+|CR}fr{bbkiSnF121V65m@2=) zcjT2B3_!9BlL0<97Np2fmD#4Nk~b>&1X<@`KF~$F0Z0~zT?DWYV1ggTMKPsSqBf&b zS5|Zmro{i|qP=xv%y5Bo=K!U7EKE_oA?=v1Ea&qlFMYL6H-_5_gn1x~z&(SG4UrP&Sq4@*uK?jJa1iWoDT(&`+b1g~ORC0d^ASExLzg^zoPk0OTKQzCx|yBl|l z^BDMOm1tXYK_RP87VaW`g;!*@D#DnW5>^XYjJk7h|7e*XUOl9cNBR85t1=EM!kC&e zLI(5EGVeGI;dSdiSA@}>61U&Q9>BXcJ{C8+tbXF5X@xw>=VxC3!f{0y z-6{7v2N*`Nhh0|hP1>rENBNBL>NR%3pHSA>bMaxU~$T&Y5JPv!Fw5AjjNP)yC_Oksx0S&-PQbD{IgYIJ4w zDdU&s&jxL@Np<1 zp8l|;O;=4%zR!m&Ic4z9R5yUb0+Hul7HPaUjI0Dc4u!JD zv{?Kgi_zL0)pb$AA=OxDzbUmQ*Yb-5-+!4ZDi&LDrkts+V9Ue?akeYL-@Bpq74>|; zG$r_E&}=zTE1AW?$+G%U6cXid!fW$~+}1})&HOw2m+SbGEšªš>š€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë•_šªš>šm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ•_šªš>š[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘•_šªš>šªš>š¯šAš¯šAš€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë—`š¯šAšm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ—`š¯šAš[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘—`š¯šAš¯šAšw�M�w�M�€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë|g�w�M�m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ|g�w�M�[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘|g�w�M�w�M�|�P�|�P�€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë~h�|�P�m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ~h�|�P�[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘~h�|�P�|�P�€ŒRŒ€ŒRŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€iŒ€ŒRŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ€iŒ€ŒRŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘€iŒ€ŒRŒ€ŒRŒ„ŒUŒ„ŒUŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‚jŒ„ŒUŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‚jŒ„ŒUŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‚jŒ„ŒUŒ„ŒUŒˆŒXŒˆŒXŒ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë„lŒˆŒXŒm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ„lŒˆŒXŒ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘„lŒˆŒXŒˆŒXŒ�Œ[Œ�Œ[Œ€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë†mŒ�Œ[Œm¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ†mŒ�Œ[Œ[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘†mŒ�Œ[Œ�Œ[Œ‘‹^‹‘‹^‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‰o‹‘‹^‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‰o‹‘‹^‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‰o‹‘‹^‹‘‹^‹–‹`‹–‹`‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë‹p‹–‹`‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ‹p‹–‹`‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘‹p‹–‹`‹–‹`‹š‹c‹š‹c‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë�q‹š‹c‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ�q‹š‹c‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘�q‹š‹c‹š‹c‹ž‹f‹ž‹f‹€€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë€ë�s‹ž‹f‹m¸QZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQZQðQ�s‹ž‹f‹[Q‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘6‘"‘�s‹ž‹f‹ž‹f‹¢‹h‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹€€€€€€€€€€€€€€€€€€€€€€€€€€‘t‹¢‹h‹¢‹h‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹§‹k‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹«‹m‹¯ŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpНŠpгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsŠ€ë€ë€ë€ë€ë€ë€ë€ëšëyгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsгŠsЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuŠœŠ{ë€ë€ëÍeBeÍeBeÍeBeÍeBe§eaë€ë€ë¸ŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuЏŠuŠ�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}€}�ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}�}‚}€}��}‚}�}‚}�}‚}�}‚}�}‚}…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|‚|‚ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|…|„|€€…|„|…|„|…|„|…|„|…|„|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe„ëƒ|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|„|ƒO»‘‰|‡|‰|‡|‰|‡|‰|‡|‰|‡|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|‡|…ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|�|‰|‡|…O‘»»£»‡…|�|‰|�|‰|�|‰|�|‰|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe‰ë†|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|‘|Œ|O‘»»£»»£»‰†|‘|Œ|‘|Œ|‘|Œ|‘|Œ|–|�|–|�|–|�|–|�|–|�|–|�|‹|‡ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|–|�|‹|‡»£»»£»»£»‹‡|–|�|–|�|–|�|–|�|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ë‰|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|š|‘|O‘»»£»»£»»£»�‰|š|‘|š|‘|š|‘|š|‘|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ëŠ|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|�|Š»£»»£»»£»»£»�Š|Ÿ|”|Ÿ|”|Ÿ|”|Ÿ|”|£{—{£{—{£{—{£{—{£{—{‘{‹ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{£{—{O‘»»£»»£»»£»»£»O»‘£{—{£{—{£{—{£{—{§{™{§{™{§{™{§{™{§{™{”{�ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{§{™{”{�»£»»£»»£»»£»»£»O»‘§{™{§{™{§{™{§{™{«{œ{«{œ{«{œ{«{œ{«{œ{§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe–ëŽ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{«{œ{–{ŽO‘»»£»»£»»£»»£»»£»O»‘«{œ{«{œ{«{œ{«{œ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe˜ë�{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{O‘»»£»»£»»£»»£»»£»»£»O»‘°{Ÿ{°{Ÿ{°{Ÿ{°{Ÿ{´z¢z´z¢z´z¢z´z¢z´z¢z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBešë‘z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢z´z¢zšz‘»£»»£»»£»»£»»£»»£»»£»O»‘´z¢z´z¢z´z¢z´z¢z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeœë’z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥z¹z¥zO‘»»£»»£»»£»»£»»£»»£»»£»»£»œ’z¹z¥z¹z¥z¹z¥z½z§z½z§z½z§z½z§z½z§z§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBežë”z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§z½z§zžz”»£»»£»»£»»£»»£»»£»»£»»£»»£»ž”z½z§z½z§z½z§zÁzªzÁzªzÁzªzÁzªzÁzªz§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe¡ë•zÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzÁzªzO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»¡•zÁzªzÁzªzÁzªzŠm¶mŠm¶mŠm¶mŠm¶mŠm¶m§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe…ë›mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶mŠm¶m€€»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»…›mŠm¶mŠm¶mŠm¶mŽl¹lŽl¹lŽl¹lŽl¹lŽl¹l‡lœëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaëŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹lŽl¹l‡lœ»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»‡œlŽl¹lŽl¹lŽl¹l’l»l’l»l’l»l’l»l’l»l‰lžëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë’l»l’l»l’l»l’l»l’l»l’l»l’l»l’l»l’l»lO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»‰žl’l»l’l»l’l»l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe‹ëŸl—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l—l¾l‹lŸ»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘—l¾l—l¾l—l¾l›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe�ë l›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl›lÁl›lÁlO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘›lÁl›lÁl›lÁlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃl�l¢ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaëŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃlŸlÃl�l¢»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘ŸlÃlŸlÃlŸlÃl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe’ë£l£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆl£lÆlO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘£lÆl£lÆl£lÆl¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk”k¤ëÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe§eaë¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk¨kÈk€€»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»O»‘¨kÈk¨kÈk¨kÈk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe–ë¦k¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk¬kËk–k¦»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»–¦k¬kËk¬kËk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk˜k§ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ë°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎk°kÎkO‘»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»»£»˜§k°kÎk°kÎkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkšk¨ë§ëaeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBeÍeBe€ë€ëµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkµkÐkšk¨€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€€š¨kµkÐkµkÐk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk�k©ë€ë€ëÍeBeÍeBeÍeBeÍeBe§eaë€ë€ë¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk¹kÓk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk€ë€ë€ë€ë€ë€ë€ë€ëŸë«k½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖk½kÖkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÂkØkÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÆjÛjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞjÊjÞj \ No newline at end of file diff --git a/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c b/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c new file mode 100644 index 00000000000..93a50b6f661 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/main/async_color_convert_example_main.c @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "mbedtls/base64.h" +#include "esp_async_color_convert.h" +#include "esp_check.h" +#include "esp_heap_caps.h" + +#define EXAMPLE_WIDTH 96 +#define EXAMPLE_HEIGHT 64 +#define EXAMPLE_BASE64_CHUNK_LEN 96 + +/* These linker symbols are generated automatically for the file added by + * EMBED_FILES in CMakeLists.txt. They let the example treat the embedded + * raw .yuv asset as a byte array stored in flash. */ +extern const uint8_t sample_96x64_uyvy_yuv_start[] asm("_binary_sample_96x64_uyvy_yuv_start"); +extern const uint8_t sample_96x64_uyvy_yuv_end[] asm("_binary_sample_96x64_uyvy_yuv_end"); + +static void print_base64_payload(const unsigned char *encoded, size_t encoded_len) +{ + /* The payload is split into short lines so the UART log stays easy to + * parse from pytest and less likely to be damaged by very long lines. */ + printf("IMAGE_BASE64_BEGIN\n"); + for (size_t offset = 0; offset < encoded_len; offset += EXAMPLE_BASE64_CHUNK_LEN) { + size_t chunk_len = encoded_len - offset; + if (chunk_len > EXAMPLE_BASE64_CHUNK_LEN) { + chunk_len = EXAMPLE_BASE64_CHUNK_LEN; + } + printf("IMAGE_BASE64 %.*s\n", (int)chunk_len, (const char *)&encoded[offset]); + } + printf("IMAGE_BASE64_END\n"); +} + +void app_main(void) +{ + /* UYVY422 stores 2 bytes per pixel on average, while BGR/RGB888 uses + * 3 bytes per pixel. The example keeps the image size small so the + * buffers and UART payload stay beginner-friendly. */ + const size_t pixel_num = EXAMPLE_WIDTH * EXAMPLE_HEIGHT; + const size_t yuv422_size = pixel_num * 2; + const size_t rgb888_size = pixel_num * 3; + const size_t embedded_size = sample_96x64_uyvy_yuv_end - sample_96x64_uyvy_yuv_start; + + printf("Loading embedded UYVY image from flash...\n"); + printf("Embedded image size: %zu bytes\n", embedded_size); + assert(embedded_size == yuv422_size); + + /* The destination buffer still needs DMA-capable internal RAM because + * DMA2D writes the converted pixels into this memory region. */ + uint8_t *dst_bgr = heap_caps_aligned_calloc(64, 1, rgb888_size, + MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + assert(dst_bgr); + + async_color_convert_config_t config = { + .backlog = 1, // because we use the blocking API, so only need 1 in-flight request at most + .dma_burst_size = 16, + }; + async_color_convert_handle_t conv_hdl = NULL; + + /* Install the async color convert driver with the DMA2D backend. + * The returned handle is used by later conversion requests. */ + ESP_ERROR_CHECK(esp_async_color_convert_install_dma2d(&config, &conv_hdl)); + + /* This request describes one full-frame conversion: + * - source buffer: UYVY422 image + * - destination buffer: BGR24 image + * - stride/height: layout of each image in memory + * - copy_width/copy_height: region to convert */ + async_color_convert_request_t req_yuv_to_bgr = { + /* DMA2D can read the source image directly from mapped flash, so the + * example does not need an extra CPU copy into internal RAM first. */ + .src_buffer = sample_96x64_uyvy_yuv_start, + .src_stride = EXAMPLE_WIDTH, + .src_height = EXAMPLE_HEIGHT, + .src_x = 0, + .src_y = 0, + .dst_buffer = dst_bgr, + .dst_stride = EXAMPLE_WIDTH, + .dst_height = EXAMPLE_HEIGHT, + .dst_x = 0, + .dst_y = 0, + .copy_width = EXAMPLE_WIDTH, + .copy_height = EXAMPLE_HEIGHT, + .src_color_format = ESP_COLOR_FOURCC_UYVY, + .dst_color_format = ESP_COLOR_FOURCC_BGR24, + .color_conv_std = COLOR_CONV_STD_RGB_YUV_BT601, + }; + + printf("Converting UYVY422 -> RGB888...\n"); + /* This example uses the blocking API for simplicity: the call returns only + * after the hardware conversion is finished and dst_bgr contains the result. */ + ESP_ERROR_CHECK(esp_color_convert_blocking(conv_hdl, &req_yuv_to_bgr, -1)); + printf("Converted image size: %zu bytes\n", rgb888_size); + + /* Base64 turns the binary BGR image into printable ASCII so it can be + * safely transported through the serial console and reconstructed by pytest. */ + size_t encoded_len = 0; + int ret = mbedtls_base64_encode(NULL, 0, &encoded_len, dst_bgr, rgb888_size); + ESP_ERROR_CHECK((ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) ? ESP_OK : ESP_FAIL); + unsigned char *encoded = calloc(encoded_len + 1, 1); + assert(encoded); + ESP_ERROR_CHECK(mbedtls_base64_encode(encoded, encoded_len + 1, &encoded_len, dst_bgr, rgb888_size) == 0 ? ESP_OK : ESP_FAIL); + + /* IMAGE_META plus the chunked IMAGE_BASE64 lines form a tiny text protocol + * that the pytest script understands and converts back into a PPM file. */ + printf("IMAGE_META width=%u height=%u format=BGR24 encoding=base64\n", EXAMPLE_WIDTH, EXAMPLE_HEIGHT); + print_base64_payload(encoded, encoded_len); + printf("Async color convert visual demo done.\n"); + + ESP_ERROR_CHECK(esp_async_color_convert_uninstall(conv_hdl)); + free(encoded); + free(dst_bgr); +} diff --git a/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py b/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py new file mode 100644 index 00000000000..eb0d0fb0eb6 --- /dev/null +++ b/examples/peripherals/dma/async_color_convert/pytest_async_color_convert.py @@ -0,0 +1,168 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 + +import base64 +import hashlib +import logging +import re +from dataclasses import dataclass +from pathlib import Path + +import pytest +from pytest_embedded import Dut +from pytest_embedded_idf.utils import idf_parametrize +from pytest_embedded_idf.utils import soc_filtered_targets + +IMAGE_META_PATTERN = r'IMAGE_META width=(\d+) height=(\d+) format=(\w+) encoding=(\w+)' +IMAGE_META_RE = re.compile(rf'^{IMAGE_META_PATTERN}$') +IMAGE_CHUNK_RE = re.compile(r'^IMAGE_BASE64 ([A-Za-z0-9+/=]+)$') +IMAGE_OUTPUT_NAME = 'async_color_convert_result.ppm' +GOLDEN_IMAGE_NAME = 'golden_result.ppm' +EXPECTED_PIXEL_FORMAT = 'BGR24' +EXPECTED_ENCODING = 'base64' +PPM_MAGIC = b'P6' +PPM_MAX_VALUE = b'255' + + +@dataclass(frozen=True) +class ImageMetadata: + width: int + height: int + pixel_format: str + encoding: str + + +@dataclass(frozen=True) +class RgbImage: + width: int + height: int + pixels_rgb888: bytes + + def __post_init__(self) -> None: + expected_size = self.width * self.height * 3 + if len(self.pixels_rgb888) != expected_size: + raise ValueError(f'Expected {expected_size} RGB bytes, got {len(self.pixels_rgb888)}') + + +def parse_image_metadata(meta_line: str) -> ImageMetadata: + match = IMAGE_META_RE.match(meta_line) + if not match: + raise ValueError(f'Invalid image metadata line: {meta_line}') + + return ImageMetadata( + width=int(match.group(1)), + height=int(match.group(2)), + pixel_format=match.group(3), + encoding=match.group(4), + ) + + +def collect_base64_payload(dut: Dut) -> list[str]: + payload_lines: list[str] = [] + while True: + match = dut.expect(r'(IMAGE_BASE64_END|IMAGE_BASE64 [A-Za-z0-9+/=]+\r?\n)') + line = match.group(1).decode('utf-8').strip() + if line == 'IMAGE_BASE64_END': + return payload_lines + + chunk_match = IMAGE_CHUNK_RE.match(line) + assert chunk_match is not None + payload_lines.append(chunk_match.group(1)) + + +def _bgr24_to_rgb888(raw_bytes: bytes) -> bytes: + rgb_bytes = bytearray(len(raw_bytes)) + for offset in range(0, len(raw_bytes), 3): + blue, green, red = raw_bytes[offset : offset + 3] + rgb_bytes[offset : offset + 3] = (red, green, blue) + return bytes(rgb_bytes) + + +def _encode_ppm(image: RgbImage) -> bytes: + header = b'%s\n%d %d\n%s\n' % (PPM_MAGIC, image.width, image.height, PPM_MAX_VALUE) + return header + image.pixels_rgb888 + + +def _load_ppm(path: Path) -> RgbImage: + ppm_bytes = path.read_bytes() + header_match = re.match(rb'^P6\s+(\d+)\s+(\d+)\s+(\d+)\s', ppm_bytes) + if not header_match: + raise ValueError('Invalid PPM header') + + width = int(header_match.group(1)) + height = int(header_match.group(2)) + max_value = header_match.group(3) + if width <= 0 or height <= 0: + raise ValueError('Unsupported PPM dimensions') + if max_value != PPM_MAX_VALUE: + raise ValueError(f'Unsupported PPM max value: {max_value.decode("ascii", errors="replace")}') + + pixel_data = ppm_bytes[header_match.end() :] + expected_size = width * height * 3 + if len(pixel_data) != expected_size: + raise ValueError(f'Expected {expected_size} PPM pixel bytes, got {len(pixel_data)}') + + return RgbImage(width=width, height=height, pixels_rgb888=pixel_data) + + +def decode_bgr24_base64_image(metadata: ImageMetadata, payload_lines: list[str]) -> RgbImage: + if metadata.pixel_format != EXPECTED_PIXEL_FORMAT: + raise ValueError(f'Unsupported pixel format: {metadata.pixel_format}') + if metadata.encoding != EXPECTED_ENCODING: + raise ValueError(f'Unsupported payload encoding: {metadata.encoding}') + + raw_bytes = base64.b64decode(''.join(payload_lines), validate=True) + expected_size = metadata.width * metadata.height * 3 + if len(raw_bytes) != expected_size: + raise ValueError(f'Expected {expected_size} decoded bytes, got {len(raw_bytes)}') + + return RgbImage(width=metadata.width, height=metadata.height, pixels_rgb888=_bgr24_to_rgb888(raw_bytes)) + + +def save_ppm_artifact(image: RgbImage, output_path: Path) -> None: + output_path.parent.mkdir(parents=True, exist_ok=True) + try: + output_path.write_bytes(_encode_ppm(image)) + except OSError: + logging.exception('Failed to save async color convert artifact to %s', output_path) + return + + logging.info('Saved async color convert artifact to %s', output_path) + + +def rgb_pixel_digest(image: RgbImage) -> str: + digest = hashlib.sha256() + digest.update(image.width.to_bytes(4, 'big')) + digest.update(image.height.to_bytes(4, 'big')) + digest.update(image.pixels_rgb888) + return digest.hexdigest() + + +def assert_image_matches_golden(result_image: RgbImage, golden_path: Path) -> None: + assert golden_path.is_file(), f'Golden image not found: {golden_path}' + golden_image = _load_ppm(golden_path) + + assert rgb_pixel_digest(result_image) == rgb_pixel_digest(golden_image), ( + f'Generated image does not match golden file: {golden_path.name}' + ) + + +@pytest.mark.generic +@idf_parametrize('target', soc_filtered_targets('SOC_DMA2D_SUPPORTED == 1'), indirect=['target']) +def test_async_color_convert_example(dut: Dut) -> None: + dut.expect_exact('Loading embedded UYVY image from flash...') + dut.expect(r'Embedded image size: \d+ bytes') + dut.expect_exact('Converting UYVY422 -> RGB888...') + dut.expect(r'Converted image size: \d+ bytes') + + metadata = parse_image_metadata(dut.expect(IMAGE_META_PATTERN).group(0).decode('utf-8')) + + dut.expect_exact('IMAGE_BASE64_BEGIN') + payload_lines = collect_base64_payload(dut) + + result_image = decode_bgr24_base64_image(metadata, payload_lines) + output_path = Path(dut.logdir) / IMAGE_OUTPUT_NAME + save_ppm_artifact(result_image, output_path) + assert_image_matches_golden(result_image, Path(__file__).with_name(GOLDEN_IMAGE_NAME)) + + dut.expect_exact('Async color convert visual demo done.')