diff --git a/.travis.yml b/.travis.yml index 6e4bd73..f6bdf9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,19 +5,27 @@ language: bash os: - linux +dist: focal + +branches: + only: + - master + + before_script: - "export DISPLAY=:99.0" - sleep 3 # give xvfb some time to start - - wget http://downloads.arduino.cc/arduino-1.8.13-linux64.tar.xz - - tar xf arduino-1.8.13-linux64.tar.xz - - mv arduino-1.8.13 $HOME/arduino_ide + - wget https://downloads.arduino.cc/arduino-1.8.19-linux64.tar.xz + - tar xf arduino-1.8.19-linux64.tar.xz + - mv arduino-1.8.19 $HOME/arduino_ide - cd $HOME/arduino_ide/hardware - mkdir esp32 - cd esp32 - - git clone --depth 1 https://github.com/espressif/arduino-esp32.git esp32 - - cd esp32 - - git submodule update --init --recursive - - cd tools + - wget https://github.com/espressif/arduino-esp32/archive/refs/tags/2.0.2.tar.gz + - tar -xzf 2.0.2.tar.gz + - mv arduino-esp32-2.0.2/ esp32 + - cd esp32/tools + - python --version - python get.py - pip install --user platformio - platformio update @@ -25,7 +33,7 @@ before_script: script: - cd $TRAVIS_BUILD_DIR - export PATH="$HOME/arduino_ide:$PATH" - - arduino --board esp32:esp32:esp32:PartitionScheme=huge_app,FlashFreq=80 --pref compiler.warning_level=all --save-prefs + - arduino --board esp32:esp32:esp32:PSRAM=enabled,PartitionScheme=min_spiffs,CPUFreq=240,FlashMode=qio,FlashFreq=80,DebugLevel=none --pref compiler.warning_level=all --save-prefs - arduino --verbose --verify esp32-cam-webserver.ino - cp --preserve --verbose myconfig.sample.h myconfig.h - arduino --verbose --verify esp32-cam-webserver.ino diff --git a/API.md b/API.md index d285159..563011e 100644 --- a/API.md +++ b/API.md @@ -6,11 +6,12 @@ The WebUI and camera server communicate entirely via HTTP requests and responses ## URI's ### Http Port * `/` - Default index -* `/?target=full|simple|portal` - Go direct to specific index +* `/?view=full|simple|portal` - Go direct to specific index * `/capture` - Return a Jpeg snapshot image * `/status` - Returns a JSON string with all camera status / pairs listed -* `/control?var=&val=` - Set to +* `/control?var=&val=` - Set `` to `` * `/dump` - Status page +* `/stop` - End all active streams ### Stream Port * `/` - Raw stream @@ -25,7 +26,8 @@ Call `/control?var=&val=` with a settings key and value to set camera #### Settings ``` lamp - Lamp value in percent; integer, 0 - 100 (-1 = disabled) -framesize - 0=QQVGA, 3=HQVGA, QVGA=4, CIF=5, VGA=6, SVGA=7, XGA=8, SXGA=9, UXGA=10, QXGA(ov3660)=11 +framesize - See below +min_frame_time - Minimal frame duration in ms; used to limit max FPS. Must be positive integer quality - 10 to 63 (ov3660: 4 to 10) contrast - -2 to 2 (ov3660: -3 to 3) brightness - -2 to 2 (ov3660: -3 to 3) @@ -63,8 +65,27 @@ cam_name - Camera Name; String code_ver - Code compile date and time; String stream_url - Raw stream URL; string ``` +##### Framesize values +These may vary between different ESP framework releases +``` + 0 - THUMB (96x96) + 1 - QQVGA (160x120) + 3 - HQVGA (240x176) + 5 - QVGA (320x240) + 6 - CIF (400x296) + 7 - HVGA (480x320) + 8 - VGA (640x480) + 9 - SVGA (800x600) +10 - XGA (1024x768) +11 - HD (1280x720) +12 - SXGA (1280x1024) +13 - UXGA (1600x1200) +Only for 3Mp+ camera modules: +14 - FHD (1920x1080) +17 - QXGA (2048x1536) +``` #### Commands -These are commands; they can be sent by calling the `/control` URI with them as the ``, the `` must be supplied, but can be any value and is ignored. +These are commands; they can be sent by calling the `/control` URI with them as the `` *(a `` must also be supplied, but can be any value and is ignored)*. ``` face_enroll - Enroll a new face in the FaceDB (only when face recognition is avctive) save_prefs - Saves preferences file @@ -72,8 +93,18 @@ clear_prefs - Deletes the preferences file reboot - Reboots the camera ``` ## Examples -* Flash light: on/off - * `http:///control?var=lamp&val=100` On - * `http:///control?var=lamp&val=50` 50% - * `http:///control?var=lamp&val=0` Off +* Flash light: on/mid/off + * `http:///control?var=lamp&val=100` + * `http:///control?var=lamp&val=50` + * `http:///control?var=lamp&val=0` +* Set resolution to VGA + * `http:///control?var=framesize&val=8` +* Show camera details and settings + * All settings are returned via single `status` call in [JSON](https://www.json.org/) format. + * `http:///status` + * Returns: + ``` {"lamp":0,"autolamp":0,"min_frame_time":0,"framesize":9,"quality":10,"xclk":8,"brightness":0,"contrast":0,"saturation":0,"sharpness":0,"special_effect":0,"wb_mode":0,"awb":1,"awb_gain":1,"aec":1,"aec2":0,"ae_level":0,"aec_value":204,"agc":1,"agc_gain":0,"gainceiling":0,"bpc":0,"wpc":1,"raw_gma":1,"lenc":1,"vflip":1,"hmirror":1,"dcw":1,"colorbar":0,"cam_name":"ESP32 test camera","code_ver":"Mar 10 2022 @ 14:00:45","rotate":"0","stream_url":"http://10.0.0.181:81/"}``` +* Reboot the camera + * `http:///control?var=reboot&val=0` +You can try these yourself in a browser address bar, from the commandline with `curl` and co. or use them programatically from your scripting language of choice. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc8b8d6..2efd161 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Pull requests are the best way to propose changes to the codebase (I use [Github 1. Fork the repo and create your branch from `master`. 2. Give your branch a clear descriptive name and do your changes there. 3. If you've changed the HTTP APIs, update the documentation. -4. Issue a pull request against a branch *of the same name* in the main repo. +4. Issue a pull request against the master branch in the main repo. 5. Clearly describe your changes and the reason for them in the pull request. ## Any contributions you make will be under the GNU Lesser General Public License v2.1 diff --git a/Docs/infodump.png b/Docs/infodump.png new file mode 100644 index 0000000..c649a0e Binary files /dev/null and b/Docs/infodump.png differ diff --git a/Docs/linearled/README.md b/Docs/linearled/README.md index f5163a7..ab87d94 100644 --- a/Docs/linearled/README.md +++ b/Docs/linearled/README.md @@ -38,7 +38,7 @@ int main(int argc, char **argv) { } ``` -Adjust the width as necesscary and compile with: +Adjust the width as necessary and compile with: $ gcc linearled.c -o linearled -lm diff --git a/Docs/ota-board-selection-small.png b/Docs/ota-board-selection-small.png new file mode 100644 index 0000000..cfeb85d Binary files /dev/null and b/Docs/ota-board-selection-small.png differ diff --git a/Docs/ota-board-selection.png b/Docs/ota-board-selection.png new file mode 100644 index 0000000..e0444c4 Binary files /dev/null and b/Docs/ota-board-selection.png differ diff --git a/Docs/screenshot.png b/Docs/screenshot.png deleted file mode 100644 index 1e59f99..0000000 Binary files a/Docs/screenshot.png and /dev/null differ diff --git a/Docs/simpleviewer.png b/Docs/simpleviewer.png new file mode 100644 index 0000000..024b494 Binary files /dev/null and b/Docs/simpleviewer.png differ diff --git a/Docs/streamview.png b/Docs/streamview.png new file mode 100644 index 0000000..49aa476 Binary files /dev/null and b/Docs/streamview.png differ diff --git a/Docs/twocatsactually.png b/Docs/twocatsactually.png new file mode 100644 index 0000000..bfc9fb4 Binary files /dev/null and b/Docs/twocatsactually.png differ diff --git a/README.md b/README.md index 973e0a6..2d87c9d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# ESP32-CAM example revisited.     [![CI Status](https://travis-ci.org/easytarget/esp32-cam-webserver.svg?branch=master)](https://travis-ci.org/github/easytarget/esp32-cam-webserver)    ![ESP-EYE logo](Docs/logo.svg) +# IMPORTANT! +This is a Old and rather Obsolete sketch; it only works with a very old version of the ESP32 Arduino core (2.0.2) and is missing all the improvements that espressif have made since then.. + +It is also quite popular (Hi YouTubers!!) so I do not simply want to 'hard abandon' it without giving people a clue about where to go next..! + +I am in the process of archiving it; expect some additional instructions and links to alternatives to appear here soon. + +# ESP32-CAM example revisited.     [![CI Status](https://travis-ci.com/easytarget/esp32-cam-webserver.svg?branch=master)](https://travis-ci.com/github/easytarget/esp32-cam-webserver)    ![ESP-EYE logo](Docs/logo.svg) ## Taken from the ESP examples, and expanded This sketch is a extension/expansion/rework of the 'official' ESP32 Camera example sketch from Espressif: @@ -7,14 +14,21 @@ https://github.com/espressif/arduino-esp32/tree/master/libraries/ESP32/examples/ But expanded with: * More options for default network and camera settings -* Control of on-board lamps, view rotation in the browser +* Save and restore settings +* Control of on-board lamps, rotate the view in the browser * Dedicated standalone stream viewer -* Lots of minor fixes and tweaks +* Over The Air firmware updates +* Lots of minor fixes and tweaks, documentation etc. + +And 'reduced' by removing the Face Recognition features +* **If you want to try the Face Recognition features** please use the [`3.x` maintenance branch](https://github.com/easytarget/esp32-cam-webserver/tree/3.x), which still recieves bugfixes, but is not receiving any further development. +* They were a demo, only worked in low resolution modes, did not preserve the face database between power cycles, and were of little use in real-world applications. +* There are other (specialised) sketches for the ESP-CAM that do use face recognitioni more effectively, if this is your thing :-) The original example is a bit incomprehensible and hard to modify as supplied. It is very focused on showing off the face recognition capabilities, and forgets the 'webcam' part. * There are many other variants of a webcam server for these modules online, but most are created for a specific scenario and not good for general, casual, webcam use. -![My Gods, it's full of cats!](Docs/mygodsitsfullofcats.png) +![Actually, there are two cats in this image!]( Docs/twocatsactually.png) Hopefully this expanded example is more useful for those users who wish to set up a simple ESP32 based webcam using the cheap(ish) modules freely available online. Especially the AI-THINKER board: @@ -26,18 +40,36 @@ I have four [AI-THINKER ESP32-CAM](https://github.com/raphaelbs/esp32-cam-ai-thi https://github.com/raphaelbs/esp32-cam-ai-thinker * The AI thinker wiki can be quite informative, when run through an online translator and read sensibly: https://wiki.ai-thinker.com/esp32-cam -* Default pinouts are also included for WRover Kit, ESP Eye and M5Stack esp32 camera modules. +* Default pinouts are also included for WRover Kit, ESP Eye and M5Stack esp32 camera modules. I do not have any of these boards, so they are untested by me. Please [let me know](https://github.com/easytarget/esp32-cam-webserver/issues) if you find issues or have a board not [in the list](./camera_pins.h). ## Troubleshooting: -Please read this excellent guide for help with some common issues seen with the camera modules: +A lot of common issues with this sketch are discussed and covered in the discussion forums: + +https://github.com/easytarget/esp32-cam-webserver/discussions/categories/common-issues + +The existing [issues list](https://github.com/easytarget/esp32-cam-webserver/issues?q=is%3Aissue) on Github is a good place to start if you have a specific issue not covered above or in the forums. + +There is also this excellent guide for help with some common issues seen with the camera modules: https://randomnerdtutorials.com/esp32-cam-troubleshooting-guide/ +### Known Issues + +Builds made with PlatformIO are currently (v4.0) broken; the stream will die shortly after starting. See https://github.com/easytarget/esp32-cam-webserver/issues/218 for more info. + +The ESP32 itself is susceptible to the usual list of WiFi problems, not helped by having small antennas, older designs, congested airwaves and demanding users. The majority of disconnects, stutters and other comms problems are simply due to 'WiFi issues'. The AI-THINKER camera module & esp32 combination is quite susceptible to power supply problems affecting both WiFi conctivity and Video quality; short cabling and decent power supplies are your friend here; also well cooled cases and, if you have the time, decoupling capacitors on the power lines. + +A basic limitation of the sketch is that it can can only support one stream at a time. If you try to connect to a cam that is already streaming (or attempting to stream) you will get no response and, eventually, a timeout. The stream itself is a [MJPEG stream](https://en.wikipedia.org/wiki/Motion_JPEG), which relies on the client (the web browser) to hold the connection open and request each new frame in turn via javascript. This can cause errors when browsers run into Javascript or caching problem, fail to request new frames or refuse to close the connection. +* If you cannot start the stream you can check the `/dump` page of the cam to see if it currently reports the camera as streaming or not. + +Note that I do not respond to any Private Messages (via github, hackaday, or wherever) for support. + ## Setup: * For programming you will need a suitable development environment, I use the Arduino IDE, but this code should work in the Espressif development environment too. -* Make sure you are using the [latest version](https://www.arduino.cc/en/main/software#download) of the IDE and then follow [This Guide](https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md) to set up the Espressif Arduino core for the IDE. +* Make sure you are using the [latest version](https://www.arduino.cc/en/main/software#download) of the IDE and then follow [This Guide](https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html) to set up the Espressif Arduino core for the IDE. +* _I do not recommend or support running with development builds of either the IDE or the ESP arduino core._ * If you have a development board (anything that can be programmed via a standard USB cable/jack on the board itself) you are in luck. Just plug it in and skip ahead to the [config](#config) section. Remember to set your board model. * The AI-THINKER board requires use of an external **3.3v** serial adapter to program; I use a `FTDI Friend` adapter, for more about this read AdaFruits excellent [FTDI Friend guide](https://learn.adafruit.com/ftdi-friend). * Be careful not to use a 5v serial adapter since this will damage the ESP32. @@ -51,42 +83,62 @@ Is pretty simple, You just need jumper wires, no soldering really required, see * The **GPIO0** pin of the ESP32 must be held LOW (to ground) when the unit is powered up to allow it to enter it's programming mode. This can be done with simple jumper cable connected at poweron, fitting a switch for this is useful if you will be reprogramming a lot. * You must supply 5v to the ESP32 in order to power it during programming, the FTDI board can supply this. +### Download the Sketch, Unpack and Rename +Download the latest release of the sketch from https://github.com/easytarget/esp32-cam-webserver/releases/latest +- You can get the latest stable development release by cloning / downloading the `master` branch of the repo. + +This will give you an archive file with the Version number in it, eg.`esp32-cam-webserver-4.0.zip`. You need to unpack this into your Arduino sketch folder, and then you need to **rename the folder you extracted to remove the version number**, eg.`esp32-cam-webserver-4.0` becomes `esp32-cam-webserver`. + +Once you have done that you can open the sketch in the IDE by going to the `esp32-cam-webserver` sketch folder and selecting `esp32-cam-webserver.ino`. + ### Config By default the sketch assumes you have an AI-THINKER board, it creates an AccessPoint called `ESP32-CAM-CONNECT` and with the password `InsecurePassword`; connect to that and then browse to [`http://192.168.4.1/`](http://192.168.4.1/). This is nice and easy for testing and demo purposes. -To make a permanent config for a different board, or with your home wifi settings etc. copy (or rename) the file `myconfig.sample.h` in the sketch folder to `myconfig.h` +To make a permanent config with your home wifi settings, different defaults or a different board; copy (or rename) the file `myconfig.sample.h` in the sketch folder to `myconfig.h` and edit that, all the usable defaults are in that file. Because this is your private copy of the config it will not get overwritten if you update the main sketch! -You can now set a camera name, board model, wifi settings and some other defaults in that file. And because this is your private copy it will not get overwritten if you update the main sketch! +### Programming -### Programming +Assuming you are using the latest Espressif Arduino core the `ESP32 Dev Module` board will appear in the ESP32 Arduino section of the boards list. Select this (do not use the `AI-THINKER` entry listed in the boiards menu, it is not OTA compatible, and will caus the module to crash and reboot rather than updating if you use it. +![IDE board config](Docs/ota-board-selection.png) -Assuming you are using the latest Espressif Arduino core the AI-THINKER board (or whatever you use) will appear in the ESP32 Arduino section of the boards list. -![IDE board config](Docs/board-selection-small.png) +Make sure you select the `Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS)` partition scheme and turn `PSRAM` on. -Compile and upload the code from the IDE, when the `Connecting...` appears in the console reboot the ESP32 module while keeping **GPIO0** grounded. You can release GPO0 once the sketch is uploading, most boards have a 'boot' button to trigger a reboot. +The first time you program (or if OTA is failing) you need to compile and upload the code from the IDE, and when the `Connecting...` appears in the console reboot the ESP32 module while keeping **GPIO0** grounded. You can release GPO0 once the sketch is uploading, most boards have a 'boot' button to trigger a reboot. Once the upload completes (be patient, it can be a bit slow) open the serial monitor in the IDE and reboot the board again without GPIO0 grounded. In the serial monitor you should see the board start, connect to the wifi and then report the IP address it has been assigned. +Once you have the initial upload done and the board is connected to the wifi network you should see it appearing in the `network ports` list of the IDE, and you can upload wirelessly. + If you have a status LED configured it will give a double flash when it begins attempting to conenct to WiFi, and five short flashes once it has succeeded. It will also flash briefly when you access the camera to change settings. Go to the URL given in the serial output, the web UI should appear with the settings panel open. Click away! ## My Modifications: +![The simplified viewer](Docs/simpleviewer.png)
*The new default Simple view, just the basics* + The WiFi details can be stored in an (optional) header file to allow easier code development, and a camera name for the UI title can be configured. The lamp and status LED's are optional, and the lamp uses a exponential scale for brightness so that the control has some finess. -The compressed and binary encoded HTML used in the example has been unpacked to raw text, this makes it much easier to access and modify the Javascript and UI elements. Given the relatively small size of the index page there is very little benefit from compressing it. +All of the face recognition code has been removed as of V4.0; this reduces the code size enough to allow OTA programming while improving compile and programming times. -The streamviewer, lamp control, and all the other new features have been added. I have tried to retain the basic structure of the original example,extending where necesscary. +The compressed and binary encoded HTML used in the example has been unpacked to raw text, this makes it much easier to access and modify the Javascript and UI elements. Given the relatively small size of the index page there is very little benefit from compressing it. -I have left all the Face Recognition code untouched, it works, and with good lighting and camera position it can work quite well. But you can only use it in low-resolution modes, and it is not something I will be using. +The streamviewer, lamp control, and all the other new features have been added. I have tried to retain the basic structure of the original example,extending where necessary. -The web UI has had minor changes to add the lamp control (only when enabled), I also made the 'Start Stream' and 'Snapshot' controls more prominent, and added feedback of the camera name + firmware. +The web UI has had changes to add the lamp control (only when enabled) and make the streamm window rotate and resize appropriately. I also made the 'Start Stream' and 'Snapshot' controls more prominent, and added feedback of the camera name + firmware. I would also like to shoutout to @jmfloyd; who suggested rotating the image in the browser since the esp32 itself cannot do this. -## Notes: +![The stream viewer](Docs/streamview.png)
*Standalone StreamViewer; No decoration or controls, the image is resizable, and you can doubleclick it for fullscreen* + +![The info page](Docs/infodump.png)
*Boring Details, useful when debugging or if you want to check stats* + +### API +The communications between the web browser and the camera module can also be used to send commands directly to the camera (eg to automate it, etc) and form, in effect, an API for the camera. +* I have [documented this here](https://github.com/easytarget/esp32-cam-webserver/blob/master/API.md). + +## Notes: * I only have AI-THINKER modules with OV2640 camera installed; so I have only been able to test with this combination. I have attempted to preserve all the code for other boards and the OV3660 module, and I have merged all changes for the WebUI etc, but I cannot guarantee operation for these. * I created a small board with a handy switch for power, a pushbutton for the GPIO0 programming switch, and a socket for the AI-THINKER board. This proved very useful for development work and programming multiple devices. @@ -102,10 +154,11 @@ Contributions are welcome; please see the [Contribution guidelines](CONTRIBUTING Time allowing; my Current plan is: -V4 Remove face recognition entirely; -* Dont try to make it optional, this is a code and maintenance nightmare. V3 can be maintained on a branch for those who need it. +V4 * Investigate using SD card to capture images -* implement OTA and a better network stack for remembering multiple AP's, auto-config etc. +* Implement a better network stack for remembering multiple AP's, auto-config etc. + * Advanced (web upload) OTA might be nice to have if possible * UI Skinning/Theming - -You can check the [enhancement list](https://github.com/easytarget/esp32-cam-webserver/issues?q=is%3Aissue+label%3Aenhancement) (past and present), and add any thoghts you may have there. Things that have occurred to me are, in no particular order: +* OSD + * Temperature/humidity/pressure sensor support (bme20,dht11) +You can check the [enhancement list](https://github.com/easytarget/esp32-cam-webserver/issues?q=is%3Aissue+label%3Aenhancement) (past and present), and add any thoughts you may have there. diff --git a/app_httpd.cpp b/app_httpd.cpp index 01d5df4..f2b11a9 100644 --- a/app_httpd.cpp +++ b/app_httpd.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include @@ -32,6 +31,7 @@ // Functions from the main .ino extern void flashLED(int flashtime); extern void setLamp(int newVal); +extern void printLocalTime(bool extraData); // External variables declared in the main .ino extern char myName[]; @@ -49,41 +49,23 @@ extern char httpURL[]; extern char streamURL[]; extern char default_index[]; extern int8_t streamCount; +extern unsigned long streamsServed; +extern unsigned long imagesServed; extern int myRotation; +extern int minFrameTime; extern int lampVal; extern bool autoLamp; -extern int8_t detection_enabled; -extern int8_t recognition_enabled; extern bool filesystem; extern String critERR; extern bool debugData; +extern bool haveTime; extern int sketchSize; extern int sketchSpace; extern String sketchMD5; - -#include "fb_gfx.h" -#include "fd_forward.h" -#include "fr_forward.h" - -#define ENROLL_CONFIRM_TIMES 5 -#define FACE_ID_SAVE_NUMBER 7 - -#define FACE_COLOR_WHITE 0x00FFFFFF -#define FACE_COLOR_BLACK 0x00000000 -#define FACE_COLOR_RED 0x000000FF -#define FACE_COLOR_GREEN 0x0000FF00 -#define FACE_COLOR_BLUE 0x00FF0000 -#define FACE_COLOR_YELLOW (FACE_COLOR_RED | FACE_COLOR_GREEN) -#define FACE_COLOR_CYAN (FACE_COLOR_BLUE | FACE_COLOR_GREEN) -#define FACE_COLOR_PURPLE (FACE_COLOR_BLUE | FACE_COLOR_RED) - -typedef struct { - size_t size; //number of values used for filtering - size_t index; //current value index - size_t count; //value count - int sum; - int * values; //array to be filled with values -} ra_filter_t; +extern bool otaEnabled; +extern char otaPassword[]; +extern unsigned long xclk; +extern int sensorPID; typedef struct { httpd_req_t *req; @@ -95,167 +77,101 @@ static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n"; static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n"; -static ra_filter_t ra_filter; httpd_handle_t stream_httpd = NULL; httpd_handle_t camera_httpd = NULL; -static mtmn_config_t mtmn_config = {0}; -static int8_t is_enrolling = 0; -static face_id_list id_list = {0}; -int id_list_alloc = 0; +// Flag that can be set to kill all active streams +bool streamKill; -static ra_filter_t * ra_filter_init(ra_filter_t * filter, size_t sample_size){ - memset(filter, 0, sizeof(ra_filter_t)); - - filter->values = (int *)malloc(sample_size * sizeof(int)); - if(!filter->values){ - return NULL; - } - memset(filter->values, 0, sample_size * sizeof(int)); - - filter->size = sample_size; - return filter; +#ifdef __cplusplus +extern "C" { +#endif +uint8_t temprature_sens_read(); +#ifdef __cplusplus } +#endif -static int ra_filter_run(ra_filter_t * filter, int value) { - if(!filter->values){ - return value; - } - filter->sum -= filter->values[filter->index]; - filter->values[filter->index] = value; - filter->sum += filter->values[filter->index]; - filter->index++; - filter->index = filter->index % filter->size; - if (filter->count < filter->size) { - filter->count++; - } - return filter->sum / filter->count; -} - -static void rgb_print(dl_matrix3du_t *image_matrix, uint32_t color, const char * str){ - fb_data_t fb; - fb.width = image_matrix->w; - fb.height = image_matrix->h; - fb.data = image_matrix->item; - fb.bytes_per_pixel = 3; - fb.format = FB_BGR888; - fb_gfx_print(&fb, (fb.width - (strlen(str) * 14)) / 2, 10, color, str); -} - -static int rgb_printf(dl_matrix3du_t *image_matrix, uint32_t color, const char *format, ...){ - char loc_buf[64]; - char * temp = loc_buf; - int len; - va_list arg; - va_list copy; - va_start(arg, format); - va_copy(copy, arg); - len = vsnprintf(loc_buf, sizeof(loc_buf), format, arg); - va_end(copy); - if(len >= sizeof(loc_buf)){ - temp = (char*)malloc(len+1); - if(temp == NULL) { - return 0; - } - } - vsnprintf(temp, len+1, format, arg); - va_end(arg); - rgb_print(image_matrix, color, temp); - if(len > 64){ - free(temp); +void serialDump() { + Serial.println(); + // Module + Serial.printf("Name: %s\r\n", myName); + if (haveTime) { + Serial.print("Time: "); + printLocalTime(true); } - return len; -} - -static void draw_face_boxes(dl_matrix3du_t *image_matrix, box_array_t *boxes, int face_id){ - int x, y, w, h, i; - uint32_t color = FACE_COLOR_YELLOW; - if(face_id < 0){ - color = FACE_COLOR_RED; - } else if(face_id > 0){ - color = FACE_COLOR_GREEN; + Serial.printf("Firmware: %s (base: %s)\r\n", myVer, baseVersion); + float sketchPct = 100 * sketchSize / sketchSpace; + Serial.printf("Sketch Size: %i (total: %i, %.1f%% used)\r\n", sketchSize, sketchSpace, sketchPct); + Serial.printf("MD5: %s\r\n", sketchMD5.c_str()); + Serial.printf("ESP sdk: %s\r\n", ESP.getSdkVersion()); + if (otaEnabled) { + if (strlen(otaPassword) != 0) { + Serial.printf("OTA: Enabled, Password: %s\n\r", otaPassword); + } else { + Serial.printf("OTA: Enabled, No Password! (insecure)\n\r"); + } + } else { + Serial.printf("OTA: Disabled\n\r"); } - fb_data_t fb; - fb.width = image_matrix->w; - fb.height = image_matrix->h; - fb.data = image_matrix->item; - fb.bytes_per_pixel = 3; - fb.format = FB_BGR888; - for (i = 0; i < boxes->len; i++){ - // rectangle box - x = (int)boxes->box[i].box_p[0]; - y = (int)boxes->box[i].box_p[1]; - w = (int)boxes->box[i].box_p[2] - x + 1; - h = (int)boxes->box[i].box_p[3] - y + 1; - fb_gfx_drawFastHLine(&fb, x, y, w, color); - fb_gfx_drawFastHLine(&fb, x, y+h-1, w, color); - fb_gfx_drawFastVLine(&fb, x, y, h, color); - fb_gfx_drawFastVLine(&fb, x+w-1, y, h, color); - #if 0 - // landmark - int x0, y0, j; - for (j = 0; j < 10; j+=2) { - x0 = (int)boxes->landmark[i].landmark_p[j]; - y0 = (int)boxes->landmark[i].landmark_p[j+1]; - fb_gfx_fillRect(&fb, x0, y0, 3, 3, color); + // Network + if (accesspoint) { + if (captivePortal) { + Serial.printf("WiFi Mode: AccessPoint with captive portal\r\n"); + } else { + Serial.printf("WiFi Mode: AccessPoint\r\n"); } - #endif + Serial.printf("WiFi SSID: %s\r\n", apName); + } else { + Serial.printf("WiFi Mode: Client\r\n"); + String ssidName = WiFi.SSID(); + Serial.printf("WiFi Ssid: %s\r\n", ssidName.c_str()); + Serial.printf("WiFi Rssi: %i\r\n", WiFi.RSSI()); + String bssid = WiFi.BSSIDstr(); + Serial.printf("WiFi BSSID: %s\r\n", bssid.c_str()); } -} - -static int run_face_recognition(dl_matrix3du_t *image_matrix, box_array_t *net_boxes){ - dl_matrix3du_t *aligned_face = NULL; - int matched_id = 0; - - aligned_face = dl_matrix3du_alloc(1, FACE_WIDTH, FACE_HEIGHT, 3); - if(!aligned_face){ - Serial.println("Could not allocate face recognition buffer"); - return matched_id; + Serial.printf("WiFi IP address: %d.%d.%d.%d\r\n", ip[0], ip[1], ip[2], ip[3]); + if (!accesspoint) { + Serial.printf("WiFi Netmask: %d.%d.%d.%d\r\n", net[0], net[1], net[2], net[3]); + Serial.printf("WiFi Gateway: %d.%d.%d.%d\r\n", gw[0], gw[1], gw[2], gw[3]); } - if (align_face(net_boxes, image_matrix, aligned_face) == ESP_OK){ - if (is_enrolling == 1){ - int8_t this_face = id_list.tail; - int8_t left_sample_face = enroll_face(&id_list, aligned_face); - - if(left_sample_face == (ENROLL_CONFIRM_TIMES - 1)){ - Serial.printf("Enrolling Face ID: %d\n", this_face); - } - Serial.printf("Enrolling Face ID: %d sample %d\n", this_face, ENROLL_CONFIRM_TIMES - left_sample_face); - rgb_printf(image_matrix, FACE_COLOR_CYAN, "ID[%u] Sample[%u]", this_face, ENROLL_CONFIRM_TIMES - left_sample_face); - if (left_sample_face == 0){ - is_enrolling = 0; - Serial.printf("Enrolled Face ID: %d\n", this_face); - } - } else { - matched_id = recognize_face(&id_list, aligned_face); - if (matched_id >= 0) { - Serial.printf("Match Face ID: %u\n", matched_id); - rgb_printf(image_matrix, FACE_COLOR_GREEN, "Hello Subject %u", matched_id); - } else { - Serial.println("No Match Found"); - rgb_print(image_matrix, FACE_COLOR_RED, "Intruder Alert!"); - matched_id = -1; - } - } + Serial.printf("WiFi Http port: %i, Stream port: %i\r\n", httpPort, streamPort); + byte mac[6]; + WiFi.macAddress(mac); + Serial.printf("WiFi MAC: %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + // System + int64_t sec = esp_timer_get_time() / 1000000; + int64_t upDays = int64_t(floor(sec/86400)); + int upHours = int64_t(floor(sec/3600)) % 24; + int upMin = int64_t(floor(sec/60)) % 60; + int upSec = sec % 60; + int McuTc = (temprature_sens_read() - 32) / 1.8; // celsius + int McuTf = temprature_sens_read(); // fahrenheit + Serial.printf("System up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)\r\n", upDays, upHours, upMin, upSec); + Serial.printf("Active streams: %i, Previous streams: %lu, Images captured: %lu\r\n", streamCount, streamsServed, imagesServed); + Serial.printf("CPU Freq: %i MHz, Xclk Freq: %i MHz\r\n", ESP.getCpuFreqMHz(), xclk); + Serial.printf("MCU temperature : %i C, %i F (approximate)\r\n", McuTc, McuTf); + Serial.printf("Heap: %i, free: %i, min free: %i, max block: %i\r\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); + if(psramFound()) { + Serial.printf("Psram: %i, free: %i, min free: %i, max block: %i\r\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); } else { - Serial.println("Face Not Aligned"); - //rgb_print(image_matrix, FACE_COLOR_YELLOW, "Human Detected"); + Serial.printf("Psram: Not found; please check your board configuration.\r\n"); + Serial.printf("- High resolution/quality settings will show incomplete frames to low memory.\r\n"); } - - dl_matrix3du_free(aligned_face); - return matched_id; -} - -static size_t jpg_encode_stream(void * arg, size_t index, const void* data, size_t len){ - jpg_chunking_t *j = (jpg_chunking_t *)arg; - if(!index){ - j->len = 0; + // Filesystems + if (filesystem && (SPIFFS.totalBytes() > 0)) { + Serial.printf("Spiffs: %i, used: %i\r\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); + } else { + Serial.printf("Spiffs: No filesystem found, please check your board configuration.\r\n"); + Serial.printf("- Saving and restoring camera settings will not function without this.\r\n"); } - if(httpd_resp_send_chunk(j->req, (const char *)data, len) != ESP_OK){ - return 0; + Serial.println("Preferences file: "); + dumpPrefs(SPIFFS); + if (critERR.length() > 0) { + Serial.printf("\r\n\r\nAn error or halt has occurred with Camera Hardware, see previous messages.\r\n"); + Serial.printf("A reboot is required to recover from this.\r\nError message: (html)\r\n %s\r\n\r\n", critERR.c_str()); } - j->len += len; - return len; + Serial.println(); + return; } static esp_err_t capture_handler(httpd_req_t *req){ @@ -263,14 +179,17 @@ static esp_err_t capture_handler(httpd_req_t *req){ esp_err_t res = ESP_OK; Serial.println("Capture Requested"); - if (autoLamp && (lampVal != -1)) setLamp(lampVal); + if (autoLamp && (lampVal != -1)) { + setLamp(lampVal); + delay(75); // coupled with the status led flash this gives ~150ms for lamp to settle. + } flashLED(75); // little flash of status LED int64_t fr_start = esp_timer_get_time(); fb = esp_camera_fb_get(); if (!fb) { - Serial.println("Camera capture failed"); + Serial.println("CAPTURE: failed to acquire frame"); httpd_resp_send_500(req); if (autoLamp && (lampVal != -1)) setLamp(0); return ESP_FAIL; @@ -280,83 +199,25 @@ static esp_err_t capture_handler(httpd_req_t *req){ httpd_resp_set_hdr(req, "Content-Disposition", "inline; filename=capture.jpg"); httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); - size_t out_len, out_width, out_height; - uint8_t * out_buf; - bool s; - bool detected = false; - int face_id = 0; - if(!detection_enabled || fb->width > 400){ - size_t fb_len = 0; - if(fb->format == PIXFORMAT_JPEG){ - fb_len = fb->len; - res = httpd_resp_send(req, (const char *)fb->buf, fb->len); - } else { - jpg_chunking_t jchunk = {req, 0}; - res = frame2jpg_cb(fb, 80, jpg_encode_stream, &jchunk)?ESP_OK:ESP_FAIL; - httpd_resp_send_chunk(req, NULL, 0); - fb_len = jchunk.len; - } - esp_camera_fb_return(fb); - int64_t fr_end = esp_timer_get_time(); - if (debugData) { - Serial.printf("JPG: %uB %ums\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000)); - } - if (autoLamp && (lampVal != -1)) setLamp(0); - return res; - } - - dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); - if (!image_matrix) { - esp_camera_fb_return(fb); - Serial.println("dl_matrix3du_alloc failed"); - httpd_resp_send_500(req); - if (autoLamp && (lampVal != -1)) setLamp(0); - return ESP_FAIL; + size_t fb_len = 0; + if(fb->format == PIXFORMAT_JPEG){ + fb_len = fb->len; + res = httpd_resp_send(req, (const char *)fb->buf, fb->len); + } else { + res = ESP_FAIL; + Serial.println("Capture Error: Non-JPEG image returned by camera module"); } - - out_buf = image_matrix->item; - out_len = fb->width * fb->height * 3; - out_width = fb->width; - out_height = fb->height; - - s = fmt2rgb888(fb->buf, fb->len, fb->format, out_buf); esp_camera_fb_return(fb); - if(!s){ - dl_matrix3du_free(image_matrix); - Serial.println("to rgb888 failed"); - httpd_resp_send_500(req); - if (autoLamp && (lampVal != -1)) setLamp(0); - return ESP_FAIL; - } - - box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config); - - if (net_boxes){ - detected = true; - if(recognition_enabled){ - face_id = run_face_recognition(image_matrix, net_boxes); - } - draw_face_boxes(image_matrix, net_boxes, face_id); - free(net_boxes->score); - free(net_boxes->box); - free(net_boxes->landmark); - free(net_boxes); - } - - jpg_chunking_t jchunk = {req, 0}; - s = fmt2jpg_cb(out_buf, out_len, out_width, out_height, PIXFORMAT_RGB888, 90, jpg_encode_stream, &jchunk); - dl_matrix3du_free(image_matrix); - if(!s){ - Serial.println("JPEG compression failed"); - if (autoLamp && (lampVal != -1)) setLamp(0); - return ESP_FAIL; - } + fb = NULL; int64_t fr_end = esp_timer_get_time(); if (debugData) { - Serial.printf("FACE: %uB %ums %s%d\n", (uint32_t)(jchunk.len), (uint32_t)((fr_end - fr_start)/1000), detected?"DETECTED ":"", face_id); + Serial.printf("JPG: %uB %ums\r\n", (uint32_t)(fb_len), (uint32_t)((fr_end - fr_start)/1000)); + } + imagesServed++; + if (autoLamp && (lampVal != -1)) { + setLamp(0); } - if (autoLamp && (lampVal != -1)) setLamp(0); return res; } @@ -366,18 +227,12 @@ static esp_err_t stream_handler(httpd_req_t *req){ size_t _jpg_buf_len = 0; uint8_t * _jpg_buf = NULL; char * part_buf[64]; - dl_matrix3du_t *image_matrix = NULL; - int face_id = 0; - bool detected = false; - int64_t fr_start = 0; - int64_t fr_face = 0; - int64_t fr_recognize = 0; - int64_t fr_encode = 0; - int64_t fr_ready = 0; + + streamKill = false; Serial.println("Stream requested"); if (autoLamp && (lampVal != -1)) setLamp(lampVal); - streamCount = 1; // at present we only have one stream handler.. + streamCount = 1; // at present we only have one stream handler, so values are 0 or 1.. flashLED(75); // double flash of status LED delay(75); flashLED(75); @@ -389,89 +244,32 @@ static esp_err_t stream_handler(httpd_req_t *req){ res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE); if(res != ESP_OK){ + streamCount = 0; if (autoLamp && (lampVal != -1)) setLamp(0); + Serial.println("STREAM: failed to set HTTP response type"); return res; } httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + if(res == ESP_OK){ + res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); + } + while(true){ - detected = false; - face_id = 0; fb = esp_camera_fb_get(); if (!fb) { - Serial.println("Camera capture failed"); + Serial.println("STREAM: failed to acquire frame"); res = ESP_FAIL; } else { - fr_start = esp_timer_get_time(); - fr_ready = fr_start; - fr_face = fr_start; - fr_encode = fr_start; - fr_recognize = fr_start; - if(!detection_enabled || fb->width > 400){ - if(fb->format != PIXFORMAT_JPEG){ - bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); - esp_camera_fb_return(fb); - fb = NULL; - if(!jpeg_converted){ - Serial.println("JPEG compression failed"); - res = ESP_FAIL; - } - } else { - _jpg_buf_len = fb->len; - _jpg_buf = fb->buf; - } + if(fb->format != PIXFORMAT_JPEG){ + Serial.println("STREAM: Non-JPEG frame returned by camera module"); + res = ESP_FAIL; } else { - - image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3); - - if (!image_matrix) { - Serial.println("dl_matrix3du_alloc failed"); - res = ESP_FAIL; - } else { - if(!fmt2rgb888(fb->buf, fb->len, fb->format, image_matrix->item)){ - Serial.println("fmt2rgb888 failed"); - res = ESP_FAIL; - } else { - fr_ready = esp_timer_get_time(); - box_array_t *net_boxes = NULL; - if(detection_enabled){ - net_boxes = face_detect(image_matrix, &mtmn_config); - } - fr_face = esp_timer_get_time(); - fr_recognize = fr_face; - if (net_boxes || fb->format != PIXFORMAT_JPEG){ - if(net_boxes){ - detected = true; - if(recognition_enabled){ - face_id = run_face_recognition(image_matrix, net_boxes); - } - fr_recognize = esp_timer_get_time(); - draw_face_boxes(image_matrix, net_boxes, face_id); - free(net_boxes->score); - free(net_boxes->box); - free(net_boxes->landmark); - free(net_boxes); - } - if(!fmt2jpg(image_matrix->item, fb->width*fb->height*3, fb->width, fb->height, PIXFORMAT_RGB888, 90, &_jpg_buf, &_jpg_buf_len)){ - Serial.println("fmt2jpg failed"); - res = ESP_FAIL; - } - esp_camera_fb_return(fb); - fb = NULL; - } else { - _jpg_buf = fb->buf; - _jpg_buf_len = fb->len; - } - fr_encode = esp_timer_get_time(); - } - dl_matrix3du_free(image_matrix); - } + _jpg_buf_len = fb->len; + _jpg_buf = fb->buf; } } - if(res == ESP_OK){ - res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); - } if(res == ESP_OK){ size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len); res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen); @@ -479,6 +277,9 @@ static esp_err_t stream_handler(httpd_req_t *req){ if(res == ESP_OK){ res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len); } + if(res == ESP_OK){ + res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY)); + } if(fb){ esp_camera_fb_return(fb); fb = NULL; @@ -488,32 +289,32 @@ static esp_err_t stream_handler(httpd_req_t *req){ _jpg_buf = NULL; } if(res != ESP_OK){ + // This is the error exit point from the stream loop. + // We end the stream here only if a Hard failure has been encountered or the connection has been interrupted. + Serial.printf("Stream failed, code = %i : %s\r\n", res, esp_err_to_name(res)); break; } - - int64_t fr_end = esp_timer_get_time(); - int64_t ready_time = (fr_ready - fr_start)/1000; - int64_t face_time = (fr_face - fr_ready)/1000; - int64_t recognize_time = (fr_recognize - fr_face)/1000; - int64_t encode_time = (fr_encode - fr_recognize)/1000; - int64_t process_time = (fr_encode - fr_start)/1000; - int64_t frame_time = fr_end - last_frame; - last_frame = fr_end; + if((res != ESP_OK) || streamKill){ + // We end the stream here when a kill is signalled. + Serial.printf("Stream killed\r\n"); + break; + } + int64_t frame_time = esp_timer_get_time() - last_frame; frame_time /= 1000; - uint32_t avg_frame_time = ra_filter_run(&ra_filter, frame_time); + int32_t frame_delay = (minFrameTime > frame_time) ? minFrameTime - frame_time : 0; + delay(frame_delay); + if (debugData) { - Serial.printf("MJPG: %uB %ums (%.1ffps), AVG: %ums (%.1ffps), %u+%u+%u+%u=%u %s%d\n", + Serial.printf("MJPG: %uB %ums, delay: %ums, framerate (%.1ffps)\r\n", (uint32_t)(_jpg_buf_len), - (uint32_t)frame_time, 1000.0 / (uint32_t)frame_time, - avg_frame_time, 1000.0 / avg_frame_time, - (uint32_t)ready_time, (uint32_t)face_time, (uint32_t)recognize_time, (uint32_t)encode_time, (uint32_t)process_time, - (detected)?"DETECTED ":"", face_id - ); + (uint32_t)frame_time, frame_delay, 1000.0 / (uint32_t)(frame_time + frame_delay)); } + last_frame = esp_timer_get_time(); } + streamsServed++; + streamCount = 0; if (autoLamp && (lampVal != -1)) setLamp(0); - streamCount = 0; // at present we only have one stream handler.. Serial.println("Stream ended"); last_frame = 0; return res; @@ -524,6 +325,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ size_t buf_len; char variable[32] = {0,}; char value[32] = {0,}; + flashLED(75); buf_len = httpd_req_get_url_query_len(req) + 1; @@ -552,6 +354,8 @@ static esp_err_t cmd_handler(httpd_req_t *req){ return ESP_FAIL; } + if (critERR.length() > 0) return httpd_resp_send_500(req); + int val = atoi(value); sensor_t * s = esp_camera_sensor_get(); int res = 0; @@ -559,6 +363,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ if(s->pixformat == PIXFORMAT_JPEG) res = s->set_framesize(s, (framesize_t)val); } else if(!strcmp(variable, "quality")) res = s->set_quality(s, val); + else if(!strcmp(variable, "xclk")) { xclk = val; res = s->set_xclk(s, LEDC_TIMER_0, val); } else if(!strcmp(variable, "contrast")) res = s->set_contrast(s, val); else if(!strcmp(variable, "brightness")) res = s->set_brightness(s, val); else if(!strcmp(variable, "saturation")) res = s->set_saturation(s, val); @@ -582,19 +387,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val); else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val); else if(!strcmp(variable, "rotate")) myRotation = val; - else if(!strcmp(variable, "face_detect")) { - detection_enabled = val; - if(!detection_enabled) { - recognition_enabled = 0; - } - } - else if(!strcmp(variable, "face_enroll")) is_enrolling = val; - else if(!strcmp(variable, "face_recognize")) { - recognition_enabled = val; - if(recognition_enabled){ - detection_enabled = val; - } - } + else if(!strcmp(variable, "min_frame_time")) minFrameTime = val; else if(!strcmp(variable, "autolamp") && (lampVal != -1)) { autoLamp = val; if (autoLamp) { @@ -613,12 +406,6 @@ static esp_err_t cmd_handler(httpd_req_t *req){ setLamp(lampVal); } } - else if(!strcmp(variable, "save_face")) { - if (filesystem) saveFaceDB(SPIFFS); - } - else if(!strcmp(variable, "clear_face")) { - if (filesystem) removeFaceDB(SPIFFS); - } else if(!strcmp(variable, "save_prefs")) { if (filesystem) savePrefs(SPIFFS); } @@ -626,6 +413,7 @@ static esp_err_t cmd_handler(httpd_req_t *req){ if (filesystem) removePrefs(SPIFFS); } else if(!strcmp(variable, "reboot")) { + if (lampVal != -1) setLamp(0); // kill the lamp; otherwise it can remain on during the soft-reboot esp_task_wdt_init(3,true); // schedule a a watchdog panic event for 3 seconds in the future esp_task_wdt_add(NULL); periph_module_disable(PERIPH_I2C0_MODULE); // try to shut I2C down properly @@ -651,43 +439,45 @@ static esp_err_t cmd_handler(httpd_req_t *req){ static esp_err_t status_handler(httpd_req_t *req){ static char json_response[1024]; - sensor_t * s = esp_camera_sensor_get(); char * p = json_response; *p++ = '{'; - p+=sprintf(p, "\"lamp\":%d,", lampVal); - p+=sprintf(p, "\"autolamp\":%d,", autoLamp); - p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); - p+=sprintf(p, "\"quality\":%u,", s->status.quality); - p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); - p+=sprintf(p, "\"contrast\":%d,", s->status.contrast); - p+=sprintf(p, "\"saturation\":%d,", s->status.saturation); - p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness); - p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect); - p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode); - p+=sprintf(p, "\"awb\":%u,", s->status.awb); - p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain); - p+=sprintf(p, "\"aec\":%u,", s->status.aec); - p+=sprintf(p, "\"aec2\":%u,", s->status.aec2); - p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level); - p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value); - p+=sprintf(p, "\"agc\":%u,", s->status.agc); - p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain); - p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling); - p+=sprintf(p, "\"bpc\":%u,", s->status.bpc); - p+=sprintf(p, "\"wpc\":%u,", s->status.wpc); - p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma); - p+=sprintf(p, "\"lenc\":%u,", s->status.lenc); - p+=sprintf(p, "\"vflip\":%u,", s->status.vflip); - p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror); - p+=sprintf(p, "\"dcw\":%u,", s->status.dcw); - p+=sprintf(p, "\"colorbar\":%u,", s->status.colorbar); - p+=sprintf(p, "\"face_detect\":%u,", detection_enabled); - p+=sprintf(p, "\"face_enroll\":%u,", is_enrolling); - p+=sprintf(p, "\"face_recognize\":%u,", recognition_enabled); - p+=sprintf(p, "\"cam_name\":\"%s\",", myName); - p+=sprintf(p, "\"code_ver\":\"%s\",", myVer); - p+=sprintf(p, "\"rotate\":\"%d\",", myRotation); - p+=sprintf(p, "\"stream_url\":\"%s\"", streamURL); + // Do not get attempt to get sensor when in error; causes a panic.. + if (critERR.length() == 0) { + sensor_t * s = esp_camera_sensor_get(); + p+=sprintf(p, "\"lamp\":%d,", lampVal); + p+=sprintf(p, "\"autolamp\":%d,", autoLamp); + p+=sprintf(p, "\"min_frame_time\":%d,", minFrameTime); + p+=sprintf(p, "\"framesize\":%u,", s->status.framesize); + p+=sprintf(p, "\"quality\":%u,", s->status.quality); + p+=sprintf(p, "\"xclk\":%u,", xclk); + p+=sprintf(p, "\"brightness\":%d,", s->status.brightness); + p+=sprintf(p, "\"contrast\":%d,", s->status.contrast); + p+=sprintf(p, "\"saturation\":%d,", s->status.saturation); + p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness); + p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect); + p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode); + p+=sprintf(p, "\"awb\":%u,", s->status.awb); + p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain); + p+=sprintf(p, "\"aec\":%u,", s->status.aec); + p+=sprintf(p, "\"aec2\":%u,", s->status.aec2); + p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level); + p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value); + p+=sprintf(p, "\"agc\":%u,", s->status.agc); + p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain); + p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling); + p+=sprintf(p, "\"bpc\":%u,", s->status.bpc); + p+=sprintf(p, "\"wpc\":%u,", s->status.wpc); + p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma); + p+=sprintf(p, "\"lenc\":%u,", s->status.lenc); + p+=sprintf(p, "\"vflip\":%u,", s->status.vflip); + p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror); + p+=sprintf(p, "\"dcw\":%u,", s->status.dcw); + p+=sprintf(p, "\"colorbar\":%u,", s->status.colorbar); + p+=sprintf(p, "\"cam_name\":\"%s\",", myName); + p+=sprintf(p, "\"code_ver\":\"%s\",", myVer); + p+=sprintf(p, "\"rotate\":\"%d\",", myRotation); + p+=sprintf(p, "\"stream_url\":\"%s\"", streamURL); + } *p++ = '}'; *p++ = 0; httpd_resp_set_type(req, "application/json"); @@ -735,9 +525,8 @@ static esp_err_t logo_svg_handler(httpd_req_t *req){ static esp_err_t dump_handler(httpd_req_t *req){ flashLED(75); - Serial.println("\nDump Requested"); - Serial.print("Preferences file: "); - dumpPrefs(SPIFFS); + Serial.println("\r\nDump requested via Web"); + serialDump(); static char dumpOut[2000] = ""; char * d = dumpOut; // Header @@ -749,88 +538,88 @@ static esp_err_t dump_handler(httpd_req_t *req){ d+= sprintf(d,"\n"); d+= sprintf(d,"\n"); d+= sprintf(d,"\n"); - d+= sprintf(d,"\n"); + d+= sprintf(d,"\n"); if (critERR.length() > 0) { d+= sprintf(d,"%s
\n", critERR.c_str()); - Serial.printf("\n\nA critical error has occurred when initialising Hardware, see startup megssages\n\n\n"); } d+= sprintf(d,"

