├── .github └── workflows │ ├── build-esp32.yml │ ├── build-esp8266.yml │ ├── clean_workflow.yml │ └── versioning.yml ├── .gitignore ├── LICENSE ├── README.md ├── built-in-webpages ├── edit │ ├── beautified.htm │ ├── edit.htm │ ├── edit.min.htm │ ├── edit.min.htm.c │ └── edit.min.htm.gz ├── readme.md └── setup │ ├── app.js │ ├── build_setup │ ├── min │ │ ├── all.htm │ │ ├── all.htm.gz │ │ ├── app.js │ │ └── style.css │ ├── minify.js │ ├── package-lock.json │ ├── package.json │ ├── readme.md │ ├── setup_htm.h │ └── stringConverter.js │ ├── setup.htm │ └── style.css ├── examples ├── binaryWebSocket │ └── binaryWebSocket.ino ├── csvLogger │ ├── csvLogger.ino │ ├── data │ │ ├── assets │ │ │ ├── css │ │ │ │ ├── index.css │ │ │ │ └── style.css │ │ │ └── js │ │ │ │ ├── csv.js │ │ │ │ └── index.js │ │ ├── csv │ │ │ └── 2024_01_10.csv │ │ └── index.htm │ └── readme.md ├── csvLoggerSD │ ├── csvLoggerSD.ino │ ├── data │ │ ├── assets │ │ │ ├── css │ │ │ │ ├── index.css │ │ │ │ └── style.css │ │ │ └── js │ │ │ │ ├── csv.js │ │ │ │ └── index.js │ │ └── index.htm │ └── readme.md ├── customHTML │ ├── .vscode │ │ ├── arduino.json │ │ ├── c_cpp_properties.json │ │ └── settings.json │ ├── customElements.h │ ├── customHTML.ino │ └── thingsboard.h ├── customOptions │ ├── .vscode │ │ ├── arduino.json │ │ ├── c_cpp_properties.json │ │ └── settings.json │ └── customOptions.ino ├── esp32-cam │ ├── camera_pins.h │ ├── data │ │ ├── index.htm │ │ └── www │ │ │ ├── app.js │ │ │ ├── espressif.jpg │ │ │ ├── index.htm │ │ │ └── styles.css │ └── esp32-cam.ino ├── gpio_list │ ├── data │ │ ├── index.htm │ │ ├── pico.classless.min.css.gz │ │ └── script.js │ ├── gpio_list.ino │ └── readme.md ├── handleFormData │ ├── data │ │ ├── index.htm │ │ ├── myScript.js │ │ └── myStyle.css │ └── handleFormData.ino ├── highcharts │ ├── data │ │ ├── index.htm │ │ ├── script.js │ │ └── style.css │ ├── highcharts.ino │ └── readme.md ├── localRFID │ ├── JsonDB.hpp │ ├── data │ │ ├── html_login.h │ │ ├── html_rfid.h │ │ ├── login │ │ └── rfid │ ├── localRFID.ino │ ├── readme.md │ └── webserver.hpp ├── mysqlRFID │ ├── html_flash_files.h │ ├── mysqlRFID.ino │ ├── mysql_impl.h │ └── webserver_impl.h ├── remoteOTA │ ├── data │ │ ├── espressif.jpg │ │ └── index.htm │ ├── fw-esp32 │ │ ├── firmware.bin │ │ └── readme.md │ ├── fw-esp8266 │ │ ├── firmware.bin │ │ └── readme.md │ ├── remoteOTA.ino │ ├── version-esp32.json │ └── version-esp8266.json ├── simpleServer │ ├── data │ │ ├── css │ │ │ └── pico.fluid.classless.css.gz │ │ ├── favicon.ico │ │ ├── img │ │ │ └── espressif.jpg │ │ └── index.htm │ └── simpleServer.ino ├── simpleServerCaptive │ ├── .vscode │ │ ├── arduino.json │ │ └── c_cpp_properties.json │ ├── data │ │ ├── css │ │ │ └── pico.fluid.classless.css.gz │ │ ├── favicon.ico │ │ ├── img │ │ │ └── espressif.jpg │ │ └── index.htm │ └── simpleServerCaptive.ino └── withWebSocket │ ├── index_htm.h │ ├── readme.md │ └── withWebSocket.ino ├── keywords.txt ├── library.properties ├── notepad++.session ├── partitions-4MB.csv ├── pio_ci_ex_ALL.bat ├── platformio.ini └── src ├── AsyncFsWebServer.cpp ├── AsyncFsWebServer.h ├── CaptivePortal.hpp ├── SerialLog.h ├── SetupConfig.hpp ├── Version.cpp ├── edit_htm.h └── setup_htm.h /.github/workflows/build-esp32.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Build (ESP32) 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - master 10 | - release/* 11 | pull_request: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | arduino-esp32: 19 | name: ESP32 (arduino-cli) - Release 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Install arduino-cli 23 | run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh 24 | 25 | - name: Update core index 26 | run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json 27 | 28 | - name: Install core 29 | run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json esp32:esp32 30 | 31 | - name: Install ArduinoJson 32 | run: arduino-cli lib install ArduinoJson 33 | 34 | - name: Install AsyncTCP (ESP32) 35 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.3.7 36 | 37 | - name: Install ESPAsyncWebServer 38 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncWebServer#v3.7.3 39 | 40 | - name: Install Arduino-MySQL 41 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/cotestatnt/Arduino-MySQL 42 | 43 | - name: Install Arduino_MFRC522v2 44 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/OSSLibraries/Arduino_MFRC522v2 45 | 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | 49 | - name: Build Examples 50 | run: | 51 | for i in `ls examples`; do 52 | echo "=============================================================" 53 | echo "Building examples/$i..." 54 | echo "=============================================================" 55 | arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino" 56 | done 57 | 58 | platformio-esp32-arduino2: 59 | name: ESP32 (pio) - Arduino 2 60 | runs-on: ubuntu-latest 61 | strategy: 62 | fail-fast: false 63 | matrix: 64 | board: 65 | - esp32dev 66 | - esp32-s3-devkitc-1 67 | 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v4 71 | 72 | - name: Cache PlatformIO 73 | uses: actions/cache@v4 74 | with: 75 | key: ${{ runner.os }}-pio 76 | path: | 77 | ~/.cache/pip 78 | ~/.platformio 79 | 80 | - name: Python 81 | uses: actions/setup-python@v5 82 | with: 83 | python-version: "3.x" 84 | 85 | - name: Install PIO 86 | run: | 87 | python -m pip install --upgrade pip 88 | pip install --upgrade platformio 89 | 90 | - name: Build Examples 91 | run: | 92 | for i in `ls examples`; do 93 | echo "=============================================================" 94 | echo "Building examples/$i..." 95 | echo "=============================================================" 96 | PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-2 97 | done 98 | 99 | platformio-esp32-arduino-3: 100 | name: ESP32 (pio) - Arduino 3 101 | runs-on: ubuntu-latest 102 | strategy: 103 | fail-fast: false 104 | matrix: 105 | board: 106 | - esp32dev 107 | - esp32-s3-devkitc-1 108 | 109 | steps: 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | 113 | - name: Cache PlatformIO 114 | uses: actions/cache@v4 115 | with: 116 | key: ${{ runner.os }}-pio 117 | path: | 118 | ~/.cache/pip 119 | ~/.platformio 120 | 121 | - name: Python 122 | uses: actions/setup-python@v5 123 | with: 124 | python-version: "3.x" 125 | 126 | - name: Install PIO 127 | run: | 128 | python -m pip install --upgrade pip 129 | pip install --upgrade platformio 130 | 131 | - name: Build Examples 132 | run: | 133 | for i in `ls examples`; do 134 | echo "=============================================================" 135 | echo "Building examples/$i..." 136 | echo "=============================================================" 137 | PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3 138 | done 139 | 140 | platformio-esp32-arduino-latest: 141 | name: ESP32 (pio) - Arduino Latest 142 | runs-on: ubuntu-latest 143 | strategy: 144 | fail-fast: false 145 | matrix: 146 | board: 147 | - esp32dev 148 | - esp32-s3-devkitc-1 149 | 150 | steps: 151 | - name: Checkout 152 | uses: actions/checkout@v4 153 | 154 | - name: Cache PlatformIO 155 | uses: actions/cache@v4 156 | with: 157 | key: ${{ runner.os }}-pio 158 | path: | 159 | ~/.cache/pip 160 | ~/.platformio 161 | 162 | - name: Python 163 | uses: actions/setup-python@v5 164 | with: 165 | python-version: "3.x" 166 | 167 | - name: Install PIO 168 | run: | 169 | python -m pip install --upgrade pip 170 | pip install --upgrade platformio 171 | 172 | - name: Build Examples 173 | run: | 174 | for i in `ls examples`; do 175 | echo "=============================================================" 176 | echo "Building examples/$i..." 177 | echo "=============================================================" 178 | PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3-latest 179 | done 180 | -------------------------------------------------------------------------------- /.github/workflows/build-esp8266.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Build (ESP8266) 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - master 10 | - release/* 11 | pull_request: 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | arduino-esp8266: 19 | name: ESP8266 (arduino-cli) 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Install arduino-cli 23 | run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh 24 | 25 | - name: Update core index 26 | run: arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json 27 | 28 | - name: Install core 29 | run: arduino-cli core install --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266 30 | 31 | - name: Install ArduinoJson 32 | run: arduino-cli lib install ArduinoJson 33 | 34 | - name: Install ESPAsyncTCP (ESP8266) 35 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncTCP#v2.0.0 36 | 37 | - name: Install ESPAsyncWebServer 38 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncWebServer#v3.7.3 39 | 40 | - name: Install Arduino-MySQL 41 | run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/cotestatnt/Arduino-MySQL 42 | 43 | - name: Checkout 44 | uses: actions/checkout@v4 45 | 46 | - name: Build Examples 47 | run: | 48 | EXCLUDE_EXAMPLES="esp32-cam binaryWebSocket csvLoggerSD localRFID mysqlRFID" 49 | 50 | for i in `ls examples`; do 51 | # skip excluded examples 52 | if [[ $EXCLUDE_EXAMPLES == *"$i"* ]]; then 53 | echo "Skipping: $i" 54 | continue 55 | fi 56 | 57 | echo "=============================================================" 58 | echo "Building examples/$i..." 59 | echo "=============================================================" 60 | arduino-cli compile --library . --warnings none -b esp8266:esp8266:huzzah "examples/$i/$i.ino" 61 | done 62 | 63 | platformio-esp8266: 64 | name: ESP8266 (pio) 65 | runs-on: ubuntu-latest 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | board: 70 | - huzzah 71 | - d1_mini 72 | 73 | steps: 74 | - name: Checkout 75 | uses: actions/checkout@v4 76 | 77 | - name: Cache PlatformIO 78 | uses: actions/cache@v4 79 | with: 80 | key: ${{ runner.os }}-pio 81 | path: | 82 | ~/.cache/pip 83 | ~/.platformio 84 | 85 | - name: Python 86 | uses: actions/setup-python@v5 87 | with: 88 | python-version: "3.x" 89 | 90 | - name: Install PIO 91 | run: | 92 | python -m pip install --upgrade pip 93 | pip install --upgrade platformio 94 | 95 | - name: Build Examples 96 | run: | 97 | EXCLUDE_EXAMPLES="esp32-cam binaryWebSocket csvLoggerSD localRFID mysqlRFID" 98 | 99 | for i in `ls examples`; do 100 | # skip excluded examples 101 | if [[ $EXCLUDE_EXAMPLES == *"$i"* ]]; then 102 | echo "Skipping: $i" 103 | continue 104 | fi 105 | 106 | echo "=============================================================" 107 | echo "Building examples/$i..." 108 | echo "=============================================================" 109 | PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-esp8266 110 | done 111 | -------------------------------------------------------------------------------- /.github/workflows/clean_workflow.yml: -------------------------------------------------------------------------------- 1 | name: Clean Workflow Logs 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | keep_minimum_runs: 6 | description: "Numero di workflow recenti da mantenere" 7 | default: "5" 8 | required: false 9 | jobs: 10 | clean-logs: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: write 14 | steps: 15 | - name: Delete workflow runs 16 | uses: Mattraks/delete-workflow-runs@v2 17 | with: 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | repository: ${{ github.repository }} 20 | retain_days: 0 # Imposta a 0 per ignorare la data e considerare solo keep_minimum_runs 21 | keep_minimum_runs: ${{ github.event.inputs.keep_minimum_runs }} 22 | -------------------------------------------------------------------------------- /.github/workflows/versioning.yml: -------------------------------------------------------------------------------- 1 | name: Set Lib Version 2 | on: 3 | push: 4 | branches: [ master ] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # Give the default GITHUB_TOKEN write permission to commit and push the 12 | # added or changed files to the repository. 13 | contents: write 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | persist-credentials: false # otherwise, the token used is the GITHUB_TOKEN, instead of your personal token 18 | fetch-depth: 0 # otherwise, you will failed to push refs to dest repo 19 | 20 | - name: Read value from Properties-file 21 | id: read_property 22 | uses: christian-draeger/read-properties@1.1.1 23 | with: 24 | path: 'library.properties' 25 | properties: 'version' 26 | 27 | - name: Overwrite version.h 28 | uses: "DamianReeves/write-file-action@master" 29 | with: 30 | path: src/Version.cpp 31 | write-mode: overwrite 32 | contents: | 33 | #include "AsyncFsWebServer.h" 34 | const char* AsyncFsWebServer::getVersion() { 35 | return "${{ steps.read_property.outputs.version }}"; 36 | } 37 | - name: Commit & Push 38 | uses: Andro999b/push@v1.3 39 | with: 40 | github_token: ${{ secrets.GITHUB_TOKEN }} 41 | branch: master 42 | force: true 43 | message: 'Overwritten by Github Actions' 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # PlatformIO in VScode 35 | .pio/ 36 | .vscode/ 37 | *.lnk 38 | *.bak 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | If you like this work, please consider [sponsoring this project!](https://paypal.me/cotesta) 2 | 3 | # async-esp-fs-webserver 4 | ESP32/ESP8266 WebServer, WiFi manager and ACE web editor Arduino library. Based on [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) from @ESP32Async 5 | 6 | This is the equivalent to [**esp-fs-webserver**](https://github.com/cotestatnt/esp-fs-webserver/) Arduino library, but working with the very good **ESPAsyncWebServer** library instead default webserver library. 7 | 8 | You need to install the following dependencies. Remove any previously installed clones or old versions, as they may cause conflicts. 9 | 10 | https://github.com/ESP32Async/ESPAsyncWebServer 11 | 12 | #### ESP32 13 | https://github.com/ESP32Async/AsyncTCP 14 | #### ESP8266 15 | https://github.com/ESP32Async/ESPAsyncTCP 16 | 17 | 18 | **Note**: 19 | Starting from version 2.0.0 ESP32 core for Arduino introduced the LittlsFS library like ESP8266. The examples in this library is written to work with this for both platform by default. Change according to your needs if you prefer other filesystems. 20 | 21 | ## WiFi, OTA firmware update and Options manager 22 | Thanks to the built-in page **/setup** (about 8Kb of program space) it is possible to scan and set the WiFi credentials and other freely configurable parameters. 23 | 24 | With **/setup** webpage it is also possible to perform remote firmware update (OTA-update). 25 | 26 | ![image](https://github.com/cotestatnt/async-esp-fs-webserver/assets/27758688/81a1f6db-a4bd-4f1d-b263-7bebe79cae7d) 27 | 28 | 29 | This web page can be injected also with custom HTML and Javascript code in order to create very smart and powerful web application. 30 | 31 | In the image below, for example, the HTML and Javascript code to provision the devices in the well-known [ThingsBoard IoT platform](https://thingsboard.io/) has been added at runtime starting from the Arduino sketch (check example [customHTML.ino](https://github.com/cotestatnt/async-esp-fs-webserver/tree/main/examples/customHTML)). 32 | 33 | ![image](https://github.com/cotestatnt/async-esp-fs-webserver/assets/27758688/d728c315-7271-454d-8c34-fb9db0b7a333) 34 | 35 | ## Web server file upload 36 | 37 | In addition to built-in firmware update functionality, you can also upload your web server content all at once (typically the files are placed inside the folder `data` of your sketch). 38 | 39 | ![image](https://github.com/cotestatnt/async-esp-fs-webserver/assets/27758688/7c261216-3acd-4463-9105-d11e0be3a59a) 40 | 41 | 42 | 43 | ## ACE web file editor/browser 44 | Thanks to the built-in **/edit** page, it is possible to upload, delete and edit the HTML/CSS/JavaScript source files directly from browser and immediately display the changes introduced at runtime without having to recompile the device firmware. 45 | The page can be enabled at runtime using the method `enableFsCodeEditor()` and it occupies about 6.7Kb of program space. 46 | 47 | ![image](https://github.com/cotestatnt/async-esp-fs-webserver/assets/27758688/668c0899-a060-4aed-956b-51311bf3fe13) 48 | 49 | -------------------------------------------------------------------------------- /built-in-webpages/edit/edit.min.htm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cotestatnt/async-esp-fs-webserver/f6303024b51f3086c6364932fa6447190c8ac852/built-in-webpages/edit/edit.min.htm.gz -------------------------------------------------------------------------------- /built-in-webpages/readme.md: -------------------------------------------------------------------------------- 1 | If you want customize **/setup** webpage (headers, logo etc etc): 2 | * edit the source files as your needs (setup.htm, app.js, style.css) 3 | * open a terminal in ***build_setup*** folder and run `npm i` to install all nodejs modules needed 4 | * run `node minify.js` 5 | * overwrite the content of `setup_htm.h` inside `/src` folder with the new generated file 6 | 7 | 8 | # 9 | As alternative on Windows system, is possible to use the tool [SEGGER Bin2C](https://www.segger.com/free-utilities/bin2c/). 10 | 11 | `all.htm.gz` file need to be prepared manually (for example you could use online tools for minify CSS, JavaScript and HTML) 12 | 13 | This Bin2C program will create a file named `all.htm.c`, simply copy & past the content and overwrite `setup_htm.h` file -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/min/all.htm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cotestatnt/async-esp-fs-webserver/f6303024b51f3086c6364932fa6447190c8ac852/built-in-webpages/setup/build_setup/min/all.htm.gz -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/min/style.css: -------------------------------------------------------------------------------- 1 | html{font-family:sans-serif}body{background-color:#2f4f4f;padding:20px}hr{padding:0}details,main,summary{display:block}a{background-color:transparent}a:active,a:hover{outline:0}b{font-weight:700}h1{font-size:36px;margin:10px 0 5px}h2{font-size:24px}input:-webkit-autofill,input:-webkit-autofill:active,input:-webkit-autofill:focus,input:-webkit-autofill:hover{-webkit-box-shadow:inset 0 0 0 30px #fff!important}input[type=checkbox]{box-sizing:border-box;padding:0}input[type=number],input[type=password],input[type=text],select{border:1px solid #ccc;border-radius:6px;font-size:16px;height:40px;padding:0 0 0 20px;width:90%}input[type=file]{display:none}input::placeholder{color:#5e5e5e}input:hover{border-color:rgba(0,0,0,.8);box-shadow:0 1px 4px 0 rgba(220,220,229,.9)}input:focus,select:focus{border-color:#3898ec;outline:0}input[type=number]:hover::-webkit-inner-spin-button{-webkit-appearance:none;-moz-appearance:textfield}footer{font-size:12px;margin:40px;text-align:center}.btn,button{background-color:#2e8bc0;border:0;border-radius:5px;color:#fff;cursor:pointer;display:inline-flex;justify-content:center;min-width:40%;padding:10px 15px}.btn:hover,button:hover{filter:brightness(85%)}ul{display:table;margin:0 auto 10px;text-align:left}hr{order:997;width:100%}select{width:92.5%}.input-label{align-self:self-start;background-color:#fff;border:1px solid #ccc;border-radius:6px;bottom:-5px;color:#015293;font-size:13.5px;left:5%;padding:2px 10px 0;position:relative}.input-label:hover{border-color:rgba(0,0,0,.8)}.esp-info{margin-bottom:12px;text-align:center}.row-wrapper{align-items:baseline;display:flex;flex-wrap:nowrap;gap:20px}.loading{color:#ffffff9e;text-align:center}.loader{height:auto;overflow:hidden;position:relative}.spinH{background:#ffffff2b;border-radius:100%;height:8px;position:absolute;top:50%;width:8px}.spin-1H{animation:spinnerH 3s linear infinite}.spin-2H{animation:spinnerH 3s linear .1s infinite}.spin-3H{animation:spinnerH 3s linear .2s infinite}.spin-4H{animation:spinnerH 3s linear .3s infinite}@keyframes spinnerH{0%{transform:translateX(0)}20%{transform:translate(200px)}70%{transform:translateX(505px)}to{transform:translateX(800px)}}.progress-wrap{background:#9e9e9e7d;font-size:20px;height:25px;margin:20px auto;position:relative;text-align:center;transition:all .4s ease;width:60%}.progress-wrap:not(.active){background:#9e9e9e00;cursor:pointer}.progress-wrap .progress{background:#9e9e9ea1;opacity:0;transition:all .3s ease;width:0;z-index:5}.progress-wrap.active .progress{animation:progress-anim 10s ease 0s;opacity:1}.progress-wrap[data-progress-style=fill-back] .progress{bottom:0;left:0;position:absolute;right:0;top:0}.d-modal{background-color:rgba(96,125,139,.9);border:1px solid #3333336e;border-radius:10px;box-shadow:0 3px 8px rgba(0,0,0,.24);flex-direction:column;left:50%;position:absolute;top:50%;transform:translate(-50%,-50%);width:70%}.d-modal-title{color:#111827;padding:1.5em;position:relative;width:calc(100% - 4.5em)}.d-modal-content{border-top:1px solid #e0e0e0;overflow:auto;padding:1.5em}.topnav{align-self:flex-end;background-color:#333;border-radius:5px;bottom:5px;float:right;overflow:hidden;right:20px;width:fit-content}.topnav a{color:#f2f2f2;display:block;float:left;font-size:16px;padding:5px 10px;text-align:center;text-decoration:none}.topnav a:hover{cursor:pointer;filter:brightness(55%)}.topnav a.active{background-color:#56c080;color:#fff}.topnav .icon{display:none}.logo{width:100%}.lbl-wifi{align-self:flex-end}.table{border-collapse:collapse;border-spacing:0;margin:auto;width:90%}.svg{display:flex;margin-right:10px;width:20px}.svg-e{margin-top:5px}.output{color:#fff}.form{padding:10px 0;text-align:center;width:85%}.ctn:after,.ctn:before{content:" ";display:table;grid-column-end:2;grid-column-start:1;grid-row-end:2;grid-row-start:1}.ctn:after{clear:both}.ctn{background-color:hsla(0,0%,100%,.7);border:1px solid;border-radius:10px;box-shadow:0 3px 8px rgba(0,0,0,.24);display:flex;flex-direction:column;margin:0 auto 10px;max-width:960px;overflow:visible;padding:10px 40px;position:relative}.btn-bar{grid-column-gap:30px;grid-row-gap:20px;display:flex;flex-wrap:wrap;justify-content:center;order:998;padding:20px}.tf-wrapper{align-items:center;flex-direction:column;margin-bottom:10px}.tf-wrapper,.title{display:flex}.heading{margin-right:0;text-align:right;width:100%}.heading-2{color:#0d4869;font-weight:400;margin:10px auto;text-align:center}.toggle{cursor:pointer;display:block;margin:0 0 18px -10px;padding:4px;width:fit-content}.toggle-label{margin:0 20px;position:relative;top:2px}.toggle-switch{background:#ccc;border-radius:16px;display:inline-block;height:32px;position:relative;transition:background .25s;vertical-align:middle;width:58px}.toggle-switch:after,.toggle-switch:before{content:""}.toggle-switch:before{background:linear-gradient(180deg,#fff 0,#eee);border-radius:50%;box-shadow:0 0 0 1px rgba(0,0,0,.25);display:block;height:24px;left:4px;position:absolute;top:4px;transition:left .25s;width:24px}.toggle:hover .toggle-switch:before{background:linear-gradient(180deg,#fff 0,#fff);box-shadow:0 0 0 1px rgba(0,0,0,.5)}.t-check:checked+.toggle-switch{background:#56c080}.t-check:checked+.toggle-switch:before{left:30px}.t-check{position:absolute;visibility:hidden}.pswd{align-items:center;display:flex;flex-direction:row;justify-content:center;width:100%}.show-hide-wrap{cursor:pointer;position:absolute;right:8%}.firmware{align-items:center;display:flex;flex-direction:column}.fw-upload{background:dimgray;border-radius:5px;color:#fff;cursor:pointer;display:inline-block;margin-bottom:20px;padding:12px 20px}.fw-upload:hover{filter:brightness(85%)}.btn,.submit{min-width:25%}#about{color:#d3d3d3}#esp-ip{display:inline-block;margin-bottom:5px}@media screen and (max-width:991px){.wifi-connect{padding-left:20px;padding-right:20px}}@media screen and (max-width:767px){.wifi-connect{padding-left:10px;padding-right:10px}select{width:93.5%}}@media screen and (max-width:608px){h1{font-size:24px}.btn-bar{flex-direction:column;margin-top:20px}.topnav{margin-bottom:5px;width:55%}.topnav a{padding:5px 20px}.topnav a:not(.active){display:none}.topnav a.icon{display:block;float:right}.topnav.responsive .icon{position:absolute;right:0;top:0}.topnav.responsive a{display:block;float:none;text-align:left}.hide-tiny{display:none}.toggle-label{margin:0 5px}.show-hide-wrap{right:4%}}@media screen and (max-width:479px){body{padding:2px}select{width:95.5%}.heading-2{margin-top:10px;padding:0}.form{padding:0}.ctn{max-width:100%;padding:0 10px}.row-wrapper{flex-direction:column;gap:0}}.hide{display:none} -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/minify.js: -------------------------------------------------------------------------------- 1 | // Finalize Nodejs Script 2 | // 1 - Append JS in HTML Document 3 | // 2 - Gzip HTML 4 | // 3 - Covert to Raw Bytes 5 | // 4 - ( Save to File: webpage.h ) in dist Folder 6 | 7 | const fs = require('fs'); 8 | const minify = require('@node-minify/core'); 9 | const terser = require('@node-minify/terser'); 10 | const cssnano = require('@node-minify/cssnano'); 11 | // const csso = require('@node-minify/csso'); 12 | // const uglifyjs = require('@node-minify/uglify-js'); 13 | // const gcc = require('@node-minify/google-closure-compiler'); 14 | // const htmlMinifier = require('@node-minify/html-minifier'); 15 | 16 | const converter = require('./stringConverter'); 17 | 18 | minify({ 19 | compressor: terser, 20 | input: '../app.js', 21 | output: './min/app.js', 22 | sync: true, 23 | callback: function (err, min) { if (err) console.log(err); } 24 | }); 25 | 26 | minify({ 27 | compressor: cssnano, 28 | input: '../style.css', 29 | output: './min/style.css', 30 | sync: true, 31 | callback: function (err, min) { if (err) console.log(err); } 32 | }); 33 | 34 | 35 | let html = fs.readFileSync('../setup.htm').toString(); 36 | let css = fs.readFileSync('./min/style.css'); 37 | let appjs = fs.readFileSync('./min/app.js'); 38 | console.log(css.length); 39 | console.log(appjs.length); 40 | 41 | html = html.replace('', ``); 42 | html = html.replace('', ``); 43 | 44 | // minify({ 45 | // compressor: htmlMinifier, 46 | // content: html 47 | // }).then(function (min) { 48 | // console.log('html min'); 49 | // }); 50 | 51 | fs.writeFile('./min/all.htm', html, function (err) { 52 | if (err) return console.log(err); 53 | }); 54 | 55 | const { createGzip } = require('node:zlib'); 56 | const { pipeline } = require('node:stream'); 57 | const { createReadStream, createWriteStream } = require('node:fs'); 58 | 59 | const gzip = createGzip(); 60 | const source = createReadStream('./min/all.htm'); 61 | var destination = createWriteStream('./min/all.htm.gz'); 62 | 63 | pipeline(source, gzip, destination, (err) => { 64 | if (err) { 65 | console.error('An error occurred:', err); 66 | process.exitCode = 1; 67 | } 68 | 69 | var c_array = converter.toString(fs.readFileSync('./min/all.htm.gz'), 16); 70 | fs.writeFileSync('setup_htm.h', c_array, 'utf8'); 71 | }) -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@gfx/zopfli": "^1.0.15", 4 | "@node-minify/core": "^7.1.0", 5 | "@node-minify/cssnano": "^7.1.0", 6 | "@node-minify/csso": "^7.1.0", 7 | "@node-minify/html-minifier": "^7.1.0", 8 | "@node-minify/terser": "^7.1.0" 9 | }, 10 | "name": "build", 11 | "description": "If you want customize **/setup** webpage (headers, logo etc etc): * edit the source files as your needs (setup.htm, app.js, style.css) * open a terminal in *build* folder and run `npm i` to install all nodejs modules needed * run `node finalize.js` * overwrite the content of *setup_htm.h* in src folder", 12 | "version": "1.0.0", 13 | "main": "minify.js", 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "author": "", 18 | "license": "ISC" 19 | } 20 | -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/readme.md: -------------------------------------------------------------------------------- 1 | If you want customize **/setup** webpage (headers, logo etc etc): 2 | * edit the source files as your needs (setup.htm, app.js, style.css) 3 | * open a terminal in *build* folder and run `npm i` to install all nodejs modules needed 4 | * run `node minify.js` 5 | * overwrite the content of *setup_htm.h* in src folder with the new generated file 6 | -------------------------------------------------------------------------------- /built-in-webpages/setup/build_setup/stringConverter.js: -------------------------------------------------------------------------------- 1 | var stringConverter = { 2 | convertByte: function (oneByte, bytesPerPixel) { 3 | var stringByte = '0x' + oneByte.toString(16).padStart(bytesPerPixel * 2, '0'); 4 | return stringByte; 5 | }, 6 | convert: function (dataLength, bytesPerPixel, multiLine, colNumber, data) { 7 | var resultString = ''; 8 | for (var i = 0; i < dataLength; i++) { 9 | var stringByte = ''; 10 | // need to use bigint, so we can use 32bit integers (4byte per pixel) 11 | let combinedByte = BigInt("0b00000000000000000000000000000000"); 12 | for (let j = 0; j < bytesPerPixel; j++) { 13 | let pixelByte = BigInt(data[(i * bytesPerPixel) + j]); 14 | if (j != 0) { 15 | combinedByte = combinedByte << BigInt(8); 16 | } 17 | combinedByte = combinedByte | pixelByte; 18 | } 19 | stringByte = this.convertByte(combinedByte, bytesPerPixel) + ', '; 20 | if (multiLine && ((i + 1) % colNumber == 0)) { 21 | stringByte += '\n '; 22 | } 23 | resultString += stringByte; 24 | } 25 | resultString = resultString.substr(0, resultString.lastIndexOf(',')).trim(); 26 | // add the array definition 27 | return resultString; 28 | } 29 | }; 30 | 31 | 32 | module.exports = { 33 | 34 | toString : function(data, colNum) { 35 | console.log('Converting data to string'); 36 | 37 | var dataLength = data.byteLength; 38 | console.log('actualDataLength: ' + dataLength); 39 | 40 | var resultString = '/* C-file generated by minify.js script */\n\n'; 41 | resultString += 'static const unsigned char _acsetup_min_htm[' + dataLength +' + 1] PROGMEM = {\n'; 42 | resultString += stringConverter.convert(dataLength, 1, true, 16, data); 43 | resultString += '\n};'; 44 | return resultString; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /built-in-webpages/setup/style.css: -------------------------------------------------------------------------------- 1 | html{font-family:sans-serif;} 2 | body{padding:20px;background-color:darkslategray;} 3 | hr {padding: 0;} 4 | details,main,summary{display:block;} 5 | a{background-color:transparent;} 6 | a:active,a:hover{outline:0;} 7 | b{font-weight:700;} 8 | h1{font-size:36px;margin: 10px 0 5px 0;} 9 | h2{font-size:24px;} 10 | input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,input:-webkit-autofill:active{-webkit-box-shadow:0 0 0 30px #fff inset!important;} 11 | input[type='checkbox']{box-sizing:border-box;padding:0;} 12 | input[type='text'], input[type='number'], input[type='password'], select { 13 | height:40px; 14 | width:90%; 15 | padding: 0 0 0 20px; 16 | border:1px solid #ccc; 17 | border-radius: 6px; 18 | font-size: 16px; 19 | } 20 | input[type=file] {display: none;} 21 | input::placeholder{color:#5e5e5e;} 22 | input:hover{border-color:rgba(0, 0, 0, 0.8);box-shadow:0 1px 4px 0 rgba(220, 220, 229, 0.9);} 23 | input:focus, select:focus {border-color:#3898EC;outline:0;} 24 | input[type=number]:hover::-webkit-inner-spin-button {-webkit-appearance: none;-moz-appearance: textfield;} 25 | footer {text-align:center;font-size:12px;margin:40px} 26 | button,.btn{display:inline-flex;padding:10px 15px;background-color:#2E8BC0;color:#fff;border:0;cursor:pointer;min-width:40%;border-radius:5px;justify-content:center;} 27 | button:hover,.btn:hover{ filter: brightness(85%);} 28 | ul {display: table; text-align: left; margin: 0 auto 10px auto;} 29 | hr {width: 100%; order:997;} 30 | select {width:92.5%} 31 | 32 | .input-label{ 33 | align-self: self-start; 34 | color: #015293; 35 | position: relative; 36 | left: 5%; 37 | bottom: -5px; 38 | border-radius: 6px; 39 | border: 1px solid #ccc; 40 | background-color: #fff; 41 | font-size: 13.5px; 42 | padding: 2px 10px 0 10px; 43 | } 44 | .input-label:hover{border-color:rgba(0, 0, 0, 0.8);} 45 | .esp-info{text-align: center; margin-bottom: 12px;} 46 | .row-wrapper{display: flex; flex-wrap: nowrap;align-items: baseline;gap: 20px;} 47 | .loading {color: #ffffff9e; text-align: center} 48 | .loader{position: relative; height: auto;overflow: hidden;} 49 | .spinH{top: 50%; position: absolute; height: 8px;width: 8px;background: #ffffff2b; border-radius: 100%;} 50 | .spin-1H{ animation: spinnerH 3s infinite linear; } 51 | .spin-2H{ animation: spinnerH 3s 0.1s infinite linear; } 52 | .spin-3H{ animation: spinnerH 3s 0.2s infinite linear; } 53 | .spin-4H{ animation: spinnerH 3s 0.3s infinite linear; } 54 | @keyframes spinnerH{0% { transform: translateX(0) } 20% { transform: translate(200px) } 70% { transform: translateX(505px)} 100% { transform: translateX(800px)}} 55 | 56 | .progress-wrap {text-align: center; position: relative; width: 60%; height: 25px; margin: 20px auto;background: #9e9e9e7d;font-size: 20px;transition: all 0.4s ease;} 57 | .progress-wrap:not(.active) {cursor: pointer;background:#9e9e9e00} 58 | .progress-wrap .progress {width: 0;z-index: 5; background: #9e9e9ea1;opacity: 0; transition: all 0.3s ease;} 59 | .progress-wrap.active .progress { opacity: 1; animation: progress-anim 10s ease 0s;} 60 | .progress-wrap[data-progress-style='fill-back'] .progress {position: absolute;left: 0;top: 0; right: 0; bottom: 0;} 61 | .d-modal{ 62 | width: 70%; border-radius: 10px; border-style: solid;border-width: 1px;border-color: #3333336e;background-color: hsl(199.53deg 18.3% 46.08% / 90%); 63 | box-shadow: rgba(0, 0, 0, 0.24) 0 3px 8px;left: 50%;position: absolute;top: 50%;transform: translate(-50%, -50%);flex-direction: column; 64 | } 65 | .d-modal-title{color:#111827;padding:1.5em;position:relative;width:calc(100% - 4.5em);} 66 | .d-modal-content{border-top:1px solid #e0e0e0;padding:1.5em;overflow:auto;} 67 | .topnav{background-color:#333;overflow:hidden;width:fit-content;float:right;border-radius:5px;align-self: flex-end;bottom: 5px;right: 20px;} 68 | .topnav a{float:left;display:block;color:#f2f2f2;text-align:center;padding:5px 10px;text-decoration:none;font-size:16px;} 69 | .topnav a:hover{filter: brightness(55%);cursor: pointer;} 70 | .topnav a.active{background-color:#56c080;color:white;} 71 | .topnav .icon{display:none;} 72 | .logo{width: 100%;} 73 | .lbl-wifi {align-self: flex-end;} 74 | .table{width:90%;margin:auto;border-collapse:collapse;border-spacing:0;} 75 | .svg{display:flex;width:20px;margin-right:10px;} 76 | .svg-e{margin-top: 5px;} 77 | 78 | .output {color: white; } 79 | .form{padding:10px 0;width:85%;text-align:center;} 80 | 81 | .ctn:before, 82 | .ctn:after {content: " ";display: table;grid-column-start: 1;grid-row-start: 1;grid-column-end: 2;grid-row-end: 2;} 83 | .ctn:after {clear: both;} 84 | .ctn { 85 | display: flex; 86 | flex-direction: column; 87 | position: relative; 88 | overflow: visible; 89 | max-width: 960px; 90 | margin: 0 auto 10px auto; 91 | padding: 10px 40px; 92 | border-radius: 10px; 93 | border: 1px solid; 94 | background-color: hsla(0, 0%, 100%, 0.7); 95 | box-shadow: rgba(0, 0, 0, 0.24) 0 3px 8px; 96 | } 97 | 98 | .btn-bar{display:flex;padding:20px;justify-content:center;flex-wrap:wrap;grid-column-gap:30px;grid-row-gap:20px; order:998;} 99 | .tf-wrapper{display:flex;margin-bottom:10px;flex-direction:column;align-items:center;} 100 | 101 | .title{display:flex;} 102 | .heading{width:100%;text-align: right;margin-right: 0;} 103 | .heading-2{font-weight:400;text-align: center;margin: 10px auto; color:#0d4869} 104 | .toggle{cursor: pointer;display: block; padding: 4px; width: fit-content; margin: 0 0 18px -10px;} 105 | .toggle-label{margin: 0 20px 0 20px;position: relative;top: 2px;} 106 | .toggle-switch{display:inline-block;background:#ccc;border-radius:16px;width:58px;height:32px;position:relative;vertical-align:middle;transition:background .25s;} 107 | .toggle-switch:before,.toggle-switch:after{content:"";} 108 | .toggle-switch:before{display:block;background:linear-gradient(to bottom, #fff 0%, #eee 100%);border-radius:50%;box-shadow:0 0 0 1px rgba(0, 0, 0, 0.25);width:24px;height:24px;position:absolute;top:4px;left:4px;transition:left .25s;} 109 | .toggle:hover .toggle-switch:before{background:linear-gradient(to bottom, #fff 0%, #fff 100%);box-shadow:0 0 0 1px rgba(0, 0, 0, 0.5);} 110 | .t-check:checked+.toggle-switch{background:#56c080;} 111 | .t-check:checked+.toggle-switch:before{left:30px;} 112 | .t-check{position:absolute;visibility:hidden;} 113 | 114 | .pswd {display: flex; flex-direction: row; align-items: center;width: 100%; justify-content: center;} 115 | .show-hide-wrap{position: absolute;right: 8%;cursor: pointer;} 116 | 117 | .firmware{display: flex;flex-direction: column;align-items: center;} 118 | .fw-upload {display: inline-block;padding: 12px 20px;cursor: pointer;border-radius: 5px;margin-bottom: 20px;background: dimgray;color: #fff;} 119 | .fw-upload:hover {filter: brightness(85%);} 120 | .btn, .submit { min-width: 25%; } 121 | 122 | #about {color: lightgray} 123 | #esp-ip{display: inline-block;margin-bottom: 5px} 124 | 125 | @media screen and (max-width: 991px){ 126 | .wifi-connect{padding-right:20px;padding-left:20px;} 127 | } 128 | @media screen and (max-width: 767px){ 129 | .wifi-connect{padding-right:10px;padding-left:10px;} 130 | select {width:93.5%} 131 | } 132 | @media screen and (max-width: 608px){ 133 | h1{font-size:24px;} 134 | .btn-bar{margin-top:20px;flex-direction:column;} 135 | .topnav {margin-bottom: 5px; width: 55%} 136 | .topnav a{padding:5px 20px;} 137 | .topnav a:not(.active){display:none;} 138 | .topnav a.icon{float:right;display:block;} 139 | .topnav.responsive .icon{position:absolute;right:0;top:0;} 140 | .topnav.responsive a{float:none;display:block;text-align:left;} 141 | .hide-tiny{display:none;} 142 | .toggle-label {margin: 0 5px 0 5px;} 143 | .show-hide-wrap {right: 4%;} 144 | } 145 | @media screen and (max-width: 479px){ 146 | body{padding:2px;} 147 | select {width:95.5%} 148 | .heading-2{margin-top:10px;padding:0} 149 | .form{padding:0;} 150 | .ctn{max-width:100%; padding:0 10px 0 10px;} 151 | .row-wrapper{flex-direction: column; gap:0} 152 | } 153 | 154 | .hide{display:none;} -------------------------------------------------------------------------------- /examples/csvLogger/csvLogger.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | AsyncFsWebServer server(80, LittleFS, "esphost"); 6 | bool captiveRun = false; 7 | 8 | // Timezone definition to get properly time from NTP server 9 | #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" 10 | #include 11 | 12 | struct tm ntpTime; 13 | const char* basePath = "/csv"; 14 | 15 | //////////////////////////////// Filesystem ///////////////////////////////////////// 16 | bool startFilesystem(){ 17 | if (LittleFS.begin()){ 18 | server.printFileList(LittleFS, "/", 2); 19 | return true; 20 | } 21 | else { 22 | Serial.println("ERROR on mounting filesystem. It will be formmatted!"); 23 | LittleFS.format(); 24 | ESP.restart(); 25 | } 26 | return false; 27 | } 28 | 29 | //////////////////////////// Append a row to csv file /////////////////////////////////// 30 | bool appenRow() { 31 | 32 | getLocalTime(&ntpTime, 10); 33 | 34 | char filename[32]; 35 | snprintf(filename, sizeof(filename), 36 | "%s/%04d_%02d_%02d.csv", 37 | basePath, 38 | ntpTime.tm_year + 1900, 39 | ntpTime.tm_mon + 1, 40 | ntpTime.tm_mday 41 | ); 42 | 43 | File file; 44 | if (LittleFS.exists(filename)) { 45 | file = LittleFS.open(filename, "a"); // Append to existing file 46 | } 47 | else { 48 | file = LittleFS.open(filename, "w"); // Create a new file 49 | file.println("timestamp, free heap, largest free block, connected, wifi strength"); 50 | } 51 | 52 | if (file) { 53 | char timestamp[25]; 54 | strftime(timestamp, sizeof(timestamp), "%c", &ntpTime); 55 | 56 | char row[64]; 57 | #ifdef ESP32 58 | snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", 59 | timestamp, 60 | heap_caps_get_free_size(0), 61 | heap_caps_get_largest_free_block(0), 62 | (WiFi.status() == WL_CONNECTED) ? "true" : "false", 63 | WiFi.RSSI() 64 | ); 65 | #elif defined(ESP8266) 66 | uint32_t free; 67 | uint32_t max; 68 | ESP.getHeapStats(&free, &max, nullptr); 69 | snprintf(row, sizeof(row), 70 | "%s, %d, %d, %s, %d", 71 | timestamp, free, max, 72 | (WiFi.status() == WL_CONNECTED) ? "true" : "false", 73 | WiFi.RSSI() 74 | ); 75 | #endif 76 | Serial.println(row); 77 | file.println(row); 78 | file.close(); 79 | return true; 80 | } 81 | 82 | return false; 83 | } 84 | 85 | 86 | void setup() { 87 | Serial.begin(115200); 88 | delay(1000); 89 | startFilesystem(); 90 | 91 | // Try to connect to WiFi (will start AP if not connected after timeout) 92 | if (!server.startWiFi(10000)) { 93 | Serial.println("\nWiFi not connected! Starting AP mode..."); 94 | server.startCaptivePortal("ESP32_LOGGER", "123456789", "/setup"); 95 | captiveRun = true; 96 | } 97 | 98 | // Enable ACE FS file web editor and add FS info callback fucntion 99 | server.enableFsCodeEditor(); 100 | #ifdef ESP32 101 | server.setFsInfoCallback([](fsInfo_t* fsInfo) { 102 | fsInfo->totalBytes = LittleFS.totalBytes(); 103 | fsInfo->usedBytes = LittleFS.usedBytes(); 104 | fsInfo->fsName = "LittleFS"; 105 | }); 106 | #endif 107 | 108 | // Start server 109 | server.init(); 110 | Serial.print(F("Async ESP Web Server started on IP Address: ")); 111 | Serial.println(server.getServerIP()); 112 | Serial.println(F( 113 | "This is \"scvLogger.ino\" example.\n" 114 | "Open /setup page to configure optional parameters.\n" 115 | "Open /edit page to view, edit or upload example or your custom webserver source files." 116 | )); 117 | 118 | // Set NTP servers 119 | #ifdef ESP8266 120 | configTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); 121 | #elif defined(ESP32) 122 | configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); 123 | #endif 124 | // Wait for NTP sync (with timeout) 125 | getLocalTime(&ntpTime, 5000); 126 | 127 | 128 | // Create csv logs folder if not exists 129 | if (!LittleFS.exists(basePath)) { 130 | LittleFS.mkdir(basePath); 131 | } 132 | } 133 | 134 | void loop() { 135 | if (captiveRun) 136 | server.updateDNS(); 137 | 138 | static uint32_t updateTime; 139 | if (millis()- updateTime > 30000) { 140 | updateTime = millis(); 141 | appenRow(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /examples/csvLogger/data/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* Misc */ 2 | html, body { 3 | font-family: "Helvetica","Arial",sans-serif; 4 | font-size: 14px; 5 | font-weight: 400; 6 | line-height: 20px; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 11 | font-size: 1rem; 12 | font-weight: 400; 13 | line-height: 1.5; 14 | color: #212529; 15 | text-align: left; 16 | background-color: #fff; 17 | } 18 | 19 | h1 { 20 | margin: 5px; 21 | text-align: center; 22 | } 23 | 24 | p { 25 | font-size: 0.9rem; 26 | margin: 0.5rem 0 1.5rem 0; 27 | } 28 | 29 | a, 30 | a:visited { 31 | color: #08C; 32 | text-decoration: none; 33 | } 34 | 35 | a:hover, 36 | a:focus { 37 | color: #69c773; 38 | cursor: pointer; 39 | } 40 | 41 | a.delete-file, 42 | a.delete-file:visited { 43 | color: #CC0000; 44 | margin-left: 0.5rem; 45 | vertical-align: middle; 46 | } 47 | 48 | button { 49 | display: inline-block; 50 | border-radius: 3px; 51 | border: none; 52 | font-size: 0.9rem; 53 | padding: 0.5rem 1em; 54 | background: #86b32d; 55 | border-bottom: 1px solid #5d7d1f; 56 | color: white; 57 | margin: 5px 0; 58 | text-align: center; 59 | } 60 | 61 | button:hover { 62 | opacity: 0.75; 63 | cursor: pointer; 64 | } 65 | 66 | #page-wrapper { 67 | width: 95%; 68 | background: #FFF; 69 | padding: 1.25rem; 70 | margin: 1rem auto; 71 | min-height: 800px; 72 | border-top: 5px solid #69c773; 73 | box-shadow: 0 2px 10px rgba(0,0,0,0.8); 74 | } 75 | 76 | #content { 77 | width: 85%; 78 | overflow: auto; 79 | height: 86vh; 80 | } 81 | 82 | #files { 83 | width: 15%; 84 | } 85 | 86 | #files ul { 87 | margin: 20px 0; 88 | padding: 0.5rem 1rem; 89 | overflow-y: auto; 90 | list-style: square; 91 | background: #F7F7F7; 92 | border: 1px solid #D9D9D9; 93 | border-radius: 5px; 94 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); 95 | max-height: 75vh; 96 | } 97 | 98 | #files li { 99 | margin-left: 8px; 100 | font-size: 14px; 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | } 105 | 106 | .container { 107 | display: flex; 108 | flex-direction: row; 109 | align-content: flex-start; 110 | justify-content: space-between; 111 | align-items: flex-start; 112 | column-gap: 10px; 113 | height: 86vh; 114 | } 115 | 116 | .delete { 117 | font-size: 24px; 118 | transition: 0.3s; 119 | margin-top:5px; 120 | } 121 | 122 | .delete-all{ 123 | color: #f44336; 124 | font-size: 12px; 125 | background-color: transparent; 126 | background-repeat: no-repeat; 127 | border: none; 128 | cursor: pointer; 129 | overflow: hidden; 130 | } 131 | 132 | /* Tables */ 133 | .table-holder { 134 | margin-top: 20px; 135 | border: 1px solid lightgray; 136 | border-radius: 5px; 137 | border-bottom: 0px; 138 | border-bottom-left-radius: 0px; 139 | border-bottom-right-radius: 0px; 140 | } 141 | 142 | .tables { 143 | margin-bottom: 50px; 144 | } 145 | 146 | table { 147 | width: 100%; 148 | border-bottom: 0px; 149 | } 150 | 151 | table, th, td { 152 | border: 1px solid lightgrey; 153 | border-collapse: collapse; 154 | padding-left: 5px; 155 | } 156 | 157 | table tr:nth-child(even) { 158 | background-color: white; 159 | } 160 | 161 | table tr:nth-child(odd) { 162 | background-color: #f2f2f2; 163 | } 164 | 165 | table th { 166 | background-color: #e1e1e1; 167 | color: black; 168 | } 169 | 170 | /* Sections */ 171 | 172 | .section { 173 | box-shadow: 10px 10px 10px 10px; 174 | background-color: #e5e5e5; 175 | padding: 10px; 176 | padding-top: 20px; 177 | font-size: 18px; 178 | } 179 | 180 | .section-lightgrey { 181 | background-color: #f9f9f9; 182 | } 183 | 184 | /* 100% Image Width on Smaller Screens */ 185 | @media only screen and (max-width: 700px){ 186 | .modal-content { 187 | width: 100%; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/csvLogger/data/assets/css/style.css: -------------------------------------------------------------------------------- 1 | /* Misc */ 2 | html, body { 3 | font-family: "Helvetica","Arial",sans-serif; 4 | font-size: 14px; 5 | font-weight: 400; 6 | line-height: 20px; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 11 | font-size: 1rem; 12 | font-weight: 400; 13 | line-height: 1.5; 14 | color: #212529; 15 | text-align: left; 16 | background-color: #fff; 17 | } 18 | 19 | h1 { 20 | margin: 5px; 21 | text-align: center; 22 | } 23 | 24 | p { 25 | font-size: 0.9rem; 26 | margin: 0.5rem 0 1.5rem 0; 27 | } 28 | 29 | a, 30 | a:visited { 31 | color: #08C; 32 | text-decoration: none; 33 | } 34 | 35 | a:hover, 36 | a:focus { 37 | color: #69c773; 38 | cursor: pointer; 39 | } 40 | 41 | a.delete-file, 42 | a.delete-file:visited { 43 | color: #CC0000; 44 | margin-left: 0.5rem; 45 | vertical-align: middle; 46 | } 47 | 48 | button { 49 | display: inline-block; 50 | border-radius: 3px; 51 | border: none; 52 | font-size: 0.9rem; 53 | padding: 0.5rem 1em; 54 | background: #86b32d; 55 | border-bottom: 1px solid #5d7d1f; 56 | color: white; 57 | margin: 5px 0; 58 | text-align: center; 59 | } 60 | 61 | button:hover { 62 | opacity: 0.75; 63 | cursor: pointer; 64 | } 65 | 66 | #page-wrapper { 67 | width: 95%; 68 | background: #FFF; 69 | padding: 1.25rem; 70 | margin: 1rem auto; 71 | min-height: 800px; 72 | border-top: 5px solid #69c773; 73 | box-shadow: 0 2px 10px rgba(0,0,0,0.8); 74 | } 75 | 76 | #content { 77 | width: 85%; 78 | overflow: auto; 79 | height: 86vh; 80 | } 81 | 82 | #files { 83 | width: 15%; 84 | line-height: 1; 85 | } 86 | 87 | #files ul { 88 | margin: 20px 0; 89 | padding: 0.5rem 1rem; 90 | overflow-y: auto; 91 | list-style: square; 92 | background: #F7F7F7; 93 | border: 1px solid #D9D9D9; 94 | border-radius: 5px; 95 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); 96 | max-height: 75vh; 97 | } 98 | 99 | #files li { 100 | font-size: 14px; 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | } 105 | 106 | .container { 107 | display: flex; 108 | flex-direction: row; 109 | align-content: flex-start; 110 | justify-content: space-between; 111 | align-items: flex-start; 112 | column-gap: 10px; 113 | height: 86vh; 114 | } 115 | 116 | .delete { 117 | font-size: 24px; 118 | transition: 0.3s; 119 | margin-top:5px; 120 | } 121 | 122 | .delete-all{ 123 | color: #f44336; 124 | font-size: 12px; 125 | background-color: transparent; 126 | background-repeat: no-repeat; 127 | border: none; 128 | cursor: pointer; 129 | overflow: hidden; 130 | } 131 | 132 | /* Tables */ 133 | .table-holder { 134 | margin-top: 20px; 135 | border: 1px solid lightgray; 136 | border-radius: 5px; 137 | border-bottom: 0px; 138 | border-bottom-left-radius: 0px; 139 | border-bottom-right-radius: 0px; 140 | } 141 | 142 | .tables { 143 | margin-bottom: 50px; 144 | } 145 | 146 | table { 147 | width: 100%; 148 | border-bottom: 0px; 149 | } 150 | 151 | table, th, td { 152 | border: 1px solid lightgrey; 153 | border-collapse: collapse; 154 | padding-left: 5px; 155 | } 156 | 157 | table tr:nth-child(even) { 158 | background-color: white; 159 | } 160 | 161 | table tr:nth-child(odd) { 162 | background-color: #f2f2f2; 163 | } 164 | 165 | table th { 166 | background-color: #e1e1e1; 167 | color: black; 168 | } 169 | 170 | /* Sections */ 171 | 172 | .section { 173 | box-shadow: 10px 10px 10px 10px; 174 | background-color: #e5e5e5; 175 | padding: 10px; 176 | padding-top: 20px; 177 | font-size: 18px; 178 | } 179 | 180 | .section-lightgrey { 181 | background-color: #f9f9f9; 182 | } 183 | 184 | /* 100% Image Width on Smaller Screens */ 185 | @media only screen and (max-width: 700px){ 186 | .modal-content { 187 | width: 100%; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/csvLogger/data/assets/js/index.js: -------------------------------------------------------------------------------- 1 | var dataFolder = document.getElementById("csv-path").value; 2 | var fileList = document.getElementById('file-list'); 3 | var currentFile = ""; 4 | 5 | // Fetch the list of files and fill the filelist 6 | function listFiles() { 7 | var url = '/list?dir=' + dataFolder; 8 | if (url.charAt(url.length - 1) === '/') 9 | url = url.slice(0, -1); // Remove the last character 10 | fetch(url) // Do the request 11 | .then(response => response.json()) // Parse the response 12 | .then(obj => { // DO something with response 13 | fileList.innerHTML = ''; 14 | obj.forEach(function(entry, i) { 15 | addEntry(entry.name); 16 | }); 17 | // Load last file 18 | loadCsv(dataFolder + obj[obj.length -1].name); 19 | }); 20 | } 21 | 22 | // Load selected image inside the preview content 23 | function loadFile(filename) { 24 | loadCsv(filename); 25 | } 26 | 27 | // Delete selected file in SD 28 | async function deleteFile(filename) { 29 | var isExecuted = confirm("Are you sure to delete "+ filename + "?"); 30 | if(isExecuted){ 31 | const data = new URLSearchParams(); 32 | data.append('path', filename); 33 | fetch('/edit', { 34 | method: 'DELETE', 35 | body: data 36 | }); 37 | // Update the file browser. 38 | listFiles(); 39 | } 40 | } 41 | 42 | async function deleteAll() { 43 | var isExecuted = confirm("Are you sure to delete all files in "+ dataFolder + " folder?"); 44 | if(isExecuted){ 45 | var ul = document.getElementById("file-list"); 46 | var items = ul.getElementsByClassName("edit-file"); 47 | for (var i=0; i 2 | 3 | 4 | 5 | ESP32 CSV List 6 | 7 | 8 | 9 | 10 |
11 |
12 |

CSV list web interface

13 |
14 |
15 |
    16 |
    17 | 18 |

    19 | 20 | 21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/csvLogger/readme.md: -------------------------------------------------------------------------------- 1 | An example for logging to a CSV file and viewing the content with the browser. 2 | It is also possible to modify or download the file. 3 | 4 | ![image](https://github.com/cotestatnt/esp-fs-webserver/assets/27758688/a776a217-f634-480c-873c-8914e82f87e3) -------------------------------------------------------------------------------- /examples/csvLoggerSD/csvLoggerSD.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // Timezone definition to get properly time from NTP server 5 | #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" 6 | #include 7 | 8 | #define PIN_CS 14 9 | #define PIN_SCK 13 10 | #define PIN_MOSI 12 11 | #define PIN_MISO 11 12 | 13 | 14 | AsyncFsWebServer server(80, SD, "myServer"); 15 | bool captiveRun = false; 16 | struct tm ntpTime; 17 | const char* basePath = "/csv"; 18 | 19 | //////////////////////////////// NTP Time ///////////////////////////////////////// 20 | void getUpdatedtime(const uint32_t timeout) { 21 | uint32_t start = millis(); 22 | do { 23 | time_t now = time(nullptr); 24 | ntpTime = *localtime(&now); 25 | delay(1); 26 | } while (millis() - start < timeout && ntpTime.tm_year <= (1970 - 1900)); 27 | } 28 | 29 | 30 | //////////////////////////////// Filesystem ///////////////////////////////////////// 31 | bool startFilesystem(){ 32 | if (SD.begin(PIN_CS)){ 33 | server.printFileList(SD, "/", 2); 34 | return true; 35 | } 36 | else { 37 | Serial.println("ERROR on mounting filesystem. It will be formmatted!"); 38 | ESP.restart(); 39 | } 40 | return false; 41 | } 42 | 43 | //////////////////////////// Append a row to csv file /////////////////////////////////// 44 | bool appenRow() { 45 | getUpdatedtime(10); 46 | 47 | char filename[24]; 48 | snprintf(filename, sizeof(filename), 49 | "%s/%04d_%02d_%02d.csv", 50 | basePath, 51 | ntpTime.tm_year + 1900, 52 | ntpTime.tm_mon + 1, 53 | ntpTime.tm_mday 54 | ); 55 | 56 | File file; 57 | if (SD.exists(filename)) { 58 | file = SD.open(filename, "a"); // Append to existing file 59 | } 60 | else { 61 | file = SD.open(filename, "w"); // Create a new file 62 | file.println("timestamp, free heap, largest free block, connected, wifi strength"); 63 | } 64 | 65 | if (file) { 66 | char timestamp[25]; 67 | strftime(timestamp, sizeof(timestamp), "%c", &ntpTime); 68 | 69 | char row[64]; 70 | #ifdef ESP32 71 | snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", 72 | timestamp, 73 | heap_caps_get_free_size(0), 74 | heap_caps_get_largest_free_block(0), 75 | (WiFi.status() == WL_CONNECTED) ? "true" : "false", 76 | WiFi.RSSI() 77 | ); 78 | #elif defined(ESP8266) 79 | uint32_t free; 80 | uint16_t max; 81 | ESP.getHeapStats(&free, &max, nullptr); 82 | snprintf(row, sizeof(row), 83 | "%s, %d, %d, %s, %d", 84 | timestamp, free, max, 85 | (WiFi.status() == WL_CONNECTED) ? "true" : "false", 86 | WiFi.RSSI() 87 | ); 88 | #endif 89 | Serial.println(row); 90 | file.println(row); 91 | file.close(); 92 | return true; 93 | } 94 | 95 | return false; 96 | } 97 | 98 | 99 | void setup() { 100 | SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CS); 101 | Serial.begin(115200); 102 | 103 | delay(1000); 104 | startFilesystem(); 105 | 106 | // Create csv logs folder if not exists 107 | if (!SD.exists(basePath)) { 108 | SD.mkdir(basePath); 109 | } 110 | 111 | // Try to connect to WiFi (will start AP if not connected after timeout) 112 | if (!server.startWiFi(10000)) { 113 | Serial.println("\nWiFi not connected! Starting AP mode..."); 114 | server.startCaptivePortal("ESP_AP", "123456789", "/setup"); 115 | captiveRun = true; 116 | } 117 | 118 | // Enable ACE FS file web editor and add FS info callback fucntion 119 | server.enableFsCodeEditor(); 120 | #ifdef ESP32 121 | server.setFsInfoCallback([](fsInfo_t* fsInfo) { 122 | fsInfo->totalBytes = SD.totalBytes(); 123 | fsInfo->usedBytes = SD.usedBytes(); 124 | fsInfo->fsName = "SD"; 125 | }); 126 | #endif 127 | 128 | // Start server 129 | server.init(); 130 | Serial.print(F("\nAsync ESP Web Server started on IP Address: ")); 131 | Serial.println(server.getServerIP()); 132 | Serial.println(F( 133 | "This is \"scvLoggerSdFat.ino\" example.\n" 134 | "Open /setup page to configure optional parameters.\n" 135 | "Open /edit page to view, edit or upload example or your custom webserver source files." 136 | )); 137 | 138 | // Set NTP servers 139 | #ifdef ESP8266 140 | configTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); 141 | #elif defined(ESP32) 142 | configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); 143 | #endif 144 | 145 | // Wait for NTP sync (with timeout) 146 | getUpdatedtime(5000); 147 | 148 | appenRow(); 149 | } 150 | 151 | void loop() { 152 | if (captiveRun) 153 | server.updateDNS(); 154 | 155 | static uint32_t updateTime; 156 | if (millis()- updateTime > 30000) { 157 | updateTime = millis(); 158 | appenRow(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /examples/csvLoggerSD/data/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* Misc */ 2 | html, body { 3 | font-family: "Helvetica","Arial",sans-serif; 4 | font-size: 14px; 5 | font-weight: 400; 6 | line-height: 20px; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 11 | font-size: 1rem; 12 | font-weight: 400; 13 | line-height: 1.5; 14 | color: #212529; 15 | text-align: left; 16 | background-color: #fff; 17 | } 18 | 19 | h1 { 20 | margin: 5px; 21 | text-align: center; 22 | } 23 | 24 | p { 25 | font-size: 0.9rem; 26 | margin: 0.5rem 0 1.5rem 0; 27 | } 28 | 29 | a, 30 | a:visited { 31 | color: #08C; 32 | text-decoration: none; 33 | } 34 | 35 | a:hover, 36 | a:focus { 37 | color: #69c773; 38 | cursor: pointer; 39 | } 40 | 41 | a.delete-file, 42 | a.delete-file:visited { 43 | color: #CC0000; 44 | margin-left: 0.5rem; 45 | vertical-align: middle; 46 | } 47 | 48 | button { 49 | display: inline-block; 50 | border-radius: 3px; 51 | border: none; 52 | font-size: 0.9rem; 53 | padding: 0.5rem 1em; 54 | background: #86b32d; 55 | border-bottom: 1px solid #5d7d1f; 56 | color: white; 57 | margin: 5px 0; 58 | text-align: center; 59 | } 60 | 61 | button:hover { 62 | opacity: 0.75; 63 | cursor: pointer; 64 | } 65 | 66 | #page-wrapper { 67 | width: 95%; 68 | background: #FFF; 69 | padding: 1.25rem; 70 | margin: 1rem auto; 71 | min-height: 800px; 72 | border-top: 5px solid #69c773; 73 | box-shadow: 0 2px 10px rgba(0,0,0,0.8); 74 | } 75 | 76 | #content { 77 | width: 85%; 78 | overflow: auto; 79 | height: 86vh; 80 | } 81 | 82 | #files { 83 | width: 15%; 84 | } 85 | 86 | #files ul { 87 | margin: 20px 0; 88 | padding: 0.5rem 1rem; 89 | overflow-y: auto; 90 | list-style: square; 91 | background: #F7F7F7; 92 | border: 1px solid #D9D9D9; 93 | border-radius: 5px; 94 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); 95 | max-height: 75vh; 96 | } 97 | 98 | #files li { 99 | margin-left: 8px; 100 | font-size: 14px; 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | } 105 | 106 | .container { 107 | display: flex; 108 | flex-direction: row; 109 | align-content: flex-start; 110 | justify-content: space-between; 111 | align-items: flex-start; 112 | column-gap: 10px; 113 | height: 86vh; 114 | } 115 | 116 | .delete { 117 | font-size: 24px; 118 | transition: 0.3s; 119 | margin-top:5px; 120 | } 121 | 122 | .delete-all{ 123 | color: #f44336; 124 | font-size: 12px; 125 | background-color: transparent; 126 | background-repeat: no-repeat; 127 | border: none; 128 | cursor: pointer; 129 | overflow: hidden; 130 | } 131 | 132 | /* Tables */ 133 | .table-holder { 134 | margin-top: 20px; 135 | border: 1px solid lightgray; 136 | border-radius: 5px; 137 | border-bottom: 0px; 138 | border-bottom-left-radius: 0px; 139 | border-bottom-right-radius: 0px; 140 | } 141 | 142 | .tables { 143 | margin-bottom: 50px; 144 | } 145 | 146 | table { 147 | width: 100%; 148 | border-bottom: 0px; 149 | } 150 | 151 | table, th, td { 152 | border: 1px solid lightgrey; 153 | border-collapse: collapse; 154 | padding-left: 5px; 155 | } 156 | 157 | table tr:nth-child(even) { 158 | background-color: white; 159 | } 160 | 161 | table tr:nth-child(odd) { 162 | background-color: #f2f2f2; 163 | } 164 | 165 | table th { 166 | background-color: #e1e1e1; 167 | color: black; 168 | } 169 | 170 | /* Sections */ 171 | 172 | .section { 173 | box-shadow: 10px 10px 10px 10px; 174 | background-color: #e5e5e5; 175 | padding: 10px; 176 | padding-top: 20px; 177 | font-size: 18px; 178 | } 179 | 180 | .section-lightgrey { 181 | background-color: #f9f9f9; 182 | } 183 | 184 | /* 100% Image Width on Smaller Screens */ 185 | @media only screen and (max-width: 700px){ 186 | .modal-content { 187 | width: 100%; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/csvLoggerSD/data/assets/css/style.css: -------------------------------------------------------------------------------- 1 | /* Misc */ 2 | html, body { 3 | font-family: "Helvetica","Arial",sans-serif; 4 | font-size: 14px; 5 | font-weight: 400; 6 | line-height: 20px; 7 | } 8 | body { 9 | margin: 0; 10 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"; 11 | font-size: 1rem; 12 | font-weight: 400; 13 | line-height: 1.5; 14 | color: #212529; 15 | text-align: left; 16 | background-color: #fff; 17 | } 18 | 19 | h1 { 20 | margin: 5px; 21 | text-align: center; 22 | } 23 | 24 | p { 25 | font-size: 0.9rem; 26 | margin: 0.5rem 0 1.5rem 0; 27 | } 28 | 29 | a, 30 | a:visited { 31 | color: #08C; 32 | text-decoration: none; 33 | } 34 | 35 | a:hover, 36 | a:focus { 37 | color: #69c773; 38 | cursor: pointer; 39 | } 40 | 41 | a.delete-file, 42 | a.delete-file:visited { 43 | color: #CC0000; 44 | margin-left: 0.5rem; 45 | vertical-align: middle; 46 | } 47 | 48 | button { 49 | display: inline-block; 50 | border-radius: 3px; 51 | border: none; 52 | font-size: 0.9rem; 53 | padding: 0.5rem 1em; 54 | background: #86b32d; 55 | border-bottom: 1px solid #5d7d1f; 56 | color: white; 57 | margin: 5px 0; 58 | text-align: center; 59 | } 60 | 61 | button:hover { 62 | opacity: 0.75; 63 | cursor: pointer; 64 | } 65 | 66 | #page-wrapper { 67 | width: 95%; 68 | background: #FFF; 69 | padding: 1.25rem; 70 | margin: 1rem auto; 71 | min-height: 800px; 72 | border-top: 5px solid #69c773; 73 | box-shadow: 0 2px 10px rgba(0,0,0,0.8); 74 | } 75 | 76 | #content { 77 | width: 85%; 78 | overflow: auto; 79 | height: 86vh; 80 | } 81 | 82 | #files { 83 | width: 15%; 84 | line-height: 1; 85 | } 86 | 87 | #files ul { 88 | margin: 20px 0; 89 | padding: 0.5rem 1rem; 90 | overflow-y: auto; 91 | list-style: square; 92 | background: #F7F7F7; 93 | border: 1px solid #D9D9D9; 94 | border-radius: 5px; 95 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); 96 | max-height: 75vh; 97 | } 98 | 99 | #files li { 100 | font-size: 14px; 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | } 105 | 106 | .container { 107 | display: flex; 108 | flex-direction: row; 109 | align-content: flex-start; 110 | justify-content: space-between; 111 | align-items: flex-start; 112 | column-gap: 10px; 113 | height: 86vh; 114 | } 115 | 116 | .delete { 117 | font-size: 24px; 118 | transition: 0.3s; 119 | margin-top:5px; 120 | } 121 | 122 | .delete-all{ 123 | color: #f44336; 124 | font-size: 12px; 125 | background-color: transparent; 126 | background-repeat: no-repeat; 127 | border: none; 128 | cursor: pointer; 129 | overflow: hidden; 130 | } 131 | 132 | /* Tables */ 133 | .table-holder { 134 | margin-top: 20px; 135 | border: 1px solid lightgray; 136 | border-radius: 5px; 137 | border-bottom: 0px; 138 | border-bottom-left-radius: 0px; 139 | border-bottom-right-radius: 0px; 140 | } 141 | 142 | .tables { 143 | margin-bottom: 50px; 144 | } 145 | 146 | table { 147 | width: 100%; 148 | border-bottom: 0px; 149 | } 150 | 151 | table, th, td { 152 | border: 1px solid lightgrey; 153 | border-collapse: collapse; 154 | padding-left: 5px; 155 | } 156 | 157 | table tr:nth-child(even) { 158 | background-color: white; 159 | } 160 | 161 | table tr:nth-child(odd) { 162 | background-color: #f2f2f2; 163 | } 164 | 165 | table th { 166 | background-color: #e1e1e1; 167 | color: black; 168 | } 169 | 170 | /* Sections */ 171 | 172 | .section { 173 | box-shadow: 10px 10px 10px 10px; 174 | background-color: #e5e5e5; 175 | padding: 10px; 176 | padding-top: 20px; 177 | font-size: 18px; 178 | } 179 | 180 | .section-lightgrey { 181 | background-color: #f9f9f9; 182 | } 183 | 184 | /* 100% Image Width on Smaller Screens */ 185 | @media only screen and (max-width: 700px){ 186 | .modal-content { 187 | width: 100%; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /examples/csvLoggerSD/data/assets/js/index.js: -------------------------------------------------------------------------------- 1 | var dataFolder = document.getElementById("csv-path").value; 2 | var fileList = document.getElementById('file-list'); 3 | var currentFile = ""; 4 | 5 | // Fetch the list of files and fill the filelist 6 | function listFiles() { 7 | var url = '/list?dir=' + dataFolder; 8 | if (url.charAt(url.length - 1) === '/') 9 | url = url.slice(0, -1); // Remove the last character 10 | fetch(url) // Do the request 11 | .then(response => response.json()) // Parse the response 12 | .then(obj => { // DO something with response 13 | fileList.innerHTML = ''; 14 | obj.forEach(function(entry, i) { 15 | addEntry(entry.name); 16 | }); 17 | // Load last file 18 | loadCsv(dataFolder + obj[obj.length -1].name); 19 | }); 20 | } 21 | 22 | // Load selected image inside the preview content 23 | function loadFile(filename) { 24 | loadCsv(filename); 25 | } 26 | 27 | // Delete selected file in SD 28 | async function deleteFile(filename) { 29 | var isExecuted = confirm("Are you sure to delete "+ filename + "?"); 30 | if(isExecuted){ 31 | const data = new URLSearchParams(); 32 | data.append('path', filename); 33 | fetch('/edit', { 34 | method: 'DELETE', 35 | body: data 36 | }); 37 | // Update the file browser. 38 | listFiles(); 39 | } 40 | } 41 | 42 | async function deleteAll() { 43 | var isExecuted = confirm("Are you sure to delete all files in "+ dataFolder + " folder?"); 44 | if(isExecuted){ 45 | var ul = document.getElementById("file-list"); 46 | var items = ul.getElementsByClassName("edit-file"); 47 | for (var i=0; i 2 | 3 | 4 | 5 | ESP32 CSV List 6 | 7 | 8 | 9 | 10 |
    11 |
    12 |

    CSV list web interface

    13 |
    14 |
    15 |
      16 |
      17 | 18 |

      19 | 20 | 21 |
      22 |
      23 | 24 |
      25 |
      26 |
      27 |
      28 |
      29 |
      30 |
      31 |
      32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /examples/csvLoggerSD/readme.md: -------------------------------------------------------------------------------- 1 | An example for logging to a CSV file and viewing the content with the browser. 2 | It is also possible to modify or download the file. 3 | 4 | ![image](https://github.com/cotestatnt/esp-fs-webserver/assets/27758688/a776a217-f634-480c-873c-8914e82f87e3) -------------------------------------------------------------------------------- /examples/customHTML/.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "board": "esp32:esp32:esp32s3", 3 | "port": "COM23", 4 | "sketch": "customHTML.ino", 5 | "configuration": "JTAGAdapter=default,PSRAM=disabled,FlashMode=qio,FlashSize=4M,LoopCore=1,EventsCore=1,USBMode=hwcdc,CDCOnBoot=default,MSCOnBoot=default,DFUOnBoot=default,UploadMode=default,PartitionScheme=default,CPUFreq=240,UploadSpeed=921600,DebugLevel=none,EraseFlash=none" 6 | } -------------------------------------------------------------------------------- /examples/customHTML/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.tcc": "cpp", 4 | "ostream": "cpp", 5 | "new": "cpp" 6 | } 7 | } -------------------------------------------------------------------------------- /examples/customHTML/customElements.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * This HTML code will be injected in /setup webpage using a
      element as parent 4 | * The parent element will have the HTML id properties equal to 'raw-html-' 5 | * where the id value will be equal to the id parameter passed to the function addHTML(html_code, id). 6 | */ 7 | static const char custom_html[] PROGMEM = R"EOF( 8 | 9 | 10 |
      11 |
      12 | 13 |
      14 | 15 | 16 | 17 | Fecth url 18 | 19 |
      20 |
      
       21 | )EOF";
       22 | 
       23 | 
       24 | /*
       25 | * In this example, a style sections is added in order to render properly the new
       26 | * 
      34 |             
      35 |           
      36 |         
      37 |     
      38 |         
      39 |

      Select image

      40 |
        41 | 42 |
        43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/esp32-cam/data/www/styles.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | -moz-box-sizing: border-box; 3 | -webkit-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-family: Helvetica, Arial, sans-serif; 9 | font-size: 100%; 10 | background: #333; 11 | color: #33383D; 12 | -webkit-font-smoothing: antialiased; 13 | } 14 | 15 | #page-wrapper { 16 | width: 960px; 17 | background: #FFF; 18 | padding: 1.25rem; 19 | margin: 1rem auto; 20 | min-height: 300px; 21 | border-top: 5px solid #69c773; 22 | box-shadow: 0 2px 10px rgba(0,0,0,0.8); 23 | } 24 | 25 | 26 | h2 { 27 | margin-top: 0; 28 | font-size: 0.9rem; 29 | letter-spacing: 1px; 30 | color: #999; 31 | } 32 | 33 | p { 34 | font-size: 0.9rem; 35 | margin: 0.5rem 0 1.5rem 0; 36 | } 37 | 38 | a, 39 | a:visited { 40 | color: #08C; 41 | text-decoration: none; 42 | } 43 | 44 | a:hover, 45 | a:focus { 46 | color: #69c773; 47 | cursor: pointer; 48 | } 49 | 50 | a.delete-file, 51 | a.delete-file:visited { 52 | color: #CC0000; 53 | margin-left: 0.5rem; 54 | vertical-align: middle; 55 | } 56 | 57 | 58 | 59 | .field { 60 | margin-bottom: 1rem; 61 | } 62 | 63 | button { 64 | width: 150px; 65 | display: inline-block; 66 | border-radius: 3px; 67 | border: none; 68 | font-size: 0.9rem; 69 | padding: 0.6rem 1em; 70 | background: #86b32d; 71 | border-bottom: 1px solid #5d7d1f; 72 | color: white; 73 | margin: 0 0.25rem; 74 | text-align: center; 75 | } 76 | 77 | button:hover { 78 | opacity: 0.75; 79 | cursor: pointer; 80 | } 81 | 82 | #file-form { 83 | width: 75%; 84 | float: left; 85 | } 86 | 87 | #files { 88 | width: 23%; 89 | float: right; 90 | } 91 | 92 | #files ul { 93 | margin: 0; 94 | padding: 0.5rem 1rem; 95 | max-height: 600px; 96 | overflow-y: auto; 97 | list-style: square; 98 | background: #F7F7F7; 99 | border: 1px solid #D9D9D9; 100 | border-radius: 3px; 101 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); 102 | } 103 | 104 | #files li { 105 | margin-left: 8px; 106 | font-size: 14px; 107 | } 108 | 109 | 110 | /* Clearfix Utils */ 111 | 112 | .clearfix { 113 | *zoom: 1; 114 | } 115 | 116 | .clearfix:before, 117 | .clearfix:after { 118 | display: table; 119 | line-height: 0; 120 | content: ""; 121 | } 122 | 123 | .clearfix:after { 124 | clear: both; 125 | } 126 | 127 | #image-content { 128 | border-radius: 5px; 129 | cursor: pointer; 130 | transition: 0.3s; 131 | } 132 | 133 | #image-content:hover {opacity: 0.7;} 134 | 135 | /* The Modal (background) */ 136 | .modal { 137 | display: none; /* Hidden by default */ 138 | position: fixed; /* Stay in place */ 139 | z-index: 1; /* Sit on top */ 140 | padding-top: 100px; /* Location of the box */ 141 | left: 0; 142 | top: 0; 143 | width: 100%; /* Full width */ 144 | height: 100%; /* Full height */ 145 | overflow: auto; /* Enable scroll if needed */ 146 | background-color: rgb(0,0,0); /* Fallback color */ 147 | background-color: rgba(0,0,0,0.9); /* Black w/ opacity */ 148 | } 149 | 150 | 151 | /* Modal Content (image) */ 152 | .modal-content { 153 | margin: auto; 154 | display: block; 155 | width: 90%; 156 | } 157 | 158 | /* Caption of Modal Image */ 159 | #caption { 160 | margin: auto; 161 | display: block; 162 | text-align: center; 163 | color: #ccc; 164 | padding: 10px 0; 165 | } 166 | 167 | /* Add Animation */ 168 | .modal-content, #caption { 169 | -webkit-animation-name: zoom; 170 | -webkit-animation-duration: 0.6s; 171 | animation-name: zoom; 172 | animation-duration: 0.6s; 173 | } 174 | 175 | @-webkit-keyframes zoom { 176 | from {-webkit-transform:scale(0)} 177 | to {-webkit-transform:scale(1)} 178 | } 179 | 180 | @keyframes zoom { 181 | from {transform:scale(0)} 182 | to {transform:scale(1)} 183 | } 184 | 185 | /* The Close Button */ 186 | .close { 187 | position: absolute; 188 | top: 15px; 189 | right: 35px; 190 | color: #f1f1f1; 191 | font-size: 40px; 192 | font-weight: bold; 193 | transition: 0.3s; 194 | } 195 | 196 | .close:hover, 197 | .close:focus { 198 | color: #bbb; 199 | text-decoration: none; 200 | cursor: pointer; 201 | } 202 | 203 | .delete { 204 | font-size: 30px; 205 | font-weight: bold; 206 | transition: 0.3s; 207 | } 208 | 209 | .delete-all{ 210 | color: #f44336; 211 | font-size: 12px; 212 | background-color: transparent; 213 | background-repeat: no-repeat; 214 | border: none; 215 | cursor: pointer; 216 | overflow: hidden; 217 | outline: none; 218 | } 219 | /* 100% Image Width on Smaller Screens */ 220 | @media only screen and (max-width: 700px){ 221 | .modal-content { 222 | width: 100%; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /examples/esp32-cam/esp32-cam.ino: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include // https://github.com/cotestatnt/async-esp-fs-webserver/ 5 | #include "esp_camera.h" 6 | #include "soc/soc.h" // Brownout error fix 7 | #include "soc/rtc_cntl_reg.h" // Brownout error fix 8 | 9 | #if ESP_ARDUINO_VERSION_MAJOR >= 3 10 | #include "soc/soc_caps.h" 11 | #endif 12 | 13 | #define FILESYSTEM SD_MMC 14 | AsyncFsWebServer server(80, FILESYSTEM); 15 | 16 | // Local include files 17 | #include "camera_pins.h" 18 | 19 | uint16_t grabInterval = 0; // Grab a picture every x seconds 20 | uint32_t lastGrabTime = 0; 21 | 22 | // Timezone definition to get properly time from NTP server 23 | #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" 24 | 25 | // Struct for saving time datas (needed for time-naming the image files) 26 | struct tm tInfo; 27 | 28 | // Functions prototype 29 | void listDir(const char *, uint8_t); 30 | void setLamp(int); 31 | 32 | // Grab a picture from CAM and store on SD or in flash 33 | void getPicture(AsyncWebServerRequest *); 34 | const char* getFolder = "/img"; 35 | 36 | /////////////////////////////////// SETUP /////////////////////////////////////// 37 | void setup() { 38 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detect 39 | 40 | // Flash LED setup 41 | pinMode(LAMP_PIN, OUTPUT); // set the lamp pin as output 42 | #if ESP_ARDUINO_VERSION_MAJOR >= 3 43 | ledcAttach(LAMP_PIN, 1000, 8); 44 | #else 45 | ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel 46 | ledcAttachPin(LAMP_PIN, lampChannel); 47 | #endif 48 | setLamp(0); // set default value 49 | 50 | Serial.begin(115200); 51 | Serial.println(); 52 | 53 | // Try to connect to WiFi (will start AP if not connected after timeout) 54 | if (!server.startWiFi(10000)) { 55 | Serial.println("\nWiFi not connected! Starting AP mode..."); 56 | server.startCaptivePortal("ESP32CAM_AP", "123456789", "/setup"); 57 | } 58 | 59 | // Sync time with NTP 60 | configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); 61 | 62 | /* 63 | Init onboard SD filesystem (format if necessary) 64 | SD_MMC.begin(const char * mountpoint, bool mode1bit, bool format_if_mount_failed, int sdmmc_frequency, uint8_t maxOpenFiles) 65 | To avoid led glowing, set mode1bit = true (SD HS_DATA1 is tied to GPIO4, the same of on-board flash led) 66 | */ 67 | if (!SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_HIGHSPEED, 5)) { 68 | Serial.println("\nSD Mount Failed.\n"); 69 | } 70 | 71 | if (!SD_MMC.exists(getFolder)) { 72 | if(SD_MMC.mkdir(getFolder)) 73 | Serial.println("Dir created"); 74 | else 75 | Serial.println("mkdir failed"); 76 | } 77 | listDir(getFolder, 1); 78 | 79 | // Enable ACE FS file web editor and add FS info callback function 80 | server.enableFsCodeEditor(); 81 | server.setFsInfoCallback([](fsInfo_t* fsInfo) { 82 | fsInfo->totalBytes = SD_MMC.totalBytes(); 83 | fsInfo->usedBytes = SD_MMC.usedBytes(); 84 | }); 85 | 86 | // Add custom handlers to webserver 87 | server.on("/getPicture", getPicture); 88 | server.on("/setInterval", setInterval); 89 | 90 | // Start server with built-in websocket event handler 91 | server.init(); 92 | Serial.print(F("\nESP Web Server started on IP Address: ")); 93 | Serial.println(server.getServerIP()); 94 | Serial.println(F( 95 | "This is \"remoteOTA.ino\" example.\n" 96 | "Open /setup page to configure optional parameters.\n" 97 | "Open /edit page to view, edit or upload example or your custom webserver source files." 98 | )); 99 | 100 | // Init the camera module (according the camera_config_t defined) 101 | init_camera(); 102 | } 103 | 104 | /////////////////////////////////// LOOP /////////////////////////////////////// 105 | void loop() { 106 | if (grabInterval) { 107 | if (millis() - lastGrabTime > grabInterval *1000) { 108 | lastGrabTime = millis(); 109 | getPicture(nullptr); 110 | } 111 | } 112 | } 113 | 114 | ////////////////////////////////// FUNCTIONS////////////////////////////////////// 115 | void setInterval(AsyncWebServerRequest *request) { 116 | if (request->hasArg("val")) { 117 | grabInterval = request->arg("val").toInt(); 118 | Serial.printf("Set grab interval every %d seconds\n", grabInterval); 119 | } 120 | request->send(200, "text/plain", "OK"); 121 | } 122 | 123 | // Lamp Control 124 | void setLamp(int newVal) { 125 | if (newVal != -1) { 126 | // Apply a logarithmic function to the scale. 127 | int brightness = round((pow(2, (1 + (newVal * 0.02))) - 2) / 6 * pwmMax); 128 | ledcWrite(lampChannel, brightness); 129 | Serial.print("Lamp: "); 130 | Serial.print(newVal); 131 | Serial.print("%, pwm = "); 132 | Serial.println(brightness); 133 | } 134 | } 135 | 136 | // Send a picture taken from CAM to a Telegram chat 137 | void getPicture(AsyncWebServerRequest *request) { 138 | 139 | // Take Picture with Camera; 140 | Serial.println("Camera capture requested"); 141 | 142 | // Take Picture with Camera and store in ram buffer fb 143 | setLamp(100); 144 | delay(100); 145 | camera_fb_t *fb = esp_camera_fb_get(); 146 | setLamp(0); 147 | 148 | if (!fb) { 149 | Serial.println("Camera capture failed"); 150 | if (request != nullptr) 151 | request->send(500, "text/plain", "ERROR. Image grab failed"); 152 | return; 153 | } 154 | 155 | // Keep files on SD memory, filename is time based (YYYYMMDD_HHMMSS.jpg) 156 | // Embedded filesystem is too small to keep all images, overwrite the same file 157 | char filename[20]; 158 | time_t now = time(nullptr); 159 | tInfo = *localtime(&now); 160 | strftime(filename, sizeof(filename), "%Y%m%d_%H%M%S.jpg", &tInfo); 161 | 162 | char filePath[30]; 163 | strcpy(filePath, getFolder); 164 | strcat(filePath, "/"); 165 | strcat(filePath, filename); 166 | File file = SD_MMC.open(filePath, "w"); 167 | if (!file) { 168 | Serial.println("Failed to open file in writing mode"); 169 | if(request != nullptr) 170 | request->send(500, "text/plain", "ERROR. Image grab failed"); 171 | return; 172 | } 173 | // size_t _jpg_buf_len = 0; 174 | // uint8_t *_jpg_buf = NULL; 175 | // bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); 176 | file.write(fb->buf, fb->len); 177 | file.close(); 178 | Serial.printf("Saved file to path: %s - %zu bytes\n", filePath, fb->len); 179 | 180 | // Clear buffer 181 | esp_camera_fb_return(fb); 182 | if (request != nullptr) 183 | request->send(200, "text/plain", filename); 184 | } 185 | 186 | // List all files saved in the selected filesystem 187 | void listDir(const char *dirname, uint8_t levels) { 188 | uint32_t freeBytes = SD_MMC.totalBytes() - SD_MMC.usedBytes(); 189 | Serial.print("\nTotal space: "); 190 | Serial.println(SD_MMC.totalBytes()); 191 | Serial.print("Free space: "); 192 | Serial.println(freeBytes); 193 | 194 | Serial.printf("Listing directory: %s\r\n", dirname); 195 | File root = SD_MMC.open(dirname); 196 | if (!root) { 197 | Serial.println("- failed to open directory\n"); 198 | return; 199 | } 200 | if (!root.isDirectory()) { 201 | Serial.println(" - not a directory\n"); 202 | return; 203 | } 204 | File file = root.openNextFile(); 205 | while (file) { 206 | if (file.isDirectory()) { 207 | if (levels) 208 | listDir(file.name(), levels - 1); 209 | } 210 | else { 211 | Serial.printf("|__ FILE: %s (%d bytes)\n",file.name(), file.size()); 212 | } 213 | file = root.openNextFile(); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /examples/gpio_list/data/index.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ESP GPIO dinamic list 6 | 7 | 8 | 15 | 16 | 17 |
        18 |
        19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

        GPIO status list

        29 |
        30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
        Pin NamePin numberTypeLevel
        43 |
        44 |
        45 |
        46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/gpio_list/data/pico.classless.min.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cotestatnt/async-esp-fs-webserver/f6303024b51f3086c6364932fa6447190c8ac852/examples/gpio_list/data/pico.classless.min.css.gz -------------------------------------------------------------------------------- /examples/gpio_list/data/script.js: -------------------------------------------------------------------------------- 1 | const svgLightOn = '' ; 2 | const svgLightOff = ''; 3 | 4 | /** 5 | * Custom selector "JQuery style", but in plain "Vanilla JS" 6 | */ 7 | var $ = function(el) { 8 | return document.getElementById(el); 9 | }; 10 | 11 | /** 12 | * Start a websocket client and set event callback functions 13 | */ 14 | function ws_connect() { 15 | var ws = new WebSocket('ws://'+document.location.host+'/ws',['arduino']); 16 | ws.onopen = function() { 17 | ws.send('Connected - ' + new Date()); 18 | getGpioList(); 19 | }; 20 | ws.onmessage = function(e) { 21 | parseMessage(e.data); 22 | }; 23 | ws.onclose = function(e) { 24 | setTimeout(function() { 25 | ws_connect(); 26 | }, 1000); 27 | }; 28 | ws.onerror = function(err) { 29 | ws.close(); 30 | }; 31 | return ws; 32 | } 33 | 34 | /** 35 | * Send data "cmds" to ESP 36 | */ 37 | function sendCommand(cmd, pin, level) { 38 | var data = { 39 | cmd: cmd, 40 | pin: parseInt(pin), 41 | level: level 42 | }; 43 | console.log(data); 44 | connection.send(JSON.stringify(data)); 45 | } 46 | 47 | /** 48 | * Parse messages receveid via websocket 49 | */ 50 | function parseMessage(msg) { 51 | const obj = JSON.parse(msg); 52 | if (typeof obj === 'object' && obj !== null) { 53 | if (obj.esptime !== null) { 54 | var date = new Date(0); // The 0 sets the date to epoch 55 | if( date.setUTCSeconds(obj.esptime)) 56 | document.getElementById("esp-time").innerHTML = date; 57 | } 58 | updateGpiosList(obj); 59 | } 60 | } 61 | 62 | /** 63 | * Read GPIO list status 64 | */ 65 | function getGpioList() { 66 | fetch('/getGpioList') // Do the request 67 | .then(response => response.json()) // Parse the response 68 | .then(obj => { // DO something with response 69 | updateGpiosList(obj); 70 | }); 71 | } 72 | 73 | 74 | /** 75 | * Iterate to the gpio list passed as parameter anc create DOMs dinamically 76 | */ 77 | function updateGpiosList(elems) { 78 | 79 | // Get reference to gpio-list element and clear content 80 | const list = document.querySelector('#gpio-list'); 81 | list.innerHTML = ""; 82 | 83 | // Draw all input rows 84 | const inputs = Object.entries(elems).filter((item) => item[1].type === 'input'); 85 | 86 | inputs.forEach(el => { 87 | const obj = el[1]; 88 | var lbl = obj.level ? `${svgLightOn} HIGH` : `${svgLightOff} LOW`; 89 | // Create a single row with all columns 90 | var row = document.createElement('tr'); 91 | row.innerHTML = '' + obj.label + ''; 92 | row.innerHTML += '' + obj.pin + ''; 93 | row.innerHTML += '' + obj.type + ''; 94 | row.innerHTML += `` ; 95 | // Append this row to list 96 | list.appendChild(row); 97 | }); 98 | 99 | // Draw all output rows 100 | const outputs = Object.entries(elems).filter((item) => item[1].type === 'output'); 101 | 102 | outputs.forEach(el => { 103 | const obj = el[1]; 104 | var lbl = obj.level ? ` checked>Turn OFF` : `>Turn ON`; 105 | // Create a single row with all columns 106 | var row = document.createElement('tr'); 107 | row.innerHTML = '' + obj.label + ''; 108 | row.innerHTML += '' + obj.pin + ''; 109 | row.innerHTML += '' + obj.type + ''; 110 | row.innerHTML += `