diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1eb8b54 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: cnadler86 +#patreon: # Replace with a single Patreon username +#open_collective: # Replace with a single Open Collective username +#ko_fi: # Replace with a single Ko-fi username +#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +#liberapay: # Replace with a single Liberapay username +#issuehunt: # Replace with a single IssueHunt username +#lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +#polar: # Replace with a single Polar username +#buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +#thanks_dev: # Replace with a single thanks.dev username +#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..cf131bf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,26 @@ +--- +name: Bug report +about: Report a bug and help improving the API +title: '' +labels: bug +assignees: cnadler86 + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Code to reproduce + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment:** + - Board: e.g. ESP32S3 XIAO + - Camera drivers version: e.g. 2.0.15 + +**Additional context** +Add any other context about the problem here. + +Note: Also disconnect any peripherals to the board and try without them before raising a bug. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/general-issue.md b/.github/ISSUE_TEMPLATE/general-issue.md new file mode 100644 index 0000000..3e6d63d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/general-issue.md @@ -0,0 +1,11 @@ +--- +name: General issue +about: General issue +title: '' +labels: '' +assignees: '' + +--- + +PLEASE, read the documentation, search [previous issues](https://github.com/cnadler86/micropython-camera-API/issues?q=is%3Aissue) and/or ask ChatGPT before! +Also disconnect any peripherals to the board and try without them before raising an issue. diff --git a/.github/workflows/ESP32.yml b/.github/workflows/ESP32.yml index ce5a7d1..3ad8407 100644 --- a/.github/workflows/ESP32.yml +++ b/.github/workflows/ESP32.yml @@ -3,11 +3,19 @@ name: ESP32 on: workflow_dispatch: push: + branches: + - '**' paths: - 'src/**' + - '.github/workflows/*.yml' + tags-ignore: + - 'v*' pull_request: branches: - master + paths: + - 'src/**' + - '.github/workflows/*.yml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -45,16 +53,39 @@ jobs: sudo apt-get update sudo apt-get install -y git wget flex bison gperf python3 python3-pip python3-venv cmake ninja-build ccache libffi-dev libssl-dev dfu-util libusb-1.0-0 + # Download and set up ESP-IDF (if not cached) + - name: Set up ESP-IDF + id: export-idf + if: steps.cache_esp_idf.outputs.cache-hit != 'true' + run: | + cd ~ + git clone --depth 1 --branch v5.4.2 https://github.com/espressif/esp-idf.git + # git clone --depth 1 --branch ${{ env.IDF_VER }} https://github.com/espressif/esp-idf.git + git -C esp-idf submodule update --init --recursive --filter=tree:0 + cd esp-idf + ./install.sh all + cd components + # latest_cam_driver=$(curl -s https://api.github.com/repos/espressif/esp32-camera/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') + # git clone --depth 1 --branch $latest_cam_driver https://github.com/espressif/esp32-camera.git + git clone https://github.com/cnadler86/esp32-camera.git + cd ~/esp-idf/ + source ./export.sh + cd ~ + git clone https://github.com/espressif/esp-adf-libs.git + cp -r ~/esp-adf-libs/esp_new_jpeg ~/esp-idf/components/ + # Clone the latest MicroPython release (if not cached) - name: Clone MicroPython latest release id: clone-micropython if: steps.cache_esp_idf.outputs.cache-hit != 'true' run: | echo "Cloning MicroPython release: $MPY_RELEASE" + cd ~/esp-idf/ + source ./export.sh cd ~ git clone --depth 1 --branch ${{ env.MPY_RELEASE }} https://github.com/micropython/micropython.git cd micropython - git submodule update --init --depth 1 + # git submodule update --init --depth 1 cd mpy-cross make cd ~/micropython/ports/esp32 @@ -62,22 +93,6 @@ jobs: echo "Micropython setup successfully" source ~/micropython/tools/ci.sh && echo "IDF_VER=$IDF_VER" >> $GITHUB_ENV - # Download and set up ESP-IDF (if not cached) - - name: Set up ESP-IDF - id: export-idf - if: steps.cache_esp_idf.outputs.cache-hit != 'true' - run: | - cd ~ - git clone --depth 1 --branch release/v5.2 https://github.com/espressif/esp-idf.git - # git clone --depth 1 --branch ${{ env.IDF_VER }} https://github.com/espressif/esp-idf.git - git -C esp-idf submodule update --init --recursive --filter=tree:0 - cd esp-idf - ./install.sh all - cd components - git clone https://github.com/espressif/esp32-camera - cd ~/esp-idf/ - source ./export.sh - # Dynamically create jobs for each board build: needs: setup-environment @@ -85,12 +100,30 @@ jobs: strategy: fail-fast: false matrix: - board: + board: - ESP32_GENERIC-SPIRAM - ESP32_GENERIC_S2 - ESP32_GENERIC_S3 - ESP32_GENERIC_S3-SPIRAM_OCT - - ESP32_GENERIC_S3-FLASH_4M + - ESP32_GENERIC-SPIRAM@WROVER_KIT + - ESP32_GENERIC-SPIRAM@ESP_EYE + - ESP32_GENERIC-SPIRAM@M5STACK_PSRAM + - ESP32_GENERIC-SPIRAM@M5STACK_V2_PSRAM + - ESP32_GENERIC-SPIRAM@M5STACK_WIDE + - ESP32_GENERIC-SPIRAM@M5STACK_ESP32CAM + - ESP32_GENERIC-SPIRAM@M5STACK_UNITCAM + - ESP32_GENERIC-SPIRAM@AI_THINKER + - ESP32_GENERIC-SPIRAM@TTGO_T_JOURNAL + - ESP32_GENERIC-SPIRAM@TTGO_T_CAMERA_PLUS + - ESP32_GENERIC_S3-SPIRAM_OCT@M5STACK_CAMS3_UNIT + - ESP32_GENERIC_S3-SPIRAM_OCT@M5STACK_ATOM_S3R + - ESP32_GENERIC_S3-SPIRAM_OCT@XIAO_ESP32S3 + - ESP32_GENERIC_S3-SPIRAM_OCT@ESP32S3_CAM_LCD + - ESP32_GENERIC_S3-SPIRAM_OCT@ESP32S3_EYE + - ESP32_GENERIC_S3-SPIRAM_OCT@FREENOVE_ESP32S3_CAM + - ESP32_GENERIC_S3-SPIRAM_OCT@DFRobot_ESP32S3 + - ESP32_GENERIC_S3-SPIRAM_OCT@NEW_ESPS3_RE1_0 + - ESP32_GENERIC_S3-SPIRAM_OCT@XENOIONEX steps: # Get the latest MicroPython release @@ -123,21 +156,41 @@ jobs: # Build MicroPython for each board - name: Build MicroPython run: | + cd ${{ github.workspace }} + cd .. + git clone https://github.com/cnadler86/mp_jpeg.git + cd ~/esp-idf/components/esp32-camera + CAM_DRIVER=$(git describe --tags --always --dirty) cd ~/micropython/ports/esp32 source ~/esp-idf/export.sh - # Check if a variant is defined and adjust the make command - IFS='-' read -r BOARD_NAME BOARD_VARIANT <<< "${{ matrix.board }}" + # Check if a variant is defined and adjust the idf.py command + IFS='@' read -r BUILD_TARGET CAMERA_MODEL <<< "${{ matrix.board }}" + IFS='-' read -r BOARD_NAME BOARD_VARIANT <<< "${BUILD_TARGET}" + if [ -n "${BOARD_VARIANT}" ]; then - make USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake BOARD=$BOARD_NAME BOARD_VARIANT=$BOARD_VARIANT all + IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -D MICROPY_BOARD_VARIANT=$BOARD_VARIANT -B build-$BUILD_TARGET -D MP_CAMERA_DRIVER_VERSION=$CAM_DRIVER" + else + IDF_CMD="idf.py -D MICROPY_BOARD=$BOARD_NAME -D USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake -B build-$BUILD_TARGET -D MP_CAMERA_DRIVER_VERSION=$CAM_DRIVER" + fi + if [ -n "${CAMERA_MODEL}" ]; then + echo "FW_NAME=${CAMERA_MODEL}" >> $GITHUB_ENV + FINAL_CMD="${IDF_CMD} -D MICROPY_CAMERA_MODEL=${CAMERA_MODEL} build" else - make USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake BOARD=$BOARD_NAME all + echo "FW_NAME=${BUILD_TARGET}" >> $GITHUB_ENV + FINAL_CMD="${IDF_CMD} build" fi - mv ~/micropython/ports/esp32/build-${{ matrix.board }}/firmware.bin ~/${{ matrix.board }}.bin + make USER_C_MODULES=${{ github.workspace }}/src/micropython.cmake BOARD=$BOARD_NAME submodules + echo "Running command: $FINAL_CMD" + eval $FINAL_CMD + cd ~/micropython/ports/esp32/build-${BUILD_TARGET} + python ../makeimg.py sdkconfig bootloader/bootloader.bin partition_table/partition-table.bin micropython.bin firmware.bin micropython.uf2 + mkdir -p ~/artifacts + mv ~/micropython/ports/esp32/build-${BUILD_TARGET}/firmware.bin ~/artifacts/firmware.bin - name: Upload firmware artifact uses: actions/upload-artifact@v4 with: - name: firmware-${{ matrix.board }} - path: ~/${{ matrix.board }}.bin - retention-days: 90 \ No newline at end of file + name: mpy_cam-${{ env.MPY_RELEASE }}-${{ env.FW_NAME }} + path: ~/artifacts/** + retention-days: 5 \ No newline at end of file diff --git a/README.md b/README.md index 3c54928..3768354 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,84 @@ # Camera API for micropython + [![ESP32 Port](https://github.com/cnadler86/micropython-camera-API/actions/workflows/ESP32.yml/badge.svg)](https://github.com/cnadler86/micropython-camera-API/actions/workflows/ESP32.yml) -This project aims to support cameras in different ports in micropython, starting with the ESP32-Port and omnivision (OV2640 & OV5640) cameras. The project implements a general API for cameras in micropython (such as circuitpython have done). -At the moment, this is a micropython user module, but it might get in the micropython repo in the future. -The API is stable, but it might change without previous anounce. +This project aims to support various cameras (e.g. OV2640, OV5640) on different MicroPython ports, starting with the ESP32 port. The project implements a general API, has precompiled firmware images and supports a lot of cameras out of the box. Defaults are set to work with the OV2640. + +The API is stable, but it might change. Please look into the release section for the latest changes. + +I tied to make things as easy as possible. If you find this project useful, please consider donating to support my work. Thanks! +[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/cnadler) + +If you want to play arround with AI, take a look at the [micropython binding for esp-dl](https://github.com/cnadler86/mp_esp_dl_models). + +## Content + +- [Precompiled firmware (the easy way)](#Precompiled-firmware-the-easy-way) +- [Using the API](#using-the-api) + - [Importing the camera module](#importing-the-camera-module) + - [Creating a camera object](#creating-a-camera-object) + - [Initializing the camera](#initializing-the-camera) + - [Capture image](#capture-image) + - [Camera reconfiguration](#camera-reconfiguration) + - [Freeing the buffer](#freeing-the-buffer) + - [Is a frame available](#is-frame-available) + - [Additional methods](#additional-methods) + - [Additional information](#additional-information) +- [Build your custom firmware](#build-your-custom-firmware) + - [Setting up the build environment (DIY method)](#setting-up-the-build-environment-diy-method) + - [Add camera configurations to your board (optional, but recommended)](#add-camera-configurations-to-your-board-optional-but-recommended) + - [Build the API](#build-the-api) +- [Notes](#notes) +- [Benchmark](#benchmark) +- [Troubleshooting](#troubleshooting) +- [Donate](#donate) + +## Precompiled firmware (the easy way) -## Precomiled FW (the easy way) -If you are not familiar with building a custom firmware, you can go to the [releases](https://github.com/cnadler86/micropython-camera-API/releases) page and download one of the generic FWs that suits your board. +If you are not familiar with building custom firmware, visit the [releases](https://github.com/cnadler86/micropython-camera-API/releases) page to download firmware that suits your board. **There are over 20 precompiled board images with the latest micropython!** +To flash the firmware, you can use for example [https://esp.huhn.me/](https://esp.huhn.me/). + +These firmware binaries also include the [mp_jpeg modul](https://github.com/cnadler86/mp_jpeg) to encode/decode JPEGs. ## Using the API + +### Importing the camera module + +There general way of using the api is as follow: + ```python from camera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling +``` + +There is also a camera class with asyncio support! To use it, you need to import from the acamera package: + +```python +from acamera import Camera, GrabMode, PixelFormat, FrameSize, GainCeiling +``` + +### Creating a camera object + +Camera construction using defaults. This is the case if you are using a **non-generic** precompiled firmware or if you specified the camera model or pins in mpconfigboard.h during your build. Then you can just call the construction without any keyword arguments. + +```python +cam = Camera() +``` + +or with relevant keyword arguments: + +```python +cam = Camera(pixel_format=PixelFormat.JPEG, + frame_size=FrameSize.QVGA, + jpeg_quality=90, + fb_count=2, + grab_mode=GrabMode.WHEN_EMPTY) +``` + +When using a **generic** precompiled firmware, the camera constructor requires specific keyword arguments (namely the camera pins to be used). +These pins are just examples and if used as-is, a error will occur. Adapt them to your board! -# Camera construction and initialization -camera = Camera( +```python +cam = Camera( data_pins=[1,2,3,4,5,6,7,8], vsync_pin=9, href_pin=10, @@ -24,46 +89,188 @@ camera = Camera( xclk_freq=20000000, powerdown_pin=-1, reset_pin=-1, - pixel_format=PixelFormat.RGB565, - frame_size=FrameSize.QVGA, - jpeg_quality=15, - fb_count=1, - grab_mode=GrabMode.WHEN_EMPTY ) +``` + +**Keyword arguments for construction:** + +- data_pins: List of data pins +- pclk_pin: Pixel clock pin +- vsync_pin: VSYNC pin +- href_pin: HREF pin +- sda_pin: SDA pin +- scl_pin: SCL pin +- xclk_pin: XCLK pin ( set to -1, if you have an external clock source) +- xclk_freq: XCLK frequency in Hz (consult the camera sensor specification) +- powerdown_pin: Powerdown pin (set to -1 if not used) +- reset_pin: Reset pin (set to -1 if not used) +- pixel_format: Pixel format as PixelFormat +- frame_size: Frame size as FrameSize +- jpeg_quality: JPEG quality +- fb_count: Frame buffer count +- grab_mode: Grab mode as GrabMode +- init: Initialize camera at construction time (default: True) -#Camera construction using defaults (if you specified them in mpconfigboard.h) -camera = Camera() +**Default values:** -# Capture image -img = camera.capture() +The following keyword arguments have default values: -# Camera reconfiguration -camera.reconfigure(pixel_format=PixelFormat.JPEG,frame_size=FrameSize.QVGA,grab_mode=GrabMode.LATEST, fb_count=2) -camera.set_quality(10) +- xclk_freq: 20MHz // Default for OV2640 (normally either 10 MHz or 20 MHz). Plase adapt it to your camera sensor. +- frame_size: QQVGA +- pixel_format: RGB565 +- jpeg_quality: 85 // Quality of JPEG output in percent. Higher means higher quality. +- powerdown_pin and reset_pin: -1 ( = not used/available/needed) +- fb_count: + - 2 for ESP32S3 boards + - 1 for all other +- grab_mode: + - LATEST for ESP32S3 boards + - WHEN_EMPTY for all other + +### Initializing the camera + +```python +cam.init() ``` -You can get and set sensor properties by the respective methods (e.g. camera.get_brightness() or camera.set_vflip(True). See autocompletitions in Thonny in order to see the list of methods. -If you want more insides in the methods and what they actually do, you can find a very good documentation [here](https://docs.circuitpython.org/en/latest/shared-bindings/espcamera/index.html). -Notice that for the methods in here you need to prefix a get/set, depending that you want to do. +### Capture image -## Build your custom FW -### Setup build environment (the DIY way) -To build the project, follow the following instructions: -- [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/v5.2.3/esp32/get-started/index.html): I used version 5.2.2, but it might work with other versions (see notes). -- Clone the micropython repo and this repo in a folder, e.g. "MyESPCam". I used the actual micropython master branch (between v1.23 and before 1.24). -- You will have to add the ESP32-Camera driver (I used v2.0.12). To do this, add the following to the respective idf_component.yml file (e.g. in micropython/ports/esp32/main_esp32s3/idf_component.yml): +The general way of capturing an image is calling the `capture` method: + +```python +img = cam.capture() ``` + +Each time you call the method, you will receive a new frame as memoryview. +You can convert it to bytes and free the memoryview buffer, so a new frame can be pushed to it. This will reduce the image latency but need more RAM. (see [freeing the buffer](#freeing-the-buffer)) + +The probably better way of capturing an image would be in an asyncio-loop: + +```python +img = await cam.acapture() #To access this method, you need to import from acamera +``` + +Please consult the [asyncio documentation](https://docs.micropython.org/en/latest/library/asyncio.html), if you have questions on this. + +### Camera reconfiguration + +```python +cam.reconfigure(pixel_format=PixelFormat.JPEG,frame_size=FrameSize.QVGA,grab_mode=GrabMode.LATEST, fb_count=2) +``` + +Keyword arguments for reconfigure + +- frame_size: Frame size as FrameSize (optional) +- pixel_format: Pixel format as PixelFormat(optional) +- grab_mode: Grab mode as GrabMode (optional) +- fb_count: Frame buffer count (optional) + +### Freeing the buffer + +This is optional, but can reduce the latency of capturing an image in some cases (especially with fb_count = 1).. + +```python +Img = bytes(cam.capture()) #Create a new bytes object from the memoryview (because we want to free it afterwards) +cam.free_buffer() # This will free the captured image or in other words "deleting"" the memoryview +``` + +### Is frame available + +```python +Img = bytes(cam.capture()) +cam.free_buffer() +while not cam.frame_available(): + +print('The frame is available now. You can grab the image by the capture method =)') +``` + +This gives you the possibility of creating an asynchronous application without using asyncio. + +### Additional methods and examples + +Here are just a few examples: + +```python +cam.set_quality(90) # The quality goes from 0% to 100%, meaning 100% is the highest but has probably no compression +camera.get_brightness() +camera.set_vflip(True) #Enable vertical flip +``` + +See autocompletions in Thonny in order to see the list of methods. +If you want more insights in the methods and what they actually do, you can find a very good documentation [here](https://docs.circuitpython.org/en/latest/shared-bindings/espcamera/index.html). +Note that each method requires a "get_" or "set_" prefix, depending on the desired action. + +Take also a look in the examples folder. + +To get the version of the camera driver used: + +```python +import camera +vers = camera.Version() +``` + +### Additional information + +The firmware images support the following cameras out of the box, but is therefore big: OV7670, OV7725, OV2640, OV3660, OV5640, NT99141, GC2145, GC032A, GC0308, BF3005, BF20A6, SC030IOT + +## Build your custom firmware + +### Setting up the build environment (DIY method) + +To build the project, follow these instructions: + +- [ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/v5.2.3/esp32/get-started/index.html): I used version 5.2.3, but it might work with other versions (see notes). +- Clone the micropython repo and this repo in a folder, e.g. "MyESPCam". MicroPython version 1.24 or higher is required (at least commit 92484d8). +- You will have to add the ESP32-Camera driver from my fork. To do this, add the following to the respective idf_component.yml file (e.g. in micropython/ports/esp32/main_esp32s3/idf_component.yml): + +```yml espressif/esp32-camera: - git: https://github.com/espressif/esp32-camera + git: https://github.com/cnadler86/esp32-camera.git ``` -You can also clone the https://github.com/espressif/esp32-camera repository inside the esp-idf/components folder instead of altering the idf_component.yml file. -### Add camera configurations to your board (Optional, but recomended) -To make things easier, add the following lines to your board config-file "mpconfigboard.h" with the respective pins and camera parameters. Otherwise you will need to pass all parameters during construction. -Don't forget the empty line at the buttom. -Example for xiao sense: +Alternatively, you can clone the repository inside the esp-idf/components folder instead of altering the idf_component.yml file. + +### Add camera configurations to your board (optional, but recommended) + +#### Supported camera models + +This project supports various boards with camera interface out of the box. You typically only need to add a single line to your board config file ("mpconfigboard.h). +Example (don't forget to add the empty line at the bottom): + +```c +#define MICROPY_CAMERA_MODEL_WROVER_KIT 1 ``` + +Below is a list of supported `MICROPY_CAMERA_MODEL_xxx` definitions: + +- MICROPY_CAMERA_MODEL_WROVER_KIT - [ESP32-WROVER-KIT](https://www.espressif.com/en/products/devkits/esp32-wrover-kit/overview) +- MICROPY_CAMERA_MODEL_ESP_EYE - [ESP-EYE](https://www.espressif.com/en/products/devkits/esp-eye/overview) +- MICROPY_CAMERA_MODEL_M5STACK_PSRAM - [M5Stack PSRAM](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_UNITCAM - [M5Stack UnitCam](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_V2_PSRAM - [M5Stack V2 PSRAM](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_WIDE - [M5Stack Wide](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_ESP32CAM - [M5Stack ESP32CAM](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_CAMS3_UNIT - [M5Stack CAMS3 Unit](https://shop.m5stack.com/collections/m5-cameras) +- MICROPY_CAMERA_MODEL_M5STACK_ATOM_S3R - [M5Stack Atom S3R](https://docs.m5stack.com/en/core/AtomS3R-M12) +- MICROPY_CAMERA_MODEL_AI_THINKER - [AI-Thinker ESP32-CAM] +- MICROPY_CAMERA_MODEL_XIAO_ESP32S3 - [XIAO ESP32S3](https://www.seeedstudio.com/xiao-series-page) +- MICROPY_CAMERA_MODEL_ESP32_MP_CAMERA_BOARD - [ESP32 MP Camera Board] +- MICROPY_CAMERA_MODEL_ESP32S3_CAM_LCD - [ESP32-S3 CAM LCD] +- MICROPY_CAMERA_MODEL_ESP32S3_EYE - [ESP32-S3 EYE](https://www.espressif.com/en/products/devkits/esp32-s3-eye/overview) +- MICROPY_CAMERA_MODEL_FREENOVE_ESP32S3_CAM - [Freenove ESP32-S3 CAM](https://store.freenove.com/products/fnk0085) +- MICROPY_CAMERA_MODEL_DFRobot_ESP32S3 - [DFRobot ESP32-S3](https://www.dfrobot.com/) +- MICROPY_CAMERA_MODEL_TTGO_T_JOURNAL - [TTGO T-Journal](https://www.lilygo.cc/products/) +- MICROPY_CAMERA_MODEL_TTGO_T_CAMERA_PLUS - [TTGO T-Camera Plus](https://www.lilygo.cc/products/) +- MICROPY_CAMERA_MODEL_NEW_ESPS3_RE1_0 - [New ESP32-S3 RE:1.0] +- MICROPY_CAMERA_MODEL_XENOIONEX - [Xenoionex] + +#### For unsupported camera models + +If your board is not yet supported, add the following lines to your board config-file "mpconfigboard.h" with the respective pins and camera parameters. Otherwise, you will need to pass all parameters during construction. +Example for Xiao sense: + +```c #define MICROPY_CAMERA_PIN_D0 (15) #define MICROPY_CAMERA_PIN_D1 (17) #define MICROPY_CAMERA_PIN_D2 (18) @@ -78,26 +285,75 @@ Example for xiao sense: #define MICROPY_CAMERA_PIN_XCLK (10) #define MICROPY_CAMERA_PIN_PWDN (-1) #define MICROPY_CAMERA_PIN_RESET (-1) -#define MICROPY_CAMERA_PIN_SIOD (40) -#define MICROPY_CAMERA_PIN_SIOC (39) -#define MICROPY_CAMERA_XCLK_FREQ (20000000) -#define MICROPY_CAMERA_FB_COUNT (2) -#define MICROPY_CAMERA_JPEG_QUALITY (10) -#define MICROPY_CAMERA_GRAB_MODE (1) +#define MICROPY_CAMERA_PIN_SIOD (40) // SDA +#define MICROPY_CAMERA_PIN_SIOC (39) // SCL +#define MICROPY_CAMERA_XCLK_FREQ (20000000) // Frequencies are normally either 10 MHz or 20 MHz +#define MICROPY_CAMERA_FB_COUNT (2) // The value is between 1 (slow) and 2 (fast, but more load on CPU and more ram usage) +#define MICROPY_CAMERA_JPEG_QUALITY (85) // Quality of JPEG output in percent. Higher means higher quality. +#define MICROPY_CAMERA_GRAB_MODE (1) // 0=WHEN_EMPTY (might have old data, but less resources), 1=LATEST (best, but more resources) ``` +#### Customize additional camera settings + +If you want to customize additional camera setting or reduce the firmware size by removing support for unused camera sensors, then take a look at the kconfig file of the esp32-camera driver and specify these on the sdkconfig file of your board. + +#### (Optional) Add the mp_jpeg module + +If you also want to include the [mp_jpeg module](https://github.com/cnadler86/mp_jpeg) in your build, clone the mp_jpeg repo at the same level and folder as the mp_camera_api repo and meet the requirements from the mp_jpeg repo. ### Build the API + To build the project, you could do it the following way: ```bash -$ . /esp-idf/export.sh -$ cd MyESPCam/micropython/ports/esp32 -$ make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= clean -$ make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= submodules -$ make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= all +. /esp-idf/export.sh +cd MyESPCam/micropython/ports/esp32 +make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= clean +make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= submodules +make USER_C_MODULES=../../../../micropython-camera-API/src/micropython.cmake BOARD= all ``` -if you experience problems, visit [MicroPython external C modules](https://docs.micropython.org/en/latest/develop/cmodules.html). + +Micropython and camera-api folders are at the same level. Note that you need those extra "/../"s while been inside the esp32 port folder. +If you experience problems, visit [MicroPython external C modules](https://docs.micropython.org/en/latest/develop/cmodules.html). ## Notes -If your target board is a ESP32, I recomend using IDF >= 5.2, since older versions may lead to IRAM overflow during build. Alternatively you can modify your sdkconfig-file (see [issue #1](https://github.com/cnadler86/micropython-camera-API/issues/1)). \ No newline at end of file + +- For ESP32, do not use sizes above QVGA when not JPEG. The performance of the ESP32-S series has significantly improved, but JPEG mode always gives better frame rates. +- The OV5640 pinout is compatible with boards designed for the OV2640 but the voltage supply is too high for the internal 1.5V regulator, so the camera overheats unless a heat sink is applied. For recording purposes the OV5640 should only be used with an ESP32S3 board. Frame sizes above FHD framesize should only be used for still images due to memory limitations. +- If your target board is a ESP32, I recommend using IDF >= 5.2, since older versions may lead to IRAM overflow during build. Alternatively you can modify your sdkconfig-file (see [issue #1](https://github.com/cnadler86/micropython-camera-API/issues/1)). +- The driver requires PSRAM to be installed and activated. +- Most of the precompiled firmware images are untested, but the only difference between them are the target architecture and pin definitions, so they should work out of the box. If not, please raise an issue. +- Every sensor has its own ranges and settings, please consult the respective specifications. + +## Benchmark + +I didn't use a calibrated oscilloscope, but here is a FPS benchmark with my ESP32S3 (xclk_freq = 20MHz, GrabMode=LATEST, fb_count = 1, jpeg_quality=85%) and OV2640. +Using fb_count=2 theoretically can double the FPS (see JPEG with fb_count=2). This might also apply for other PixelFormats. + +| Frame Size | GRAYSCALE | RGB565 | YUV422 | JPEG | JPEG (fb=2) | +|------------|-----------|--------|--------|--------|-------------| +| R96X96 | 12.5 | 12.5 | 12.5 | No img | No img | +| QQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | +| QCIF | 11 | 11 | 11.5 | 25 | 50 | +| HQVGA | 12.5 | 12.5 | 12.5 | 25 | 50 | +| R240X240 | 12.5 | 12.5 | 11.5 | 25 | 50 | +| QVGA | 12 | 11 | 12 | 25 | 50 | +| CIF | 12.5 | No img | No img | 6.3 | 12.5 | +| HVGA | 3 | 3 | 2.5 | 12.5 | 25 | +| VGA | 3 | 3 | 3 | 12.5 | 25 | +| SVGA | 3 | 3 | 3 | 12.5 | 25 | +| XGA | No img | No img | No img | 6.3 | 12.5 | +| HD | No img | No img | No img | 6.3 | 12.5 | +| SXGA | 2 | 2 | 2 | 6.3 | 12.5 | +| UXGA | No img | No img | No img | 6.3 | 12.5 | + +## Troubleshooting + +You can find information on the following sites: +- [ESP-FAQ](https://docs.espressif.com/projects/esp-faq/en/latest/application-solution/camera-application.html) +- [ChatGPT](https://chatgpt.com/) +- [Issues in here](https://github.com/cnadler86/micropython-camera-API/issues?q=is%3Aissue) + +## Donate + +If you enjoy this work and would like to share your enjoyment, please feel free to [donate](https://github.com/sponsors/cnadler86?frequency=one-time) and contribute to the project. Thanks! :blush: diff --git a/examples/CameraSettings.html b/examples/CameraSettings.html new file mode 100644 index 0000000..e392005 --- /dev/null +++ b/examples/CameraSettings.html @@ -0,0 +1,323 @@ + + + + Micropython Camera Stream + + + + +
+