ESP32 Cam Webserver

\n"); // Module d+= sprintf(d,"Name: %s
\n", myName); - Serial.printf("Name: %s\n", myName); d+= sprintf(d,"Firmware: %s (base: %s)
\n", myVer, baseVersion); - Serial.printf("Firmware: %s (base: %s)\n", myVer, baseVersion); float sketchPct = 100 * sketchSize / sketchSpace; d+= sprintf(d,"Sketch Size: %i (total: %i, %.1f%% used)
\n", sketchSize, sketchSpace, sketchPct); - Serial.printf("Sketch Size: %i (total: %i, %.1f%% used)\n", sketchSize, sketchSpace, sketchPct); d+= sprintf(d,"MD5: %s
\n", sketchMD5.c_str()); - Serial.printf("MD5: %s\n", sketchMD5.c_str()); d+= sprintf(d,"ESP sdk: %s
\n", ESP.getSdkVersion()); - Serial.printf("ESP sdk: %s\n", ESP.getSdkVersion()); // Network d+= sprintf(d,"

WiFi

\n"); if (accesspoint) { if (captivePortal) { d+= sprintf(d,"Mode: AccessPoint with captive portal
\n"); - Serial.printf("Mode: AccessPoint with captive portal\n"); } else { d+= sprintf(d,"Mode: AccessPoint
\n"); - Serial.printf("Mode: AccessPoint\n"); } d+= sprintf(d,"SSID: %s
\n", apName); - Serial.printf("SSID: %s\n", apName); } else { d+= sprintf(d,"Mode: Client
\n"); - Serial.printf("Mode: Client\n"); String ssidName = WiFi.SSID(); d+= sprintf(d,"SSID: %s
\n", ssidName.c_str()); - Serial.printf("Ssid: %s\n", ssidName.c_str()); d+= sprintf(d,"Rssi: %i
\n", WiFi.RSSI()); - Serial.printf("Rssi: %i\n", WiFi.RSSI()); String bssid = WiFi.BSSIDstr(); d+= sprintf(d,"BSSID: %s
\n", bssid.c_str()); - Serial.printf("BSSID: %s\n", bssid.c_str()); } d+= sprintf(d,"IP address: %d.%d.%d.%d
\n", ip[0], ip[1], ip[2], ip[3]); - Serial.printf("IP address: %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]); if (!accesspoint) { d+= sprintf(d,"Netmask: %d.%d.%d.%d
\n", net[0], net[1], net[2], net[3]); - Serial.printf("Netmask: %d.%d.%d.%d\n", net[0], net[1], net[2], net[3]); d+= sprintf(d,"Gateway: %d.%d.%d.%d
\n", gw[0], gw[1], gw[2], gw[3]); - Serial.printf("Gateway: %d.%d.%d.%d\n", gw[0], gw[1], gw[2], gw[3]); } d+= sprintf(d,"Http port: %i, Stream port: %i
\n", httpPort, streamPort); - Serial.printf("Http port: %i, Stream port: %i\n", httpPort, streamPort); byte mac[6]; WiFi.macAddress(mac); d+= sprintf(d,"MAC: %02X:%02X:%02X:%02X:%02X:%02X
\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - Serial.printf("MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); // System d+= sprintf(d,"

System

\n"); + if (haveTime) { + struct tm timeinfo; + if(getLocalTime(&timeinfo)){ + char timeStringBuff[50]; //50 chars should be enough + strftime(timeStringBuff, sizeof(timeStringBuff), "%H:%M:%S, %A, %B %d %Y", &timeinfo); + //print like "const char*" + d+= sprintf(d,"Time: %s
\n", timeStringBuff); + } + } int64_t sec = esp_timer_get_time() / 1000000; int64_t upDays = int64_t(floor(sec/86400)); int upHours = int64_t(floor(sec/3600)) % 24; int upMin = int64_t(floor(sec/60)) % 60; int upSec = sec % 60; + int McuTc = (temprature_sens_read() - 32) / 1.8; // celsius + int McuTf = temprature_sens_read(); // fahrenheit + d+= sprintf(d,"Up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)
\n", upDays, upHours, upMin, upSec); - Serial.printf("Up: %" PRId64 ":%02i:%02i:%02i (d:h:m:s)\n", upDays, upHours, upMin, upSec); - d+= sprintf(d,"Freq: %i MHz
\n", ESP.getCpuFreqMHz()); - Serial.printf("Freq: %i MHz\n", ESP.getCpuFreqMHz()); + d+= sprintf(d,"Active streams: %i, Previous streams: %lu, Images captured: %lu
\n", streamCount, streamsServed, imagesServed); + d+= sprintf(d,"CPU Freq: %i MHz, Xclk Freq: %i MHz
\n", ESP.getCpuFreqMHz(), xclk); + d+= sprintf(d,""); + d+= sprintf(d,"MCU temperature : %i °C, %i °F\n
", McuTc, McuTf); d+= sprintf(d,"Heap: %i, free: %i, min free: %i, max block: %i
\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); - Serial.printf("Heap: %i, free: %i, min free: %i, max block: %i\n", ESP.getHeapSize(), ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap()); - d+= sprintf(d,"Psram: %i, free: %i, min free: %i, max block: %i
\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); - Serial.printf("Psram: %i, free: %i, min free: %i, max block: %i\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); - if (filesystem) { + if (psramFound()) { + d+= sprintf(d,"Psram: %i, free: %i, min free: %i, max block: %i
\n", ESP.getPsramSize(), ESP.getFreePsram(), ESP.getMinFreePsram(), ESP.getMaxAllocPsram()); + } else { + d+= sprintf(d,"Psram: Not found, please check your board configuration.
\n"); + d+= sprintf(d,"- High resolution/quality images & streams will show incomplete frames due to low memory.
\n"); + } + if (filesystem && (SPIFFS.totalBytes() > 0)) { d+= sprintf(d,"Spiffs: %i, used: %i
\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); - Serial.printf("Spiffs: %i, used: %i\n", SPIFFS.totalBytes(), SPIFFS.usedBytes()); + } else { + d+= sprintf(d,"Spiffs: No filesystem found, please check your board configuration.
\n"); + d+= sprintf(d,"- saving and restoring camera settings will not function without this.
\n"); } - d+= sprintf(d,"Enrolled faces: %i (max %i)
\n", id_list.count, id_list.size); - Serial.printf("Enrolled faces: %i (max %i)\n", id_list.count, id_list.size); // Footer d+= sprintf(d,"
\n"); d+= sprintf(d,"\n"); + d+= sprintf(d,"\n"); d+= sprintf(d,"\n"); d+= sprintf(d,"
\n\n"); // A javascript timer to refresh the page every minute. @@ -842,6 +631,15 @@ static esp_err_t dump_handler(httpd_req_t *req){ return httpd_resp_send(req, dumpOut, strlen(dumpOut)); } +static esp_err_t stop_handler(httpd_req_t *req){ + flashLED(75); + Serial.println("\r\nStream stop requested via Web"); + streamKill = true; + httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); + return httpd_resp_send(req, NULL, 0); +} + + static esp_err_t style_handler(httpd_req_t *req){ httpd_resp_set_type(req, "text/css"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); @@ -850,7 +648,7 @@ static esp_err_t style_handler(httpd_req_t *req){ static esp_err_t streamviewer_handler(httpd_req_t *req){ flashLED(75); - Serial.println("Stream Viewer requested"); + Serial.println("Stream viewer requested"); httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)streamviewer_html, streamviewer_html_len); @@ -858,7 +656,7 @@ static esp_err_t streamviewer_handler(httpd_req_t *req){ static esp_err_t error_handler(httpd_req_t *req){ flashLED(75); - Serial.println("Sending Error page"); + Serial.println("Sending error page"); std::string s(error_html); size_t index; while ((index = s.find("")) != std::string::npos) @@ -910,15 +708,16 @@ static esp_err_t index_handler(httpd_req_t *req){ if (strncmp(view,"simple", sizeof(view)) == 0) { Serial.println("Simple index page requested"); + if (critERR.length() > 0) return error_handler(req); httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); return httpd_resp_send(req, (const char *)index_simple_html, index_simple_html_len); } else if(strncmp(view,"full", sizeof(view)) == 0) { Serial.println("Full index page requested"); + if (critERR.length() > 0) return error_handler(req); httpd_resp_set_type(req, "text/html"); httpd_resp_set_hdr(req, "Content-Encoding", "identity"); - sensor_t * s = esp_camera_sensor_get(); - if (s->id.PID == OV3660_PID) { + if (sensorPID == OV3660_PID) { return httpd_resp_send(req, (const char *)index_ov3660_html, index_ov3660_html_len); } return httpd_resp_send(req, (const char *)index_ov2640_html, index_ov2640_html_len); @@ -946,7 +745,7 @@ static esp_err_t index_handler(httpd_req_t *req){ void startCameraServer(int hPort, int sPort){ httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.max_uri_handlers = 12; // we use more than the default 8 (on port 80) + config.max_uri_handlers = 16; // we use more than the default 8 (on port 80) httpd_uri_t index_uri = { .uri = "/", @@ -1008,6 +807,12 @@ void startCameraServer(int hPort, int sPort){ .handler = dump_handler, .user_ctx = NULL }; + httpd_uri_t stop_uri = { + .uri = "/stop", + .method = HTTP_GET, + .handler = stop_handler, + .user_ctx = NULL + }; httpd_uri_t stream_uri = { .uri = "/", .method = HTTP_GET, @@ -1039,33 +844,10 @@ void startCameraServer(int hPort, int sPort){ .user_ctx = NULL }; - // Filter list; used during face detection - ra_filter_init(&ra_filter, 20); - - // Mtmn config values (face detection and recognition parameters) - mtmn_config.type = FAST; - mtmn_config.min_face = 80; - mtmn_config.pyramid = 0.707; - mtmn_config.pyramid_times = 4; - mtmn_config.p_threshold.score = 0.6; - mtmn_config.p_threshold.nms = 0.7; - mtmn_config.p_threshold.candidate_number = 20; - mtmn_config.r_threshold.score = 0.7; - mtmn_config.r_threshold.nms = 0.7; - mtmn_config.r_threshold.candidate_number = 10; - mtmn_config.o_threshold.score = 0.7; - mtmn_config.o_threshold.nms = 0.7; - mtmn_config.o_threshold.candidate_number = 1; - - // Face ID list (settings + pointer to the data allocation) - face_id_init(&id_list, FACE_ID_SAVE_NUMBER, ENROLL_CONFIRM_TIMES); - // The size of the allocated data block; calculated in dl_lib_calloc() - - // Request Handlers; config.max_uri_handlers (above) must be >= the number of handlers config.server_port = hPort; config.ctrl_port = hPort; - Serial.printf("Starting web server on port: '%d'\n", config.server_port); + Serial.printf("Starting web server on port: '%d'\r\n", config.server_port); if (httpd_start(&camera_httpd, &config) == ESP_OK) { if (critERR.length() > 0) { httpd_register_uri_handler(camera_httpd, &error_uri); @@ -1081,11 +863,12 @@ void startCameraServer(int hPort, int sPort){ httpd_register_uri_handler(camera_httpd, &favicon_ico_uri); httpd_register_uri_handler(camera_httpd, &logo_svg_uri); httpd_register_uri_handler(camera_httpd, &dump_uri); + httpd_register_uri_handler(camera_httpd, &stop_uri); } config.server_port = sPort; config.ctrl_port = sPort; - Serial.printf("Starting stream server on port: '%d'\n", config.server_port); + Serial.printf("Starting stream server on port: '%d'\r\n", config.server_port); if (httpd_start(&stream_httpd, &config) == ESP_OK) { if (critERR.length() > 0) { httpd_register_uri_handler(camera_httpd, &error_uri); diff --git a/camera_pins.h b/camera_pins.h index 3853863..ce4b93d 100644 --- a/camera_pins.h +++ b/camera_pins.h @@ -1,13 +1,39 @@ -/* +/* * Pin definitions for some common ESP-CAM modules - * + * * Select the module to use in myconfig.h * Defaults to AI-THINKER CAM module - * + * */ -#if defined(CAMERA_MODEL_WROVER_KIT) +#if defined(CAMERA_MODEL_AI_THINKER) // - // ESP WROVER + // AI Thinker + // https://github.com/SeeedDocument/forum_doc/raw/master/reg/ESP32_CAM_V1.6.pdf + // + #define PWDN_GPIO_NUM 32 + #define RESET_GPIO_NUM -1 + #define XCLK_GPIO_NUM 0 + #define SIOD_GPIO_NUM 26 + #define SIOC_GPIO_NUM 27 + #define Y9_GPIO_NUM 35 + #define Y8_GPIO_NUM 34 + #define Y7_GPIO_NUM 39 + #define Y6_GPIO_NUM 36 + #define Y5_GPIO_NUM 21 + #define Y4_GPIO_NUM 19 + #define Y3_GPIO_NUM 18 + #define Y2_GPIO_NUM 5 + #define VSYNC_GPIO_NUM 25 + #define HREF_GPIO_NUM 23 + #define PCLK_GPIO_NUM 22 + #define LED_PIN 33 // Status led + #define LED_ON LOW // - Pin is inverted. + #define LED_OFF HIGH // + #define LAMP_PIN 4 // LED FloodLamp. + +#elif defined(CAMERA_MODEL_WROVER_KIT) + // + // ESP WROVER // https://dl.espressif.com/dl/schematics/ESP-WROVER-KIT_SCH-2.pdf // #define PWDN_GPIO_NUM -1 @@ -161,35 +187,9 @@ // #define LED_OFF LOW // // #define LAMP_PIN x // LED FloodLamp. -#elif defined(CAMERA_MODEL_AI_THINKER) - // - // AI Thinker - // https://github.com/SeeedDocument/forum_doc/raw/master/reg/ESP32_CAM_V1.6.pdf - // - #define PWDN_GPIO_NUM 32 - #define RESET_GPIO_NUM -1 - #define XCLK_GPIO_NUM 0 - #define SIOD_GPIO_NUM 26 - #define SIOC_GPIO_NUM 27 - #define Y9_GPIO_NUM 35 - #define Y8_GPIO_NUM 34 - #define Y7_GPIO_NUM 39 - #define Y6_GPIO_NUM 36 - #define Y5_GPIO_NUM 21 - #define Y4_GPIO_NUM 19 - #define Y3_GPIO_NUM 18 - #define Y2_GPIO_NUM 5 - #define VSYNC_GPIO_NUM 25 - #define HREF_GPIO_NUM 23 - #define PCLK_GPIO_NUM 22 - #define LED_PIN 33 // Status led - #define LED_ON LOW // - Pin is inverted. - #define LED_OFF HIGH // - #define LAMP_PIN 4 // LED FloodLamp. - #elif defined(CAMERA_MODEL_TTGO_T_JOURNAL) // - // LilyGO TTGO T-Journal ESP32; with OLED! but not used here.. :-( + // LilyGO TTGO T-Journal ESP32; with OLED! but not used here.. :-( #define PWDN_GPIO_NUM 0 #define RESET_GPIO_NUM 15 #define XCLK_GPIO_NUM 27 @@ -212,6 +212,31 @@ // #define LED_OFF HIGH // // #define LAMP_PIN 4 // LED FloodLamp. +#elif defined(CAMERA_MODEL_ARDUCAM_ESP32S_UNO) + // Pins from user @rdragonrydr + // https://github.com/ArduCAM/ArduCAM_ESP32S_UNO/ + // Based on AI-THINKER definitions + #define PWDN_GPIO_NUM 32 + #define RESET_GPIO_NUM -1 + #define XCLK_GPIO_NUM 0 + #define SIOD_GPIO_NUM 26 + #define SIOC_GPIO_NUM 27 + #define Y9_GPIO_NUM 35 + #define Y8_GPIO_NUM 34 + #define Y7_GPIO_NUM 39 + #define Y6_GPIO_NUM 36 + #define Y5_GPIO_NUM 21 + #define Y4_GPIO_NUM 19 + #define Y3_GPIO_NUM 18 + #define Y2_GPIO_NUM 5 + #define VSYNC_GPIO_NUM 25 + #define HREF_GPIO_NUM 23 + #define PCLK_GPIO_NUM 22 + #define LED_PIN 2 // Status led + #define LED_ON HIGH // - Pin is not inverted. + #define LED_OFF LOW // + //#define LAMP_PIN x // No LED FloodLamp. + #else // Well. // that went badly... diff --git a/css.h b/css.h index d519469..66a0deb 100644 --- a/css.h +++ b/css.h @@ -2,7 +2,7 @@ * Master CSS file for the camera pages */ -const uint8_t style_css[] = R"=====(/* +const uint8_t style_css[] = R"=====(/* * CSS for the esp32 cam webserver */ @@ -33,6 +33,7 @@ section.main { #menu { display: none; flex-wrap: nowrap; + color: #EFEFEF; width: 380px; background: #363636; padding: 8px; @@ -87,6 +88,10 @@ section#buttons { display: flex } +#quality { + transform: rotateY(180deg); +} + .input-group { display: flex; flex-wrap: nowrap; diff --git a/esp32-cam-webserver.ino b/esp32-cam-webserver.ino index 8b1fd7a..64d6483 100644 --- a/esp32-cam-webserver.ino +++ b/esp32-cam-webserver.ino @@ -3,7 +3,11 @@ #include #include #include +#include +#include #include "src/parsebytes.h" +#include "time.h" +#include /* This sketch is a extension/expansion/reork of the 'official' ESP32 Camera example @@ -14,7 +18,7 @@ * greater feedback via a status LED, and the HTML contents are present in plain text * for easy modification. * - * A camera name can now be configured, and wifi details can be stored in an optional + * A camera name can now be configured, and wifi details can be stored in an optional * header file to allow easier updated of the repo. * * The web UI has had changes to add the lamp control, rotation, a standalone viewer, @@ -24,7 +28,7 @@ */ -/* +/* * FOR NETWORK AND HARDWARE SETTINGS COPY OR RENAME 'myconfig.sample.h' TO 'myconfig.h' AND EDIT THAT. * * By default this sketch will assume an AI-THINKER ESP-CAM and create @@ -40,7 +44,7 @@ #warning "Using Defaults: Copy myconfig.sample.h to myconfig.h and edit that to use your own settings" #define WIFI_AP_ENABLE #define CAMERA_MODEL_AI_THINKER - struct station { const char ssid[65]; const char password[65]; const bool dhcp;} + struct station { const char ssid[65]; const char password[65]; const bool dhcp;} stationList[] = {{"ESP32-CAM-CONNECT","InsecurePassword", true}}; #endif @@ -50,8 +54,11 @@ // Pin Mappings #include "camera_pins.h" +// Camera config structure +camera_config_t config; + // Internal filesystem (SPIFFS) -// used for non-volatile camera settings and face DB store +// used for non-volatile camera settings #include "storage.h" // Sketch Info @@ -70,14 +77,21 @@ IPAddress gw; // Declare external function from app_httpd.cpp extern void startCameraServer(int hPort, int sPort); +extern void serialDump(); -// A Name for the Camera. (set in myconfig.h) +// Names for the Camera. (set these in myconfig.h) #if defined(CAM_NAME) char myName[] = CAM_NAME; #else char myName[] = "ESP32 camera server"; #endif +#if defined(MDNS_NAME) + char mdnsName[] = MDNS_NAME; +#else + char mdnsName[] = "esp32-cam"; +#endif + // Ports for http and stream (override in myconfig.h) #if defined(HTTP_PORT) int httpPort = HTTP_PORT; @@ -92,7 +106,7 @@ extern void startCameraServer(int hPort, int sPort); #endif #if !defined(WIFI_WATCHDOG) - #define WIFI_WATCHDOG 5000 + #define WIFI_WATCHDOG 15000 #endif // Number of known networks in stationList[] @@ -100,12 +114,12 @@ int stationCount = sizeof(stationList)/sizeof(stationList[0]); // If we have AP mode enabled, ignore first entry in the stationList[] #if defined(WIFI_AP_ENABLE) - int firstStation = 1; + int firstStation = 1; #else int firstStation = 0; #endif -// Select bvetween full and simple index as the default. +// Select between full and simple index as the default. #if defined(DEFAULT_INDEX_FULL) char default_index[] = "full"; #else @@ -122,12 +136,27 @@ char apName[64] = "Undefined"; char httpURL[64] = {"Undefined"}; char streamURL[64] = {"Undefined"}; -// Count number of active streams -int8_t streamCount = 0; +// Counters for info screens and debug +int8_t streamCount = 0; // Number of currently active streams +unsigned long streamsServed = 0; // Total completed streams +unsigned long imagesServed = 0; // Total image requests // This will be displayed to identify the firmware char myVer[] PROGMEM = __DATE__ " @ " __TIME__; +// This will be set to the sensors PID (identifier) during initialisation +//camera_pid_t sensorPID; +int sensorPID; + +// Camera module bus communications frequency. +// Originally: config.xclk_freq_mhz = 20000000, but this lead to visual artifacts on many modules. +// See https://github.com/espressif/esp32-camera/issues/150#issuecomment-726473652 et al. +#if !defined (XCLK_FREQ_MHZ) + unsigned long xclk = 8; +#else + unsigned long xclk = XCLK_FREQ_MHZ; +#endif + // initial rotation // can be set in myconfig.h #if !defined(CAM_ROTATION) @@ -135,7 +164,13 @@ char myVer[] PROGMEM = __DATE__ " @ " __TIME__; #endif int myRotation = CAM_ROTATION; -// Illumination LAMP/LED +// minimal frame duration in ms, effectively 1/maxFPS +#if !defined(MIN_FRAME_TIME) + #define MIN_FRAME_TIME 0 +#endif +int minFrameTime = MIN_FRAME_TIME; + +// Illumination LAMP and status LED #if defined(LAMP_DISABLE) int lampVal = -1; // lamp is disabled in config #elif defined(LAMP_PIN) @@ -144,9 +179,14 @@ int myRotation = CAM_ROTATION; #else int lampVal = 0; //default to off #endif -#else +#else int lampVal = -1; // no lamp pin assigned #endif + +#if defined(LED_DISABLE) + #undef LED_PIN // undefining this disables the notification LED +#endif + bool autoLamp = false; // Automatic lamp (auto on while camera running) int lampChannel = 7; // a free PWM channel (some channels used by camera) @@ -160,32 +200,64 @@ const int pwmMax = pow(2,pwmresolution)-1; bool filesystem = true; #endif -#if defined(FACE_DETECTION) - int8_t detection_enabled = 1; - #if defined(FACE_RECOGNITION) - int8_t recognition_enabled = 1; - #else - int8_t recognition_enabled = 0; - #endif +#if defined(NO_OTA) + bool otaEnabled = false; +#else + bool otaEnabled = true; +#endif + +#if defined(OTA_PASSWORD) + char otaPassword[] = OTA_PASSWORD; +#else + char otaPassword[] = ""; +#endif + +#if defined(NTPSERVER) + bool haveTime = true; + const char* ntpServer = NTPSERVER; + const long gmtOffset_sec = NTP_GMT_OFFSET; + const int daylightOffset_sec = NTP_DST_OFFSET; #else - int8_t detection_enabled = 0; - int8_t recognition_enabled = 0; + bool haveTime = false; + const char* ntpServer = ""; + const long gmtOffset_sec = 0; + const int daylightOffset_sec = 0; #endif // Critical error string; if set during init (camera hardware failure) it // will be returned for all http requests String critERR = ""; -// Debug Data for stream and capture -#if defined(DEBUG_DEFAULT_ON) - bool debugData = true; -#else - bool debugData = false; -#endif +// Debug flag for stream and capture data +bool debugData; -// Notification LED +void debugOn() { + debugData = true; + Serial.println("Camera debug data is enabled (send 'd' for status dump, or any other char to disable debug)"); +} + +void debugOff() { + debugData = false; + Serial.println("Camera debug data is disabled (send 'd' for status dump, or any other char to enable debug)"); +} + +// Serial input (debugging controls) +void handleSerial() { + if (Serial.available()) { + char cmd = Serial.read(); + if (cmd == 'd' ) { + serialDump(); + } else { + if (debugData) debugOff(); + else debugOn(); + } + } + while (Serial.available()) Serial.read(); // chomp the buffer +} + +// Notification LED void flashLED(int flashtime) { -#ifdef LED_PIN // If we have it; flash it. +#if defined(LED_PIN) // If we have it; flash it. digitalWrite(LED_PIN, LED_ON); // On at full power. delay(flashtime); // delay digitalWrite(LED_PIN, LED_OFF); // turn Off @@ -196,6 +268,7 @@ void flashLED(int flashtime) { // Lamp Control void setLamp(int newVal) { +#if defined(LAMP_PIN) if (newVal != -1) { // Apply a logarithmic function to the scale. int brightness = round((pow(2,(1+(newVal*0.02)))-2)/6*pwmMax); @@ -205,6 +278,169 @@ void setLamp(int newVal) { Serial.print("%, pwm = "); Serial.println(brightness); } +#endif +} + +void printLocalTime(bool extraData=false) { + struct tm timeinfo; + if(!getLocalTime(&timeinfo)){ + Serial.println("Failed to obtain time"); + } else { + Serial.println(&timeinfo, "%H:%M:%S, %A, %B %d %Y"); + } + if (extraData) { + Serial.printf("NTP Server: %s, GMT Offset: %li(s), DST Offset: %i(s)\r\n", ntpServer, gmtOffset_sec, daylightOffset_sec); + } +} + +void calcURLs() { + // Set the URL's + #if defined(URL_HOSTNAME) + if (httpPort != 80) { + sprintf(httpURL, "http://%s:%d/", URL_HOSTNAME, httpPort); + } else { + sprintf(httpURL, "http://%s/", URL_HOSTNAME); + } + sprintf(streamURL, "http://%s:%d/", URL_HOSTNAME, streamPort); + #else + Serial.println("Setting httpURL"); + if (httpPort != 80) { + sprintf(httpURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], httpPort); + } else { + sprintf(httpURL, "http://%d.%d.%d.%d/", ip[0], ip[1], ip[2], ip[3]); + } + sprintf(streamURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], streamPort); + #endif +} + +void StartCamera() { + // Populate camera config structure with hardware and other defaults + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sscb_sda = SIOD_GPIO_NUM; + config.pin_sscb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = xclk * 1000000; + config.pixel_format = PIXFORMAT_JPEG; + // Low(ish) default framesize and quality + config.frame_size = FRAMESIZE_SVGA; + config.jpeg_quality = 12; + config.fb_location = CAMERA_FB_IN_PSRAM; + config.fb_count = 2; + config.grab_mode = CAMERA_GRAB_LATEST; + + #if defined(CAMERA_MODEL_ESP_EYE) + pinMode(13, INPUT_PULLUP); + pinMode(14, INPUT_PULLUP); + #endif + + // camera init + esp_err_t err = esp_camera_init(&config); + if (err != ESP_OK) { + delay(100); // need a delay here or the next serial o/p gets missed + Serial.printf("\r\n\r\nCRITICAL FAILURE: Camera sensor failed to initialise.\r\n\r\n"); + Serial.printf("A full (hard, power off/on) reboot will probably be needed to recover from this.\r\n"); + Serial.printf("Meanwhile; this unit will reboot in 1 minute since these errors sometime clear automatically\r\n"); + // Reset the I2C bus.. may help when rebooting. + periph_module_disable(PERIPH_I2C0_MODULE); // try to shut I2C down properly in case that is the problem + periph_module_disable(PERIPH_I2C1_MODULE); + periph_module_reset(PERIPH_I2C0_MODULE); + periph_module_reset(PERIPH_I2C1_MODULE); + // And set the error text for the UI + critERR = "

Error!


Camera module failed to initialise!

Please reset (power off/on) the camera.

"; + critERR += "

We will continue to reboot once per minute since this error sometimes clears automatically.

"; + // Start a 60 second watchdog timer + esp_task_wdt_init(60,true); + esp_task_wdt_add(NULL); + } else { + Serial.println("Camera init succeeded"); + + // Get a reference to the sensor + sensor_t * s = esp_camera_sensor_get(); + + // Dump camera module, warn for unsupported modules. + sensorPID = s->id.PID; + switch (sensorPID) { + case OV9650_PID: Serial.println("WARNING: OV9650 camera module is not properly supported, will fallback to OV2640 operation"); break; + case OV7725_PID: Serial.println("WARNING: OV7725 camera module is not properly supported, will fallback to OV2640 operation"); break; + case OV2640_PID: Serial.println("OV2640 camera module detected"); break; + case OV3660_PID: Serial.println("OV3660 camera module detected"); break; + default: Serial.println("WARNING: Camera module is unknown and not properly supported, will fallback to OV2640 operation"); + } + + // OV3660 initial sensors are flipped vertically and colors are a bit saturated + if (sensorPID == OV3660_PID) { + s->set_vflip(s, 1); //flip it back + s->set_brightness(s, 1); //up the blightness just a bit + s->set_saturation(s, -2); //lower the saturation + } + + // M5 Stack Wide has special needs + #if defined(CAMERA_MODEL_M5STACK_WIDE) + s->set_vflip(s, 1); + s->set_hmirror(s, 1); + #endif + + // Config can override mirror and flip + #if defined(H_MIRROR) + s->set_hmirror(s, H_MIRROR); + #endif + #if defined(V_FLIP) + s->set_vflip(s, V_FLIP); + #endif + + // set initial frame rate + #if defined(DEFAULT_RESOLUTION) + s->set_framesize(s, DEFAULT_RESOLUTION); + #endif + + /* + * Add any other defaults you want to apply at startup here: + * uncomment the line and set the value as desired (see the comments) + * + * these are defined in the esp headers here: + * https://github.com/espressif/esp32-camera/blob/master/driver/include/sensor.h#L149 + */ + + //s->set_framesize(s, FRAMESIZE_SVGA); // FRAMESIZE_[QQVGA|HQVGA|QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA|QXGA(ov3660)]); + //s->set_quality(s, val); // 10 to 63 + //s->set_brightness(s, 0); // -2 to 2 + //s->set_contrast(s, 0); // -2 to 2 + //s->set_saturation(s, 0); // -2 to 2 + //s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) + //s->set_whitebal(s, 1); // aka 'awb' in the UI; 0 = disable , 1 = enable + //s->set_awb_gain(s, 1); // 0 = disable , 1 = enable + //s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) + //s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable + //s->set_aec2(s, 0); // 0 = disable , 1 = enable + //s->set_ae_level(s, 0); // -2 to 2 + //s->set_aec_value(s, 300); // 0 to 1200 + //s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable + //s->set_agc_gain(s, 0); // 0 to 30 + //s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 + //s->set_bpc(s, 0); // 0 = disable , 1 = enable + //s->set_wpc(s, 1); // 0 = disable , 1 = enable + //s->set_raw_gma(s, 1); // 0 = disable , 1 = enable + //s->set_lenc(s, 1); // 0 = disable , 1 = enable + //s->set_hmirror(s, 0); // 0 = disable , 1 = enable + //s->set_vflip(s, 0); // 0 = disable , 1 = enable + //s->set_dcw(s, 1); // 0 = disable , 1 = enable + //s->set_colorbar(s, 0); // 0 = disable , 1 = enable + } + // We now have camera with default init } void WifiSetup() { @@ -213,6 +449,11 @@ void WifiSetup() { delay(100); flashLED(300); Serial.println("Starting WiFi"); + + // Disable power saving on WiFi to improve responsiveness + // (https://github.com/espressif/arduino-esp32/issues/1484) + WiFi.setSleep(false); + Serial.print("Known external SSIDs: "); if (stationCount > firstStation) { for (int i=firstStation; i < stationCount; i++) Serial.printf(" '%s'", stationList[i].ssid); @@ -222,17 +463,17 @@ void WifiSetup() { Serial.println(); byte mac[6] = {0,0,0,0,0,0}; WiFi.macAddress(mac); - Serial.printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - + Serial.printf("MAC address: %02X:%02X:%02X:%02X:%02X:%02X\r\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + int bestStation = -1; long bestRSSI = -1024; char bestSSID[65] = ""; uint8_t bestBSSID[6]; if (stationCount > firstStation) { - // We have a list to scan - Serial.printf("Scanning local Wifi Networks\n"); + // We have a list to scan + Serial.printf("Scanning local Wifi Networks\r\n"); int stationsFound = WiFi.scanNetworks(); - Serial.printf("%i networks found\n", stationsFound); + Serial.printf("%i networks found\r\n", stationsFound); if (stationsFound > 0) { for (int i = 0; i < stationsFound; ++i) { // Print SSID and RSSI for each network found @@ -242,7 +483,7 @@ void WifiSetup() { Serial.printf("%3i : [%s] %s (%i)", i + 1, thisBSSID.c_str(), thisSSID.c_str(), thisRSSI); // Scan our list of known external stations for (int sta = firstStation; sta < stationCount; sta++) { - if ((strcmp(stationList[sta].ssid, thisSSID.c_str()) == 0) || + if ((strcmp(stationList[sta].ssid, thisSSID.c_str()) == 0) || (strcmp(stationList[sta].ssid, thisBSSID.c_str()) == 0)) { Serial.print(" - Known!"); // Chose the strongest RSSI seen @@ -250,7 +491,7 @@ void WifiSetup() { bestStation = sta; strncpy(bestSSID, thisSSID.c_str(), 64); // Convert char bssid[] to a byte array - parseBytes(thisBSSID.c_str(), ':', bestBSSID, 6, 16); + parseBytes(thisBSSID.c_str(), ':', bestBSSID, 6, 16); bestRSSI = thisRSSI; } } @@ -260,11 +501,11 @@ void WifiSetup() { } } else { // No list to scan, therefore we are an accesspoint - accesspoint = true; + accesspoint = true; } if (bestStation == -1) { - if (!accesspoint) { + if (!accesspoint) { #if defined(WIFI_AP_ENABLE) Serial.println("No known networks found, entering AccessPoint fallback mode"); accesspoint = true; @@ -275,14 +516,14 @@ void WifiSetup() { Serial.println("AccessPoint mode selected in config"); } } else { - Serial.printf("Connecting to Wifi Network %d: [%02X:%02X:%02X:%02X:%02X:%02X] %s \n", - bestStation, bestBSSID[0], bestBSSID[1], bestBSSID[2], bestBSSID[3], + Serial.printf("Connecting to Wifi Network %d: [%02X:%02X:%02X:%02X:%02X:%02X] %s \r\n", + bestStation, bestBSSID[0], bestBSSID[1], bestBSSID[2], bestBSSID[3], bestBSSID[4], bestBSSID[5], bestSSID); // Apply static settings if necesscary if (stationList[bestStation].dhcp == false) { #if defined(ST_IP) Serial.println("Applying static IP settings"); - #if !defined (ST_GATEWAY) || !defined (ST_NETMASK) + #if !defined (ST_GATEWAY) || !defined (ST_NETMASK) #error "You must supply both Gateway and NetMask when specifying a static IP address" #endif IPAddress staticIP(ST_IP); @@ -304,15 +545,13 @@ void WifiSetup() { #endif } - #if defined(HOSTNAME) - WiFi.setHostname(HOSTNAME); - #endif + WiFi.setHostname(mdnsName); // Initiate network connection request (3rd argument, channel = 0 is 'auto') WiFi.begin(bestSSID, stationList[bestStation].password, 0, bestBSSID); // Wait to connect, or timeout - unsigned long start = millis(); + unsigned long start = millis(); while ((millis() - start <= WIFI_WATCHDOG) && (WiFi.status() != WL_CONNECTED)) { delay(500); Serial.print('.'); @@ -325,9 +564,10 @@ void WifiSetup() { ip = WiFi.localIP(); net = WiFi.subnetMask(); gw = WiFi.gatewayIP(); - Serial.printf("IP address: %d.%d.%d.%d\n",ip[0],ip[1],ip[2],ip[3]); - Serial.printf("Netmask : %d.%d.%d.%d\n",net[0],net[1],net[2],net[3]); - Serial.printf("Gateway : %d.%d.%d.%d\n",gw[0],gw[1],gw[2],gw[3]); + Serial.printf("IP address: %d.%d.%d.%d\r\n",ip[0],ip[1],ip[2],ip[3]); + Serial.printf("Netmask : %d.%d.%d.%d\r\n",net[0],net[1],net[2],net[3]); + Serial.printf("Gateway : %d.%d.%d.%d\r\n",gw[0],gw[1],gw[2],gw[3]); + calcURLs(); // Flash the LED to show we are connected for (int i = 0; i < 5; i++) { flashLED(50); @@ -372,7 +612,8 @@ void WifiSetup() { net = WiFi.subnetMask(); gw = WiFi.gatewayIP(); strcpy(apName, stationList[0].ssid); - Serial.printf("IP address: %d.%d.%d.%d\n",ip[0],ip[1],ip[2],ip[3]); + Serial.printf("IP address: %d.%d.%d.%d\r\n",ip[0],ip[1],ip[2],ip[3]); + calcURLs(); // Flash the LED to show we are connected for (int i = 0; i < 5; i++) { flashLED(150); @@ -388,8 +629,6 @@ void WifiSetup() { } void setup() { - // This might reduce boot loops caused by camera init failures when soft rebooting - // See, for instance, https://esp32.com/viewtopic.php?t=3152 Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); @@ -400,11 +639,23 @@ void setup() { Serial.println(myVer); Serial.print("Base Release: "); Serial.println(baseVersion); + Serial.println(); + + // Warn if no PSRAM is detected (typically user error with board selection in the IDE) + if(!psramFound()){ + Serial.println("\r\nFatal Error; Halting"); + while (true) { + Serial.println("No PSRAM found; camera cannot be initialised: Please check the board config for your module."); + delay(5000); + } + } if (stationCount == 0) { - Serial.println("\nFatal Error; Halting"); - Serial.println("No wifi ssid details have been configured; we cannot connect to WiFi or start our own AccessPoint"); - while (true) delay(1000); + Serial.println("\r\nFatal Error; Halting"); + while (true) { + Serial.println("No wifi details have been configured; we cannot connect to existing WiFi or start our own AccessPoint, there is no point in proceeding."); + delay(5000); + } } #if defined(LED_PIN) // If we have a notification LED, set it to output @@ -412,224 +663,164 @@ void setup() { digitalWrite(LED_PIN, LED_ON); #endif - // Create camera config structure; and populate with hardware and other defaults - camera_config_t config; - config.ledc_channel = LEDC_CHANNEL_0; - config.ledc_timer = LEDC_TIMER_0; - config.pin_d0 = Y2_GPIO_NUM; - config.pin_d1 = Y3_GPIO_NUM; - config.pin_d2 = Y4_GPIO_NUM; - config.pin_d3 = Y5_GPIO_NUM; - config.pin_d4 = Y6_GPIO_NUM; - config.pin_d5 = Y7_GPIO_NUM; - config.pin_d6 = Y8_GPIO_NUM; - config.pin_d7 = Y9_GPIO_NUM; - config.pin_xclk = XCLK_GPIO_NUM; - config.pin_pclk = PCLK_GPIO_NUM; - config.pin_vsync = VSYNC_GPIO_NUM; - config.pin_href = HREF_GPIO_NUM; - config.pin_sscb_sda = SIOD_GPIO_NUM; - config.pin_sscb_scl = SIOC_GPIO_NUM; - config.pin_pwdn = PWDN_GPIO_NUM; - config.pin_reset = RESET_GPIO_NUM; - config.xclk_freq_hz = 20000000; - config.pixel_format = PIXFORMAT_JPEG; - //init with highest supported specs to pre-allocate large buffers - if(psramFound()){ - config.frame_size = FRAMESIZE_UXGA; - config.jpeg_quality = 10; - config.fb_count = 2; - } else { - config.frame_size = FRAMESIZE_SVGA; - config.jpeg_quality = 12; - config.fb_count = 1; + // Start the SPIFFS filesystem before we initialise the camera + if (filesystem) { + filesystemStart(); + delay(200); // a short delay to let spi bus settle after SPIFFS init } - #if defined(CAMERA_MODEL_ESP_EYE) - pinMode(13, INPUT_PULLUP); - pinMode(14, INPUT_PULLUP); - #endif + // Start (init) the camera + StartCamera(); - // camera init - esp_err_t err = esp_camera_init(&config); - if (err != ESP_OK) { - delay(100); // need a delay here or the next serial o/p gets missed - Serial.printf("\n\nCRITICAL FAILURE: Camera sensor failed to initialise.\n\n"); - Serial.printf("A full (hard, power off/on) reboot will probably be needed to recover from this.\n"); - Serial.printf("Meanwhile; this unit will reboot in 1 minute since these errors sometime clear automatically\n"); - // Reset the I2C bus.. may help when rebooting. - periph_module_disable(PERIPH_I2C0_MODULE); // try to shut I2C down properly in case that is the problem - periph_module_disable(PERIPH_I2C1_MODULE); - periph_module_reset(PERIPH_I2C0_MODULE); - periph_module_reset(PERIPH_I2C1_MODULE); - // And set the error text for the UI - critERR = "

Error!


Camera module failed to initialise!

Please reset (power off/on) the camera.

"; - critERR += "

We will continue to reboot once per minute since this error sometimes clears automatically.

"; - // Start a 60 second watchdog timer - esp_task_wdt_init(60,true); - esp_task_wdt_add(NULL); + // Now load and apply any saved preferences + if (filesystem) { + delay(200); // a short delay to let spi bus settle after camera init + loadPrefs(SPIFFS); } else { - Serial.println("Camera init succeeded"); + Serial.println("No Internal Filesystem, cannot load or save preferences"); + } - // Get a reference to the sensor - sensor_t * s = esp_camera_sensor_get(); + /* + * Camera setup complete; initialise the rest of the hardware. + */ - // Dump camera module, warn for unsupported modules. - switch (s->id.PID) { - case OV9650_PID: Serial.println("WARNING: OV9650 camera module is not properly supported, will fallback to OV2640 operation"); break; - case OV7725_PID: Serial.println("WARNING: OV7725 camera module is not properly supported, will fallback to OV2640 operation"); break; - case OV2640_PID: Serial.println("OV2640 camera module detected"); break; - case OV3660_PID: Serial.println("OV3660 camera module detected"); break; - default: Serial.println("WARNING: Camera module is unknown and not properly supported, will fallback to OV2640 operation"); - } + // Start Wifi and loop until we are connected or have started an AccessPoint + while ((WiFi.status() != WL_CONNECTED) && !accesspoint) { + WifiSetup(); + delay(1000); + } - // OV3660 initial sensors are flipped vertically and colors are a bit saturated - if (s->id.PID == OV3660_PID) { - s->set_vflip(s, 1); //flip it back - s->set_brightness(s, 1); //up the blightness just a bit - s->set_saturation(s, -2); //lower the saturation + // Set up OTA + if (otaEnabled) { + // Start OTA once connected + Serial.println("Setting up OTA"); + // Port defaults to 3232 + // ArduinoOTA.setPort(3232); + // Hostname defaults to esp3232-[MAC] + ArduinoOTA.setHostname(mdnsName); + // No authentication by default + if (strlen(otaPassword) != 0) { + ArduinoOTA.setPassword(otaPassword); + Serial.printf("OTA Password: %s\n\r", otaPassword); + } else { + Serial.printf("\r\nNo OTA password has been set! (insecure)\r\n\r\n"); } + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + type = "filesystem"; + Serial.println("Start updating " + type); + // Stop the camera since OTA will crash the module if it is running. + // the unit will need rebooting to restart it, either by OTA on success, or manually by the user + Serial.println("Stopping Camera"); + esp_err_t err = esp_camera_deinit(); + critERR = "

OTA Has been started


Camera has Halted!

"; + critERR += "

Wait for OTA to finish and reboot, or reboot manually to recover

"; + }) + .onEnd([]() { + Serial.println("\r\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed"); + else if (error == OTA_END_ERROR) Serial.println("End Failed"); + }); + ArduinoOTA.begin(); + } else { + Serial.println("OTA is disabled"); - // M5 Stack Wide has special needs - #if defined(CAMERA_MODEL_M5STACK_WIDE) - s->set_vflip(s, 1); - s->set_hmirror(s, 1); - #endif - - // Config can override mirror and flip - #if defined(H_MIRROR) - s->set_hmirror(s, H_MIRROR); - #endif - #if defined(V_FLIP) - s->set_vflip(s, V_FLIP); - #endif - - // set initial frame rate - #if defined(DEFAULT_RESOLUTION) - s->set_framesize(s, DEFAULT_RESOLUTION); - #else - s->set_framesize(s, FRAMESIZE_SVGA); - #endif - - /* - * Add any other defaults you want to apply at startup here: - * uncomment the line and set the value as desired (see the comments) - * - * these are defined in the esp headers here: - * https://github.com/espressif/esp32-camera/blob/master/driver/include/sensor.h#L149 - */ - - //s->set_framesize(s, FRAMESIZE_SVGA); // FRAMESIZE_[QQVGA|HQVGA|QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA|QXGA(ov3660)]); - //s->set_quality(s, val); // 10 to 63 - //s->set_brightness(s, 0); // -2 to 2 - //s->set_contrast(s, 0); // -2 to 2 - //s->set_saturation(s, 0); // -2 to 2 - //s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) - //s->set_whitebal(s, 1); // aka 'awb' in the UI; 0 = disable , 1 = enable - //s->set_awb_gain(s, 1); // 0 = disable , 1 = enable - //s->set_wb_mode(s, 0); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) - //s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable - //s->set_aec2(s, 0); // 0 = disable , 1 = enable - //s->set_ae_level(s, 0); // -2 to 2 - //s->set_aec_value(s, 300); // 0 to 1200 - //s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable - //s->set_agc_gain(s, 0); // 0 to 30 - //s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 - //s->set_bpc(s, 0); // 0 = disable , 1 = enable - //s->set_wpc(s, 1); // 0 = disable , 1 = enable - //s->set_raw_gma(s, 1); // 0 = disable , 1 = enable - //s->set_lenc(s, 1); // 0 = disable , 1 = enable - //s->set_hmirror(s, 0); // 0 = disable , 1 = enable - //s->set_vflip(s, 0); // 0 = disable , 1 = enable - //s->set_dcw(s, 1); // 0 = disable , 1 = enable - //s->set_colorbar(s, 0); // 0 = disable , 1 = enable + if (!MDNS.begin(mdnsName)) { + Serial.println("Error setting up MDNS responder!"); + } + Serial.println("mDNS responder started"); + } - // We now have camera with default init - // check for saved preferences and apply them + //MDNS Config -- note that if OTA is NOT enabled this needs prior steps! + MDNS.addService("http", "tcp", 80); + Serial.println("Added HTTP service to MDNS server"); - if (filesystem) { - filesystemStart(); - loadPrefs(SPIFFS); - loadFaceDB(SPIFFS); - } else { - Serial.println("No Internal Filesystem, cannot save preferences or face DB"); - } + // Set time via NTP server when enabled + if (haveTime) { + Serial.print("Time: "); + configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); + printLocalTime(true); + } else { + Serial.println("Time functions disabled"); } - /* - * Camera setup complete; initialise the rest of the hardware. - */ + // Gather static values used when dumping status; these are slow functions, so just do them once during startup + sketchSize = ESP.getSketchSize(); + sketchSpace = ESP.getFreeSketchSpace(); + sketchMD5 = ESP.getSketchMD5(); // Initialise and set the lamp if (lampVal != -1) { - ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel - if (autoLamp) setLamp(0); // set default value - else setLamp(lampVal); - ledcAttachPin(LAMP_PIN, lampChannel); // attach the GPIO pin to the channel + #if defined(LAMP_PIN) + ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel + ledcAttachPin(LAMP_PIN, lampChannel); // attach the GPIO pin to the channel + if (autoLamp) setLamp(0); // set default value + else setLamp(lampVal); + #endif } else { Serial.println("No lamp, or lamp disabled in config"); } - // Having got this far; start Wifi and loop until we are connected or have started an AccessPoint - while ((WiFi.status() != WL_CONNECTED) && !accesspoint) { - WifiSetup(); - delay(1000); - } - - // Now we have a network we can start the two http handlers for the UI and Stream. + // Start the camera server startCameraServer(httpPort, streamPort); - #if defined(URL_HOSTNAME) - if (httpPort != 80) { - sprintf(httpURL, "http://%s:%d/", URL_HOSTNAME, httpPort); - } else { - sprintf(httpURL, "http://%s/", URL_HOSTNAME); - } - sprintf(streamURL, "http://%s:%d/", URL_HOSTNAME, streamPort); - #else - if (httpPort != 80) { - sprintf(httpURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], httpPort); - } else { - sprintf(httpURL, "http://%d.%d.%d.%d/", ip[0], ip[1], ip[2], ip[3]); - } - sprintf(streamURL, "http://%d.%d.%d.%d:%d/", ip[0], ip[1], ip[2], ip[3], streamPort); - #endif if (critERR.length() == 0) { - Serial.printf("\nCamera Ready!\nUse '%s' to connect\n", httpURL); - Serial.printf("Stream viewer available at '%sview'\n", streamURL); - Serial.printf("Raw stream URL is '%s'\n", streamURL); - if (debugData) Serial.println("Camera debug data is enabled (send any char to disable)"); - else Serial.println("Camera debug data is disabled (send any char to enable)"); + Serial.printf("\r\nCamera Ready!\r\nUse '%s' to connect\r\n", httpURL); + Serial.printf("Stream viewer available at '%sview'\r\n", streamURL); + Serial.printf("Raw stream URL is '%s'\r\n", streamURL); + #if defined(DEBUG_DEFAULT_ON) + debugOn(); + #else + debugOff(); + #endif } else { - Serial.printf("\nCamera unavailable due to initialisation errors.\n\n"); + Serial.printf("\r\nCamera unavailable due to initialisation errors.\r\n\r\n"); } - // Used when dumping status; these are slow functions, so just do them once during startup - sketchSize = ESP.getSketchSize(); - sketchSpace = ESP.getFreeSketchSpace(); - sketchMD5 = ESP.getSketchMD5(); + // Info line; use for Info messages; eg 'This is a Beta!' warnings, etc. as necesscary + // Serial.print("\r\nThis is the 4.1 beta\r\n"); + + // As a final init step chomp out the serial buffer in case we have recieved mis-keys or garbage during startup + while (Serial.available()) Serial.read(); } void loop() { - /* + /* * Just loop forever, reconnecting Wifi As necesscary in client mode * The stream and URI handler processes initiated by the startCameraServer() call at the * end of setup() will handle the camera and UI processing from now on. */ if (accesspoint) { // Accespoint is permanently up, so just loop, servicing the captive portal as needed + // Rather than loop forever, follow the watchdog, in case we later add auto re-scan. unsigned long start = millis(); while (millis() - start < WIFI_WATCHDOG ) { delay(100); + if (otaEnabled) ArduinoOTA.handle(); + handleSerial(); if (captivePortal) dnsServer.processNextRequest(); } } else { - // client mode can fail; so reconnect as appropriate + // client mode can fail; so reconnect as appropriate static bool warned = false; if (WiFi.status() == WL_CONNECTED) { // We are connected, wait a bit and re-check if (warned) { - // Tell the user if we have just reconnected + // Tell the user if we have just reconnected Serial.println("WiFi reconnected"); warned = false; } @@ -637,17 +828,8 @@ void loop() { unsigned long start = millis(); while (millis() - start < WIFI_WATCHDOG ) { delay(100); - if (Serial.available()) { - // Toggle debug output on serial input - if (debugData) { - debugData = false; - Serial.println("Camera debug data is disabled (send any char to enable)"); - } else { - debugData = true; - Serial.println("Camera debug data is enabled (send any char to disable)"); - } - } - while (Serial.available()) Serial.read(); // chomp the buffer + if (otaEnabled) ArduinoOTA.handle(); + handleSerial(); } } else { // disconnected; attempt to reconnect diff --git a/index_other.h b/index_other.h index aff496b..1eda91e 100644 --- a/index_other.h +++ b/index_other.h @@ -12,7 +12,14 @@ const uint8_t index_simple_html[] = R"=====( @@ -29,24 +36,27 @@ const uint8_t index_simple_html[] = R"=====(