diff --git a/.github/workflows/build-run-applications.yml b/.github/workflows/build-run-applications.yml index a1c6a643b..f75288f4d 100644 --- a/.github/workflows/build-run-applications.yml +++ b/.github/workflows/build-run-applications.yml @@ -1,4 +1,4 @@ -name: Build ESP-BSP apps +name: Build and Run ESP-BSP apps # This job builds all examples and test_applications in this repo # Applications are selected for build based on changes files and dependencies defined in .build-test-rules.yml @@ -47,30 +47,30 @@ jobs: - idf_ver: "latest" parallel_count: 5 parallel_index: 5 - - idf_ver: "release-v5.1" - parallel_count: 2 - parallel_index: 1 - - idf_ver: "release-v5.1" - parallel_count: 2 - parallel_index: 2 - - idf_ver: "release-v5.2" - parallel_count: 2 - parallel_index: 1 - - idf_ver: "release-v5.2" - parallel_count: 2 - parallel_index: 2 - - idf_ver: "release-v5.3" - parallel_count: 2 - parallel_index: 1 - - idf_ver: "release-v5.3" - parallel_count: 2 - parallel_index: 2 - - idf_ver: "release-v5.4" - parallel_count: 2 - parallel_index: 1 - - idf_ver: "release-v5.4" - parallel_count: 2 - parallel_index: 2 + #- idf_ver: "release-v5.1" + # parallel_count: 2 + # parallel_index: 1 + #- idf_ver: "release-v5.1" + # parallel_count: 2 + # parallel_index: 2 + #- idf_ver: "release-v5.2" + # parallel_count: 2 + # parallel_index: 1 + #- idf_ver: "release-v5.2" + # parallel_count: 2 + # parallel_index: 2 + #- idf_ver: "release-v5.3" + # parallel_count: 2 + # parallel_index: 1 + #- idf_ver: "release-v5.3" + # parallel_count: 2 + # parallel_index: 2 + #- idf_ver: "release-v5.4" + # parallel_count: 2 + # parallel_index: 1 + #- idf_ver: "release-v5.4" + # parallel_count: 2 + # parallel_index: 2 runs-on: ubuntu-latest container: espressif/idf:${{ matrix.idf_ver }} steps: @@ -124,60 +124,60 @@ jobs: - runs-on: "esp-box-3" marker: "esp_box_3" target: "esp32s3" - - runs-on: "esp32_c3_lcdkit" - marker: "esp32_c3_lcdkit" - target: "esp32c3" - - runs-on: "esp32_p4_box" - marker: "esp32_p4_box" - target: "esp32p4" - - runs-on: "esp32_p4_function_ev_board" - marker: "esp32_p4_function_ev_board" - target: "esp32p4" - - runs-on: "esp32_s3_eye" - marker: "esp32_s3_eye" - target: "esp32s3" - - runs-on: "esp32_s3_lcd_ev_board" - marker: "esp32_s3_lcd_ev_board" - target: "esp32s3" - - runs-on: "esp32_s3_lcd_ev_board" - marker: "esp32_s3_lcd_ev_board_2" - target: "esp32s3" - - runs-on: "esp32_s3_usb_otg" - marker: "esp32_s3_usb_otg" - target: "esp32s3" - - runs-on: "esp_wrover_kit" - marker: "esp_wrover_kit" - target: "esp32" - - runs-on: "m5dial" - marker: "m5dial" - target: "esp32s3" - - runs-on: "m5stack_core" - marker: "m5stack_core" - target: "esp32" - - runs-on: "m5stack_core_2" - marker: "m5stack_core_2" - target: "esp32" - - runs-on: "m5stack_core_s3" - marker: "m5stack_core_s3" - target: "esp32s3" - - runs-on: "m5stack_core_s3" - marker: "m5stack_core_s3_se" - target: "esp32s3" - - runs-on: "esp32_azure_iot_kit" - marker: "esp32_azure_iot_kit" - target: "esp32" - - runs-on: "esp_bsp_devkit" - marker: "esp_bsp_devkit" - target: "esp32s3" - - runs-on: "esp_bsp_generic" - marker: "esp_bsp_generic" - target: "esp32s3" - - runs-on: "esp32_s3_korvo_2" - marker: "esp32_s3_korvo_2" - target: "esp32s3" - - runs-on: "m5_atom_s3" - marker: "m5_atom_s3" - target: "esp32s3" + #- runs-on: "esp32_c3_lcdkit" + # marker: "esp32_c3_lcdkit" + # target: "esp32c3" + #- runs-on: "esp32_p4_box" + # marker: "esp32_p4_box" + # target: "esp32p4" + #- runs-on: "esp32_p4_function_ev_board" + # marker: "esp32_p4_function_ev_board" + # target: "esp32p4" + #- runs-on: "esp32_s3_eye" + # marker: "esp32_s3_eye" + # target: "esp32s3" + #- runs-on: "esp32_s3_lcd_ev_board" + # marker: "esp32_s3_lcd_ev_board" + # target: "esp32s3" + #- runs-on: "esp32_s3_lcd_ev_board" + # marker: "esp32_s3_lcd_ev_board_2" + # target: "esp32s3" + #- runs-on: "esp32_s3_usb_otg" + # marker: "esp32_s3_usb_otg" + # target: "esp32s3" + #- runs-on: "esp_wrover_kit" + # marker: "esp_wrover_kit" + # target: "esp32" + #- runs-on: "m5dial" + # marker: "m5dial" + # target: "esp32s3" + #- runs-on: "m5stack_core" + # marker: "m5stack_core" + # target: "esp32" + #- runs-on: "m5stack_core_2" + # marker: "m5stack_core_2" + # target: "esp32" + #- runs-on: "m5stack_core_s3" + # marker: "m5stack_core_s3" + # target: "esp32s3" + #- runs-on: "m5stack_core_s3" + # marker: "m5stack_core_s3_se" + # target: "esp32s3" + #- runs-on: "esp32_azure_iot_kit" + # marker: "esp32_azure_iot_kit" + # target: "esp32" + #- runs-on: "esp_bsp_devkit" + # marker: "esp_bsp_devkit" + # target: "esp32s3" + #- runs-on: "esp_bsp_generic" + # marker: "esp_bsp_generic" + # target: "esp32s3" + #- runs-on: "esp32_s3_korvo_2" + # marker: "esp32_s3_korvo_2" + # target: "esp32s3" + #- runs-on: "m5_atom_s3" + # marker: "m5_atom_s3" + # target: "esp32s3" env: TEST_RESULT_NAME: test_results_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }} BENCHMARK_RESULT_NAME: benchmark_${{ matrix.runner.target }}_${{ matrix.runner.marker }}_${{ matrix.idf_ver }} @@ -197,7 +197,9 @@ jobs: env: PIP_EXTRA_INDEX_URL: "https://dl.espressif.com/pypi/" run: | - pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code + pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code opencv-python numpy + apt-get update && apt-get install -y libgl1 + apt-get update && apt-get install -y v4l-utils - name: Download latest results uses: actions/download-artifact@v4 with: @@ -216,6 +218,7 @@ jobs: benchmark_*.md benchmark_*.json benchmark.json + *.jpg - name: Check if benchmark files exist id: check_files run: | diff --git a/conftest.py b/conftest.py index 138488777..4ff2844a9 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,11 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import pytest +import cv2 +import subprocess +import numpy as np +import time +from pathlib import Path def pytest_generate_tests(metafunc): @@ -38,3 +43,99 @@ def pytest_collection_modifyitems(config, items): marker_option = "[" + config.getoption("-m") + "]" if marker_option not in item.nodeid: item.add_marker(pytest.mark.skip(reason="Not for selected params")) + + +def bsp_image_correction(image, width, height): + pts_src = np.float32([ + [160, 80], # top-left + [1044, 87], # top-right + [279, 720], # bottom-left + [913, 718] # bottom-right + ]) + + pts_dst = np.float32([ + [0, 0], + [width, 0], + [0, height], + [width, height] + ]) + + matrix = cv2.getPerspectiveTransform(pts_src, pts_dst) + warped = cv2.warpPerspective(image, matrix, (width, height)) + return warped + + +def bsp_fisheye_correction(image, width, height): + K = np.array([ + [width, 0, width / 2], # Focal length x + [0, width, height / 2], # Focal length y + [0, 0, 1]]) # Principal point + # [k1, k2, p1, p2, k3] - main is k1 a k2 + dist_coeffs = np.array([-0.3, 0.1, 0, 0, 0]) + + new_K, _ = cv2.getOptimalNewCameraMatrix(K, dist_coeffs, (width, height), 1, (width, height)) + map1, map2 = cv2.initUndistortRectifyMap(K, dist_coeffs, None, new_K, (width, height), 5) + undistorted = cv2.remap(image, map1, map2, interpolation=cv2.INTER_LINEAR) + + return undistorted + + +def bsp_capture_image(image_path, board): + # Enable auto-focus + # subprocess.run(["v4l2-ctl", "-d", "/dev/video0", "--set-ctrl=focus_auto=1"]) + # Manual focus + subprocess.run(["v4l2-ctl", "-d", "/dev/video0", "--set-ctrl=focus_auto=0"]) + subprocess.run(["v4l2-ctl", "-d", "/dev/video0", "--set-ctrl=focus_absolute=50"]) + # Manual exposition + subprocess.run(["v4l2-ctl", "-d", "/dev/video0", "--set-ctrl=exposure_auto=1"]) + subprocess.run(["v4l2-ctl", "-d", "/dev/video0", "--set-ctrl=exposure_absolute=1"]) + + # Return video from the first webcam on your computer. + cap = cv2.VideoCapture(0) + # Set FullHD resolution (1920x1080) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) + + # TODO: Camera calibration + + # reads frames from a camera + # ret checks return at each frame + ret, frame = cap.read() + if ret: + # Image rotation + frame = cv2.rotate(frame, cv2.ROTATE_180) + # TODO: Camera calibration / Perspective transform + # TODO: Change size image + # TODO: Crop image for {board} + + # correction image perspective and crop + frame = bsp_image_correction(frame, 1000, 884) + # correction of fisheye + frame = bsp_fisheye_correction(frame, 1000, 884) + # crop + frame = frame[30:848, 38:980] + + # Save image + cv2.imwrite(image_path, frame) + print(f"Image saved {image_path}") + else: + print("Cannot save image.") + + # Close the window / Release webcam + cap.release() + + +def bsp_test_image(board, example, expectation): + image_file = f"snapshot_{board}_{example}.jpg" + bsp_capture_image(image_file, board) + + +@pytest.fixture() +def bsp_test_capture_image(request): + board = request.node.callspec.id + path = Path(str(request.node.fspath)) + test_name = path.parent.name + yield + time.sleep(5) # wait 5 seconds + print(f"Capturing image for: {board}") + bsp_test_image(board, test_name, "") diff --git a/examples/display/pytest_display.py b/examples/display/pytest_display.py index cc5ea168b..4d15aa6d0 100644 --- a/examples/display/pytest_display.py +++ b/examples/display/pytest_display.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: CC0-1.0 import pytest @@ -20,6 +20,6 @@ @pytest.mark.m5stack_core_s3 @pytest.mark.m5stack_core_s3_se @pytest.mark.m5_atom_s3 -def test_display_example(dut: Dut) -> None: +def test_display_example(dut: Dut, bsp_test_capture_image) -> None: dut.expect_exact('example: Display LVGL animation') dut.expect_exact('main_task: Returned from app_main()') diff --git a/examples/display_lvgl_demos/pytest_display_lvgl_demos.py b/examples/display_lvgl_demos/pytest_display_lvgl_demos.py index daf7a05a4..cadec3ebc 100644 --- a/examples/display_lvgl_demos/pytest_display_lvgl_demos.py +++ b/examples/display_lvgl_demos/pytest_display_lvgl_demos.py @@ -13,6 +13,6 @@ @pytest.mark.m5dial @pytest.mark.m5stack_core_s3 @pytest.mark.m5stack_core_s3_se -def test_display_example(dut: Dut) -> None: +def test_display_example(dut: Dut, bsp_test_capture_image) -> None: dut.expect_exact('app_main: Display LVGL demo') dut.expect_exact('main_task: Returned from app_main()')