├── .clang-format ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── Doxyfile ├── _redirects ├── build.sh ├── filter-Doxygen-warning.awk └── logo.svg ├── examples ├── AsyncCam │ ├── AsyncCam.hpp │ ├── AsyncCam.ino │ ├── README.md │ └── handlers.cpp └── WifiCam │ ├── README.md │ ├── WifiCam.hpp │ ├── WifiCam.ino │ └── handlers.cpp ├── library.properties ├── mk └── format-code.sh └── src ├── esp32cam.h └── esp32cam ├── asyncweb.cpp ├── asyncweb.hpp ├── camera.cpp ├── camera.hpp ├── config.cpp ├── config.hpp ├── frame.cpp ├── frame.hpp ├── logger.hpp ├── mjpeg.cpp ├── mjpeg.hpp ├── pins.hpp ├── resolution.cpp └── resolution.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: Mozilla 3 | AllowShortFunctionsOnASingleLine: Empty 4 | BinPackArguments: true 5 | BinPackParameters: true 6 | BreakBeforeBraces: Attach 7 | ColumnLimit: 100 8 | Cpp11BracedListStyle: true 9 | FixNamespaceComments: true 10 | IncludeIsMainRegex: '(\.t)?$' 11 | InsertBraces: true 12 | QualifierAlignment: Custom 13 | QualifierOrder: ['static', 'inline', 'const', 'constexpr', 'volatile', 'type', 'restrict'] 14 | ReflowComments: false 15 | ShortNamespaceLines: 1000000 16 | SpacesInContainerLiterals: false 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | pull_request: 5 | workflow_dispatch: 6 | permissions: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-22.04 10 | steps: 11 | - name: Install dependencies 12 | run: | 13 | sudo apt-get install -y --no-install-recommends clang-format-15 doxygen 14 | - uses: actions/checkout@v4 15 | - name: Check code style 16 | run: | 17 | mk/format-code.sh 18 | git diff --exit-code 19 | - uses: arduino/compile-sketches@v1 20 | with: 21 | fqbn: esp32:esp32:esp32cam 22 | libraries: | 23 | - source-path: ./ 24 | - name: Async TCP 25 | - name: ESP Async WebServer 26 | # https://github.com/mathieucarbou/AsyncTCP 27 | # https://github.com/mathieucarbou/ESPAsyncWebServer 28 | - name: Build docs 29 | run: docs/build.sh 30 | - name: Deploy docs 31 | uses: nwtgck/actions-netlify@v3 32 | with: 33 | publish-dir: ./docs/html/ 34 | production-deploy: true 35 | github-token: ${{ secrets.GITHUB_TOKEN }} 36 | deploy-message: ${{ github.sha }} ${{ github.run_id }} 37 | enable-pull-request-comment: false 38 | enable-commit-comment: false 39 | enable-commit-status: false 40 | enable-github-deployment: false 41 | env: 42 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 43 | NETLIFY_SITE_ID: 7de100ca-e942-419d-af2c-18e6c528b96e 44 | if: ${{ github.repository == 'yoursunny/esp32cam' && github.event_name == 'push' }} 45 | timeout-minutes: 30 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /docs/html 3 | /examples/*/build 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020-2025, Junxiao Shi 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esp32cam: OV2640 camera on ESP32-CAM 2 | 3 | [![GitHub build status](https://img.shields.io/github/actions/workflow/status/yoursunny/esp32cam/build.yml?style=flat)](https://github.com/yoursunny/esp32cam/actions) [![GitHub code size](https://img.shields.io/github/languages/code-size/yoursunny/esp32cam?style=flat)](https://github.com/yoursunny/esp32cam) 4 | 5 | **esp32cam** library provides an object oriented API to use OV2640 camera on ESP32 microcontroller. 6 | It is a wrapper of [esp32-camera library](https://github.com/espressif/esp32-camera). 7 | This library has been tested with AI Thinker [ESP32-CAM](https://docs.ai-thinker.com/en/esp32-cam) board and OV2640 camera. 8 | 9 | * [Doxygen documentation](https://esp32cam.yoursunny.dev) 10 | * [#esp32cam on Twitter](https://twitter.com/hashtag/esp32cam) 11 | * [video tutorial](https://youtu.be/Sb08leLWOgA) 12 | 13 | ![esp32cam logo](docs/logo.svg) 14 | 15 | ## Installation 16 | 17 | 1. Install [ESP32 Arduino core](https://github.com/espressif/arduino-esp32) v3.x. 18 | 2. Clone this repository under `$HOME/Arduino/libraries` directory. 19 | 3. In *Tools* - *Board* menu, select **AI Thinker ESP32-CAM** to enable 4MB external PSRAM. 20 | 4. Add `#include ` to your sketch. 21 | 5. Check out the [examples](examples/) for how to use. 22 | -------------------------------------------------------------------------------- /docs/Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = esp32cam 2 | PROJECT_BRIEF = "OV2640 camera on ESP32-CAM" 3 | OUTPUT_DIRECTORY = "." 4 | MARKDOWN_SUPPORT = NO 5 | AUTOLINK_SUPPORT = NO 6 | BUILTIN_STL_SUPPORT = YES 7 | EXTRACT_ALL = YES 8 | EXTRACT_PRIV_VIRTUAL = YES 9 | QUIET = YES 10 | INPUT = "../src" "../examples" 11 | FILE_PATTERNS = "*.h" "*.hpp" "*.c" "*.cpp" "*.ino" 12 | EXTENSION_MAPPING = "ino=C++" 13 | RECURSIVE = YES 14 | PREDEFINED = "ARDUINO=100" "ARDUINO_ARCH_ESP32=1" 15 | HTML_COLORSTYLE_HUE = 293 16 | HTML_COLORSTYLE_SAT = 255 17 | HTML_COLORSTYLE_GAMMA = 130 18 | GENERATE_LATEX = NO 19 | CLASS_DIAGRAMS = NO 20 | HAVE_DOT = NO 21 | -------------------------------------------------------------------------------- /docs/_redirects: -------------------------------------------------------------------------------- 1 | https://esp32cam.netlify.app/* https://esp32cam.yoursunny.dev/:splat 301! 2 | -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd "$( dirname "${BASH_SOURCE[0]}" )" 4 | 5 | doxygen Doxyfile 2>&1 | ./filter-Doxygen-warning.awk 1>&2 6 | 7 | find html -name '*.html' | xargs sed -i '/<\/head>/ i\' 8 | cp _redirects html/ 9 | -------------------------------------------------------------------------------- /docs/filter-Doxygen-warning.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/gawk -f 2 | /warning: The following parameters? of .* not documented:/ { 3 | skip = 1 4 | next 5 | } 6 | $0 ~ /^ parameter/ { 7 | if (!skip) { 8 | print 9 | } 10 | } 11 | $0 !~ /^ / { 12 | skip = 0 13 | print 14 | } -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | esp32 14 | cam 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/AsyncCam/AsyncCam.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ASYNCCAM_HPP 2 | #define ASYNCCAM_HPP 3 | 4 | // It is necessary to include ESPAsyncWebServer.h before esp32cam.h for Arduino builder to 5 | // recognize the dependency. 6 | #include 7 | 8 | #include 9 | 10 | extern esp32cam::Resolution initialResolution; 11 | 12 | extern AsyncWebServer server; 13 | 14 | void 15 | addRequestHandlers(); 16 | 17 | #endif // ASYNCCAM_HPP 18 | -------------------------------------------------------------------------------- /examples/AsyncCam/AsyncCam.ino: -------------------------------------------------------------------------------- 1 | #include "AsyncCam.hpp" 2 | #include 3 | 4 | static const char* WIFI_SSID = "my-ssid"; 5 | static const char* WIFI_PASS = "my-pass"; 6 | 7 | esp32cam::Resolution initialResolution; 8 | 9 | AsyncWebServer server(80); 10 | 11 | void 12 | setup() { 13 | Serial.begin(115200); 14 | Serial.println(); 15 | esp32cam::setLogger(Serial); 16 | delay(1000); 17 | 18 | WiFi.persistent(false); 19 | WiFi.mode(WIFI_STA); 20 | WiFi.begin(WIFI_SSID, WIFI_PASS); 21 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { 22 | Serial.printf("WiFi failure %d\n", WiFi.status()); 23 | delay(5000); 24 | ESP.restart(); 25 | } 26 | Serial.println("WiFi connected"); 27 | delay(1000); 28 | 29 | { 30 | using namespace esp32cam; 31 | 32 | initialResolution = Resolution::find(1024, 768); 33 | 34 | Config cfg; 35 | cfg.setPins(pins::AiThinker); 36 | cfg.setResolution(initialResolution); 37 | cfg.setBufferCount(3); 38 | cfg.setJpeg(80); 39 | 40 | bool ok = Camera.begin(cfg); 41 | if (!ok) { 42 | Serial.println("camera initialize failure"); 43 | delay(5000); 44 | ESP.restart(); 45 | } 46 | Serial.println("camera initialize success"); 47 | } 48 | 49 | Serial.println("camera starting"); 50 | Serial.print("http://"); 51 | Serial.println(WiFi.localIP()); 52 | 53 | addRequestHandlers(); 54 | server.begin(); 55 | } 56 | 57 | void 58 | loop() { 59 | // esp32cam/asyncweb.hpp depends on FreeRTOS task API including vTaskDelete, so you must have a 60 | // non-zero delay in the loop() function; otherwise, FreeRTOS kernel memory cannot be freed 61 | // properly and the system would run out of memory. 62 | delay(1); 63 | } 64 | -------------------------------------------------------------------------------- /examples/AsyncCam/README.md: -------------------------------------------------------------------------------- 1 | # AsyncCam: camera on ESPAsyncWebServer 2 | 3 | This example runs on ESP32-CAM board. 4 | It demonstrates how to use esp32cam library with [ESPAsyncWebServer](https://github.com/mathieucarbou/ESPAsyncWebServer) library. 5 | The HTTP server supports both JPEG still image and MJPEG stream, and allows changing camera resolution on the fly. 6 | To use this example, modify WiFi SSID+password, then upload to ESP32. 7 | -------------------------------------------------------------------------------- /examples/AsyncCam/handlers.cpp: -------------------------------------------------------------------------------- 1 | #include "AsyncCam.hpp" 2 | #include 3 | 4 | static const char FRONTPAGE[] = R"EOT( 5 | 6 | esp32cam AsyncCam example 7 | 8 |