Micropython Camera Stream

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ Loading stream... +
+
+ + \ No newline at end of file diff --git a/examples/CameraSettings.py b/examples/CameraSettings.py new file mode 100644 index 0000000..77d6caa --- /dev/null +++ b/examples/CameraSettings.py @@ -0,0 +1,112 @@ +import network +import asyncio +import time +from acamera import Camera, FrameSize, PixelFormat # Import the async version of the Camera class, you can also use the sync version (camera.Camera) + +cam = Camera(frame_size=FrameSize.VGA, pixel_format=PixelFormat.JPEG, jpeg_quality=85, init=False) +# WLAN config +ssid = '' +password = '' + +station = network.WLAN(network.STA_IF) +station.active(True) +station.connect(ssid, password) + +while not station.isconnected(): + time.sleep(1) + +print(f'Connected! IP: {station.ifconfig()[0]}. Open this IP in your browser') + +try: + with open("CameraSettings.html", 'r') as file: + html = file.read() +except Exception as e: + print("Error reading CameraSettings.html file. You might forgot to copy it from the examples folder.") + raise e +async def stream_camera(writer): + try: + cam.init() + await asyncio.sleep(1) + + writer.write(b'HTTP/1.1 200 OK\r\nContent-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n') + await writer.drain() + + while True: + frame = await cam.acapture() # This is the async version of capture, you can also use frame = cam.capture() instead + if frame: + writer.write(b'--frame\r\nContent-Type: image/jpeg\r\n\r\n') + writer.write(frame) + await writer.drain() + + finally: + cam.deinit() + writer.close() + await writer.wait_closed() + print("Streaming stopped and camera deinitialized.") + +async def handle_client(reader, writer): + try: + request = await reader.read(1024) + request = request.decode() + + if 'GET /stream' in request: + print("Start streaming...") + await stream_camera(writer) + + elif 'GET /set_' in request: + method_name = request.split('GET /set_')[1].split('?')[0] + value = int(request.split('value=')[1].split(' ')[0]) + set_method = getattr(cam, f'set_{method_name}', None) + if callable(set_method): + print(f"setting {method_name} to {value}") + set_method(value) + response = 'HTTP/1.1 200 OK\r\n\r\n' + writer.write(response.encode()) + await writer.drain() + else: + try: + cam.reconfigure(**{method_name: value}) + print(f"Camera reconfigured with {method_name}={value}") + print("This action restores all previous configuration!") + response = 'HTTP/1.1 200 OK\r\n\r\n' + except Exception as e: + print(f"Error with {method_name}: {e}") + response = 'HTTP/1.1 404 Not Found\r\n\r\n' + writer.write(response.encode()) + await writer.drain() + + elif 'GET /get_' in request: + method_name = request.split('GET /get_')[1].split(' ')[0] + get_method = getattr(cam, f'get_{method_name}', None) + if callable(get_method): + value = get_method() + print(f"{method_name} is {value}") + response = f'HTTP/1.1 200 OK\r\n\r\n{value}' + writer.write(response.encode()) + await writer.drain() + else: + response = 'HTTP/1.1 404 Not Found\r\n\r\n' + writer.write(response.encode()) + await writer.drain() + + else: + writer.write('HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n'.encode() + html.encode()) + await writer.drain() + except Exception as e: + print(f"Error: {e}") + finally: + writer.close() + await writer.wait_closed() + +async def start_server(): + server = await asyncio.start_server(handle_client, "0.0.0.0", 80) + print(f'Server is running on {station.ifconfig()[0]}:80') + while True: + await asyncio.sleep(3600) + +try: + asyncio.run(start_server()) +except KeyboardInterrupt: + cam.deinit() + print("Server stopped") + diff --git a/examples/WebCam.py b/examples/SimpleWebCam.py similarity index 99% rename from examples/WebCam.py rename to examples/SimpleWebCam.py index bf78488..b4f5180 100644 --- a/examples/WebCam.py +++ b/examples/SimpleWebCam.py @@ -4,7 +4,7 @@ from camera import Camera, FrameSize, PixelFormat # Cam Config -cam = Camera(frame_size = FrameSize.VGA,pixel_format=PixelFormat.JPEG) +cam = Camera(frame_size = FrameSize.VGA,pixel_format=PixelFormat.JPEG,init=False) # WLAN config ssid = '' diff --git a/examples/benchmark.py b/examples/benchmark.py new file mode 100644 index 0000000..1ba7aac --- /dev/null +++ b/examples/benchmark.py @@ -0,0 +1,109 @@ +from camera import Camera, FrameSize, PixelFormat +import time +import gc +import os +gc.enable() + +def measure_fps(duration=2): + start_time = time.ticks_ms() + while time.ticks_ms() - start_time < 500: + cam.capture() + + start_time = time.ticks_ms() + frame_count = 0 + + while time.ticks_ms() - start_time < duration*1000: + img = cam.capture() + if img: + frame_count += 1 + + end_time = time.ticks_ms() + fps = frame_count / (end_time - start_time) * 1000 + return round(fps,1) + +def print_summary_table(results, cam): + print(f"\nBenchmark {os.uname().machine} with {cam.get_sensor_name()}, GrabMode: {cam.get_grab_mode()}:") + + fb_counts = sorted(results.keys()) + frame_size_names = {getattr(FrameSize, f): f for f in dir(FrameSize) if not f.startswith('_')} + + header_row = f"{'Frame Size':<15}" + sub_header_row = " " * 15 + + for fb in fb_counts: + for p in results[fb].keys(): + header_row += f"{'fb_count ' + str(fb):<15}" + sub_header_row += f"{p:<15}" + + print(header_row) + print(sub_header_row) + + frame_sizes = list(next(iter(next(iter(results.values())).values())).keys()) + + for f in frame_sizes: + frame_size_name = frame_size_names.get(f, str(f)) + print(f"{frame_size_name:<15}", end="") + + for fb in fb_counts: + for p in results[fb].keys(): + fps = results[fb][p].get(f, "N/A") + print(f"{fps:<15}", end="") + print() + +if __name__ == "__main__": + cam = Camera() + results = {} + + try: + for fb in [1, 2]: + cam.reconfigure(fb_count=fb) + results[fb] = {} + for p in dir(PixelFormat): + if not p.startswith('_'): + p_value = getattr(PixelFormat, p) + if (p_value == PixelFormat.RGB888 and cam.get_sensor_name() == "OV2640") or (p_value != PixelFormat.JPEG and fb > 1): + continue + try: + cam.reconfigure(pixel_format=p_value) + results[fb][p] = {} + except Exception as e: + print('ERR:', e) + continue + + for f in dir(FrameSize): + if not f.startswith('_'): + f_value = getattr(FrameSize, f) + if f_value > cam.get_max_frame_size(): + continue + gc.collect() + print('Set', p, f,f'fb={fb}',':') + + try: + cam.reconfigure(frame_size=f_value) #set_frame_size fails for YUV422 + time.sleep_ms(10) + img = cam.capture() + + if img: + print('---> Image size:', len(img)) + fps = measure_fps(2) + print(f"---> FPS: {fps}") + results[fb][p][f_value] = fps + else: + print('No image captured') + results[fb][p][f_value] = 'No img' + + print(f"---> Free Memory: {gc.mem_free()}") + except Exception as e: + print('ERR:', e) + results[fb][p][f_value] = 'ERR' + finally: + time.sleep_ms(250) + gc.collect() + print('') + + except KeyboardInterrupt: + print("\nScript interrupted by user.") + + finally: + print_summary_table(results, cam) + cam.deinit() diff --git a/examples/benchmark_img_conv.py b/examples/benchmark_img_conv.py new file mode 100644 index 0000000..9a34b64 --- /dev/null +++ b/examples/benchmark_img_conv.py @@ -0,0 +1,107 @@ +from camera import Camera, FrameSize, PixelFormat +import time +import gc +import os +gc.enable() + +def measure_fps(cam,out_fmt,duration=2): + start_time = time.ticks_ms() + while time.ticks_ms() - start_time < 500: + cam.capture(out_fmt) + + start_time = time.ticks_ms() + frame_count = 0 + + while time.ticks_ms() - start_time < duration*1000: + img = cam.capture(out_fmt) + if img: + frame_count += 1 + + end_time = time.ticks_ms() + fps = frame_count / (end_time - start_time) * 1000 + return round(fps,1) + +def print_summary_table(results, cam): + print(f"\nBenchmark {os.uname().machine} with {cam.get_sensor_name()}, GrabMode: {cam.get_grab_mode()}:") + + fb_counts = sorted(results.keys()) + frame_size_names = {getattr(FrameSize, f): f for f in dir(FrameSize) if not f.startswith('_')} + + header_row = f"{'Frame Size':<15}" + sub_header_row = " " * 15 + + for fb in fb_counts: + for p in results[fb].keys(): + header_row += f"{'fb_count ' + str(fb):<15}" + sub_header_row += f"{p:<15}" + + print(header_row) + print(sub_header_row) + + frame_sizes = list(next(iter(next(iter(results.values())).values())).keys()) + + for f in frame_sizes: + frame_size_name = frame_size_names.get(f, str(f)) + print(f"{frame_size_name:<15}", end="") + + for fb in fb_counts: + for p in results[fb].keys(): + fps = results[fb][p].get(f, "N/A") + print(f"{fps:<15}", end="") + print() + +if __name__ == "__main__": + cam = Camera(pixel_format=PixelFormat.JPEG) + results = {} + + try: + for fb in [1, 2]: + cam.reconfigure(fb_count=fb, frame_size=FrameSize.QQVGA) + results[fb] = {} + for p in dir(PixelFormat): + if not p.startswith('_'): + p_value = getattr(PixelFormat, p) + try: + if p_value == PixelFormat.JPEG: + continue + cam.capture(p_value) + results[fb][p] = {} + gc.collect() + except: + continue + for f in dir(FrameSize): + if not f.startswith('_'): + f_value = getattr(FrameSize, f) + if f_value > cam.get_max_frame_size(): + continue + gc.collect() + print('Set', p, f,f'fb={fb}',':') + + try: + cam.set_frame_size(f_value) + time.sleep_ms(10) + img = cam.capture(p_value) + if img: + print('---> Image size:', len(img)) + fps = measure_fps(cam,p_value,2) + print(f"---> FPS: {fps}") + results[fb][p][f_value] = fps + else: + print('No image captured') + results[fb][p][f_value] = 'No img' + + print(f"---> Free Memory: {gc.mem_free()}") + except Exception as e: + print('ERR:', e) + results[fb][p][f_value] = 'ERR' + finally: + time.sleep_ms(250) + gc.collect() + print('') + + except KeyboardInterrupt: + print("\nScript interrupted by user.") + + finally: + print_summary_table(results, cam) + cam.deinit() diff --git a/src/acamera.py b/src/acamera.py new file mode 100644 index 0000000..46c2a6f --- /dev/null +++ b/src/acamera.py @@ -0,0 +1,12 @@ +import asyncio +from camera import Camera as _Camera +from camera import FrameSize, PixelFormat, GainCeiling, GrabMode + +class Camera(_Camera): + async def acapture(self): + self.free_buffer() # Free the buffer so the camera task grabs a new frame + while not self.frame_available(): + await asyncio.sleep(0) # Yield control to the event loop + return self.capture() + +__all__ = ['Camera', 'FrameSize', 'PixelFormat', 'GainCeiling', 'GrabMode'] \ No newline at end of file diff --git a/src/camera_pins.h b/src/camera_pins.h new file mode 100644 index 0000000..6041cae --- /dev/null +++ b/src/camera_pins.h @@ -0,0 +1,340 @@ +// Camera pins definitions for different boards +#ifndef MICROPY_CAMERA_MODEL_PINS_H +#define MICROPY_CAMERA_MODEL_PINS_H + +#if defined(MICROPY_CAMERA_MODEL_WROVER_KIT) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 21 +#define MICROPY_CAMERA_PIN_SIOD 26 +#define MICROPY_CAMERA_PIN_SIOC 27 + +#define MICROPY_CAMERA_PIN_D7 35 +#define MICROPY_CAMERA_PIN_D6 34 +#define MICROPY_CAMERA_PIN_D5 39 +#define MICROPY_CAMERA_PIN_D4 36 +#define MICROPY_CAMERA_PIN_D3 19 +#define MICROPY_CAMERA_PIN_D2 18 +#define MICROPY_CAMERA_PIN_D1 5 +#define MICROPY_CAMERA_PIN_D0 4 +#define MICROPY_CAMERA_PIN_VSYNC 25 +#define MICROPY_CAMERA_PIN_HREF 23 +#define MICROPY_CAMERA_PIN_PCLK 22 + +#elif defined(MICROPY_CAMERA_MODEL_ESP_EYE) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 4 +#define MICROPY_CAMERA_PIN_SIOD 18 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 36 +#define MICROPY_CAMERA_PIN_D6 37 +#define MICROPY_CAMERA_PIN_D5 38 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 35 +#define MICROPY_CAMERA_PIN_D2 14 +#define MICROPY_CAMERA_PIN_D1 13 +#define MICROPY_CAMERA_PIN_D0 34 +#define MICROPY_CAMERA_PIN_VSYNC 5 +#define MICROPY_CAMERA_PIN_HREF 27 +#define MICROPY_CAMERA_PIN_PCLK 25 + +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_PSRAM) || defined(MICROPY_CAMERA_MODEL_M5STACK_UNITCAM) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET 15 +#define MICROPY_CAMERA_PIN_XCLK 27 +#define MICROPY_CAMERA_PIN_SIOD 25 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 19 +#define MICROPY_CAMERA_PIN_D6 36 +#define MICROPY_CAMERA_PIN_D5 18 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 5 +#define MICROPY_CAMERA_PIN_D2 34 +#define MICROPY_CAMERA_PIN_D1 35 +#define MICROPY_CAMERA_PIN_D0 32 +#define MICROPY_CAMERA_PIN_VSYNC 22 +#define MICROPY_CAMERA_PIN_HREF 26 +#define MICROPY_CAMERA_PIN_PCLK 21 + +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_V2_PSRAM) || defined(MICROPY_CAMERA_MODEL_M5STACK_WIDE) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET 15 +#define MICROPY_CAMERA_PIN_XCLK 27 +#define MICROPY_CAMERA_PIN_SIOD 22 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 19 +#define MICROPY_CAMERA_PIN_D6 36 +#define MICROPY_CAMERA_PIN_D5 18 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 5 +#define MICROPY_CAMERA_PIN_D2 34 +#define MICROPY_CAMERA_PIN_D1 35 +#define MICROPY_CAMERA_PIN_D0 32 +#define MICROPY_CAMERA_PIN_VSYNC 25 +#define MICROPY_CAMERA_PIN_HREF 26 +#define MICROPY_CAMERA_PIN_PCLK 21 + +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_ESP32CAM) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET 15 +#define MICROPY_CAMERA_PIN_XCLK 27 +#define MICROPY_CAMERA_PIN_SIOD 25 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 19 +#define MICROPY_CAMERA_PIN_D6 36 +#define MICROPY_CAMERA_PIN_D5 18 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 5 +#define MICROPY_CAMERA_PIN_D2 34 +#define MICROPY_CAMERA_PIN_D1 35 +#define MICROPY_CAMERA_PIN_D0 17 +#define MICROPY_CAMERA_PIN_VSYNC 22 +#define MICROPY_CAMERA_PIN_HREF 26 +#define MICROPY_CAMERA_PIN_PCLK 21 + +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_CAMS3_UNIT) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET 21 +#define MICROPY_CAMERA_PIN_XCLK 11 +#define MICROPY_CAMERA_PIN_SIOD 17 +#define MICROPY_CAMERA_PIN_SIOC 41 + +#define MICROPY_CAMERA_PIN_D7 13 +#define MICROPY_CAMERA_PIN_D6 4 +#define MICROPY_CAMERA_PIN_D5 10 +#define MICROPY_CAMERA_PIN_D4 5 +#define MICROPY_CAMERA_PIN_D3 7 +#define MICROPY_CAMERA_PIN_D2 16 +#define MICROPY_CAMERA_PIN_D1 15 +#define MICROPY_CAMERA_PIN_D0 6 +#define MICROPY_CAMERA_PIN_VSYNC 42 +#define MICROPY_CAMERA_PIN_HREF 18 +#define MICROPY_CAMERA_PIN_PCLK 12 + +#elif defined(MICROPY_CAMERA_MODEL_M5STACK_ATOM_S3R) +#define MICROPY_CAMERA_PIN_PWDN -1 //needs to be low to run +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 21 +#define MICROPY_CAMERA_PIN_SIOD 12 +#define MICROPY_CAMERA_PIN_SIOC 9 + +#define MICROPY_CAMERA_PIN_D7 13 +#define MICROPY_CAMERA_PIN_D6 11 +#define MICROPY_CAMERA_PIN_D5 17 +#define MICROPY_CAMERA_PIN_D4 4 +#define MICROPY_CAMERA_PIN_D3 48 +#define MICROPY_CAMERA_PIN_D2 46 +#define MICROPY_CAMERA_PIN_D1 42 +#define MICROPY_CAMERA_PIN_D0 3 +#define MICROPY_CAMERA_PIN_VSYNC 10 +#define MICROPY_CAMERA_PIN_HREF 14 +#define MICROPY_CAMERA_PIN_PCLK 40 + +#elif defined(MICROPY_CAMERA_MODEL_AI_THINKER) +#define MICROPY_CAMERA_PIN_PWDN 32 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 0 +#define MICROPY_CAMERA_PIN_SIOD 26 +#define MICROPY_CAMERA_PIN_SIOC 27 + +#define MICROPY_CAMERA_PIN_D7 35 +#define MICROPY_CAMERA_PIN_D6 34 +#define MICROPY_CAMERA_PIN_D5 39 +#define MICROPY_CAMERA_PIN_D4 36 +#define MICROPY_CAMERA_PIN_D3 21 +#define MICROPY_CAMERA_PIN_D2 19 +#define MICROPY_CAMERA_PIN_D1 18 +#define MICROPY_CAMERA_PIN_D0 5 +#define MICROPY_CAMERA_PIN_VSYNC 25 +#define MICROPY_CAMERA_PIN_HREF 23 +#define MICROPY_CAMERA_PIN_PCLK 22 + +#elif defined(MICROPY_CAMERA_MODEL_XIAO_ESP32S3) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 10 +#define MICROPY_CAMERA_PIN_SIOD 40 +#define MICROPY_CAMERA_PIN_SIOC 39 + +#define MICROPY_CAMERA_PIN_D7 48 +#define MICROPY_CAMERA_PIN_D6 11 +#define MICROPY_CAMERA_PIN_D5 12 +#define MICROPY_CAMERA_PIN_D4 14 +#define MICROPY_CAMERA_PIN_D3 16 +#define MICROPY_CAMERA_PIN_D2 18 +#define MICROPY_CAMERA_PIN_D1 17 +#define MICROPY_CAMERA_PIN_D0 15 +#define MICROPY_CAMERA_PIN_VSYNC 38 +#define MICROPY_CAMERA_PIN_HREF 47 +#define MICROPY_CAMERA_PIN_PCLK 13 + +#elif defined(MICROPY_CAMERA_MODEL_ESP32_MP_CAMERA_BOARD) +// The 18 pin header on the board has Y5 and Y3 swapped +#define ESP32_MP_CAMERA_BOARD_HEADER 0 +#define MICROPY_CAMERA_PIN_PWDN 32 +#define MICROPY_CAMERA_PIN_RESET 33 +#define MICROPY_CAMERA_PIN_XCLK 4 +#define MICROPY_CAMERA_PIN_SIOD 18 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 36 +#define MICROPY_CAMERA_PIN_D6 19 +#define MICROPY_CAMERA_PIN_D5 21 +#define MICROPY_CAMERA_PIN_D4 39 +#if ESP32_MP_CAMERA_BOARD_HEADER +#define MICROPY_CAMERA_PIN_D3 13 +#else +#define MICROPY_CAMERA_PIN_D3 35 +#endif +#define MICROPY_CAMERA_PIN_D2 14 +#if ESP32_MP_CAMERA_BOARD_HEADER +#define MICROPY_CAMERA_PIN_D1 35 +#else +#define MICROPY_CAMERA_PIN_D1 13 +#endif +#define MICROPY_CAMERA_PIN_D0 34 +#define MICROPY_CAMERA_PIN_VSYNC 5 +#define MICROPY_CAMERA_PIN_HREF 27 +#define MICROPY_CAMERA_PIN_PCLK 25 + +#elif defined(MICROPY_CAMERA_MODEL_ESP32S3_CAM_LCD) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 40 +#define MICROPY_CAMERA_PIN_SIOD 17 +#define MICROPY_CAMERA_PIN_SIOC 18 + +#define MICROPY_CAMERA_PIN_D7 39 +#define MICROPY_CAMERA_PIN_D6 41 +#define MICROPY_CAMERA_PIN_D5 42 +#define MICROPY_CAMERA_PIN_D4 12 +#define MICROPY_CAMERA_PIN_D3 3 +#define MICROPY_CAMERA_PIN_D2 14 +#define MICROPY_CAMERA_PIN_D1 47 +#define MICROPY_CAMERA_PIN_D0 13 +#define MICROPY_CAMERA_PIN_VSYNC 21 +#define MICROPY_CAMERA_PIN_HREF 38 +#define MICROPY_CAMERA_PIN_PCLK 11 + +#elif defined(MICROPY_CAMERA_MODEL_ESP32S3_EYE) || defined(MICROPY_CAMERA_MODEL_FREENOVE_ESP32S3_CAM) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 15 +#define MICROPY_CAMERA_PIN_SIOD 4 +#define MICROPY_CAMERA_PIN_SIOC 5 + +#define MICROPY_CAMERA_PIN_D0 11 +#define MICROPY_CAMERA_PIN_D1 9 +#define MICROPY_CAMERA_PIN_D2 8 +#define MICROPY_CAMERA_PIN_D3 10 +#define MICROPY_CAMERA_PIN_D4 12 +#define MICROPY_CAMERA_PIN_D5 18 +#define MICROPY_CAMERA_PIN_D6 17 +#define MICROPY_CAMERA_PIN_D7 16 +#define MICROPY_CAMERA_PIN_VSYNC 6 +#define MICROPY_CAMERA_PIN_HREF 7 +#define MICROPY_CAMERA_PIN_PCLK 13 + +#elif defined(MICROPY_CAMERA_MODEL_DFRobot_ESP32S3) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 45 +#define MICROPY_CAMERA_PIN_SIOD 1 +#define MICROPY_CAMERA_PIN_SIOC 2 + +#define MICROPY_CAMERA_PIN_D7 48 +#define MICROPY_CAMERA_PIN_D6 46 +#define MICROPY_CAMERA_PIN_D5 8 +#define MICROPY_CAMERA_PIN_D4 7 +#define MICROPY_CAMERA_PIN_D3 4 +#define MICROPY_CAMERA_PIN_D2 41 +#define MICROPY_CAMERA_PIN_D1 40 +#define MICROPY_CAMERA_PIN_D0 39 +#define MICROPY_CAMERA_PIN_VSYNC 6 +#define MICROPY_CAMERA_PIN_HREF 42 +#define MICROPY_CAMERA_PIN_PCLK 5 + +#elif defined(MICROPY_CAMERA_MODEL_TTGO_T_JOURNAL) +#define MICROPY_CAMERA_PIN_PWDN 0 +#define MICROPY_CAMERA_PIN_RESET 15 +#define MICROPY_CAMERA_PIN_XCLK 27 +#define MICROPY_CAMERA_PIN_SIOD 25 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 19 +#define MICROPY_CAMERA_PIN_D6 36 +#define MICROPY_CAMERA_PIN_D5 18 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 5 +#define MICROPY_CAMERA_PIN_D2 34 +#define MICROPY_CAMERA_PIN_D1 35 +#define MICROPY_CAMERA_PIN_D0 17 +#define MICROPY_CAMERA_PIN_VSYNC 22 +#define MICROPY_CAMERA_PIN_HREF 26 +#define MICROPY_CAMERA_PIN_PCLK 21 + +#elif defined(MICROPY_CAMERA_MODEL_TTGO_T_CAMERA_PLUS) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 4 +#define MICROPY_CAMERA_PIN_SIOD 18 +#define MICROPY_CAMERA_PIN_SIOC 23 + +#define MICROPY_CAMERA_PIN_D7 36 +#define MICROPY_CAMERA_PIN_D6 37 +#define MICROPY_CAMERA_PIN_D5 38 +#define MICROPY_CAMERA_PIN_D4 39 +#define MICROPY_CAMERA_PIN_D3 35 +#define MICROPY_CAMERA_PIN_D2 26 +#define MICROPY_CAMERA_PIN_D1 13 +#define MICROPY_CAMERA_PIN_D0 34 +#define MICROPY_CAMERA_PIN_VSYNC 5 +#define MICROPY_CAMERA_PIN_HREF 27 +#define MICROPY_CAMERA_PIN_PCLK 25 + +#elif defined(MICROPY_CAMERA_MODEL_NEW_ESPS3_RE1_0) +// aliexpress board with label RE:1.0, uses slow 8MB QSPI PSRAM, only 4MB addressable +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 10 +#define MICROPY_CAMERA_PIN_SIOD 21 +#define MICROPY_CAMERA_PIN_SIOC 14 + +#define MICROPY_CAMERA_PIN_D7 11 +#define MICROPY_CAMERA_PIN_D6 9 +#define MICROPY_CAMERA_PIN_D5 8 +#define MICROPY_CAMERA_PIN_D4 6 +#define MICROPY_CAMERA_PIN_D3 4 +#define MICROPY_CAMERA_PIN_D2 2 +#define MICROPY_CAMERA_PIN_D1 3 +#define MICROPY_CAMERA_PIN_D0 5 +#define MICROPY_CAMERA_PIN_VSYNC 13 +#define MICROPY_CAMERA_PIN_HREF 12 +#define MICROPY_CAMERA_PIN_PCLK 7 + +#elif defined(MICROPY_CAMERA_MODEL_XENOIONEX) +#define MICROPY_CAMERA_PIN_PWDN -1 +#define MICROPY_CAMERA_PIN_RESET -1 +#define MICROPY_CAMERA_PIN_XCLK 1 // Can use +#define MICROPY_CAMERA_PIN_SIOD 8 // Can use other i2c SDA pin, set this to -1 | If not using i2c set to 8 or 47 +#define MICROPY_CAMERA_PIN_SIOC 9 // Can use other i2c SCL pin, set this to -1 | If not using i2c set to 9 or 21 + +#define MICROPY_CAMERA_PIN_D7 3 //D7 +#define MICROPY_CAMERA_PIN_D6 18 //D6 +#define MICROPY_CAMERA_PIN_D5 42 //D5 +#define MICROPY_CAMERA_PIN_D4 16 //D4 +#define MICROPY_CAMERA_PIN_D3 41 //D3 +#define MICROPY_CAMERA_PIN_D2 17 //D2 +#define MICROPY_CAMERA_PIN_D1 40 //D1 +#define MICROPY_CAMERA_PIN_D0 39 //D0 +#define MICROPY_CAMERA_PIN_VSYNC 45 +#define MICROPY_CAMERA_PIN_HREF 38 +#define MICROPY_CAMERA_PIN_PCLK 2 + +#endif // definition of camera pins for different boards +#endif // MICROPY_CAMERA_MODEL_PINS_H \ No newline at end of file diff --git a/src/manifest.py b/src/manifest.py new file mode 100644 index 0000000..ff69f76 --- /dev/null +++ b/src/manifest.py @@ -0,0 +1,4 @@ +# Include the board's default manifest. +include("$(PORT_DIR)/boards/manifest.py") +# Add custom driver +module("acamera.py") \ No newline at end of file diff --git a/src/micropython.cmake b/src/micropython.cmake index 9527545..178d39f 100644 --- a/src/micropython.cmake +++ b/src/micropython.cmake @@ -1,15 +1,93 @@ +include(${MICROPY_DIR}/py/py.cmake) + +set(MICROPY_FROZEN_MANIFEST ${CMAKE_CURRENT_LIST_DIR}/manifest.py) + add_library(usermod_mp_camera INTERFACE) + +add_dependencies(usermod_mp_camera esp32_camera) + target_sources(usermod_mp_camera INTERFACE ${CMAKE_CURRENT_LIST_DIR}/modcamera.c ${CMAKE_CURRENT_LIST_DIR}/modcamera_api.c ) -target_include_directories(usermod_mp_camera INTERFACE - ${CMAKE_CURRENT_LIST_DIR} - ${IDF_PATH}/components/esp32-camera/driver/include - ${IDF_PATH}/components/esp32-camera/driver/private_include - ${IDF_PATH}/components/esp32-camera/conversions/include - ${IDF_PATH}/components/esp32-camera/conversions/private_include - ${IDF_PATH}/components/esp32-camera/sensors/private_include + +# Prefer user-defined ESP32_CAMERA_DIR if provided +if(DEFINED ESP32_CAMERA_DIR AND EXISTS "${ESP32_CAMERA_DIR}") + message(STATUS "Using user-defined ESP32 Camera directory: ${ESP32_CAMERA_DIR}") + + target_include_directories(usermod_mp_camera INTERFACE + ${CMAKE_CURRENT_LIST_DIR} + ${ESP32_CAMERA_DIR}/driver/include + ${ESP32_CAMERA_DIR}/conversions/include + ${ESP32_CAMERA_DIR}/driver/private_include + ${ESP32_CAMERA_DIR}/conversions/private_include + ${ESP32_CAMERA_DIR}/sensors/private_include + ) + +# If no manual directory is provided, try to fetch it from ESP-IDF +elseif(EXISTS ${IDF_PATH}/components/esp32-camera) + idf_component_get_property(CAMERA_INCLUDES esp32-camera INCLUDE_DIRS) + idf_component_get_property(CAMERA_PRIV_INCLUDES esp32-camera PRIV_INCLUDE_DIRS) + idf_component_get_property(CAMERA_DIR esp32-camera COMPONENT_DIR) + + if(CAMERA_DIR) + message(STATUS "Using ESP32 Camera component from ESP-IDF: ${CAMERA_DIR}") + + # Add public include directories from ESP-IDF + if(CAMERA_INCLUDES) + list(TRANSFORM CAMERA_INCLUDES PREPEND ${CAMERA_DIR}/) + target_include_directories(usermod_mp_camera INTERFACE ${CAMERA_INCLUDES}) + endif() + + # Add private include directories from ESP-IDF + if(CAMERA_PRIV_INCLUDES) + list(TRANSFORM CAMERA_PRIV_INCLUDES PREPEND ${CAMERA_DIR}/) + target_include_directories(usermod_mp_camera INTERFACE ${CAMERA_PRIV_INCLUDES}) + endif() + else() + message(WARNING "ESP32 Camera component not found in ESP-IDF!") + target_include_directories(usermod_mp_camera PUBLIC ${CMAKE_CURRENT_LIST_DIR}) + endif() +endif() + +# Check if MP_JPEG_DIR is set or if mp_jpeg directory exists two levels up +if(DEFINED MP_JPEG_DIR AND EXISTS "${MP_JPEG_DIR}") + message(STATUS "Using user-defined MP_JPEG_DIR: ${MP_JPEG_DIR}") + set(MP_JPEG_SRC "${MP_JPEG_DIR}/src/micropython.cmake") +elseif(EXISTS "${CMAKE_CURRENT_LIST_DIR}/../../mp_jpeg") + message(STATUS "Found mp_jpeg directory two levels up") + set(MP_JPEG_SRC "${CMAKE_CURRENT_LIST_DIR}/../../mp_jpeg/src/micropython.cmake") +endif() + +# Add MP_JPEG_SRC cmake file to target_sources if it is defined +if(DEFINED MP_JPEG_SRC AND EXISTS "${MP_JPEG_SRC}") + include(${MP_JPEG_SRC}) +else() + message(WARNING "MP_JPEG_SRC not found or not defined!") +endif() + +# Define MICROPY_CAMERA_MODEL if specified +if (MICROPY_CAMERA_MODEL) + message(STATUS "Using user-defined camera model: ${MICROPY_CAMERA_MODEL}") + target_compile_definitions(usermod_mp_camera INTERFACE + MICROPY_CAMERA_MODEL_${MICROPY_CAMERA_MODEL}=1 + ) +endif() + +# Define MP_CAMERA_DRIVER_VERSION if specified +if (MP_CAMERA_DRIVER_VERSION) + target_compile_definitions(usermod_mp_camera INTERFACE + MP_CAMERA_DRIVER_VERSION=\"${MP_CAMERA_DRIVER_VERSION}\" + ) +endif() + +# Camera module strings are not suitable for compression and cause size increase +target_compile_definitions(usermod_mp_camera INTERFACE + MICROPY_ROM_TEXT_COMPRESSION=0 ) -target_compile_definitions(usermod_mp_camera INTERFACE) -target_link_libraries(usermod INTERFACE usermod_mp_camera) \ No newline at end of file + +# Link the camera module with the main usermod target +target_link_libraries(usermod INTERFACE usermod_mp_camera) + +# Gather target properties for MicroPython build system +micropy_gather_target_properties(usermod_mp_camera) diff --git a/src/micropython.mk b/src/micropython.mk index 749223e..7d5012b 100644 --- a/src/micropython.mk +++ b/src/micropython.mk @@ -1,3 +1,4 @@ CAMERA_MOD_DIR := $(USERMOD_DIR) SRC_USERMOD_C += $(addprefix $(CAMERA_MOD_DIR)/, modcamera_api.c) SRC_USERMOD_LIB_C += $(addprefix $(CAMERA_MOD_DIR)/, modcamera.c) +CFLAGS_USERMOD += -I$(CAMERA_MOD_DIR) \ No newline at end of file diff --git a/src/modcamera.c b/src/modcamera.c index e7741a8..5a0bd05 100644 --- a/src/modcamera.c +++ b/src/modcamera.c @@ -28,49 +28,80 @@ #include "modcamera.h" #include "esp_err.h" #include "esp_log.h" +#include "mphalport.h" -#define TAG "ESP32_MPY_CAMERA" +#define TAG "MPY_CAMERA" #if !CONFIG_SPIRAM #error Camera only works on boards configured with spiram #endif -void raise_micropython_error_from_esp_err(esp_err_t err) { - switch (err) { - case ESP_OK: - return; - - case ESP_ERR_NO_MEM: - mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Out of memory")); - break; +// Helper functions +static int map(int value, int fromLow, int fromHigh, int toLow, int toHigh) { + if (fromHigh == fromLow) { + mp_raise_ValueError(MP_ERROR_TEXT("fromLow und fromHigh shall not be equal")); + } + return (int)((int32_t)(value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow); +} - case ESP_ERR_INVALID_ARG: - mp_raise_ValueError(MP_ERROR_TEXT("Invalid argument")); - break; +static inline int get_mapped_jpeg_quality(int8_t quality) { + return map(quality, 0, 100, 63, 0); +} - case ESP_ERR_INVALID_STATE: - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Invalid state")); - break; +static inline void check_init(mp_camera_obj_t *self) { + if (!self->initialized) { + mp_raise_OSError(ENOENT); + } +} - case ESP_ERR_NOT_FOUND: - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Camera not found")); - break; +static void set_check_xclk_freq(mp_camera_obj_t *self, int32_t xclk_freq_hz) { + if ( xclk_freq_hz > 40000000) { + mp_raise_ValueError(MP_ERROR_TEXT("xclk frequency cannot be grather than 40MHz")); + } else { + self->camera_config.xclk_freq_hz = xclk_freq_hz; + } +} - case ESP_ERR_NOT_SUPPORTED: - mp_raise_NotImplementedError(MP_ERROR_TEXT("Operation/Function not supported/implemented")); - break; +static void set_check_fb_count(mp_camera_obj_t *self, mp_int_t fb_count) { + if (fb_count > 2) { + self->camera_config.fb_count = 2; + mp_warning(NULL, "Frame buffer size limited to 2"); + } else if (fb_count < 1) { + self->camera_config.fb_count = 1; + mp_warning(NULL, "Frame buffer size must be >0. Setting it to 1"); + } + else { + self->camera_config.fb_count = fb_count; + } +} - case ESP_ERR_TIMEOUT: - mp_raise_OSError(MP_ETIMEDOUT); - break; +static void set_check_grab_mode(mp_camera_obj_t *self, mp_camera_grabmode_t grab_mode) { + if (grab_mode != CAMERA_GRAB_WHEN_EMPTY && grab_mode != CAMERA_GRAB_LATEST) { + mp_raise_ValueError(MP_ERROR_TEXT("Invalid grab_mode")); + } else { + self->camera_config.grab_mode = grab_mode; + } +} - default: - mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("Unknown error 0x%04x"), err); - // mp_raise_msg_varg(&mp_type_RuntimeError, MP_ERROR_TEXT("Unknown error")); - break; +static void set_check_pixel_format(mp_camera_obj_t *self, mp_camera_pixformat_t pixel_format) { + if ( pixel_format > PIXFORMAT_RGB555) { //Maximal enum value, but validation should be better since wrong pixelformat leads to reboot. + mp_raise_ValueError(MP_ERROR_TEXT("Invalid pixel_format")); + } else { + self->camera_config.pixel_format = pixel_format; } } +static bool init_camera(mp_camera_obj_t *self) { + // Correct the quality before it is passed to esp32 driver and then "undo" the correction in the camera_config + int8_t api_jpeg_quality = self->camera_config.jpeg_quality; + self->camera_config.jpeg_quality = get_mapped_jpeg_quality(api_jpeg_quality); + esp_err_t err = esp_camera_init(&self->camera_config); + self->camera_config.jpeg_quality = api_jpeg_quality; + check_esp_err(err); + return true; +} + +// Camera HAL Funcitons void mp_camera_hal_construct( mp_camera_obj_t *self, int8_t data_pins[8], @@ -89,9 +120,6 @@ void mp_camera_hal_construct( int8_t fb_count, mp_camera_grabmode_t grab_mode) { // configure camera based on arguments - self->camera_config.pixel_format = pixel_format; - self->camera_config.frame_size = frame_size; - self->camera_config.jpeg_quality = jpeg_quality; //0-63 lower number means higher quality. TODO: Harmonization in API and Validation self->camera_config.pin_d0 = data_pins[0]; self->camera_config.pin_d1 = data_pins[1]; self->camera_config.pin_d2 = data_pins[2]; @@ -108,13 +136,18 @@ void mp_camera_hal_construct( self->camera_config.pin_xclk = external_clock_pin; self->camera_config.pin_sscb_sda = sccb_sda_pin; self->camera_config.pin_sscb_scl = sccb_scl_pin; - self->camera_config.xclk_freq_hz = xclk_freq_hz; - self->camera_config.fb_count = fb_count; //if more than one, i2s runs in continuous mode. TODO: Test with others than JPEG - self->camera_config.grab_mode = grab_mode; + + self->camera_config.frame_size = frame_size; + self->camera_config.jpeg_quality = jpeg_quality; //save value in here, but will be corrected (with map) before passing it to the esp32-driver + + set_check_pixel_format(self, pixel_format); + set_check_xclk_freq(self, xclk_freq_hz); + set_check_fb_count(self, fb_count); + set_check_grab_mode(self, grab_mode); // defaul parameters self->camera_config.fb_location = CAMERA_FB_IN_PSRAM; - self->camera_config.ledc_timer = LEDC_TIMER_0; + self->camera_config.ledc_timer = LEDC_TIMER_3; self->camera_config.ledc_channel = LEDC_CHANNEL_0; self->initialized = false; @@ -125,205 +158,158 @@ void mp_camera_hal_init(mp_camera_obj_t *self) { if (self->initialized) { return; } + #ifndef CONFIG_IDF_TARGET_ESP32S3 + if (self->camera_config.fb_count > 1 && self->camera_config.pixel_format != PIXFORMAT_JPEG) { + mp_warning(NULL, "It is recomended to use a frame buffer size of 1 for non-JPEG pixel format"); + } + #endif ESP_LOGI(TAG, "Initializing camera"); - camera_config_t temp_config = self->camera_config; - temp_config.frame_size = FRAMESIZE_QVGA; //use values supported by all cameras - temp_config.pixel_format = PIXFORMAT_RGB565; //use values supported by all cameras - esp_err_t err = esp_camera_init(&temp_config); - if (err != ESP_OK) { - self->initialized = false; - raise_micropython_error_from_esp_err(err); - } else { - self->initialized = true; - } - mp_camera_hal_reconfigure(self, self->camera_config.frame_size, self->camera_config.pixel_format, - self->camera_config.grab_mode, self->camera_config.fb_count); + self->initialized = init_camera(self); ESP_LOGI(TAG, "Camera initialized successfully"); } void mp_camera_hal_deinit(mp_camera_obj_t *self) { if (self->initialized) { if (self->captured_buffer) { - esp_camera_fb_return(self->captured_buffer); + esp_camera_return_all(); self->captured_buffer = NULL; } esp_err_t err = esp_camera_deinit(); - raise_micropython_error_from_esp_err(err); + check_esp_err(err); self->initialized = false; ESP_LOGI(TAG, "Camera deinitialized"); } } void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize_t frame_size, mp_camera_pixformat_t pixel_format, mp_camera_grabmode_t grab_mode, mp_int_t fb_count) { - if (self->initialized) { - ESP_LOGI(TAG, "Reconfiguring camera"); - sensor_t *sensor = esp_camera_sensor_get(); - camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); - - if (PIXFORMAT_JPEG == self->camera_config.pixel_format && (!sensor_info->support_jpeg)) { - mp_raise_NotImplementedError(MP_ERROR_TEXT("Sensor does not support JPEG")); - } + check_init(self); + ESP_LOGI(TAG, "Reconfiguring camera with frame size: %d, pixel format: %d, grab mode: %d, fb count: %d", (int)frame_size, (int)pixel_format, (int)grab_mode, (int)fb_count); + + sensor_t *sensor = esp_camera_sensor_get(); + camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); + if (frame_size > sensor_info->max_size) { + mp_warning(NULL, "Frame size will be scaled down to maximal frame size supported by the camera sensor"); + self->camera_config.frame_size = sensor_info->max_size; + } else { + self->camera_config.frame_size = frame_size; + } - if (frame_size > sensor_info->max_size) { - mp_warning(NULL, "Frame size will be scaled down to maximal frame size supported by the camera sensor"); - self->camera_config.frame_size = sensor_info->max_size; - } else { - self->camera_config.frame_size = frame_size; - } + set_check_pixel_format(self, pixel_format); + set_check_grab_mode(self, grab_mode); + set_check_fb_count(self, fb_count); - if ( pixel_format > PIXFORMAT_RGB555) { //Maximal enum value, but validation should be better since wrong pixelformat leads to reboot. - mp_raise_ValueError(MP_ERROR_TEXT("Invalid pixel_format")); - } else { - self->camera_config.pixel_format = pixel_format; - } - - if (grab_mode != CAMERA_GRAB_WHEN_EMPTY && grab_mode != CAMERA_GRAB_LATEST) { - mp_raise_ValueError(MP_ERROR_TEXT("Invalid grab_mode")); - } else { - self->camera_config.grab_mode = grab_mode; - } - - if (fb_count > 2) { - self->camera_config.fb_count = 2; - mp_warning(NULL, "Frame buffer size limited to 2"); - } else { - self->camera_config.fb_count = fb_count; - } - - raise_micropython_error_from_esp_err(esp_camera_deinit()); - - // sensor->set_pixformat(sensor, self->camera_config.pixel_format); //seems to be needed because of some bug? - // sensor->set_framesize(sensor, self->camera_config.frame_size); //seems to be needed because of some bug? - - esp_err_t err = esp_camera_init(&self->camera_config); - if (err != ESP_OK) { - self->initialized = false; - raise_micropython_error_from_esp_err(err); - } else { - ESP_LOGI(TAG, "Camera reconfigured successfully"); - } - } + check_esp_err(esp_camera_deinit()); + self->initialized = false; + self->initialized = init_camera(self); + ESP_LOGI(TAG, "Camera reconfigured successfully"); } -mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int timeout_ms) { - // Timeout not used at the moment - if (!self->initialized) { - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to capture image: Camera not initialized")); - } +mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self) { + check_init(self); if (self->captured_buffer) { esp_camera_fb_return(self->captured_buffer); self->captured_buffer = NULL; } + ESP_LOGI(TAG, "Capturing image"); self->captured_buffer = esp_camera_fb_get(); - if (self->captured_buffer) { - if (self->camera_config.pixel_format == PIXFORMAT_JPEG) { - ESP_LOGI(TAG, "Captured image in JPEG format"); - return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); - } else { - ESP_LOGI(TAG, "Captured image in raw format"); - return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); - // TODO: Stub at the moment in order to return raw data, but it sould be implemented to return a Bitmap, see following circuitpython example: - // - // int width = common_hal_espcamera_camera_get_width(self); - // int height = common_hal_espcamera_camera_get_height(self); - // displayio_bitmap_t *bitmap = m_new_obj(displayio_bitmap_t); - // bitmap->base.type = &displayio_bitmap_type; - // common_hal_displayio_bitmap_construct_from_buffer(bitmap, width, height, (format == PIXFORMAT_RGB565) ? 16 : 8, (uint32_t *)(void *)result->buf, true); - // return bitmap; - } - } else { - esp_camera_fb_return(self->captured_buffer); - self->captured_buffer = NULL; + if (!self->captured_buffer) { + ESP_LOGE(TAG, "Failed to capture image"); return mp_const_none; } + return mp_obj_new_memoryview('b', self->captured_buffer->len, self->captured_buffer->buf); + +} + +mp_obj_t mp_camera_hal_frame_available(mp_camera_obj_t *self) { + check_init(self); + return mp_obj_new_bool(esp_camera_available_frames()); } bool mp_camera_hal_initialized(mp_camera_obj_t *self){ return self->initialized; } +void mp_camera_hal_free_buffer(mp_camera_obj_t *self) { + if (self->captured_buffer) { + esp_camera_fb_return(self->captured_buffer); + self->captured_buffer = NULL; + } +} + const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[] = { - { MP_ROM_QSTR(MP_QSTR_JPEG), MP_ROM_INT(PIXFORMAT_JPEG) }, - { MP_ROM_QSTR(MP_QSTR_YUV422), MP_ROM_INT(PIXFORMAT_YUV422) }, - { MP_ROM_QSTR(MP_QSTR_GRAYSCALE), MP_ROM_INT(PIXFORMAT_GRAYSCALE) }, - { MP_ROM_QSTR(MP_QSTR_RGB565), MP_ROM_INT(PIXFORMAT_RGB565) }, + { MP_ROM_QSTR(MP_QSTR_JPEG), MP_ROM_INT((mp_uint_t)PIXFORMAT_JPEG) }, + { MP_ROM_QSTR(MP_QSTR_YUV422), MP_ROM_INT((mp_uint_t)PIXFORMAT_YUV422) }, + { MP_ROM_QSTR(MP_QSTR_YUV420), MP_ROM_INT((mp_uint_t)PIXFORMAT_YUV420) }, + { MP_ROM_QSTR(MP_QSTR_GRAYSCALE), MP_ROM_INT((mp_uint_t)PIXFORMAT_GRAYSCALE) }, + { MP_ROM_QSTR(MP_QSTR_RGB565), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB565) }, + { MP_ROM_QSTR(MP_QSTR_RGB888), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB888) }, + { MP_ROM_QSTR(MP_QSTR_RAW), MP_ROM_INT((mp_uint_t)PIXFORMAT_RAW) }, + { MP_ROM_QSTR(MP_QSTR_RGB444), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB444) }, + { MP_ROM_QSTR(MP_QSTR_RGB555), MP_ROM_INT((mp_uint_t)PIXFORMAT_RGB555) }, }; const mp_rom_map_elem_t mp_camera_hal_frame_size_table[] = { - { MP_ROM_QSTR(MP_QSTR_R96X96), MP_ROM_INT(FRAMESIZE_96X96) }, - { MP_ROM_QSTR(MP_QSTR_QQVGA), MP_ROM_INT(FRAMESIZE_QQVGA) }, - { MP_ROM_QSTR(MP_QSTR_QCIF), MP_ROM_INT(FRAMESIZE_QCIF) }, - { MP_ROM_QSTR(MP_QSTR_HQVGA), MP_ROM_INT(FRAMESIZE_HQVGA) }, - { MP_ROM_QSTR(MP_QSTR_R240X240), MP_ROM_INT(FRAMESIZE_240X240) }, - { MP_ROM_QSTR(MP_QSTR_QVGA), MP_ROM_INT(FRAMESIZE_QVGA) }, - { MP_ROM_QSTR(MP_QSTR_CIF), MP_ROM_INT(FRAMESIZE_CIF) }, - { MP_ROM_QSTR(MP_QSTR_HVGA), MP_ROM_INT(FRAMESIZE_HVGA) }, - { MP_ROM_QSTR(MP_QSTR_VGA), MP_ROM_INT(FRAMESIZE_VGA) }, - { MP_ROM_QSTR(MP_QSTR_SVGA), MP_ROM_INT(FRAMESIZE_SVGA) }, - { MP_ROM_QSTR(MP_QSTR_XGA), MP_ROM_INT(FRAMESIZE_XGA) }, - { MP_ROM_QSTR(MP_QSTR_HD), MP_ROM_INT(FRAMESIZE_HD) }, - { MP_ROM_QSTR(MP_QSTR_SXGA), MP_ROM_INT(FRAMESIZE_SXGA) }, - { MP_ROM_QSTR(MP_QSTR_UXGA), MP_ROM_INT(FRAMESIZE_UXGA) }, - { MP_ROM_QSTR(MP_QSTR_FHD), MP_ROM_INT(FRAMESIZE_FHD) }, - { MP_ROM_QSTR(MP_QSTR_P_HD), MP_ROM_INT(FRAMESIZE_P_HD) }, - { MP_ROM_QSTR(MP_QSTR_P_3MP), MP_ROM_INT(FRAMESIZE_P_3MP) }, - { MP_ROM_QSTR(MP_QSTR_QXGA), MP_ROM_INT(FRAMESIZE_QXGA) }, - { MP_ROM_QSTR(MP_QSTR_QHD), MP_ROM_INT(FRAMESIZE_QHD) }, - { MP_ROM_QSTR(MP_QSTR_WQXGA), MP_ROM_INT(FRAMESIZE_WQXGA) }, - { MP_ROM_QSTR(MP_QSTR_P_FHD), MP_ROM_INT(FRAMESIZE_P_FHD) }, - { MP_ROM_QSTR(MP_QSTR_QSXGA), MP_ROM_INT(FRAMESIZE_QSXGA) }, + { MP_ROM_QSTR(MP_QSTR_R96X96), MP_ROM_INT((mp_uint_t)FRAMESIZE_96X96) }, + { MP_ROM_QSTR(MP_QSTR_QQVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QQVGA) }, + { MP_ROM_QSTR(MP_QSTR_R128x128), MP_ROM_INT((mp_uint_t)FRAMESIZE_128X128) }, + { MP_ROM_QSTR(MP_QSTR_QCIF), MP_ROM_INT((mp_uint_t)FRAMESIZE_QCIF) }, + { MP_ROM_QSTR(MP_QSTR_HQVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_HQVGA) }, + { MP_ROM_QSTR(MP_QSTR_R240X240), MP_ROM_INT((mp_uint_t)FRAMESIZE_240X240) }, + { MP_ROM_QSTR(MP_QSTR_QVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QVGA) }, + { MP_ROM_QSTR(MP_QSTR_R320X320), MP_ROM_INT((mp_uint_t)FRAMESIZE_320X320) }, + { MP_ROM_QSTR(MP_QSTR_CIF), MP_ROM_INT((mp_uint_t)FRAMESIZE_CIF) }, + { MP_ROM_QSTR(MP_QSTR_HVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_HVGA) }, + { MP_ROM_QSTR(MP_QSTR_VGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_VGA) }, + { MP_ROM_QSTR(MP_QSTR_SVGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_SVGA) }, + { MP_ROM_QSTR(MP_QSTR_XGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_XGA) }, + { MP_ROM_QSTR(MP_QSTR_HD), MP_ROM_INT((mp_uint_t)FRAMESIZE_HD) }, + { MP_ROM_QSTR(MP_QSTR_SXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_SXGA) }, + { MP_ROM_QSTR(MP_QSTR_UXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_UXGA) }, + { MP_ROM_QSTR(MP_QSTR_FHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_FHD) }, + { MP_ROM_QSTR(MP_QSTR_P_HD), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_HD) }, + { MP_ROM_QSTR(MP_QSTR_P_3MP), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_3MP) }, + { MP_ROM_QSTR(MP_QSTR_QXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QXGA) }, + { MP_ROM_QSTR(MP_QSTR_QHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_QHD) }, + { MP_ROM_QSTR(MP_QSTR_WQXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_WQXGA) }, + { MP_ROM_QSTR(MP_QSTR_P_FHD), MP_ROM_INT((mp_uint_t)FRAMESIZE_P_FHD) }, + { MP_ROM_QSTR(MP_QSTR_QSXGA), MP_ROM_INT((mp_uint_t)FRAMESIZE_QSXGA) }, }; const mp_rom_map_elem_t mp_camera_hal_grab_mode_table[] = { - { MP_ROM_QSTR(MP_QSTR_WHEN_EMPTY), MP_ROM_INT(CAMERA_GRAB_WHEN_EMPTY) }, - { MP_ROM_QSTR(MP_QSTR_LATEST), MP_ROM_INT(CAMERA_GRAB_LATEST) }, + { MP_ROM_QSTR(MP_QSTR_WHEN_EMPTY), MP_ROM_INT((mp_uint_t)CAMERA_GRAB_WHEN_EMPTY) }, + { MP_ROM_QSTR(MP_QSTR_LATEST), MP_ROM_INT((mp_uint_t)CAMERA_GRAB_LATEST) }, }; const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { - { MP_ROM_QSTR(MP_QSTR_2X), MP_ROM_INT(GAINCEILING_2X) }, - { MP_ROM_QSTR(MP_QSTR_4X), MP_ROM_INT(GAINCEILING_4X) }, - { MP_ROM_QSTR(MP_QSTR_8X), MP_ROM_INT(GAINCEILING_8X) }, - { MP_ROM_QSTR(MP_QSTR_16X), MP_ROM_INT(GAINCEILING_16X) }, - { MP_ROM_QSTR(MP_QSTR_32X), MP_ROM_INT(GAINCEILING_32X) }, - { MP_ROM_QSTR(MP_QSTR_64X), MP_ROM_INT(GAINCEILING_64X) }, - { MP_ROM_QSTR(MP_QSTR_128X), MP_ROM_INT(GAINCEILING_128X) }, + { MP_ROM_QSTR(MP_QSTR_2X), MP_ROM_INT((mp_uint_t)GAINCEILING_2X) }, + { MP_ROM_QSTR(MP_QSTR_4X), MP_ROM_INT((mp_uint_t)GAINCEILING_4X) }, + { MP_ROM_QSTR(MP_QSTR_8X), MP_ROM_INT((mp_uint_t)GAINCEILING_8X) }, + { MP_ROM_QSTR(MP_QSTR_16X), MP_ROM_INT((mp_uint_t)GAINCEILING_16X) }, + { MP_ROM_QSTR(MP_QSTR_32X), MP_ROM_INT((mp_uint_t)GAINCEILING_32X) }, + { MP_ROM_QSTR(MP_QSTR_64X), MP_ROM_INT((mp_uint_t)GAINCEILING_64X) }, + { MP_ROM_QSTR(MP_QSTR_128X), MP_ROM_INT((mp_uint_t)GAINCEILING_128X) }, }; -//TODO: Makros with convertion function, since the API will use standarized values. // Helper functions to get and set camera and sensor information -#define SENSOR_STATUS_GETSET_IN_RANGE(type, name, status_field_name, setter_function_name, min_val, max_val) \ - SENSOR_GETSET_IN_RANGE(type, name, status.status_field_name, setter_function_name, min_val, max_val) - #define SENSOR_STATUS_GETSET(type, name, status_field_name, setter_function_name) \ SENSOR_GETSET(type, name, status.status_field_name, setter_function_name) // For subsequent modules using this as example, you will probably only need the makros below. #define SENSOR_GETSET(type, name, field_name, setter_function_name) \ - SENSOR_GET(type, name, field_name, setter_function_name) \ + SENSOR_GET(type, name, field_name) \ SENSOR_SET(type, name, setter_function_name) -#define SENSOR_GETSET_IN_RANGE(type, name, field_name, setter_function_name, min_val, max_val) \ - SENSOR_GET(type, name, field_name, setter_function_name) \ - SENSOR_SET_IN_RANGE(type, name, setter_function_name, min_val, max_val) - -#define SENSOR_GET(type, name, status_field_name, getter_function_name) \ +#define SENSOR_GET(type, name, status_field_name) \ type mp_camera_hal_get_##name(mp_camera_obj_t * self) { \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ + check_init(self); \ sensor_t *sensor = esp_camera_sensor_get(); \ - if (!sensor->getter_function_name) { \ - mp_raise_ValueError(MP_ERROR_TEXT("No attribute " #name)); \ - } \ return sensor->status_field_name; \ } #define SENSOR_SET(type, name, setter_function_name) \ void mp_camera_hal_set_##name(mp_camera_obj_t * self, type value) { \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ + check_init(self); \ sensor_t *sensor = esp_camera_sensor_get(); \ if (!sensor->setter_function_name) { \ mp_raise_ValueError(MP_ERROR_TEXT("No attribute " #name)); \ @@ -333,30 +319,13 @@ const mp_rom_map_elem_t mp_camera_hal_gainceiling_table[] = { } \ } -#define SENSOR_SET_IN_RANGE(type, name, setter_function_name, min_val, max_val) \ - void mp_camera_hal_set_##name(mp_camera_obj_t * self, type value) { \ - sensor_t *sensor = esp_camera_sensor_get(); \ - if (!self->initialized) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Camera not initialized")); \ - } \ - if (value < min_val || value > max_val) { \ - mp_raise_ValueError(MP_ERROR_TEXT(#name " value must be between " #min_val " and " #max_val)); \ - } \ - if (!sensor->setter_function_name) { \ - mp_raise_ValueError(MP_ERROR_TEXT("No attribute " #name)); \ - } \ - if (sensor->setter_function_name(sensor, value) < 0) { \ - mp_raise_ValueError(MP_ERROR_TEXT("Invalid setting for " #name)); \ - } \ - } - -SENSOR_STATUS_GETSET_IN_RANGE(int, contrast, contrast, set_contrast, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, brightness, brightness, set_brightness, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, saturation, saturation, set_saturation, -2, 2); -SENSOR_STATUS_GETSET_IN_RANGE(int, sharpness, sharpness, set_sharpness, -2, 2); +SENSOR_GET(framesize_t, frame_size, status.framesize); +SENSOR_STATUS_GETSET(int, contrast, contrast, set_contrast); +SENSOR_STATUS_GETSET(int, brightness, brightness, set_brightness); +SENSOR_STATUS_GETSET(int, saturation, saturation, set_saturation); +SENSOR_STATUS_GETSET(int, sharpness, sharpness, set_sharpness); SENSOR_STATUS_GETSET(int, denoise, denoise, set_denoise); SENSOR_STATUS_GETSET(mp_camera_gainceiling_t, gainceiling, gainceiling, set_gainceiling); -SENSOR_STATUS_GETSET(int, quality, quality, set_quality); //in_Range not needed since driver limits value SENSOR_STATUS_GETSET(bool, colorbar, colorbar, set_colorbar); SENSOR_STATUS_GETSET(bool, whitebal, awb, set_whitebal); SENSOR_STATUS_GETSET(bool, gain_ctrl, agc, set_gain_ctrl); @@ -365,23 +334,56 @@ SENSOR_STATUS_GETSET(bool, hmirror, hmirror, set_hmirror); SENSOR_STATUS_GETSET(bool, vflip, vflip, set_vflip); SENSOR_STATUS_GETSET(bool, aec2, aec2, set_aec2); SENSOR_STATUS_GETSET(bool, awb_gain, awb_gain, set_awb_gain); -SENSOR_STATUS_GETSET(int, agc_gain, agc_gain, set_agc_gain); //in_Range not needed since driver limits value -SENSOR_STATUS_GETSET(int, aec_value, aec_value, set_aec_value); //in_Range not needed since driver limits value -SENSOR_STATUS_GETSET_IN_RANGE(int, special_effect, special_effect, set_special_effect, 0, 6); -SENSOR_STATUS_GETSET_IN_RANGE(int, wb_mode, wb_mode, set_wb_mode, 0, 4); -SENSOR_STATUS_GETSET_IN_RANGE(int, ae_level, ae_level, set_ae_level, -2, 2); +SENSOR_STATUS_GETSET(int, agc_gain, agc_gain, set_agc_gain); +SENSOR_STATUS_GETSET(int, aec_value, aec_value, set_aec_value); +SENSOR_STATUS_GETSET(int, special_effect, special_effect, set_special_effect); +SENSOR_STATUS_GETSET(int, wb_mode, wb_mode, set_wb_mode); +SENSOR_STATUS_GETSET(int, ae_level, ae_level, set_ae_level); SENSOR_STATUS_GETSET(bool, dcw, dcw, set_dcw); SENSOR_STATUS_GETSET(bool, bpc, bpc, set_bpc); SENSOR_STATUS_GETSET(bool, wpc, wpc, set_wpc); SENSOR_STATUS_GETSET(bool, raw_gma, raw_gma, set_raw_gma); SENSOR_STATUS_GETSET(bool, lenc, lenc, set_lenc); -mp_camera_pixformat_t mp_camera_hal_get_pixel_format(mp_camera_obj_t *self) { - return self->camera_config.pixel_format; +void mp_camera_hal_set_frame_size(mp_camera_obj_t * self, framesize_t value) { + check_init(self); + sensor_t *sensor = esp_camera_sensor_get(); + if (!sensor->set_framesize) { + mp_raise_ValueError(MP_ERROR_TEXT("No attribute frame_size")); + } + + if (self->captured_buffer) { + esp_camera_return_all(); + self->captured_buffer = NULL; + } + + if (sensor->set_framesize(sensor, value) < 0) { + mp_raise_ValueError(MP_ERROR_TEXT("Invalid setting for frame_size")); + } else { + self->camera_config.frame_size = value; + } +} + +int mp_camera_hal_get_quality(mp_camera_obj_t * self) { + check_init(self); + return self->camera_config.jpeg_quality; +} + +void mp_camera_hal_set_quality(mp_camera_obj_t * self, int value) { + check_init(self); + sensor_t *sensor = esp_camera_sensor_get(); + if (!sensor->set_quality) { + mp_raise_ValueError(MP_ERROR_TEXT("No attribute quality")); + } + if (sensor->set_quality(sensor, get_mapped_jpeg_quality(value)) < 0) { + mp_raise_ValueError(MP_ERROR_TEXT("Invalid setting for quality")); + } else { + self->camera_config.jpeg_quality = value; + } } -mp_camera_framesize_t mp_camera_hal_get_frame_size(mp_camera_obj_t *self) { - return self->camera_config.frame_size; +mp_camera_pixformat_t mp_camera_hal_get_pixel_format(mp_camera_obj_t *self) { + return self->camera_config.pixel_format; } camera_grab_mode_t mp_camera_hal_get_grab_mode(mp_camera_obj_t *self) { @@ -393,36 +395,42 @@ int mp_camera_hal_get_fb_count(mp_camera_obj_t *self) { } const char *mp_camera_hal_get_sensor_name(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->name; } bool mp_camera_hal_get_supports_jpeg(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->support_jpeg; } mp_camera_framesize_t mp_camera_hal_get_max_frame_size(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->max_size; } int mp_camera_hal_get_address(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); camera_sensor_info_t *sensor_info = esp_camera_sensor_get_info(&sensor->id); return sensor_info->sccb_addr; } int mp_camera_hal_get_pixel_width(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); framesize_t framesize = sensor->status.framesize; return resolution[framesize].width; } int mp_camera_hal_get_pixel_height(mp_camera_obj_t *self) { + check_init(self); sensor_t *sensor = esp_camera_sensor_get(); framesize_t framesize = sensor->status.framesize; return resolution[framesize].height; diff --git a/src/modcamera.h b/src/modcamera.h index d16ef25..a3ce749 100644 --- a/src/modcamera.h +++ b/src/modcamera.h @@ -42,10 +42,11 @@ #include "esp_camera.h" #include "sensor.h" +#include "camera_pins.h" -#if defined(MICROPY_CAMERA_PIN_SIOD) && defined(MICROPY_CAMERA_PIN_SIOC) && defined(MICROPY_CAMERA_PIN_D0) && defined(MICROPY_CAMERA_PIN_D1) && defined(MICROPY_CAMERA_PIN_D2) && \ -defined(MICROPY_CAMERA_PIN_D3) && defined(MICROPY_CAMERA_PIN_D4) && defined(MICROPY_CAMERA_PIN_D5) && defined(MICROPY_CAMERA_PIN_D6) && defined(MICROPY_CAMERA_PIN_D7) && \ -defined(MICROPY_CAMERA_PIN_PCLK) && defined(MICROPY_CAMERA_PIN_VSYNC) && defined(MICROPY_CAMERA_PIN_HREF) && defined(MICROPY_CAMERA_PIN_XCLK) +#if defined (MICROPY_CAMERA_PIN_SIOD) && defined (MICROPY_CAMERA_PIN_SIOC) && defined (MICROPY_CAMERA_PIN_D0) && defined (MICROPY_CAMERA_PIN_D1) && defined (MICROPY_CAMERA_PIN_D2) && \ +defined (MICROPY_CAMERA_PIN_D3) && defined (MICROPY_CAMERA_PIN_D4) && defined (MICROPY_CAMERA_PIN_D5) && defined (MICROPY_CAMERA_PIN_D6) && defined (MICROPY_CAMERA_PIN_D7) && \ +defined (MICROPY_CAMERA_PIN_PCLK) && defined (MICROPY_CAMERA_PIN_VSYNC) && defined (MICROPY_CAMERA_PIN_HREF) && defined (MICROPY_CAMERA_PIN_XCLK) #define MICROPY_CAMERA_ALL_REQ_PINS_DEFINED (1) #endif @@ -58,41 +59,35 @@ defined(MICROPY_CAMERA_PIN_PCLK) && defined(MICROPY_CAMERA_PIN_VSYNC) && defined #endif #ifndef MICROPY_CAMERA_XCLK_FREQ -#define MICROPY_CAMERA_XCLK_FREQ (10) +#define MICROPY_CAMERA_XCLK_FREQ (20) #endif -#ifndef MICROPY_CAMERA_DEFAULT_FRAME_SIZE -#define MICROPY_CAMERA_DEFAULT_FRAME_SIZE FRAMESIZE_QQVGA +#if !defined (MICROPY_CAMERA_GRAB_MODE) && defined (CONFIG_IDF_TARGET_ESP32S3) +#define MICROPY_CAMERA_GRAB_MODE CAMERA_GRAB_LATEST +#elif !defined(MICROPY_CAMERA_GRAB_MODE) +#define MICROPY_CAMERA_GRAB_MODE CAMERA_GRAB_WHEN_EMPTY #endif -#ifndef MICROPY_CAMERA_DEFAULT_PIXEL_FORMAT -#define MICROPY_CAMERA_DEFAULT_PIXEL_FORMAT PIXFORMAT_RGB565 +#if !defined (MICROPY_CAMERA_FB_COUNT) && defined (CONFIG_IDF_TARGET_ESP32S3) +#define MICROPY_CAMERA_FB_COUNT (2) +#elif !defined(MICROPY_CAMERA_FB_COUNT) +#define MICROPY_CAMERA_FB_COUNT (1) #endif -#ifndef MICROPY_CAMERA_GRAB_MODE -#define MICROPY_CAMERA_GRAB_MODE CAMERA_GRAB_WHEN_EMPTY +#ifndef MICROPY_CAMERA_DEFAULT_FRAME_SIZE +#define MICROPY_CAMERA_DEFAULT_FRAME_SIZE FRAMESIZE_QQVGA #endif -#ifndef MICROPY_CAMERA_FB_COUNT -#define MICROPY_CAMERA_FB_COUNT (1) +#ifndef MICROPY_CAMERA_DEFAULT_PIXEL_FORMAT +#define MICROPY_CAMERA_DEFAULT_PIXEL_FORMAT PIXFORMAT_RGB565 #endif #ifndef MICROPY_CAMERA_JPEG_QUALITY -#define MICROPY_CAMERA_JPEG_QUALITY (15) -#endif - -//Supported Camera sensors -#ifndef CONFIG_OV2640_SUPPORT -#define CONFIG_OV2640_SUPPORT 1 -#endif - -#ifndef CONFIG_OV5640_SUPPORT -#define CONFIG_OV5640_SUPPORT 1 +#define MICROPY_CAMERA_JPEG_QUALITY (85) #endif typedef pixformat_t hal_camera_pixformat_t; typedef framesize_t hal_camera_framesize_t; -typedef camera_fb_location_t hal_camera_fb_location_t; typedef camera_grab_mode_t hal_camera_grabmode_t; typedef gainceiling_t hal_camera_gainceiling_t; @@ -111,9 +106,7 @@ typedef hal_camera_pixformat_t mp_camera_pixformat_t; typedef hal_camera_framesize_t mp_camera_framesize_t; typedef hal_camera_grabmode_t mp_camera_grabmode_t; typedef hal_camera_gainceiling_t mp_camera_gainceiling_t; -// typedef hal_camera_fb_location_t mp_camera_fb_location_t; //not used at the moment, but might be used in the future -// TODO: Define how to integrate external time source in constructor (e.g. in ESP is LED-Timer). /** * @brief Constructs the camera hardware abstraction layer. * @details The Port-plattform shall define a default pwm-time source and also frame buffer location (no input) @@ -189,22 +182,36 @@ extern void mp_camera_hal_reconfigure(mp_camera_obj_t *self, mp_camera_framesize * @brief Captures an image and returns it as mp_obj_t (e.g. mp_obj_new_memoryview). * * @param self Pointer to the camera object. - * @param timeout_ms Timeout in milliseconds. * @return Captured image as micropython object. */ -extern mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self, int timeout_ms); +extern mp_obj_t mp_camera_hal_capture(mp_camera_obj_t *self); + +/** + * @brief Returns true, if a frame is available. + * + * @param self Pointer to the camera object. + * @return True, if a frame is available. + */ +extern mp_obj_t mp_camera_hal_frame_available(mp_camera_obj_t *self); + +/** + * @brief Frees the buffer of the camera object. + * + * @param self Pointer to the camera object. + */ +extern void mp_camera_hal_free_buffer(mp_camera_obj_t *self); /** * @brief Table mapping pixel formats API to their corresponding values at HAL. * @details Needs to be defined in the port-specific implementation. */ -extern const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[4]; +extern const mp_rom_map_elem_t mp_camera_hal_pixel_format_table[9]; /** * @brief Table mapping frame sizes API to their corresponding values at HAL. * @details Needs to be defined in the port-specific implementation. */ -extern const mp_rom_map_elem_t mp_camera_hal_frame_size_table[22]; +extern const mp_rom_map_elem_t mp_camera_hal_frame_size_table[24]; /** * @brief Table mapping gainceiling API to their corresponding values at HAL. @@ -262,7 +269,7 @@ DECLARE_CAMERA_HAL_GETSET(bool, wpc) DECLARE_CAMERA_HAL_GET(int, address) DECLARE_CAMERA_HAL_GET(int, fb_count) -DECLARE_CAMERA_HAL_GET(mp_camera_framesize_t, frame_size) +DECLARE_CAMERA_HAL_GETSET(mp_camera_framesize_t, frame_size) DECLARE_CAMERA_HAL_GET(camera_grab_mode_t, grab_mode) DECLARE_CAMERA_HAL_GET(mp_camera_framesize_t, max_frame_size) DECLARE_CAMERA_HAL_GET(mp_camera_pixformat_t, pixel_format) diff --git a/src/modcamera_api.c b/src/modcamera_api.c index 79013db..39afa71 100644 --- a/src/modcamera_api.c +++ b/src/modcamera_api.c @@ -40,7 +40,7 @@ const mp_obj_type_t camera_type; //Constructor static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { - enum { ARG_data_pins, ARG_pixel_clock_pin, ARG_vsync_pin, ARG_href_pin, ARG_sda_pin, ARG_scl_pin, ARG_xclock_pin, ARG_xclock_frequency, ARG_powerdown_pin, ARG_reset_pin, ARG_pixel_format, ARG_frame_size, ARG_jpeg_quality, ARG_fb_count, ARG_grab_mode, NUM_ARGS }; + enum { ARG_data_pins, ARG_pixel_clock_pin, ARG_vsync_pin, ARG_href_pin, ARG_sda_pin, ARG_scl_pin, ARG_xclock_pin, ARG_xclock_frequency, ARG_powerdown_pin, ARG_reset_pin, ARG_pixel_format, ARG_frame_size, ARG_jpeg_quality, ARG_fb_count, ARG_grab_mode, ARG_init, NUM_ARGS }; static const mp_arg_t allowed_args[] = { #ifdef MICROPY_CAMERA_ALL_REQ_PINS_DEFINED { MP_QSTR_data_pins, MP_ARG_OBJ | MP_ARG_KW_ONLY , { .u_obj = MP_ROM_NONE } }, @@ -67,6 +67,7 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz { MP_QSTR_jpeg_quality, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = MICROPY_CAMERA_JPEG_QUALITY } }, { MP_QSTR_fb_count, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = MICROPY_CAMERA_FB_COUNT } }, { MP_QSTR_grab_mode, MP_ARG_INT | MP_ARG_KW_ONLY, { .u_int = MICROPY_CAMERA_GRAB_MODE } }, + { MP_QSTR_init, MP_ARG_BOOL | MP_ARG_KW_ONLY, { .u_bool = true } }, }; mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; @@ -119,6 +120,9 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz mp_camera_pixformat_t pixel_format = args[ARG_pixel_format].u_int; mp_camera_framesize_t frame_size = args[ARG_frame_size].u_int; int8_t jpeg_quality = args[ARG_jpeg_quality].u_int; + if ((jpeg_quality < 0) || (jpeg_quality > 100)) { + mp_raise_ValueError(MP_ERROR_TEXT("jpeg quality must be in range 0-100")); + } int8_t fb_count = args[ARG_fb_count].u_int; mp_camera_grabmode_t grab_mode = args[ARG_grab_mode].u_int; @@ -129,27 +133,40 @@ static mp_obj_t mp_camera_make_new(const mp_obj_type_t *type, size_t n_args, siz sda_pin, scl_pin, xclock_frequency, pixel_format, frame_size, jpeg_quality, fb_count, grab_mode); mp_camera_hal_init(self); - - if (mp_camera_hal_capture(self, 100) == mp_const_none){ + if (mp_camera_hal_capture(self) == mp_const_none){ mp_camera_hal_deinit(self); - mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to capture initial frame. \ - Run reconfigure method or construct a new object with appropriate configuration (e.g. FrameSize).")); - return MP_OBJ_FROM_PTR(self); + mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("Failed to capture initial frame. Construct a new object with appropriate configuration.")); } else { + if ( !args[ARG_init].u_bool ){ + mp_camera_hal_deinit(self); + } else { + mp_camera_hal_free_buffer(self); + } return MP_OBJ_FROM_PTR(self); } -} +} // camera_construct // Main methods -static mp_obj_t camera_capture(size_t n_args, const mp_obj_t *args){ - mp_camera_obj_t *self = MP_OBJ_TO_PTR(args[0]); - mp_float_t timeout = n_args < 2 ? MICROPY_FLOAT_CONST(0.25) : mp_obj_get_float(args[1]); - return mp_camera_hal_capture(self, timeout); +static mp_obj_t camera_capture(mp_obj_t self_in){ + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_camera_hal_capture(self); } -static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(camera_capture_obj, 1, 2, camera_capture); +static MP_DEFINE_CONST_FUN_OBJ_1(camera_capture_obj, camera_capture); + +static mp_obj_t camera_frame_available(mp_obj_t self_in){ + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_camera_hal_frame_available(self); +} +static MP_DEFINE_CONST_FUN_OBJ_1(camera_frame_available_obj, camera_frame_available); + +static mp_obj_t camera_free_buf(mp_obj_t self_in) { + mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_camera_hal_free_buffer(self); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(camera_free_buf_obj, camera_free_buf); static mp_obj_t camera_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args){ - //OPEN: Validate inputs mp_camera_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); enum { ARG_frame_size, ARG_pixel_format, ARG_grab_mode, ARG_fb_count }; static const mp_arg_t allowed_args[] = { @@ -174,7 +191,7 @@ static mp_obj_t camera_reconfigure(size_t n_args, const mp_obj_t *pos_args, mp_m args[ARG_grab_mode].u_obj != MP_ROM_NONE ? args[ARG_grab_mode].u_int : mp_camera_hal_get_grab_mode(self); - uint8_t fb_count = + mp_int_t fb_count = args[ARG_fb_count].u_obj != MP_ROM_NONE ? args[ARG_fb_count].u_int : mp_camera_hal_get_fb_count(self); @@ -198,14 +215,15 @@ static mp_obj_t mp_camera_deinit(mp_obj_t self_in) { } static MP_DEFINE_CONST_FUN_OBJ_1(mp_camera_deinit_obj, mp_camera_deinit); +// Destructor static mp_obj_t mp_camera_obj___exit__(size_t n_args, const mp_obj_t *args) { (void)n_args; return mp_camera_deinit(args[0]); } static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_camera___exit___obj, 4, 4, mp_camera_obj___exit__); -// Camera propertiy functions -// Camera sensor propertiy functions +// Camera property functions +// Camera sensor property functions #define CREATE_GETTER(property, get_function) \ static mp_obj_t camera_get_##property(const mp_obj_t self_in) { \ mp_camera_obj_t *self = MP_OBJ_TO_PTR(self_in); \ @@ -233,12 +251,14 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_camera___exit___obj, 4, 4, mp_came { MP_ROM_QSTR(MP_QSTR_get_##property), MP_ROM_PTR(&camera_get_##property##_obj) }, \ { MP_ROM_QSTR(MP_QSTR_set_##property), MP_ROM_PTR(&camera_set_##property##_obj) } -CREATE_GETTER(frame_size, mp_obj_new_int) -CREATE_GETTER(pixel_format, mp_obj_new_int) -CREATE_GETTER(grab_mode, mp_obj_new_int) -CREATE_GETTER(fb_count, mp_obj_new_int) -CREATE_GETTER(pixel_width, mp_obj_new_int) -CREATE_GETTER(pixel_height, mp_obj_new_int) +CREATE_GETSET_FUNCTIONS(frame_size, MP_OBJ_NEW_SMALL_INT, mp_obj_get_int); +CREATE_GETTER(pixel_format, mp_obj_new_int); +CREATE_GETTER(grab_mode, mp_obj_new_int); +CREATE_GETTER(fb_count, mp_obj_new_int); +CREATE_GETTER(pixel_width, mp_obj_new_int); +CREATE_GETTER(pixel_height, mp_obj_new_int); +CREATE_GETTER(max_frame_size, mp_obj_new_int); +CREATE_GETTER(sensor_name, mp_obj_new_str_from_cstr); CREATE_GETSET_FUNCTIONS(contrast, MP_OBJ_NEW_SMALL_INT, mp_obj_get_int); CREATE_GETSET_FUNCTIONS(brightness, MP_OBJ_NEW_SMALL_INT, mp_obj_get_int); CREATE_GETSET_FUNCTIONS(saturation, MP_OBJ_NEW_SMALL_INT, mp_obj_get_int); @@ -269,17 +289,21 @@ CREATE_GETSET_FUNCTIONS(lenc, mp_obj_new_bool, mp_obj_is_true); static const mp_rom_map_elem_t camera_camera_locals_table[] = { { MP_ROM_QSTR(MP_QSTR_reconfigure), MP_ROM_PTR(&camera_reconfigure_obj) }, { MP_ROM_QSTR(MP_QSTR_capture), MP_ROM_PTR(&camera_capture_obj) }, + { MP_ROM_QSTR(MP_QSTR_frame_available), MP_ROM_PTR(&camera_frame_available_obj) }, + { MP_ROM_QSTR(MP_QSTR_free_buffer), MP_ROM_PTR(&camera_free_buf_obj) }, { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&camera_init_obj) }, { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mp_camera_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_camera_deinit_obj) }, { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) }, { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&mp_camera___exit___obj) }, - { MP_ROM_QSTR(MP_QSTR_get_framesize), MP_ROM_PTR(&camera_get_frame_size_obj) }, { MP_ROM_QSTR(MP_QSTR_get_pixel_format), MP_ROM_PTR(&camera_get_pixel_format_obj) }, { MP_ROM_QSTR(MP_QSTR_get_grab_mode), MP_ROM_PTR(&camera_get_grab_mode_obj) }, { MP_ROM_QSTR(MP_QSTR_get_fb_count), MP_ROM_PTR(&camera_get_fb_count_obj) }, { MP_ROM_QSTR(MP_QSTR_get_pixel_width), MP_ROM_PTR(&camera_get_pixel_width_obj) }, { MP_ROM_QSTR(MP_QSTR_get_pixel_height), MP_ROM_PTR(&camera_get_pixel_height_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_max_frame_size), MP_ROM_PTR(&camera_get_max_frame_size_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_sensor_name), MP_ROM_PTR(&camera_get_sensor_name_obj) }, + ADD_PROPERTY_TO_TABLE(frame_size), ADD_PROPERTY_TO_TABLE(contrast), ADD_PROPERTY_TO_TABLE(brightness), ADD_PROPERTY_TO_TABLE(saturation), @@ -339,6 +363,13 @@ MP_CREATE_CONST_TYPE(GainCeiling, mp_camera_gainceiling); static MP_DEFINE_CONST_DICT(mp_camera_grab_mode_locals_dict,mp_camera_hal_grab_mode_table); MP_CREATE_CONST_TYPE(GrabMode, mp_camera_grab_mode); +#ifdef MP_CAMERA_DRIVER_VERSION + static mp_obj_t mp_camera_driver_version(void) { + return mp_obj_new_str(MP_CAMERA_DRIVER_VERSION, strlen(MP_CAMERA_DRIVER_VERSION)); + } + static MP_DEFINE_CONST_FUN_OBJ_0(mp_camera_driver_version_obj, mp_camera_driver_version); +#endif + static const mp_rom_map_elem_t camera_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_camera) }, { MP_ROM_QSTR(MP_QSTR_Camera), MP_ROM_PTR(&camera_type) }, @@ -346,6 +377,9 @@ static const mp_rom_map_elem_t camera_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR_FrameSize), MP_ROM_PTR(&mp_camera_frame_size_type) }, { MP_ROM_QSTR(MP_QSTR_GainCeiling), MP_ROM_PTR(&mp_camera_gainceiling_type) }, { MP_ROM_QSTR(MP_QSTR_GrabMode), MP_ROM_PTR(&mp_camera_grab_mode_type) }, + #ifdef MP_CAMERA_DRIVER_VERSION + { MP_ROM_QSTR(MP_QSTR_Version), MP_ROM_PTR(&mp_camera_driver_version_obj) }, + #endif }; static MP_DEFINE_CONST_DICT(camera_module_globals, camera_module_globals_table); diff --git a/tests/esp32_test.py b/tests/esp32_test.py index dfa2169..e29c79f 100644 --- a/tests/esp32_test.py +++ b/tests/esp32_test.py @@ -1,28 +1,82 @@ +import time from camera import Camera, FrameSize, PixelFormat -import uinspect as inspect def test_property_get_frame_size(): - camera = Camera() - Frame_Size = FrameSize.VGA - camera.reconfigure(frame_size=Frame_Size.VGA) - assert camera.get_frame_size == Frame_Size - assert camera.get_pixel_width == 640 - assert camera.get_pixel_height == 480 + with Camera() as cam: + print("Test frame size") + Frame_Size = FrameSize.VGA + cam.reconfigure(frame_size=Frame_Size) + assert cam.get_frame_size() == Frame_Size + assert cam.get_pixel_width() == 640 + assert cam.get_pixel_height() == 480 def test_property_get_pixel_format(): - camera = Camera() - Pixel_Format = PixelFormat.RGB565 - camera.reconfigure(pixel_format=PixelFormat.RGB) - assert camera.get_pixel_format == Pixel_Format + with Camera() as cam: + print("Test pixel format") + for Pixel_Format_Name in dir(PixelFormat): + Pixel_Format = getattr(PixelFormat, Pixel_Format_Name) + try: + if Pixel_Format_Name.startswith("_") or Pixel_Format_Name.startswith("RGB888"): + continue + cam.reconfigure(pixel_format=Pixel_Format) + assert cam.get_pixel_format() == Pixel_Format + except Exception: + print("\tFailed test for pixel format", Pixel_Format) +def test_must_be_initialized(): + with Camera(init=False) as cam: + print(f"Testing capture without initalization") + try: + cam.capture() + assert False, "Capture should have failed" + except Exception as e: + if e == "Camera not initialized": + assert False, "Capture should have failed" + else: + assert True + def test_camera_properties(): - camera = Camera() - for name in dir(camera): - if name.startswith('get_'): - prop_name = name[4:] - set_method_name = f'set_{prop_name}' - if hasattr(camera, set_method_name): - set_method = getattr(camera, set_method_name) - get_method = getattr(camera, name) - set_method(1) - assert get_method() == 1, f"Failed for property {prop_name}" \ No newline at end of file + with Camera() as cam: + print(f"Testing get/set methods") + for name in dir(cam): + if name.startswith('get_'): + prop_name = name[4:] + set_method_name = f'set_{prop_name}' + if hasattr(cam, set_method_name): + set_method = getattr(cam, set_method_name) + get_method = getattr(cam, name) + try: + set_method(1) + assert get_method() == 1 + except Exception: + print("\tFailed test for property", prop_name) + +def test_invalid_settings(): + print(f"Testing invalid settings") + invalid_settings = [ + {"xclk_freq": 21000000}, + {"frame_size": 23}, + {"pixel_format": 7}, + {"jpeg_quality": 101}, + {"jpeg_quality": -1}, + {"grab_mode": 3}, + ] + Delay= 10 + + for settings in invalid_settings: + param, invalid_value = next(iter(settings.items())) + try: + print("testing",param,"=",invalid_value) + time.sleep_ms(Delay) + with Camera(**{param: invalid_value}) as cam: + print(f"\tFailed test for {param} with value {invalid_value}") + except Exception as e: + time.sleep_ms(Delay) + +if __name__ == "__main__": + test_property_get_frame_size() + test_property_get_pixel_format() + test_must_be_initialized() + test_camera_properties() + test_invalid_settings() + diff --git a/typings/acamera.pyi b/typings/acamera.pyi new file mode 100644 index 0000000..88ede9f --- /dev/null +++ b/typings/acamera.pyi @@ -0,0 +1,9 @@ +from camera import Camera as _Camera + +class Camera(_Camera): + async def acapture(self) -> memoryview: + """Asynchronously capture a frame and return it as a memoryview. + + Yields control back to the event loop while waiting for a frame to become available. + """ + ... \ No newline at end of file diff --git a/typings/camera.pyi b/typings/camera.pyi new file mode 100644 index 0000000..679d9be --- /dev/null +++ b/typings/camera.pyi @@ -0,0 +1,348 @@ +from __future__ import annotations +from typing import Final + +class GainCeiling(): + X2: Final[int] = 0 # 2X gain + X4: Final[int] = 1 # 4X gain + X8: Final[int] = 2 # 8X gain + X16: Final[int] = 3 # 16X gain + X32: Final[int] = 4 # 32X gain + X64: Final[int] = 5 # 64X gain + X128: Final[int] = 6 # 128X gain + def __init__(self, *argv, **kwargs) -> None: + ... + + +class GrabMode(): + WHEN_EMPTY: Final[int] = 0 + LATEST: Final[int] = 1 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class PixelFormat(): + RGB555: Final[int] = 8 + RGB565: Final[int] = 0 + RGB888: Final[int] = 5 + YUV420: Final[int] = 2 + YUV422: Final[int] = 1 + RGB444: Final[int] = 7 + GRAYSCALE: Final[int] = 3 + JPEG: Final[int] = 4 + RAW: Final[int] = 6 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class FrameSize(): + R240X240: Final[int] = 5 + XGA: Final[int] = 12 + R320X320: Final[int] = 7 + QVGA: Final[int] = 6 + R128x128: int = 2 + QXGA: Final[int] = 19 + VGA: Final[int] = 10 + R96X96: Final[int] = 0 + WQXGA: Final[int] = 21 + SVGA: Final[int] = 11 + UXGA: Final[int] = 15 + SXGA: Final[int] = 14 + HQVGA: Final[int] = 4 + QSXGA: Final[int] = 23 + HVGA: Final[int] = 9 + CIF: Final[int] = 8 + HD: Final[int] = 13 + FHD: Final[int] = 16 + QHD: Final[int] = 20 + P_3MP: Final[int] = 18 + QQVGA: Final[int] = 1 + P_FHD: Final[int] = 22 + QCIF: Final[int] = 3 + P_HD: Final[int] = 17 + def __init__(self, *argv, **kwargs) -> None: + ... + + +class Camera(): + def __init__(self, *, + data_pins: list[int] | None = None, + pclk_pin: int | None = None, + vsync_pin: int | None = None, + href_pin: int | None = None, + sda_pin: int | None = None, + scl_pin: int | None = None, + xclk_pin: int | None = None, + xclk_freq: int = 20000000, + powerdown_pin: int = -1, + reset_pin: int = -1, + pixel_format: int = PixelFormat.RGB565, + frame_size: int = FrameSize.QVGA, + jpeg_quality: int = 85, + fb_count: int = 1, + grab_mode: int = GrabMode.WHEN_EMPTY, + init: bool = True) -> None: + ... + + def get_special_effect(self) -> int: + """Get the current special effect setting.""" + ... + + def set_special_effect(self, value: int) -> None: + """Set the special effect.""" + ... + + def set_awb_gain(self, value: bool) -> None: + """Enable/disable Auto White Balance Gain.""" + ... + + def get_awb_gain(self) -> bool: + """Get Auto White Balance Gain state.""" + ... + + def set_agc_gain(self, value: int) -> None: + """Set the Auto Gain Control gain value.""" + ... + + def get_agc_gain(self) -> int: + """Get the Auto Gain Control gain value.""" + ... + + def set_aec_value(self, value: int) -> None: + """Set the Auto Exposure Control value.""" + ... + + def get_aec_value(self) -> int: + """Get the Auto Exposure Control value.""" + ... + + def set_bpc(self, value: bool) -> None: + """Enable/disable Bad Pixel Correction.""" + ... + + def get_bpc(self) -> bool: + """Get Bad Pixel Correction state.""" + ... + + def set_contrast(self, value: int) -> None: + """Set the contrast level (-2 to 2).""" + ... + + def get_contrast(self) -> int: + """Get the contrast level.""" + ... + + def set_colorbar(self, value: bool) -> None: + """Enable/disable color bar test pattern.""" + ... + + def get_colorbar(self) -> bool: + """Get color bar test pattern state.""" + ... + + def set_brightness(self, value: int) -> None: + """Set the brightness level (-2 to 2).""" + ... + + def get_brightness(self) -> int: + """Get the brightness level.""" + ... + + def set_aec2(self, value: bool) -> None: + """Enable/disable AEC DSP.""" + ... + + def get_aec2(self) -> bool: + """Get AEC DSP state.""" + ... + + def set_whitebal(self, value: bool) -> None: + """Enable/disable white balance.""" + ... + + def get_whitebal(self) -> bool: + """Get white balance state.""" + ... + + def set_wb_mode(self, value: int) -> None: + """Set white balance mode.""" + ... + + def get_wb_mode(self) -> int: + """Get white balance mode.""" + ... + + def set_vflip(self, value: bool) -> None: + """Enable/disable vertical flip.""" + ... + + def get_vflip(self) -> bool: + """Get vertical flip state.""" + ... + + def set_wpc(self, value: bool) -> None: + """Enable/disable White Pixel Correction.""" + ... + + def get_wpc(self) -> bool: + """Get White Pixel Correction state.""" + ... + + def set_ae_level(self, value: int) -> None: + """Set Auto Exposure level (-2 to 2).""" + ... + + def get_ae_level(self) -> int: + """Get Auto Exposure level.""" + ... + + def reconfigure(self, *, frame_size: int | None = None, + pixel_format: int | None = None, + grab_mode: int | None = None, + fb_count: int | None = None) -> None: + """Reconfigure camera with new settings.""" + ... + + def init(self) -> None: + """Initialize the camera.""" + ... + + def deinit(self) -> None: + """Deinitialize the camera.""" + ... + + def set_dcw(self, value: bool) -> None: + """Enable/disable DCW (Downsize EN).""" + ... + + def get_dcw(self) -> bool: + """Get DCW (Downsize EN) state.""" + ... + + def set_sharpness(self, value: int) -> None: + """Set the sharpness level (-2 to 2).""" + ... + + def get_sharpness(self) -> int: + """Get the sharpness level.""" + ... + + def set_saturation(self, value: int) -> None: + """Set the saturation level (-2 to 2).""" + ... + + def get_saturation(self) -> int: + """Get the saturation level.""" + ... + + def set_raw_gma(self, value: bool) -> None: + """Enable/disable GMA (Gamma) Correction.""" + ... + + def get_raw_gma(self) -> bool: + """Get GMA (Gamma) Correction state.""" + ... + + def set_quality(self, value: int) -> None: + """Set JPEG quality (0-63).""" + ... + + def get_quality(self) -> int: + """Get JPEG quality.""" + ... + + def set_frame_size(self, value: int) -> None: + """Set frame size.""" + ... + + def get_frame_size(self) -> int: + """Get frame size.""" + ... + + def set_exposure_ctrl(self, value: bool) -> None: + """Enable/disable Auto Exposure Control.""" + ... + + def get_exposure_ctrl(self) -> bool: + """Get Auto Exposure Control state.""" + ... + + def set_denoise(self, value: int) -> None: + """Set denoise level.""" + ... + + def get_denoise(self) -> int: + """Get denoise level.""" + ... + + def set_gain_ctrl(self, value: bool) -> None: + """Enable/disable Auto Gain Control.""" + ... + + def get_gain_ctrl(self) -> bool: + """Get Auto Gain Control state.""" + ... + + def set_lenc(self, value: bool) -> None: + """Enable/disable Lens Correction.""" + ... + + def get_lenc(self) -> bool: + """Get Lens Correction state.""" + ... + + def set_hmirror(self, value: bool) -> None: + """Enable/disable horizontal mirror.""" + ... + + def get_hmirror(self) -> bool: + """Get horizontal mirror state.""" + ... + + def set_gainceiling(self, value: int) -> None: + """Set gain ceiling.""" + ... + + def get_gainceiling(self) -> int: + """Get gain ceiling.""" + ... + + def frame_available(self) -> bool: + """Check if a frame is available.""" + ... + + def capture(self) -> memoryview: + """Capture a frame and return it as a memoryview.""" + ... + + def free_buffer(self) -> None: + """Free the frame buffer.""" + ... + + def get_pixel_width(self) -> int: + """Get frame width in pixels.""" + ... + + def get_pixel_height(self) -> int: + """Get frame height in pixels.""" + ... + + def get_pixel_format(self) -> int: + """Get current pixel format.""" + ... + + def get_sensor_name(self) -> str: + """Get camera sensor name.""" + ... + + def get_max_frame_size(self) -> int: + """Get maximum supported frame size.""" + ... + + def get_fb_count(self) -> int: + """Get frame buffer count.""" + ... + + def get_grab_mode(self) -> int: + """Get current grab mode.""" + ... +