esp32cam AsyncCam example

9 |

10 | 11 | %brightness% 12 | %contrast% 13 | %saturation% 14 | 15 | 16 | 17 | %hmirror% 18 | %vflip% 19 | %rawGma% 20 | %lensCorrection% 21 | 22 |

23 |

24 | 25 | 26 | 27 | 28 |

29 |
30 | 31 | 69 | )EOT"; 70 | 71 | static String 72 | rewriteFrontpage(const esp32cam::Settings& s, const String& var) { 73 | StreamString b; 74 | 75 | if (var == "resolution") { 76 | for (const auto& r : esp32cam::Camera.listResolutions()) { 77 | b.printf(""); 83 | } 84 | } else if (var == "gain") { 85 | #define SHOW_GAIN(val, dsp) \ 86 | b.printf("", val, s.gain == val ? " selected" : "", dsp) 87 | b.printf(""); 88 | for (int i = 1; i <= 31; ++i) { 89 | SHOW_GAIN(i, i); 90 | } 91 | b.printf(""); 92 | b.printf(""); 93 | for (int i = 2; i <= 128; i <<= 1) { 94 | SHOW_GAIN(-i, i); 95 | } 96 | b.printf(""); 97 | #undef SHOW_GAIN 98 | } else if (var == "lightMode") { 99 | #define SHOW_LM(MODE, symbol) \ 100 | b.printf("", \ 101 | static_cast(esp32cam::LightMode::MODE), #MODE, \ 102 | s.lightMode == esp32cam::LightMode::MODE ? " selected" : "", symbol) 103 | SHOW_LM(NONE, "🚫"); 104 | SHOW_LM(AUTO, "⭕"); 105 | SHOW_LM(SUNNY, "☀️"); 106 | SHOW_LM(CLOUDY, "☁️"); 107 | SHOW_LM(OFFICE, "🏢"); 108 | SHOW_LM(HOME, "🏠"); 109 | #undef SHOW_LM 110 | } else if (var == "specialEffect") { 111 | #define SHOW_SE(MODE, symbol) \ 112 | b.printf("", \ 113 | static_cast(esp32cam::SpecialEffect::MODE), #MODE, \ 114 | s.specialEffect == esp32cam::SpecialEffect::MODE ? " selected" : "", symbol) 115 | SHOW_SE(NONE, "🚫"); 116 | SHOW_SE(NEGATIVE, "⬜"); 117 | SHOW_SE(BLACKWHITE, "⬛"); 118 | SHOW_SE(REDDISH, "🟥"); 119 | SHOW_SE(GREENISH, "🟩"); 120 | SHOW_SE(BLUISH, "🟦"); 121 | SHOW_SE(ANTIQUE, "🖼️"); 122 | #undef SHOW_SE 123 | } 124 | 125 | #define SHOW_INT(MEM, MIN, MAX) \ 126 | else if (var == #MEM) { \ 127 | b.printf("", \ 129 | s.MEM, MIN, MAX); \ 130 | } 131 | 132 | #define SHOW_BOOL(MEM) \ 133 | else if (var == #MEM) { \ 134 | b.printf("", \ 135 | s.MEM ? " checked" : ""); \ 136 | } 137 | 138 | SHOW_INT(brightness, -2, 2) 139 | SHOW_INT(contrast, -2, 2) 140 | SHOW_INT(saturation, -2, 2) 141 | SHOW_BOOL(hmirror) 142 | SHOW_BOOL(vflip) 143 | SHOW_BOOL(rawGma) 144 | SHOW_BOOL(lensCorrection) 145 | #undef SHOW_INT 146 | #undef SHOW_BOOL 147 | return b; 148 | } 149 | 150 | static void 151 | handleFrontpage(AsyncWebServerRequest* req) { 152 | auto settings = esp32cam::Camera.status(); 153 | req->send(200, "text/html", reinterpret_cast(FRONTPAGE), sizeof(FRONTPAGE), 154 | [=](const String& var) { return rewriteFrontpage(settings, var); }); 155 | } 156 | 157 | static void 158 | handleUpdate(AsyncWebServerRequest* req) { 159 | bool ok = esp32cam::Camera.update([=](esp32cam::Settings& s) { 160 | #define SAVE_BOOL(MEM) s.MEM = req->hasArg(#MEM) 161 | #define SAVE_INT(MEM) s.MEM = decltype(s.MEM)(req->arg(#MEM).toInt()) 162 | SAVE_INT(resolution); 163 | SAVE_INT(brightness); 164 | SAVE_INT(contrast); 165 | SAVE_INT(saturation); 166 | SAVE_INT(gain); 167 | SAVE_INT(lightMode); 168 | SAVE_INT(specialEffect); 169 | SAVE_BOOL(hmirror); 170 | SAVE_BOOL(vflip); 171 | SAVE_BOOL(rawGma); 172 | SAVE_BOOL(lensCorrection); 173 | #undef SAVE_BOOL 174 | #undef SAVE_INT 175 | }); 176 | 177 | if (!ok) { 178 | req->send(500, "text/plain", "update settings error\n"); 179 | return; 180 | } 181 | req->send(204); 182 | } 183 | 184 | void 185 | addRequestHandlers() { 186 | server.on("/robots.txt", HTTP_GET, [](AsyncWebServerRequest* req) { 187 | req->send(200, "text/plain", "User-Agent: *\nDisallow: /\n"); 188 | }); 189 | 190 | server.on("/", HTTP_GET, handleFrontpage); 191 | server.on("/update.cgi", HTTP_POST, handleUpdate); 192 | 193 | server.on("/cam.jpg", esp32cam::asyncweb::handleStill); 194 | server.on("/cam.mjpeg", esp32cam::asyncweb::handleMjpeg); 195 | server.on("/continuous.mjpeg", HTTP_GET, [](AsyncWebServerRequest* req) { 196 | esp32cam::MjpegConfig cfg; 197 | cfg.minInterval = -1; 198 | req->send(new esp32cam::asyncweb::MjpegResponse(cfg)); 199 | }); 200 | } 201 | -------------------------------------------------------------------------------- /examples/WifiCam/README.md: -------------------------------------------------------------------------------- 1 | # WifiCam: WiFi camera HTTP server 2 | 3 | This example runs on ESP32-CAM board. 4 | It provides an HTTP server where you can access BMP, JPG, and MJPEG formats in various resolutions. 5 | To use this example, modify WiFi SSID+password, then upload to ESP32. 6 | 7 | ESP32 `WebServer` can only serve one TCP connection at a time. 8 | If you have accessed an MJPEG stream in a browser, you need to click *Stop* button to terminate the connection. 9 | Otherwise, you won't be able to open another page or picture. 10 | 11 | Due to memory constraints, it's not recommended to access BMP format in high resolution. 12 | -------------------------------------------------------------------------------- /examples/WifiCam/WifiCam.hpp: -------------------------------------------------------------------------------- 1 | #ifndef WIFICAM_HPP 2 | #define WIFICAM_HPP 3 | 4 | #include 5 | 6 | #include 7 | 8 | extern esp32cam::Resolution initialResolution; 9 | 10 | extern WebServer server; 11 | 12 | void 13 | addRequestHandlers(); 14 | 15 | #endif // WIFICAM_HPP 16 | -------------------------------------------------------------------------------- /examples/WifiCam/WifiCam.ino: -------------------------------------------------------------------------------- 1 | #include "WifiCam.hpp" 2 | #include 3 | 4 | static const char* WIFI_SSID = "my-ssid"; 5 | static const char* WIFI_PASS = "my-pass"; 6 | 7 | esp32cam::Resolution initialResolution; 8 | 9 | WebServer server(80); 10 | 11 | void 12 | setup() { 13 | Serial.begin(115200); 14 | Serial.println(); 15 | esp32cam::setLogger(Serial); 16 | delay(1000); 17 | 18 | WiFi.persistent(false); 19 | WiFi.mode(WIFI_STA); 20 | WiFi.begin(WIFI_SSID, WIFI_PASS); 21 | if (WiFi.waitForConnectResult() != WL_CONNECTED) { 22 | Serial.printf("WiFi failure %d\n", WiFi.status()); 23 | delay(5000); 24 | ESP.restart(); 25 | } 26 | Serial.println("WiFi connected"); 27 | delay(1000); 28 | 29 | { 30 | using namespace esp32cam; 31 | 32 | initialResolution = Resolution::find(1024, 768); 33 | 34 | Config cfg; 35 | cfg.setPins(pins::AiThinker); 36 | cfg.setResolution(initialResolution); 37 | cfg.setJpeg(80); 38 | 39 | bool ok = Camera.begin(cfg); 40 | if (!ok) { 41 | Serial.println("camera initialize failure"); 42 | delay(5000); 43 | ESP.restart(); 44 | } 45 | Serial.println("camera initialize success"); 46 | } 47 | 48 | Serial.println("camera starting"); 49 | Serial.print("http://"); 50 | Serial.println(WiFi.localIP()); 51 | 52 | addRequestHandlers(); 53 | server.begin(); 54 | } 55 | 56 | void 57 | loop() { 58 | server.handleClient(); 59 | } 60 | -------------------------------------------------------------------------------- /examples/WifiCam/handlers.cpp: -------------------------------------------------------------------------------- 1 | #include "WifiCam.hpp" 2 | #include 3 | #include 4 | 5 | static const char FRONTPAGE[] = R"EOT( 6 | 7 | esp32cam WifiCam example 8 | 14 | 15 |

esp32cam WifiCam example

16 | 17 | 18 | 20 |
BMPJPGMJPEG 19 |
loading 21 |
22 | 23 | 41 | )EOT"; 42 | 43 | static void 44 | serveStill(bool wantBmp) { 45 | auto frame = esp32cam::capture(); 46 | if (frame == nullptr) { 47 | Serial.println("capture() failure"); 48 | server.send(500, "text/plain", "still capture error\n"); 49 | return; 50 | } 51 | Serial.printf("capture() success: %dx%d %zub\n", frame->getWidth(), frame->getHeight(), 52 | frame->size()); 53 | 54 | if (wantBmp) { 55 | if (!frame->toBmp()) { 56 | Serial.println("toBmp() failure"); 57 | server.send(500, "text/plain", "convert to BMP error\n"); 58 | return; 59 | } 60 | Serial.printf("toBmp() success: %dx%d %zub\n", frame->getWidth(), frame->getHeight(), 61 | frame->size()); 62 | } 63 | 64 | server.setContentLength(frame->size()); 65 | server.send(200, wantBmp ? "image/bmp" : "image/jpeg"); 66 | frame->writeTo(server.client()); 67 | } 68 | 69 | static void 70 | serveMjpeg() { 71 | Serial.println("MJPEG streaming begin"); 72 | auto startTime = millis(); 73 | int nFrames = esp32cam::Camera.streamMjpeg(server.client()); 74 | auto duration = millis() - startTime; 75 | Serial.printf("MJPEG streaming end: %dfrm %0.2ffps\n", nFrames, 1000.0 * nFrames / duration); 76 | } 77 | 78 | void 79 | addRequestHandlers() { 80 | server.on("/", HTTP_GET, [] { 81 | server.setContentLength(sizeof(FRONTPAGE)); 82 | server.send(200, "text/html"); 83 | server.sendContent(FRONTPAGE, sizeof(FRONTPAGE)); 84 | }); 85 | 86 | server.on("/robots.txt", HTTP_GET, 87 | [] { server.send(200, "text/html", "User-Agent: *\nDisallow: /\n"); }); 88 | 89 | server.on("/resolutions.csv", HTTP_GET, [] { 90 | StreamString b; 91 | for (const auto& r : esp32cam::Camera.listResolutions()) { 92 | b.println(r); 93 | } 94 | server.send(200, "text/csv", b); 95 | }); 96 | 97 | server.on(UriBraces("/{}x{}.{}"), HTTP_GET, [] { 98 | long width = server.pathArg(0).toInt(); 99 | long height = server.pathArg(1).toInt(); 100 | String format = server.pathArg(2); 101 | if (width == 0 || height == 0 || !(format == "bmp" || format == "jpg" || format == "mjpeg")) { 102 | server.send(404); 103 | return; 104 | } 105 | 106 | auto r = esp32cam::Camera.listResolutions().find(width, height); 107 | if (!r.isValid()) { 108 | server.send(404, "text/plain", "non-existent resolution\n"); 109 | return; 110 | } 111 | if (r.getWidth() != width || r.getHeight() != height) { 112 | server.sendHeader("Location", 113 | String("/") + r.getWidth() + "x" + r.getHeight() + "." + format); 114 | server.send(302); 115 | return; 116 | } 117 | 118 | if (!esp32cam::Camera.update([=](esp32cam::Settings& settings) { settings.resolution = r; }, 119 | 500)) { 120 | server.send(500, "text/plain", "update resolution error\n"); 121 | return; 122 | } 123 | 124 | if (format == "bmp") { 125 | serveStill(true); 126 | } else if (format == "jpg") { 127 | serveStill(false); 128 | } else if (format == "mjpeg") { 129 | serveMjpeg(); 130 | } 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=esp32cam 2 | version=0.0.20250112 3 | author=Junxiao Shi 4 | maintainer=Junxiao Shi 5 | sentence=OV2640 camera on ESP32-CAM. 6 | paragraph=This library provides an object oriented API to use OV2640 camera on ESP32 microcontroller. 7 | category=Sensors 8 | url=https://esp32cam.yoursunny.dev/ 9 | architectures=esp32 10 | -------------------------------------------------------------------------------- /mk/format-code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | cd "$( dirname "${BASH_SOURCE[0]}" )"/.. 4 | 5 | find -name '*.h' -or -name '*.[hc]pp' -or -name '*.ino' | \ 6 | xargs clang-format-15 -i -style=file 7 | -------------------------------------------------------------------------------- /src/esp32cam.h: -------------------------------------------------------------------------------- 1 | /** 2 | * @mainpage esp32cam 3 | * 4 | * https://github.com/yoursunny/esp32cam 5 | */ 6 | 7 | #ifndef ESP32CAM_H 8 | #define ESP32CAM_H 9 | 10 | #include "esp32cam/asyncweb.hpp" 11 | #include "esp32cam/camera.hpp" 12 | #include "esp32cam/logger.hpp" 13 | 14 | namespace esp32cam { 15 | 16 | /** @brief Capture a frame with the camera. */ 17 | inline std::unique_ptr 18 | capture() { 19 | return Camera.capture(); 20 | } 21 | 22 | } // namespace esp32cam 23 | 24 | #endif // ESP32CAM_H 25 | -------------------------------------------------------------------------------- /src/esp32cam/asyncweb.cpp: -------------------------------------------------------------------------------- 1 | #if __has_include() 2 | 3 | #include "asyncweb.hpp" 4 | #include "logger.hpp" 5 | #include 6 | #include 7 | 8 | #define STILL_LOG(fmt, ...) ESP32CAM_LOG("StillResponse(%p) " fmt, this, ##__VA_ARGS__) 9 | #define MJPEG_LOG(fmt, ...) ESP32CAM_LOG("MjpegResponse(%p) " fmt, this, ##__VA_ARGS__) 10 | 11 | namespace esp32cam { 12 | namespace detail { 13 | 14 | CaptureTask::CaptureTask(uint32_t queueLength, uint32_t priority) { 15 | m_queue = xQueueCreate(queueLength, sizeof(Frame*)); 16 | if (m_queue == nullptr) { 17 | return; 18 | } 19 | 20 | TaskHandle_t task = nullptr; 21 | if (xTaskCreatePinnedToCore(run, "esp32cam-capture", 2048, this, priority, &task, 22 | xPortGetCoreID()) == pdPASS) { 23 | m_task = task; 24 | }; 25 | } 26 | 27 | CaptureTask::~CaptureTask() { 28 | if (m_task != nullptr) { 29 | vTaskDelete(reinterpret_cast(m_task)); 30 | m_task = nullptr; 31 | } 32 | 33 | if (m_queue != nullptr) { 34 | Frame* frame = nullptr; 35 | while (xQueueReceive(reinterpret_cast(m_queue), &frame, 0) == pdTRUE) { 36 | delete frame; 37 | } 38 | vQueueDelete(reinterpret_cast(m_queue)); 39 | m_queue = nullptr; 40 | } 41 | } 42 | 43 | void 44 | CaptureTask::request(bool continuous) { 45 | if (m_task == nullptr || m_continuous) { 46 | return; 47 | } 48 | m_continuous = continuous; 49 | xTaskNotify(reinterpret_cast(m_task), 1, eSetValueWithOverwrite); 50 | } 51 | 52 | std::unique_ptr 53 | CaptureTask::retrieve() { 54 | Frame* frame = nullptr; 55 | if (m_queue == nullptr || 56 | xQueueReceive(reinterpret_cast(m_queue), &frame, 0) != pdTRUE) { 57 | return nullptr; 58 | } 59 | return std::unique_ptr(frame); 60 | } 61 | 62 | void 63 | CaptureTask::run(void* ctx) { 64 | auto self = reinterpret_cast(ctx); 65 | while (true) { 66 | if (!self->m_continuous) { 67 | uint32_t value = 0; 68 | xTaskNotifyWait(0, UINT32_MAX, &value, pdMS_TO_TICKS(10000)); 69 | if (value == 0) { 70 | continue; 71 | } 72 | } 73 | 74 | auto frame = Camera.capture().release(); 75 | while (xQueueSend(reinterpret_cast(self->m_queue), &frame, 76 | pdMS_TO_TICKS(10000)) != pdTRUE) { 77 | ; 78 | } 79 | } 80 | } 81 | 82 | } // namespace detail 83 | namespace asyncweb { 84 | 85 | StillResponse::StillResponse() 86 | : m_task(1) { 87 | STILL_LOG("created"); 88 | if (!m_task) { 89 | _code = 500; 90 | return; 91 | } 92 | 93 | _code = 200; 94 | _contentType = "image/jpeg"; 95 | _sendContentLength = false; 96 | m_task.request(); 97 | } 98 | 99 | StillResponse::~StillResponse() { 100 | STILL_LOG("deleted"); 101 | } 102 | 103 | size_t 104 | StillResponse::_fillBuffer(uint8_t* buf, size_t buflen) { 105 | if (!m_frame) { 106 | if (m_frame = m_task.retrieve(); !m_frame) { 107 | return RESPONSE_TRY_AGAIN; 108 | } 109 | STILL_LOG("frame has %zu octets", m_frame->size()); 110 | } 111 | 112 | if (m_index >= m_frame->size()) { 113 | STILL_LOG("sent to client"); 114 | return 0; 115 | } 116 | 117 | size_t len = std::min(buflen, m_frame->size() - m_index); 118 | std::copy_n(m_frame->data() + m_index, len, buf); 119 | m_index += len; 120 | return len; 121 | } 122 | 123 | MjpegResponse::MjpegResponse(const MjpegConfig& cfg) 124 | : m_task(2) 125 | , m_ctrl(cfg) { 126 | MJPEG_LOG("created"); 127 | if (!m_task) { 128 | _code = 500; 129 | m_ctrl.notifyFail(); 130 | return; 131 | } 132 | 133 | _code = 200; 134 | m_hdr.prepareResponseContentType(); 135 | _contentType = String(m_hdr.buf, m_hdr.size); 136 | _sendContentLength = false; 137 | } 138 | 139 | MjpegResponse::~MjpegResponse() { 140 | int nFrames = m_ctrl.countSentFrames(); 141 | float fps = 1000.0 * nFrames / (millis() - m_createTime); 142 | MJPEG_LOG("deleted after %d frames at fps %f", m_ctrl.countSentFrames(), fps); 143 | } 144 | 145 | size_t 146 | MjpegResponse::_fillBuffer(uint8_t* buf, size_t buflen) { 147 | auto act = m_ctrl.decideAction(); 148 | switch (act) { 149 | case Ctrl::RETURN: { 150 | if (auto frame = m_task.retrieve(); frame) { 151 | m_ctrl.notifyReturn(std::move(frame)); 152 | } 153 | m_sendSince = millis(); 154 | m_sendNext = SIPartHeader; 155 | m_sendRemain = 0; 156 | 157 | if (m_ctrl.decideAction() != Ctrl::SEND) { 158 | return RESPONSE_TRY_AGAIN; 159 | } 160 | // fallthrough 161 | } 162 | case Ctrl::SEND: { 163 | size_t len = sendPart(buf, buflen); 164 | if (len == 0 && m_sendNext == SINone) { 165 | m_ctrl.notifySent(true); 166 | } else { 167 | if (static_cast(millis() - m_sendSince) > m_ctrl.cfg.frameTimeout) { 168 | m_ctrl.notifySent(false); 169 | } 170 | return len; 171 | } 172 | 173 | if (m_ctrl.decideAction() != Ctrl::CAPTURE) { 174 | return RESPONSE_TRY_AGAIN; 175 | } 176 | // fallthrough 177 | } 178 | case Ctrl::CAPTURE: { 179 | m_task.request(m_ctrl.cfg.minInterval < 0); 180 | m_ctrl.notifyCapture(); 181 | return RESPONSE_TRY_AGAIN; 182 | } 183 | case Ctrl::STOP: 184 | return 0; 185 | default: 186 | return RESPONSE_TRY_AGAIN; 187 | } 188 | } 189 | 190 | size_t 191 | MjpegResponse::sendPart(uint8_t* buf, size_t buflen) { 192 | if (m_sendRemain == 0) { 193 | switch (m_sendNext) { 194 | case SIPartHeader: 195 | m_hdr.preparePartHeader(m_ctrl.getFrame()->size()); 196 | m_sendBuf = reinterpret_cast(m_hdr.buf); 197 | m_sendRemain = m_hdr.size; 198 | m_sendNext = SIFrame; 199 | break; 200 | case SIFrame: { 201 | Frame* frame = m_ctrl.getFrame(); 202 | m_sendBuf = frame->data(); 203 | m_sendRemain = frame->size(); 204 | m_sendNext = SIPartTrailer; 205 | break; 206 | } 207 | case SIPartTrailer: 208 | m_hdr.preparePartTrailer(); 209 | m_sendBuf = reinterpret_cast(m_hdr.buf); 210 | m_sendRemain = m_hdr.size; 211 | m_sendNext = SINone; 212 | break; 213 | case SINone: 214 | return 0; 215 | } 216 | } 217 | 218 | size_t len = std::min(m_sendRemain, buflen); 219 | std::copy_n(m_sendBuf, len, buf); 220 | m_sendBuf += len; 221 | m_sendRemain -= len; 222 | return len; 223 | } 224 | 225 | } // namespace asyncweb 226 | } // namespace esp32cam 227 | 228 | #endif // __has_include() 229 | -------------------------------------------------------------------------------- /src/esp32cam/asyncweb.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_ASYNCWEB_HPP 2 | #define ESP32CAM_ASYNCWEB_HPP 3 | 4 | #if __has_include() 5 | #include "camera.hpp" 6 | #include 7 | 8 | namespace esp32cam { 9 | namespace detail { 10 | 11 | class CaptureTask { 12 | public: 13 | explicit CaptureTask(uint32_t queueLength, uint32_t priority = 1); 14 | 15 | ~CaptureTask(); 16 | 17 | explicit operator bool() const { 18 | return m_queue != nullptr && m_task != nullptr; 19 | } 20 | 21 | void request(bool continuous = false); 22 | 23 | std::unique_ptr retrieve(); 24 | 25 | private: 26 | static void run(void* ctx); 27 | 28 | private: 29 | void* m_queue = nullptr; 30 | void* m_task = nullptr; 31 | bool m_continuous = false; 32 | }; 33 | 34 | } // namespace detail 35 | 36 | /** 37 | * @brief esp32cam integration with ESPAsyncWebServer library. 38 | * @sa https://github.com/mathieucarbou/ESPAsyncWebServer 39 | */ 40 | namespace asyncweb { 41 | 42 | /** 43 | * @brief HTTP response of one still image. 44 | * 45 | * Start a FreeRTOS task to capture an image frame under the current camera settings, 46 | * and respond to the HTTP request as a still JPEG image. 47 | * If multiple StillResponse instances are active concurrently, each will get a different image. 48 | * If task creation fails, respond with HTTP 500 error. 49 | * If image capture fails, the response would be empty, but still has HTTP 200 due to 50 | * ESPAsyncWebServer library limitations. 51 | */ 52 | class StillResponse : public AsyncAbstractResponse { 53 | public: 54 | explicit StillResponse(); 55 | 56 | ~StillResponse() override; 57 | 58 | bool _sourceValid() const override { 59 | return true; 60 | } 61 | 62 | size_t _fillBuffer(uint8_t* buf, size_t buflen) override; 63 | 64 | private: 65 | detail::CaptureTask m_task; 66 | std::unique_ptr m_frame; 67 | size_t m_index = 0; 68 | }; 69 | 70 | /** @brief Handle HTTP request for still image. */ 71 | inline void 72 | handleStill(AsyncWebServerRequest* request) { 73 | request->send(new StillResponse()); 74 | } 75 | 76 | /** 77 | * @brief HTTP response of MJPEG stream. 78 | * 79 | * Start a FreeRTOS task to capture image frames under the current camera settings, 80 | * and respond to the HTTP request as a Motion JPEG stream. 81 | * If multiple MjpegResponse instances are active concurrently, each stream will contain 82 | * different images. 83 | * If task creation fails, respond with HTTP 500 error. 84 | * If image capture fails, the stream is stopped. 85 | * 86 | * Normally, a new frame is captured after the prior frame has been fully sent to the client. 87 | * Setting MjpegConfig::minInternal to -1 enables continuous mode, in which a new frame is 88 | * captured while the prior frame is still being sent to the client. This improves FPS rate 89 | * but also increases video latency. This mode is effective only if there are more than one 90 | * frame buffer created during camera initialization. 91 | */ 92 | class MjpegResponse : public AsyncAbstractResponse { 93 | public: 94 | explicit MjpegResponse(const MjpegConfig& cfg = MjpegConfig()); 95 | 96 | ~MjpegResponse() override; 97 | 98 | bool _sourceValid() const override { 99 | return true; 100 | } 101 | 102 | size_t _fillBuffer(uint8_t* buf, size_t buflen) override; 103 | 104 | private: 105 | size_t sendPart(uint8_t* buf, size_t buflen); 106 | 107 | private: 108 | detail::CaptureTask m_task; 109 | using Ctrl = detail::MjpegController; 110 | Ctrl m_ctrl; 111 | detail::MjpegHeader m_hdr; 112 | 113 | enum SendItem { 114 | SINone, 115 | SIPartHeader, 116 | SIFrame, 117 | SIPartTrailer, 118 | }; 119 | SendItem m_sendNext = SINone; 120 | const uint8_t* m_sendBuf = nullptr; 121 | size_t m_sendRemain = 0; 122 | unsigned long m_createTime = millis(); 123 | unsigned long m_sendSince = 0; 124 | }; 125 | 126 | /** @brief Handle HTTP request for MJPEG stream. */ 127 | inline void 128 | handleMjpeg(AsyncWebServerRequest* request) { 129 | request->send(new MjpegResponse()); 130 | } 131 | 132 | } // namespace asyncweb 133 | } // namespace esp32cam 134 | 135 | #endif // __has_include() 136 | #endif // ESP32CAM_ASYNCWEB_HPP 137 | -------------------------------------------------------------------------------- /src/esp32cam/camera.cpp: -------------------------------------------------------------------------------- 1 | #include "camera.hpp" 2 | 3 | #include 4 | #include 5 | 6 | namespace esp32cam { 7 | 8 | Print* LogOutput = nullptr; 9 | CameraClass Camera; 10 | 11 | bool 12 | CameraClass::begin(const Config& config) { 13 | return esp_camera_init(reinterpret_cast(config.m_cfg)) == ESP_OK; 14 | } 15 | 16 | bool 17 | CameraClass::end() { 18 | return esp_camera_deinit() == ESP_OK; 19 | } 20 | 21 | ResolutionList 22 | CameraClass::listResolutions() const { 23 | sensor_t* sensor = esp_camera_sensor_get(); 24 | if (sensor == nullptr) { 25 | return ResolutionList(); 26 | } 27 | 28 | camera_sensor_info_t* info = esp_camera_sensor_get_info(&sensor->id); 29 | if (info == nullptr) { 30 | return ResolutionList(); 31 | } 32 | 33 | return ResolutionList(info->max_size + 1); 34 | } 35 | 36 | bool 37 | CameraClass::changeResolution(const Resolution& resolution, int sleepFor) { 38 | sensor_t* sensor = esp_camera_sensor_get(); 39 | if (sensor == nullptr) { 40 | return false; 41 | } 42 | 43 | framesize_t frameSize = resolution.as(); 44 | if (sensor->status.framesize == frameSize) { 45 | return true; 46 | } 47 | 48 | if (sensor->set_framesize(sensor, frameSize) != 0) { 49 | return false; 50 | } 51 | if (sleepFor > 0) { 52 | delay(sleepFor); 53 | } 54 | return true; 55 | } 56 | 57 | std::unique_ptr 58 | CameraClass::capture() { 59 | camera_fb_t* fb = esp_camera_fb_get(); 60 | if (fb == nullptr) { 61 | return nullptr; 62 | } 63 | return std::unique_ptr(new Frame(fb)); 64 | } 65 | 66 | int 67 | CameraClass::streamMjpeg(Client& client, const MjpegConfig& cfg) { 68 | detail::MjpegHeader hdr; 69 | hdr.prepareResponseHeaders(); 70 | hdr.writeTo(client); 71 | 72 | using Ctrl = detail::MjpegController; 73 | Ctrl ctrl(cfg); 74 | while (true) { 75 | auto act = ctrl.decideAction(); 76 | switch (act) { 77 | case Ctrl::CAPTURE: { 78 | ctrl.notifyCapture(); 79 | break; 80 | } 81 | case Ctrl::RETURN: { 82 | ctrl.notifyReturn(capture()); 83 | break; 84 | } 85 | case Ctrl::SEND: { 86 | hdr.preparePartHeader(ctrl.getFrame()->size()); 87 | hdr.writeTo(client); 88 | ctrl.notifySent(ctrl.getFrame()->writeTo(client, cfg.frameTimeout)); 89 | hdr.preparePartTrailer(); 90 | hdr.writeTo(client); 91 | break; 92 | } 93 | case Ctrl::STOP: { 94 | client.stop(); 95 | return ctrl.countSentFrames(); 96 | } 97 | default: { 98 | delay(act); 99 | break; 100 | } 101 | } 102 | } 103 | } 104 | 105 | } // namespace esp32cam 106 | -------------------------------------------------------------------------------- /src/esp32cam/camera.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_CAMERA_HPP 2 | #define ESP32CAM_CAMERA_HPP 3 | 4 | #include "config.hpp" 5 | #include "mjpeg.hpp" 6 | #include 7 | 8 | namespace esp32cam { 9 | 10 | class CameraClass { 11 | public: 12 | /** 13 | * @brief Enable camera. 14 | * @return whether success. 15 | */ 16 | bool begin(const Config& config); 17 | 18 | /** 19 | * @brief Disable camera. 20 | * @return whether success. 21 | */ 22 | bool end(); 23 | 24 | /** 25 | * @brief Retrieve list of resolutions (likely) supported by hardware. 26 | * @pre Camera is enabled. 27 | */ 28 | ResolutionList listResolutions() const; 29 | 30 | /** 31 | * @brief Change camera resolution. 32 | * @pre Camera is enabled. 33 | * @param resolution new resolution; must be no higher than initial resolution. 34 | * @param sleepFor how long to wait for stabilization (millis). 35 | * @deprecated Use @c update instead. 36 | */ 37 | bool changeResolution(const Resolution& resolution, int sleepFor = 500); 38 | 39 | /** 40 | * @brief Retrieve runtime settings. 41 | * @pre Camera is enabled. 42 | */ 43 | Settings status() const; 44 | 45 | /** 46 | * @brief Update runtime settings. 47 | * @param settings updated settings. 48 | * @param sleepFor how long to wait for stabilization (millis). 49 | * @pre Camera is enabled. 50 | * @post Camera may be left in inconsistent state in case of failure. 51 | * @return whether success. 52 | */ 53 | bool update(const Settings& settings, int sleepFor = 0); 54 | 55 | /** 56 | * @brief Update runtime settings using modifier function. 57 | * @param modifier function to modify settings. 58 | * @param sleepFor how long to wait for stabilization (millis). 59 | * @pre Camera is enabled. 60 | * @post Camera may be left in inconsistent state in case of failure. 61 | * @return whether success. 62 | */ 63 | template 64 | std::enable_if_t, Fn>, bool> // 65 | update(const Fn& modifier, int sleepFor = 0) { 66 | Settings settings = status(); 67 | modifier(settings); 68 | return update(settings, sleepFor); 69 | } 70 | 71 | /** 72 | * @brief Capture a frame of picture. 73 | * @pre Camera is enabled. 74 | * @return the picture frame, or nullptr on error. 75 | */ 76 | std::unique_ptr capture(); 77 | 78 | /** 79 | * @brief Stream Motion JPEG. 80 | * @pre The camera has been initialized to JPEG mode. 81 | * @return number of frames streamed. 82 | */ 83 | int streamMjpeg(Client& client, const MjpegConfig& cfg = MjpegConfig()); 84 | }; 85 | 86 | /** @brief ESP32 camera API. */ 87 | extern CameraClass Camera; 88 | 89 | } // namespace esp32cam 90 | 91 | #endif // ESP32CAM_CAMERA_HPP 92 | -------------------------------------------------------------------------------- /src/esp32cam/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.hpp" 2 | #include "camera.hpp" 3 | #include "logger.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace esp32cam { 10 | namespace detail { 11 | 12 | int 13 | convertJpegQuality(int quality) { 14 | return std::min(63, std::max(0, 100 - quality)); 15 | } 16 | 17 | } // namespace detail 18 | 19 | class Config::CameraConfigT : public camera_config_t {}; 20 | 21 | Config::Config() 22 | : m_cfg(new CameraConfigT) { 23 | std::memset(m_cfg, 0, sizeof(*m_cfg)); 24 | m_cfg->xclk_freq_hz = 20000000; 25 | m_cfg->ledc_timer = LEDC_TIMER_0; 26 | m_cfg->ledc_channel = LEDC_CHANNEL_0; 27 | m_cfg->pixel_format = PIXFORMAT_RGB565; 28 | m_cfg->frame_size = FRAMESIZE_QQVGA; 29 | m_cfg->fb_count = 2; 30 | m_cfg->grab_mode = CAMERA_GRAB_LATEST; 31 | } 32 | 33 | Config::~Config() { 34 | delete m_cfg; 35 | } 36 | 37 | Config& 38 | Config::setPins(const Pins& pins) { 39 | m_cfg->pin_pwdn = pins.PWDN; 40 | m_cfg->pin_reset = pins.RESET; 41 | m_cfg->pin_xclk = pins.XCLK; 42 | m_cfg->pin_sccb_sda = pins.SDA; 43 | m_cfg->pin_sccb_scl = pins.SCL; 44 | m_cfg->pin_d7 = pins.D7; 45 | m_cfg->pin_d6 = pins.D6; 46 | m_cfg->pin_d5 = pins.D5; 47 | m_cfg->pin_d4 = pins.D4; 48 | m_cfg->pin_d3 = pins.D3; 49 | m_cfg->pin_d2 = pins.D2; 50 | m_cfg->pin_d1 = pins.D1; 51 | m_cfg->pin_d0 = pins.D0; 52 | m_cfg->pin_vsync = pins.VSYNC; 53 | m_cfg->pin_href = pins.HREF; 54 | m_cfg->pin_pclk = pins.PCLK; 55 | return *this; 56 | } 57 | 58 | Config& 59 | Config::setResolution(const Resolution& resolution) { 60 | m_cfg->frame_size = resolution.as(); 61 | return *this; 62 | } 63 | 64 | Config& 65 | Config::setBufferCount(int n) { 66 | m_cfg->fb_count = std::max(1, n); 67 | return *this; 68 | } 69 | 70 | Config& 71 | Config::setRgb() { 72 | m_cfg->pixel_format = PIXFORMAT_RGB565; 73 | return *this; 74 | } 75 | 76 | Config& 77 | Config::setYuv() { 78 | m_cfg->pixel_format = PIXFORMAT_YUV422; 79 | return *this; 80 | } 81 | 82 | Config& 83 | Config::setGrayscale() { 84 | m_cfg->pixel_format = PIXFORMAT_GRAYSCALE; 85 | return *this; 86 | } 87 | 88 | Config& 89 | Config::setJpeg(int quality) { 90 | m_cfg->pixel_format = PIXFORMAT_JPEG; 91 | m_cfg->jpeg_quality = detail::convertJpegQuality(quality); 92 | return *this; 93 | } 94 | 95 | Settings 96 | CameraClass::status() const { 97 | Settings result; 98 | sensor_t* sensor = esp_camera_sensor_get(); 99 | if (sensor == nullptr) { 100 | return result; 101 | } 102 | 103 | const auto& ss = sensor->status; 104 | result.resolution = Resolution(ss.framesize); 105 | result.brightness = ss.brightness; 106 | result.contrast = ss.contrast; 107 | result.saturation = ss.saturation; 108 | result.lightMode = ss.awb_gain ? static_cast(ss.wb_mode) : LightMode::NONE; 109 | result.specialEffect = static_cast(ss.special_effect); 110 | result.gain = ss.agc ? ((-2) << ss.gainceiling) : (1 + static_cast(ss.agc_gain)); 111 | result.hmirror = ss.hmirror != 0; 112 | result.vflip = ss.vflip != 0; 113 | result.rawGma = ss.raw_gma != 0; 114 | result.lensCorrection = ss.lenc != 0; 115 | return result; 116 | } 117 | 118 | bool 119 | CameraClass::update(const Settings& settings, int sleepFor) { 120 | sensor_t* sensor = esp_camera_sensor_get(); 121 | if (sensor == nullptr) { 122 | return false; 123 | } 124 | 125 | #define CHECK_RANGE(MEM, MIN, MAX) \ 126 | do { \ 127 | int next = static_cast(settings.MEM); \ 128 | if (!(next >= MIN && next <= MAX)) { \ 129 | ESP32CAM_LOG("update " #MEM " %d out of range [%d,%d]", next, MIN, MAX); \ 130 | return false; \ 131 | } \ 132 | } while (false) 133 | 134 | #define UPDATE4(STATUS_MEM, value, SETTER_MEM, SETTER_TYP) \ 135 | do { \ 136 | int prev = static_cast(sensor->status.STATUS_MEM); \ 137 | int desired = static_cast(value); \ 138 | if (prev != desired) { \ 139 | int res = sensor->SETTER_MEM(sensor, static_cast(desired)); \ 140 | ESP32CAM_LOG("update " #STATUS_MEM " %d => %d %s", prev, desired, \ 141 | res == 0 ? "success" : "failure"); \ 142 | if (res != 0) { \ 143 | return false; \ 144 | } \ 145 | } \ 146 | } while (false) 147 | #define UPDATE3(STATUS_MEM, value, SETTER_TYP) \ 148 | UPDATE4(STATUS_MEM, (value), set_##STATUS_MEM, SETTER_TYP) 149 | #define UPDATE2(STATUS_MEM, value) UPDATE3(STATUS_MEM, (value), int) 150 | #define UPDATE1(MEM) UPDATE2(MEM, settings.MEM) 151 | 152 | CHECK_RANGE(brightness, -2, 2); 153 | CHECK_RANGE(contrast, -2, 2); 154 | CHECK_RANGE(saturation, -2, 2); 155 | CHECK_RANGE(lightMode, -1, 4); 156 | CHECK_RANGE(specialEffect, 0, 6); 157 | CHECK_RANGE(gain, -128, 31); 158 | 159 | UPDATE3(framesize, settings.resolution.as(), framesize_t); 160 | UPDATE1(brightness); 161 | UPDATE1(contrast); 162 | UPDATE1(saturation); 163 | UPDATE2(special_effect, settings.specialEffect); 164 | UPDATE1(hmirror); 165 | UPDATE1(vflip); 166 | UPDATE2(raw_gma, settings.rawGma); 167 | UPDATE2(lenc, settings.lensCorrection); 168 | 169 | if (settings.gain > 0) { 170 | UPDATE4(agc, 0, set_gain_ctrl, int); 171 | UPDATE2(agc_gain, settings.gain - 1); 172 | UPDATE3(gainceiling, 0, gainceiling_t); 173 | } else { 174 | UPDATE4(agc, 1, set_gain_ctrl, int); 175 | UPDATE2(agc_gain, 0); 176 | UPDATE3(gainceiling, __builtin_ctz(static_cast(-settings.gain)) - 1, gainceiling_t); 177 | } 178 | 179 | if (settings.lightMode == LightMode::NONE) { 180 | UPDATE2(awb_gain, 0); 181 | UPDATE2(wb_mode, 0); 182 | } else { 183 | UPDATE2(awb_gain, 1); 184 | UPDATE2(wb_mode, settings.lightMode); 185 | } 186 | 187 | #undef CHECK_RANGE 188 | #undef UPDATE4 189 | #undef UPDATE3 190 | #undef UPDATE2 191 | #undef UPDATE1 192 | 193 | if (sleepFor > 0) { 194 | delay(sleepFor); 195 | } 196 | return true; 197 | } 198 | 199 | } // namespace esp32cam 200 | -------------------------------------------------------------------------------- /src/esp32cam/config.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_CONFIG_HPP 2 | #define ESP32CAM_CONFIG_HPP 3 | 4 | #include "pins.hpp" 5 | #include "resolution.hpp" 6 | 7 | namespace esp32cam { 8 | namespace detail { 9 | 10 | /** @brief Convert JPEG quality from 0-100 scale to 63-0 scale. */ 11 | int 12 | convertJpegQuality(int quality); 13 | 14 | } // namespace detail 15 | 16 | /** @brief Camera initialization configuration. */ 17 | class Config { 18 | public: 19 | Config(); 20 | 21 | ~Config(); 22 | 23 | Config& setPins(const Pins& pins); 24 | 25 | Config& setResolution(const Resolution& resolution); 26 | 27 | /** 28 | * @brief Set number of frame buffers. 29 | * @param n >=1, having more frame buffers allows better streaming fps. 30 | */ 31 | Config& setBufferCount(int n); 32 | 33 | /** @brief Change pixel format to RGB565. */ 34 | Config& setRgb(); 35 | 36 | /** @brief Change pixel format to YUV422. */ 37 | Config& setYuv(); 38 | 39 | /** @brief Change pixel format to grayscale. */ 40 | Config& setGrayscale(); 41 | 42 | /** 43 | * @brief Change pixel format to JPEG. 44 | * @param quality JPEG quality between 0 (worst) and 100 (best). 45 | */ 46 | Config& setJpeg(int quality); 47 | 48 | private: 49 | class CameraConfigT; ///< camera_config_t 50 | CameraConfigT* m_cfg; 51 | 52 | friend class CameraClass; 53 | }; 54 | 55 | /** @brief Light mode / white balance values. */ 56 | enum class LightMode { 57 | NONE = -1, 58 | AUTO = 0, 59 | SUNNY = 1, 60 | CLOUDY = 2, 61 | OFFICE = 3, 62 | HOME = 4, 63 | }; 64 | 65 | /** @brief Special effect values. */ 66 | enum class SpecialEffect { 67 | NONE = 0, 68 | NEGATIVE = 1, 69 | BLACKWHITE = 2, 70 | REDDISH = 3, 71 | GREENISH = 4, 72 | BLUISH = 5, 73 | ANTIQUE = 6, 74 | }; 75 | 76 | /** @brief Camera runtime settings. */ 77 | struct Settings { 78 | /** @brief Picture resolution. */ 79 | Resolution resolution; 80 | 81 | /** @brief Image brightness, between -2 and +2. */ 82 | int8_t brightness; 83 | 84 | /** @brief Image contrast, between -2 and +2. */ 85 | int8_t contrast; 86 | 87 | /** @brief Image saturation, between -2 and +2. */ 88 | int8_t saturation; 89 | 90 | /** 91 | * @brief Gain control, with or without Automatic Gain Control (AGC). 92 | * 93 | * - AGC disabled: a positive number between 1 and 31, which corresponds to 1x ~ 31x gain. 94 | * - AGC enabled: -2,-4,-8,-16,-32,-64,-128, which corresponds to 2x ~ 128x gain. 95 | */ 96 | int8_t gain; 97 | 98 | /** @brief Image light mode. */ 99 | LightMode lightMode; 100 | 101 | /** @brief Image special effect. */ 102 | SpecialEffect specialEffect; 103 | 104 | /** @brief Horizontal flip. */ 105 | bool hmirror = false; 106 | 107 | /** @brief Vertical flip. */ 108 | bool vflip = false; 109 | 110 | /** @brief Raw gamma mode. */ 111 | bool rawGma = false; 112 | 113 | /** @brief Lens correction mode. */ 114 | bool lensCorrection = false; 115 | }; 116 | 117 | } // namespace esp32cam 118 | 119 | #endif // ESP32CAM_CONFIG_HPP 120 | -------------------------------------------------------------------------------- /src/esp32cam/frame.cpp: -------------------------------------------------------------------------------- 1 | #include "frame.hpp" 2 | #include "config.hpp" 3 | 4 | #include 5 | #include 6 | 7 | namespace esp32cam { 8 | 9 | static const int PIXFORMAT_BMP = -101; 10 | 11 | struct Frame::CameraFbT : public camera_fb_t {}; 12 | 13 | Frame::Frame() = default; 14 | 15 | Frame::Frame(void* fb) 16 | : m_fb(static_cast(fb)) 17 | , m_data(m_fb->buf) 18 | , m_size(m_fb->len) 19 | , m_width(m_fb->width) 20 | , m_height(m_fb->height) 21 | , m_pixFormat(m_fb->format) {} 22 | 23 | Frame::~Frame() { 24 | releaseFb(); 25 | if (m_data != nullptr) { 26 | free(m_data); 27 | } 28 | } 29 | 30 | void 31 | Frame::releaseFb() { 32 | if (m_fb != nullptr) { 33 | esp_camera_fb_return(m_fb); 34 | m_fb = nullptr; 35 | m_data = nullptr; 36 | } 37 | } 38 | 39 | bool 40 | Frame::writeTo(Print& os, int timeout) { 41 | return writeToImpl(os, timeout, nullptr); 42 | } 43 | 44 | bool 45 | Frame::writeTo(Client& os, int timeout) { 46 | return writeToImpl(os, timeout, &os); 47 | } 48 | 49 | bool 50 | Frame::writeToImpl(Print& os, int timeout, Client* client) { 51 | auto startTime = millis(); 52 | for (size_t i = 0; i < m_size; i += os.write(&m_data[i], m_size - i)) { 53 | if (millis() - startTime > static_cast(timeout) || 54 | (client != nullptr && !client->connected())) { 55 | return false; 56 | } 57 | yield(); 58 | } 59 | return true; 60 | } 61 | 62 | bool 63 | Frame::isJpeg() const { 64 | return m_pixFormat == PIXFORMAT_JPEG; 65 | } 66 | 67 | bool 68 | Frame::toJpeg(int quality) { 69 | uint8_t* data; 70 | size_t size; 71 | bool ok = fmt2jpg(m_data, m_size, m_width, m_height, static_cast(m_pixFormat), 72 | detail::convertJpegQuality(quality), &data, &size); 73 | if (!ok) { 74 | return false; 75 | } 76 | releaseFb(); 77 | m_data = data; 78 | m_size = size; 79 | m_pixFormat = PIXFORMAT_JPEG; 80 | return true; 81 | } 82 | 83 | bool 84 | Frame::isBmp() const { 85 | return m_pixFormat == PIXFORMAT_BMP; 86 | } 87 | 88 | bool 89 | Frame::toBmp() { 90 | uint8_t* data; 91 | size_t size; 92 | bool ok = 93 | fmt2bmp(m_data, m_size, m_width, m_height, static_cast(m_pixFormat), &data, &size); 94 | if (!ok) { 95 | return false; 96 | } 97 | releaseFb(); 98 | m_data = data; 99 | m_size = size; 100 | m_pixFormat = PIXFORMAT_BMP; 101 | return true; 102 | } 103 | 104 | } // namespace esp32cam 105 | -------------------------------------------------------------------------------- /src/esp32cam/frame.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_FRAME_HPP 2 | #define ESP32CAM_FRAME_HPP 3 | 4 | #include 5 | #include 6 | 7 | class Client; 8 | class Print; 9 | 10 | namespace esp32cam { 11 | 12 | /** @brief A frame of picture. */ 13 | class Frame { 14 | public: // access 15 | ~Frame(); 16 | 17 | uint8_t* data() const { 18 | return m_data; 19 | } 20 | 21 | size_t size() const { 22 | return m_size; 23 | } 24 | 25 | int getWidth() const { 26 | return m_width; 27 | } 28 | 29 | int getHeight() const { 30 | return m_height; 31 | } 32 | 33 | /** 34 | * @brief Write frame buffer to @p os . 35 | * @param os output stream. 36 | * @param timeout total time limit in millis. 37 | * @retval true writing completed. 38 | * @retval false writing disrupted by timeout. 39 | */ 40 | bool writeTo(Print& os, int timeout = 10000); 41 | 42 | /** 43 | * @brief Write frame buffer to @p os . 44 | * @param os output socket. 45 | * @param timeout total time limit in millis. 46 | * @retval true writing completed. 47 | * @retval false writing disrupted by timeout or socket error. 48 | */ 49 | bool writeTo(Client& os, int timeout = 10000); 50 | 51 | public: // conversion 52 | bool isJpeg() const; 53 | 54 | /** 55 | * @brief Convert frame to JPEG. 56 | * @param quality JPEG quality between 0 (worst) and 100 (best). 57 | */ 58 | bool toJpeg(int quality); 59 | 60 | bool isBmp() const; 61 | 62 | /** @brief Convert frame to BMP. */ 63 | bool toBmp(); 64 | 65 | private: 66 | Frame(); 67 | 68 | explicit Frame(void* fb); 69 | 70 | bool writeToImpl(Print& os, int timeout, Client* client); 71 | 72 | void releaseFb(); 73 | 74 | private: 75 | class CameraFbT; ///< camera_fb_t 76 | CameraFbT* m_fb = nullptr; 77 | uint8_t* m_data = nullptr; 78 | size_t m_size = 0; 79 | int m_width = -1; 80 | int m_height = -1; 81 | int m_pixFormat = -1; 82 | 83 | friend class CameraClass; 84 | }; 85 | 86 | } // namespace esp32cam 87 | 88 | #endif // ESP32CAM_FRAME_HPP 89 | -------------------------------------------------------------------------------- /src/esp32cam/logger.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_LOGGER_HPP 2 | #define ESP32CAM_LOGGER_HPP 3 | 4 | #include 5 | 6 | namespace esp32cam { 7 | 8 | extern Print* LogOutput; 9 | 10 | /** 11 | * @brief Set logger output. 12 | * @param output Output printer, such as @c Serial . 13 | */ 14 | inline void 15 | setLogger(Print& output) { 16 | LogOutput = &output; 17 | } 18 | 19 | #define ESP32CAM_LOG(fmt, ...) \ 20 | do { \ 21 | if (LogOutput != nullptr) { \ 22 | LogOutput->printf("[%8lu] " fmt "\n", millis(), ##__VA_ARGS__); \ 23 | } \ 24 | } while (false) 25 | 26 | } // namespace esp32cam 27 | 28 | #endif // ESP32CAM_LOGGER_HPP 29 | -------------------------------------------------------------------------------- /src/esp32cam/mjpeg.cpp: -------------------------------------------------------------------------------- 1 | #include "mjpeg.hpp" 2 | #include "logger.hpp" 3 | 4 | #define MC_LOG(fmt, ...) ESP32CAM_LOG("MjpegController(%p) " fmt, this, ##__VA_ARGS__) 5 | 6 | namespace esp32cam { 7 | namespace detail { 8 | 9 | MjpegController::MjpegController(MjpegConfig cfg) 10 | : cfg(cfg) 11 | , m_nextCaptureTime(millis()) {} 12 | 13 | int 14 | MjpegController::decideAction() { 15 | if (m_nextAction == CAPTURE) { 16 | auto t = static_cast(m_nextCaptureTime - millis()); 17 | if (t > 0) { 18 | return t; 19 | } 20 | return CAPTURE; 21 | } 22 | return m_nextAction; 23 | } 24 | 25 | void 26 | MjpegController::notifyCapture() { 27 | m_nextAction = RETURN; 28 | m_nextCaptureTime = millis() + static_cast(std::max(0, cfg.minInterval)); 29 | MC_LOG("notifyCapture next=%lu", m_nextCaptureTime); 30 | } 31 | 32 | void 33 | MjpegController::notifyReturn(std::unique_ptr frame) { 34 | if (frame == nullptr) { 35 | MC_LOG("notifyReturn frame=nullptr"); 36 | notifyFail(); 37 | return; 38 | } 39 | m_frame = std::move(frame); 40 | MC_LOG("notifyReturn frame=%p size=%zu dimension=%dx%d", m_frame->data(), m_frame->size(), 41 | m_frame->getWidth(), m_frame->getHeight()); 42 | m_nextAction = SEND; 43 | } 44 | 45 | void 46 | MjpegController::notifySent(bool ok) { 47 | ++m_count; 48 | MC_LOG("notifySent count=%d ok=%d", m_count, static_cast(ok)); 49 | if (!ok) { 50 | notifyFail(); 51 | return; 52 | } 53 | m_frame.reset(); 54 | m_nextAction = cfg.maxFrames < 0 || m_count < cfg.maxFrames ? CAPTURE : STOP; 55 | } 56 | 57 | void 58 | MjpegController::notifyFail() { 59 | MC_LOG("notifyFail"); 60 | m_frame.reset(); 61 | m_nextAction = STOP; 62 | } 63 | 64 | #define BOUNDARY "e8b8c539-047d-4777-a985-fbba6edff11e" 65 | 66 | void 67 | MjpegHeader::prepareResponseHeaders() { 68 | size = snprintf(buf, sizeof(buf), 69 | "HTTP/1.1 200 OK\r\n" 70 | "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" 71 | "\r\n"); 72 | } 73 | 74 | void 75 | MjpegHeader::prepareResponseContentType() { 76 | size = snprintf(buf, sizeof(buf), "multipart/x-mixed-replace;boundary=" BOUNDARY); 77 | } 78 | 79 | void 80 | MjpegHeader::preparePartHeader(size_t contentLength) { 81 | size = snprintf(buf, sizeof(buf), 82 | "Content-Type: image/jpeg\r\n" 83 | "Content-Length: %zu\r\n" 84 | "\r\n", 85 | contentLength); 86 | } 87 | 88 | void 89 | MjpegHeader::preparePartTrailer() { 90 | size = snprintf(buf, sizeof(buf), "\r\n--" BOUNDARY "\r\n"); 91 | } 92 | 93 | size_t 94 | MjpegHeader::writeTo(Print& os) { 95 | return os.write(buf, size); 96 | } 97 | 98 | } // namespace detail 99 | } // namespace esp32cam 100 | -------------------------------------------------------------------------------- /src/esp32cam/mjpeg.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_MJPEG_HPP 2 | #define ESP32CAM_MJPEG_HPP 3 | 4 | #include "frame.hpp" 5 | 6 | #include 7 | 8 | namespace esp32cam { 9 | 10 | /** MJPEG stream options. */ 11 | struct MjpegConfig { 12 | /** 13 | * @brief Minimum interval between frame captures in millis. 14 | * 15 | * Negative value causes @c asyncweb::MjpegResponse to enter continuous mode. 16 | */ 17 | int minInterval = 0; 18 | 19 | /** 20 | * @brief Maximum number of frames before disconnecting. 21 | * 22 | * Negative value means unlimited. 23 | */ 24 | int maxFrames = -1; 25 | 26 | /** @brief Time limit of writing one frame in millis. */ 27 | int frameTimeout = 10000; 28 | }; 29 | 30 | namespace detail { 31 | 32 | /** @brief Control MJPEG stream timing. */ 33 | class MjpegController { 34 | public: 35 | explicit MjpegController(MjpegConfig cfg); 36 | 37 | /** @brief Retrieve number of sent frames. */ 38 | int countSentFrames() const { 39 | return m_count; 40 | } 41 | 42 | /** 43 | * @brief Decide what to do now. 44 | * @retval CAPTURE capture a frame. 45 | * @retval RETURN return a captured frame. 46 | * @retval SEND send current frame to the client. 47 | * @retval STOP disconnect the client. 48 | * @return if non-negative, how long to delay (millis) before the next action. 49 | */ 50 | int decideAction(); 51 | 52 | /** 53 | * @brief Notify that frame capture has started. 54 | * @post decideAction()==RETURN 55 | */ 56 | void notifyCapture(); 57 | 58 | /** 59 | * @brief Notify that frame capture has completed. 60 | * @param frame captured frame, possibly nullptr. 61 | * @post if frame==nullptr, decideAction()==STOP; otherwise, decideAction()==SEND 62 | */ 63 | void notifyReturn(std::unique_ptr frame); 64 | 65 | /** @brief Retrieve current frame. */ 66 | Frame* getFrame() const { 67 | return m_frame.get(); 68 | } 69 | 70 | /** 71 | * @brief Notify that a frame is sent to the client. 72 | * @param ok whether sent successfully. 73 | * @post getFrame()==nullptr 74 | * 75 | * The caller is expected to enforce @c MjpegConfig::frameTimeout and call 76 | * `notifySent(false)` in case of send timeout. 77 | */ 78 | void notifySent(bool ok); 79 | 80 | /** 81 | * @brief Notify that an error has occurred. 82 | * @post getFrame()==nullptr 83 | * @post decideAction()==STOP 84 | */ 85 | void notifyFail(); 86 | 87 | public: 88 | enum Action { 89 | CAPTURE = -1, 90 | RETURN = -2, 91 | SEND = -3, 92 | STOP = -4, 93 | }; 94 | 95 | const MjpegConfig cfg; 96 | 97 | private: 98 | std::unique_ptr m_frame; 99 | unsigned long m_nextCaptureTime; 100 | int m_nextAction = CAPTURE; 101 | int m_count = 0; 102 | }; 103 | 104 | /** @brief Prepare HTTP headers related to MJPEG streaming. */ 105 | class MjpegHeader { 106 | public: 107 | void prepareResponseHeaders(); 108 | 109 | void prepareResponseContentType(); 110 | 111 | void preparePartHeader(size_t contentLength); 112 | 113 | void preparePartTrailer(); 114 | 115 | size_t writeTo(Print& os); 116 | 117 | public: 118 | size_t size = 0; 119 | char buf[120]; 120 | }; 121 | 122 | } // namespace detail 123 | } // namespace esp32cam 124 | 125 | #endif // ESP32CAM_MJPEG_HPP 126 | -------------------------------------------------------------------------------- /src/esp32cam/pins.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_PINS_HPP 2 | #define ESP32CAM_PINS_HPP 3 | 4 | namespace esp32cam { 5 | 6 | /** @brief Camera pins definition. */ 7 | struct Pins { 8 | int D0; 9 | int D1; 10 | int D2; 11 | int D3; 12 | int D4; 13 | int D5; 14 | int D6; 15 | int D7; 16 | int XCLK; 17 | int PCLK; 18 | int VSYNC; 19 | int HREF; 20 | int SDA; 21 | int SCL; 22 | int RESET; 23 | int PWDN; 24 | }; 25 | 26 | namespace pins { 27 | 28 | /** @brief Pin definition for AI Thinker ESP32-CAM. */ 29 | constexpr Pins AiThinker{ 30 | D0: 5, 31 | D1: 18, 32 | D2: 19, 33 | D3: 21, 34 | D4: 36, 35 | D5: 39, 36 | D6: 34, 37 | D7: 35, 38 | XCLK: 0, 39 | PCLK: 22, 40 | VSYNC: 25, 41 | HREF: 23, 42 | SDA: 26, 43 | SCL: 27, 44 | RESET: -1, 45 | PWDN: 32, 46 | }; 47 | 48 | /** @brief Pin definition for Seeed Studio XIAO ESP32S3 Sense. */ 49 | constexpr Pins XiaoSense{ 50 | D0: 15, 51 | D1: 17, 52 | D2: 18, 53 | D3: 16, 54 | D4: 14, 55 | D5: 12, 56 | D6: 11, 57 | D7: 48, 58 | XCLK: 10, 59 | PCLK: 13, 60 | VSYNC: 38, 61 | HREF: 47, 62 | SDA: 40, 63 | SCL: 39, 64 | RESET: -1, 65 | PWDN: -1, 66 | }; 67 | 68 | /** @brief Pin definition for FREENOVE WROVER ESP32-CAM. */ 69 | constexpr Pins FreeNove{ 70 | D0: 4, 71 | D1: 5, 72 | D2: 18, 73 | D3: 19, 74 | D4: 36, 75 | D5: 39, 76 | D6: 34, 77 | D7: 35, 78 | XCLK: 21, 79 | PCLK: 22, 80 | VSYNC: 25, 81 | HREF: 23, 82 | SDA: 26, 83 | SCL: 27, 84 | RESET: -1, 85 | PWDN: -1, 86 | }; 87 | 88 | /** @brief Pin definition for M5Stack M5Camera. */ 89 | constexpr Pins M5Camera{ 90 | D0: 32, 91 | D1: 35, 92 | D2: 34, 93 | D3: 5, 94 | D4: 39, 95 | D5: 18, 96 | D6: 36, 97 | D7: 19, 98 | XCLK: 27, 99 | PCLK: 21, 100 | VSYNC: 25, 101 | HREF: 26, 102 | SDA: 22, 103 | SCL: 23, 104 | RESET: 15, 105 | PWDN: -1, 106 | }; 107 | 108 | /** 109 | * @brief Pin definition for M5Stack M5Camera with LED. 110 | * 111 | * Red LED on GPIO 14, tally light when tied to PWDN 112 | */ 113 | constexpr Pins M5CameraLED{ 114 | D0: 32, 115 | D1: 35, 116 | D2: 34, 117 | D3: 5, 118 | D4: 39, 119 | D5: 18, 120 | D6: 36, 121 | D7: 19, 122 | XCLK: 27, 123 | PCLK: 21, 124 | VSYNC: 25, 125 | HREF: 26, 126 | SDA: 22, 127 | SCL: 23, 128 | RESET: 15, 129 | PWDN: 14, 130 | }; 131 | 132 | /** @brief Pin definition for TTGO ESP32-CAM. */ 133 | constexpr Pins TTGO{ 134 | D0: 5, 135 | D1: 14, 136 | D2: 4, 137 | D3: 15, 138 | D4: 37, 139 | D5: 38, 140 | D6: 36, 141 | D7: 39, 142 | XCLK: 32, 143 | PCLK: 19, 144 | VSYNC: 27, 145 | HREF: 25, 146 | SDA: 13, 147 | SCL: 12, 148 | RESET: -1, 149 | PWDN: -1, 150 | }; 151 | 152 | } // namespace pins 153 | } // namespace esp32cam 154 | 155 | #endif // ESP32CAM_PINS_HPP 156 | -------------------------------------------------------------------------------- /src/esp32cam/resolution.cpp: -------------------------------------------------------------------------------- 1 | #include "resolution.hpp" 2 | #include 3 | 4 | #include 5 | 6 | namespace esp32cam { 7 | 8 | ResolutionList::ResolutionList(int max) 9 | : m_max(max) {} 10 | 11 | ResolutionList::Iterator 12 | ResolutionList::begin() const { 13 | return Iterator(0); 14 | } 15 | 16 | ResolutionList::Iterator 17 | ResolutionList::end() const { 18 | return Iterator(m_max); 19 | } 20 | 21 | Resolution 22 | ResolutionList::find(int minWidth, int minHeight) const { 23 | for (auto r : *this) { 24 | if (r.getWidth() >= minWidth && r.getHeight() >= minHeight) { 25 | return r; 26 | } 27 | } 28 | return Resolution(); 29 | } 30 | 31 | ResolutionList 32 | Resolution::list() { 33 | static ResolutionList list(FRAMESIZE_INVALID); 34 | return list; 35 | } 36 | 37 | Resolution 38 | Resolution::find(int minWidth, int minHeight) { 39 | return list().find(minWidth, minHeight); 40 | } 41 | 42 | Resolution::Resolution(int frameSize) 43 | : m_frameSize(frameSize) {} 44 | 45 | bool 46 | Resolution::isValid() const { 47 | return m_frameSize >= 0 && m_frameSize < FRAMESIZE_INVALID; 48 | } 49 | 50 | int 51 | Resolution::getWidth() const { 52 | if (!isValid()) { 53 | return -1; 54 | } 55 | return ::resolution[m_frameSize].width; 56 | } 57 | 58 | int 59 | Resolution::getHeight() const { 60 | if (!isValid()) { 61 | return -1; 62 | } 63 | return ::resolution[m_frameSize].height; 64 | } 65 | 66 | size_t 67 | Resolution::printTo(Print& p) const { 68 | size_t len = 0; 69 | len += p.print(getWidth()); 70 | len += p.print('x'); 71 | len += p.print(getHeight()); 72 | return len; 73 | } 74 | 75 | } // namespace esp32cam 76 | -------------------------------------------------------------------------------- /src/esp32cam/resolution.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESP32CAM_RESOLUTION_HPP 2 | #define ESP32CAM_RESOLUTION_HPP 3 | 4 | #include 5 | #include 6 | 7 | namespace esp32cam { 8 | 9 | class ResolutionList; 10 | 11 | /** @brief Picture width and height. */ 12 | class Resolution : public Printable { 13 | public: 14 | /** 15 | * @brief Return an iterable collection of possible resolutions. 16 | * 17 | * This list contains all resolutions defined in the esp32-camera library, 18 | * but not necessarily supported by the camera hardware. 19 | * Use @c Camera.listResolutions() to retrieve supported resolutions. 20 | */ 21 | static ResolutionList list(); 22 | 23 | /** 24 | * @brief Find a resolution that satisfies given constraints. 25 | * 26 | * This searches among all resolutions defined in the esp32-camera libary, 27 | * which is not necessarily supported by the camera hardware. 28 | */ 29 | static Resolution find(int minWidth, int minHeight); 30 | 31 | /** 32 | * @brief Constructor. 33 | * @param frameSize framesize_t value. 34 | */ 35 | explicit Resolution(int frameSize = -1); 36 | 37 | /** @brief Determine if this resolution is valid. */ 38 | bool isValid() const; 39 | 40 | /** @brief Return picture width in pixels. */ 41 | int getWidth() const; 42 | 43 | /** @brief Return picture height in pixels. */ 44 | int getHeight() const; 45 | 46 | /** @brief Print WxH to output stream. */ 47 | size_t printTo(Print& p) const override; 48 | 49 | /** 50 | * @brief Convert to framesize_t (internal use). 51 | * @tparam T framesize_t 52 | */ 53 | template 54 | T as() const { 55 | return static_cast(m_frameSize); 56 | } 57 | 58 | private: 59 | int m_frameSize; ///< framesize_t 60 | 61 | friend bool operator==(const Resolution& lhs, const Resolution& rhs) { 62 | return (!lhs.isValid() && !rhs.isValid()) || (lhs.m_frameSize == rhs.m_frameSize); 63 | } 64 | 65 | friend bool operator!=(const Resolution& lhs, const Resolution& rhs) { 66 | return !(lhs == rhs); 67 | } 68 | 69 | friend bool operator<(const Resolution& lhs, const Resolution& rhs) { 70 | return lhs.m_frameSize < rhs.m_frameSize; 71 | } 72 | 73 | friend bool operator>(const Resolution& lhs, const Resolution& rhs) { 74 | return lhs.m_frameSize > rhs.m_frameSize; 75 | } 76 | }; 77 | 78 | /** 79 | * @brief A collection of resolutions. 80 | * @code 81 | * for (const auto& resolution : Resolution::list()) { 82 | * Serial.println(resolution); 83 | * } 84 | * @endcode 85 | */ 86 | class ResolutionList { 87 | public: 88 | class Iterator { 89 | public: 90 | using iterator_catagory = std::forward_iterator_tag; 91 | using value_type = const Resolution; 92 | using difference_type = std::ptrdiff_t; 93 | using pointer = void; 94 | using reference = value_type; 95 | 96 | explicit Iterator(int value = -1) 97 | : m_frameSize(value) {} 98 | 99 | Iterator& operator++() { 100 | ++m_frameSize; 101 | return *this; 102 | } 103 | 104 | Iterator operator++(int) { 105 | Iterator copy(*this); 106 | ++*this; 107 | return copy; 108 | } 109 | 110 | reference operator*() { 111 | return Resolution(m_frameSize); 112 | } 113 | 114 | private: 115 | int m_frameSize; ///< framesize_t 116 | 117 | friend bool operator==(const Iterator& lhs, const Iterator& rhs) { 118 | return lhs.m_frameSize == rhs.m_frameSize; 119 | } 120 | 121 | friend bool operator!=(const Iterator& lhs, const Iterator& rhs) { 122 | return !(lhs == rhs); 123 | } 124 | }; 125 | 126 | /** 127 | * @brief Constructor. 128 | * @param max exclusive maximum framesize_t. 129 | */ 130 | explicit ResolutionList(int max = 0); 131 | 132 | Iterator begin() const; 133 | 134 | Iterator end() const; 135 | 136 | /** @brief Find a resolution that satisfies given constraints. */ 137 | Resolution find(int minWidth, int minHeight) const; 138 | 139 | private: 140 | int m_max; 141 | }; 142 | 143 | } // namespace esp32cam 144 | 145 | #endif // ESP32CAM_RESOLUTION_HPP 146 | --------------------------------------------------------------------------------