Repository: cotestatnt/esp-fs-webserver Branch: master Commit: 9f5e7e7febbb Files: 184 Total size: 1.3 MB Directory structure: gitextract_673knega/ ├── .github/ │ └── workflows/ │ ├── clean_workflow.yml │ ├── cli-build-esp32-dev.yml │ ├── cli-build-esp8266.yml │ ├── pio-build-esp32-dev.yml │ └── pio-build-esp8266.yml ├── .gitignore ├── LICENSE ├── README.md ├── built-in-webpages/ │ └── readme.md ├── docs/ │ ├── API.md │ ├── FileEditorAndFS.md │ ├── SetupAndWiFi.md │ ├── WebSocket.md │ ├── pwd_encrypt.md │ └── readme.md ├── examples/ │ ├── csvLogger/ │ │ ├── .gitignore │ │ ├── csvLogger.ino │ │ ├── data/ │ │ │ ├── assets/ │ │ │ │ ├── css/ │ │ │ │ │ ├── index.css │ │ │ │ │ └── style.css │ │ │ │ └── js/ │ │ │ │ ├── csv.js │ │ │ │ └── index.js │ │ │ ├── csv/ │ │ │ │ └── 2024_01_10.csv │ │ │ └── index.htm │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── readme.md │ ├── csvLoggerSD/ │ │ ├── csvLoggerSD.ino │ │ ├── data/ │ │ │ ├── assets/ │ │ │ │ ├── css/ │ │ │ │ │ ├── index.css │ │ │ │ │ └── style.css │ │ │ │ └── js/ │ │ │ │ ├── csv.js │ │ │ │ └── index.js │ │ │ └── index.htm │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── readme.md │ ├── customHTML/ │ │ ├── customElements.h │ │ ├── customHTML.ino │ │ └── thingsboard.h │ ├── customOptions/ │ │ ├── .gitignore │ │ ├── customOptions.ino │ │ ├── partitions.csv │ │ └── platformio.ini │ ├── esp32-cam/ │ │ ├── .gitignore │ │ ├── camera_pins.h │ │ ├── data/ │ │ │ ├── index.htm │ │ │ └── www/ │ │ │ ├── app.js │ │ │ ├── index.htm │ │ │ └── styles.css │ │ ├── esp32-cam.ino │ │ ├── partitions.csv │ │ └── platformio.ini │ ├── gpio_list/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ ├── index.htm │ │ │ └── script.js │ │ ├── gpio_list.ino │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── readme.md │ ├── handleFormData/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ ├── index.htm │ │ │ ├── myScript.js │ │ │ └── myStyle.css │ │ ├── handleFormData.ino │ │ ├── partitions.csv │ │ └── platformio.ini │ ├── leanWebserver/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ └── index.htm │ │ ├── leanWebserver.ino │ │ ├── partitions.csv │ │ └── platformio.ini │ ├── localRFID/ │ │ ├── .gitignore │ │ ├── JsonDB.hpp │ │ ├── data/ │ │ │ ├── html_login.h │ │ │ ├── html_rfid.h │ │ │ ├── login │ │ │ └── rfid │ │ ├── localRFID.ino │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ ├── readme.md │ │ └── webserver.hpp │ ├── mqtt_webserver/ │ │ ├── .gitignore │ │ ├── mqtt_webserver.ino │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── readme.md │ ├── mysqlRFID/ │ │ ├── .gitignore │ │ ├── html_flash_files.h │ │ ├── mysqlRFID.ino │ │ ├── mysql_impl.h │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── webserver_impl.h │ ├── remoteOTA/ │ │ ├── data/ │ │ │ └── index.htm │ │ ├── fw-esp32/ │ │ │ └── readme.md │ │ ├── fw-esp8266/ │ │ │ └── readme.md │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ ├── remoteOTA.ino │ │ ├── version-esp32.json │ │ └── version-esp8266.json │ ├── simpleServer/ │ │ ├── data/ │ │ │ └── index.htm │ │ └── simpleServer.ino │ ├── websocketEcharts/ │ │ ├── README.md │ │ ├── data/ │ │ │ └── index.htm │ │ └── websocketEcharts.ino │ └── withWebSocket/ │ ├── .gitignore │ ├── index_htm.h │ ├── partitions.csv │ ├── platformio.ini │ ├── readme.md │ └── withWebSocket.ino ├── keywords.txt ├── library.properties ├── partitions.csv ├── pio_examples/ │ ├── customOptions/ │ │ ├── .gitignore │ │ ├── customOptions.code-workspace │ │ ├── platformio.ini │ │ └── src/ │ │ └── customOptions.ino │ ├── esp32-p4/ │ │ ├── .gitignore │ │ ├── app3M_spiffs9M_16MB.csv │ │ ├── platformio.ini │ │ └── src/ │ │ ├── index_htm.h │ │ └── withWebSocket.cpp │ ├── leanWebserver/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ └── index.htm │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── src/ │ │ └── leanWebserver.ino │ ├── simpleServer/ │ │ ├── .gitignore │ │ ├── compile_commands.json │ │ ├── partitions.csv │ │ ├── platformio.ini │ │ └── src/ │ │ └── simpleServer.ino │ ├── websocketEcharts/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── data/ │ │ │ ├── index.htm │ │ │ └── index.html │ │ ├── platformio.ini │ │ └── src/ │ │ └── websocketEcharts.ino │ └── withWebSocket/ │ ├── .gitignore │ ├── partitions.csv │ ├── platformio.ini │ ├── readme.md │ └── src/ │ ├── index_htm.h │ └── withWebSocket.ino ├── platformio.ini └── src/ ├── ConfigUpgrader.hpp ├── CredentialManager.cpp ├── CredentialManager.h ├── FSWebServer.cpp ├── FSWebServer.h ├── Json.cpp ├── Json.h ├── SerialLog.h ├── SetupConfig.hpp ├── Version.h ├── WiFiService.cpp ├── WiFiService.h ├── assets/ │ ├── edit_htm.h │ ├── logo_svg.h │ └── setup_htm.h ├── compat/ │ └── mbedtls_aes.h ├── esp-fs-webserver.h ├── json/ │ ├── cJSON.c │ └── cJSON.h ├── mimetable/ │ ├── mimetable.cpp │ └── mimetable.h └── websocket/ ├── SocketIOclient.cpp ├── SocketIOclient.h ├── WebSockets.cpp ├── WebSockets.h ├── WebSocketsClient.cpp ├── WebSocketsClient.h ├── WebSocketsServer.cpp ├── WebSocketsServer.h ├── libb64/ │ ├── AUTHORS │ ├── LICENSE │ ├── cdecode.c │ ├── cdecode_inc.h │ ├── cencode.c │ └── cencode_inc.h └── libsha1/ ├── libsha1.c └── libsha1.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/clean_workflow.yml ================================================ name: Clean Workflow Logs on: workflow_dispatch: inputs: keep_minimum_runs: description: "Numero di workflow recenti da mantenere" default: "5" required: false jobs: clean-logs: runs-on: ubuntu-latest permissions: actions: write steps: - name: Delete workflow runs uses: Mattraks/delete-workflow-runs@v2 with: token: ${{ secrets.GITHUB_TOKEN }} repository: ${{ github.repository }} retain_days: 0 # Imposta a 0 per ignorare la data e considerare solo keep_minimum_runs keep_minimum_runs: ${{ github.event.inputs.keep_minimum_runs }} ================================================ FILE: .github/workflows/cli-build-esp32-dev.yml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json name: (ESP32) Build dev with Arduino CLI on: workflow_dispatch: push: branches: - dev pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: arduino-esp32: name: ESP32 (arduino-cli) (shard=${{ matrix.shard }}) if: github.event_name == 'workflow_dispatch' || vars.ENABLE_BUILDS == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: [1, 2, 3, 4] steps: - name: Checkout uses: actions/checkout@v4 - name: Install arduino-cli run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh - name: Cache Arduino CLI data uses: actions/cache@v4 with: key: ${{ runner.os }}-arduino-esp32-${{ hashFiles('.github/workflows/cli-build-esp32-dev.yml') }}-${{ github.run_id }}-${{ matrix.shard }} restore-keys: | ${{ runner.os }}-arduino-esp32-${{ hashFiles('.github/workflows/cli-build-esp32-dev.yml') }}- ${{ runner.os }}-arduino-esp32- path: | ~/.arduino15 ~/Arduino ~/.cache/arduino-cli - name: Update core index run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json - name: Install core run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json esp32:esp32 - name: Install ArduinoJson run: arduino-cli lib install ArduinoJson - name: Install Arduino-MySQL run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/cotestatnt/Arduino-MySQL - name: Install Arduino_MFRC522v2 run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/OSSLibraries/Arduino_MFRC522v2 - name: Install PubSubClient run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/knolleary/pubsubclient - name: Build Examples run: | set -uo pipefail total_shards=4 shard="${{ matrix.shard }}" report_file="build-report-esp32-arduino-cli-shard${{ matrix.shard }}.txt" : > "$report_file" failures=0 total=0 index=0 for dir in examples/*; do if [[ ! -d "$dir" ]]; then continue fi example="$(basename "$dir")" # Split examples across shards for faster CI. index=$((index + 1)) example_shard=$(( (index - 1) % total_shards + 1 )) if [[ "$example_shard" != "$shard" ]]; then continue fi sketch="examples/${example}/${example}.ino" total=$((total + 1)) echo "=============================================================" echo "Building ${sketch}..." echo "=============================================================" if [[ ! -f "$sketch" ]]; then failures=$((failures + 1)) printf '%-25s : FAIL (missing %s)\n' "${example}" "${sketch}" >> "$report_file" continue fi echo "::group::arduino-cli compile - ${example}" set +e arduino-cli compile \ --library . \ --warnings none \ --build-cache-path "$HOME/.cache/arduino-cli" \ -b esp32:esp32:esp32 \ "$sketch" rc=$? set -e echo "::endgroup::" if [[ $rc -eq 0 ]]; then printf '%-25s : OK\n' "${example}" >> "$report_file" else failures=$((failures + 1)) printf '%-25s : FAIL (exit=%s)\n' "${example}" "$rc" >> "$report_file" fi done echo "" echo "==================== Build report (arduino-cli) ====================" cat "$report_file" echo "===================================================================" echo "Total attempted: ${total} | Failures: ${failures}" # Do not fail the job here: the final report job will decide. - name: Upload build report if: always() uses: actions/upload-artifact@v4 with: name: arduino-cli-esp32-report-shard${{ matrix.shard }} path: build-report-esp32-arduino-cli-shard${{ matrix.shard }}.txt report: name: ESP32 (arduino-cli) - Final report runs-on: ubuntu-latest needs: arduino-esp32 if: always() && needs.arduino-esp32.result != 'skipped' steps: - name: Download all reports uses: actions/download-artifact@v4 with: pattern: arduino-cli-esp32-report-* merge-multiple: true path: reports - name: Print final report and set status run: | set -uo pipefail echo "==================== Final build report (ESP32/arduino-cli) ====================" failures=0 files=0 shopt -s nullglob for f in reports/*.txt; do files=$((files + 1)) echo "--- ${f} ---" cat "$f" if grep -q " : FAIL" "$f"; then failures=$((failures + 1)) fi done if [[ $files -eq 0 ]]; then echo "No report files found (artifact download failed?)" exit 1 fi echo "===========================================================================" if [[ $failures -gt 0 ]]; then echo "One or more shards reported FAIL." exit 1 fi ================================================ FILE: .github/workflows/cli-build-esp8266.yml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json name: (ESP8266) Build with Arduino CLI on: workflow_dispatch: push: branches: - dev pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: arduino-esp8266: name: ESP8266 (arduino-cli) (shard=${{ matrix.shard }}) if: github.event_name == 'workflow_dispatch' || vars.ENABLE_BUILDS == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: shard: [1, 2, 3, 4] steps: - name: Checkout uses: actions/checkout@v4 - name: Install arduino-cli run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh - name: Cache Arduino CLI data uses: actions/cache@v4 with: key: ${{ runner.os }}-arduino-esp8266-${{ hashFiles('.github/workflows/cli-build-esp8266.yml') }}-${{ github.run_id }}-${{ matrix.shard }} restore-keys: | ${{ runner.os }}-arduino-esp8266-${{ hashFiles('.github/workflows/cli-build-esp8266.yml') }}- ${{ runner.os }}-arduino-esp8266- path: | ~/.arduino15 ~/Arduino ~/.cache/arduino-cli - name: Update core index run: arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json - name: Install core run: arduino-cli core install --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266 - name: Install ArduinoJson run: arduino-cli lib install ArduinoJson - name: Build Examples run: | set -uo pipefail total_shards=4 shard="${{ matrix.shard }}" # Examples not intended for ESP8266 / this job EXCLUDE_EXAMPLES=( esp32-cam binaryWebSocket csvLoggerSD localRFID mysqlRFID mqtt_webserver ) report_file="build-report-esp8266-arduino-cli-shard${{ matrix.shard }}.txt" : > "$report_file" failures=0 total=0 index=0 for dir in examples/*; do if [[ ! -d "$dir" ]]; then continue fi example="$(basename "$dir")" if [[ " ${EXCLUDE_EXAMPLES[*]} " == *" ${example} "* ]]; then echo "Skipping: ${example}" printf '%-25s : SKIP\n' "${example}" >> "$report_file" continue fi # Split examples across shards for faster CI. index=$((index + 1)) example_shard=$(( (index - 1) % total_shards + 1 )) if [[ "$example_shard" != "$shard" ]]; then continue fi sketch="examples/${example}/${example}.ino" total=$((total + 1)) echo "=============================================================" echo "Building ${sketch}..." echo "=============================================================" if [[ ! -f "$sketch" ]]; then failures=$((failures + 1)) printf '%-25s : FAIL (missing %s)\n' "${example}" "${sketch}" >> "$report_file" continue fi echo "::group::arduino-cli compile - ${example}" set +e arduino-cli compile \ --library . \ --warnings none \ --build-cache-path "$HOME/.cache/arduino-cli" \ -b esp8266:esp8266:huzzah \ "$sketch" rc=$? set -e echo "::endgroup::" if [[ $rc -eq 0 ]]; then printf '%-25s : OK\n' "${example}" >> "$report_file" else failures=$((failures + 1)) printf '%-25s : FAIL (exit=%s)\n' "${example}" "$rc" >> "$report_file" fi done echo "" echo "==================== Build report (esp8266/arduino-cli) ====================" cat "$report_file" echo "=============================================================================" echo "Total attempted: ${total} | Failures: ${failures}" # Do not fail the job here: the final report job will decide. - name: Upload build report if: always() uses: actions/upload-artifact@v4 with: name: arduino-cli-esp8266-report-shard${{ matrix.shard }} path: build-report-esp8266-arduino-cli-shard${{ matrix.shard }}.txt report: name: ESP8266 (arduino-cli) - Final report runs-on: ubuntu-latest needs: arduino-esp8266 if: always() && needs.arduino-esp8266.result != 'skipped' steps: - name: Download all reports uses: actions/download-artifact@v4 with: pattern: arduino-cli-esp8266-report-* merge-multiple: true path: reports - name: Print final report and set status run: | set -uo pipefail echo "==================== Final build report (ESP8266/arduino-cli) ====================" failures=0 files=0 shopt -s nullglob for f in reports/*.txt; do files=$((files + 1)) echo "--- ${f} ---" cat "$f" if grep -q " : FAIL" "$f"; then failures=$((failures + 1)) fi done if [[ $files -eq 0 ]]; then echo "No report files found (artifact download failed?)" exit 1 fi echo "===========================================================================" if [[ $failures -gt 0 ]]; then echo "One or more shards reported FAIL." exit 1 fi ================================================ FILE: .github/workflows/pio-build-esp32-dev.yml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json name: (ESP32) Build dev with PlatformIO on: workflow_dispatch: push: branches: - dev pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: # Build with PlatformIO - ESP32 Arduino Latest platformio-esp32-arduino-latest: name: ESP32 (pio) - Arduino Latest (board=${{ matrix.board }}, shard=${{ matrix.shard }}) if: github.event_name == 'workflow_dispatch' || vars.ENABLE_BUILDS == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: board: - esp32dev - esp32-s3-devkitc-1 shard: [1, 2, 3, 4] steps: - name: Checkout uses: actions/checkout@v4 - name: Cache PlatformIO uses: actions/cache@v4 with: key: ${{ runner.os }}-pio path: | ~/.cache/pip ~/.platformio - name: Python uses: actions/setup-python@v5 with: python-version: "3.13" - name: Install PIO run: | python -m pip install --upgrade pip pip install --upgrade platformio - name: Build Examples run: | set -uo pipefail total_shards=4 shard="${{ matrix.shard }}" report_file="build-report-${{ matrix.board }}-shard${{ matrix.shard }}.txt" : > "$report_file" failures=0 total=0 index=0 for dir in examples/*; do if [[ ! -d "$dir" ]]; then continue fi example="$(basename "$dir")" board_to_use="${{ matrix.board }}" # Split examples across shards for faster CI. index=$((index + 1)) example_shard=$(( (index - 1) % total_shards + 1 )) if [[ "$example_shard" != "$shard" ]]; then continue fi # Avoid duplicating esp32-cam across the board matrix. if [[ "$example" == "esp32-cam" && "${{ matrix.board }}" != "esp32dev" ]]; then printf '%-25s : SKIP (board-specific)\n' "${example}" >> "$report_file" continue fi # Some examples are board-specific if [[ "$example" == "esp32-cam" ]]; then board_to_use="esp32cam" fi total=$((total + 1)) echo "=============================================================" echo "Building examples/${example} (board=${board_to_use})..." echo "=============================================================" echo "::group::pio run - examples/${example}" set +e # Use a per-example build directory to avoid cross-example artefacts. PLATFORMIO_BUILD_DIR=".pio/build-${board_to_use}-${example}" \ PLATFORMIO_SRC_DIR="examples/${example}" PIO_BOARD="${board_to_use}" \ pio run -e ci-arduino-3-latest rc=$? set -e echo "::endgroup::" if [[ $rc -eq 0 ]]; then printf '%-25s : OK\n' "${example}" >> "$report_file" else failures=$((failures + 1)) printf '%-25s : FAIL (exit=%s)\n' "${example}" "$rc" >> "$report_file" fi done echo "" echo "==================== Build report (board=${{ matrix.board }}) ====================" cat "$report_file" echo "=============================================================================" echo "Total attempted: ${total} | Failures: ${failures}" # Do not fail the job here: the final report job will decide. - name: Upload build report if: always() uses: actions/upload-artifact@v4 with: name: pio-esp32-report-${{ matrix.board }}-shard${{ matrix.shard }} path: build-report-${{ matrix.board }}-shard${{ matrix.shard }}.txt report: name: ESP32 (pio) - Final report runs-on: ubuntu-latest needs: platformio-esp32-arduino-latest if: always() && needs.platformio-esp32-arduino-latest.result != 'skipped' steps: - name: Download all reports uses: actions/download-artifact@v4 with: pattern: pio-esp32-report-* merge-multiple: true path: reports - name: Print final report and set status run: | set -uo pipefail echo "==================== Final build report (ESP32/pio) ====================" failures=0 files=0 shopt -s nullglob for f in reports/*.txt; do files=$((files + 1)) echo "--- ${f} ---" cat "$f" if grep -q " : FAIL" "$f"; then failures=$((failures + 1)) fi done if [[ $files -eq 0 ]]; then echo "No report files found (artifact download failed?)" exit 1 fi echo "=======================================================================" if [[ $failures -gt 0 ]]; then echo "One or more shards reported FAIL." exit 1 fi ================================================ FILE: .github/workflows/pio-build-esp8266.yml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json name: (ESP8266) Build with PlatformIO on: workflow_dispatch: push: branches: - dev pull_request: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: platformio-esp8266: name: ESP8266 (pio) (board=${{ matrix.board }}, shard=${{ matrix.shard }}) if: github.event_name == 'workflow_dispatch' || vars.ENABLE_BUILDS == 'true' runs-on: ubuntu-latest strategy: fail-fast: false matrix: board: - d1_mini shard: [1, 2, 3, 4] steps: - name: Checkout uses: actions/checkout@v4 - name: Cache PlatformIO uses: actions/cache@v4 with: key: ${{ runner.os }}-pio path: | ~/.cache/pip ~/.platformio - name: Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install PIO run: | python -m pip install --upgrade pip pip install --upgrade platformio - name: Build Examples run: | set -uo pipefail total_shards=4 shard="${{ matrix.shard }}" # Examples not intended for ESP8266 / this job EXCLUDE_EXAMPLES=( esp32-cam binaryWebSocket csvLoggerSD localRFID mysqlRFID mqtt_webserver ) report_file="build-report-esp8266-pio-${{ matrix.board }}-shard${{ matrix.shard }}.txt" : > "$report_file" failures=0 total=0 index=0 for dir in examples/*; do if [[ ! -d "$dir" ]]; then continue fi example="$(basename "$dir")" if [[ " ${EXCLUDE_EXAMPLES[*]} " == *" ${example} "* ]]; then echo "Skipping: ${example}" printf '%-25s : SKIP\n' "${example}" >> "$report_file" continue fi # Split examples across shards for faster CI. index=$((index + 1)) example_shard=$(( (index - 1) % total_shards + 1 )) if [[ "$example_shard" != "$shard" ]]; then continue fi total=$((total + 1)) echo "=============================================================" echo "Building examples/${example} (board=${{ matrix.board }}, shard=${shard})..." echo "=============================================================" echo "::group::pio run - examples/${example}" set +e # Use a per-example build directory to avoid cross-example artefacts. PLATFORMIO_BUILD_DIR=".pio/build-${{ matrix.board }}-${example}" \ PLATFORMIO_SRC_DIR="examples/${example}" PIO_BOARD="${{ matrix.board }}" \ pio run -e ci-esp8266 rc=$? set -e echo "::endgroup::" if [[ $rc -eq 0 ]]; then printf '%-25s : OK\n' "${example}" >> "$report_file" else failures=$((failures + 1)) printf '%-25s : FAIL (exit=%s)\n' "${example}" "$rc" >> "$report_file" fi done echo "" echo "==================== Build report (esp8266/pio, board=${{ matrix.board }}, shard=${{ matrix.shard }}) ====================" cat "$report_file" echo "===============================================================================================" echo "Total attempted: ${total} | Failures: ${failures}" # Do not fail the job here: the final report job will decide. - name: Upload build report if: always() uses: actions/upload-artifact@v4 with: name: pio-esp8266-report-${{ matrix.board }}-shard${{ matrix.shard }} path: build-report-esp8266-pio-${{ matrix.board }}-shard${{ matrix.shard }}.txt report: name: ESP8266 (pio) - Final report runs-on: ubuntu-latest needs: platformio-esp8266 if: always() && needs.platformio-esp8266.result != 'skipped' steps: - name: Download all reports uses: actions/download-artifact@v4 with: pattern: pio-esp8266-report-* merge-multiple: true path: reports - name: Print final report and set status run: | set -uo pipefail echo "==================== Final build report (ESP8266/pio) ====================" failures=0 files=0 shopt -s nullglob for f in reports/*.txt; do files=$((files + 1)) echo "--- ${f} ---" cat "$f" if grep -q " : FAIL" "$f"; then failures=$((failures + 1)) fi done if [[ $files -eq 0 ]]; then echo "No report files found (artifact download failed?)" exit 1 fi echo "===========================================================================" if [[ $failures -gt 0 ]]; then echo "One or more shards reported FAIL." exit 1 fi ================================================ FILE: .gitignore ================================================ # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app # PlatformIO in VScode .pio/ .vscode/ *.lnk *.bak /build /built-in-webpages/setup/build_setup/node_modules /examples/customHTML/build /pio_examples/temp /pio_examples/simpleServer/setup/build_setup/node_modules /built-in-webpages/edit/build_edit/node_modules ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # ESP-FS-WebServer A library for ESP8266/ESP32 that provides a web server with an integrated file system browser, WiFi configuration manager, and support for WebSockets. This library is based on the synchronous `WebServer` class. ![WiFi Manager](docs/wifi_manager.png) ## Features - **Dynamic WiFi Configuration**: An integrated setup page (`/setup`) allows you to scan for WiFi networks and connect the ESP to your local network. Passwords are stored using AES-256-CBC encryption (hardware on ESP32 platform) with multiple SSIDs manager (up to 5 different WiFi credentials). - **Powerful & Customizable UI**: - Easily add your own configuration parameters (text boxes, checkboxes, sliders, dropdown lists) to the setup page. - Inject custom **HTML, CSS, and JavaScript** snippets into the setup page to create rich, dynamic user interfaces for your specific project needs. - **Over-the-Air (OTA) Updates**: Update your device's firmware securely and conveniently through the web interface. You can easily upload also your entire web project's `data` folder to the ESP's filesystem. - **WebSocket Support**: Built-in support for real-time, two-way communication between the web client and the ESP. - **Advanced File Management**: An embedded file manager (`/edit`) allows you to browse, view, upload, and delete files and folders. Built-in `/setup` and `/edit` page sources are shared with the async library and are maintained in `C:\Cloud\fs-webserver-shared-pages`. Regenerate the embedded headers from that repository instead of editing generated files in `src/assets` directly. ## Documentation For more detailed information, please refer to the documentation in the `docs` folder: - **[API Reference](docs/API.md)** – Detailed overview of the public methods. - **[Setup and WiFi](docs/SetupAndWiFi.md)** – Guide to `startWiFi()`, captive portal, and the `/setup` page. - **[Filesystem and Editor](docs/FileEditorAndFS.md)** – How to serve static files and use the `/edit` page. - **[WebSocket](docs/WebSocket.md)** – Information on using the WebSocket server. - **[Password Encryption](docs/pwd_encrypt.md)** – Information on how passwords are managed. ## Dependencies - ESP8266/ESP32 Core for Arduino ## Basic Usage ```cpp #include #include #if defined(ESP8266) #include #else #include #endif #include "FSWebServer.h" // Use LittleFS fs::FS& filesystem = LittleFS; FSWebServer server(80, filesystem, "esphost"); void setup() { Serial.begin(115200); // Initialize Filesystem if (!filesystem.begin()) { Serial.println("Error mounting file system."); return; } // Start WiFi (or captive portal if no credentials) server.startWiFi(10000); // Add a handler for the root page server.on("/", []() { server.send(200, "text/html", "

Hello from FSWebServer!

"); }); // Start the server server.begin(); Serial.println("HTTP server started."); } void loop() { server.run(); } ``` ## WiFi Management Enhancements ### Dedicated WiFiService class - `WiFiService` centralizes scanning, connections, captive portal setup, MDNS, and watchdog handling so the sketch can stay focused on application logic. - When `startWiFi()` runs it loads credentials from `CredentialManager`, picks the SSID with the strongest RSSI (optionally honoring static IP data saved per credential), and reports a `WiFiStartResult` that tells `FSWebServer` whether to stay in STA or fall back to the captive portal. - `doWifiConnection()` now persists updated credentials before calling `WiFiService::connectWithParams()`, which feeds the long-timeout watchdog, reuses stored passwords when the form leaves the password blank, and advertises `http://.local` through `startMDNSOnly()` as soon as the STA link is up. `FSWebServer` logs when mDNS becomes reachable so you can verify the hostname was published. ### Setup page reconnection experience - The `/getStatus` handler exposes firmware version, active mode, hostname, IP, and the configuration file path so the UI knows where to direct the user after a network change. - After `/connect` begins switching SSIDs the browser starts polling `http://.local/getStatus` (falling back to the captured AP when necessary) until the ESP reappears on the new network, automatically updates the credential list, and surfaces a timeout message only after several failed polls. - This polling loop keeps the loader visible, shows a modal once the device is reachable again, and points users to either the new `http://.local` URL or the IP reported by the ESP without forcing them to refresh manually. ### Custom application "options manager" ![options](docs/options_manager.png) ### OTA update and `data` folder upload ![OTA](docs/ota_data.png) ### Custom 'code snippets' ![snippet](docs/custom_html.png) ### ACE file editor ![File Manager](docs/file_manager.png) ================================================ FILE: built-in-webpages/readme.md ================================================ The canonical built-in page sources are no longer maintained in this folder. Use the shared repository at `C:\Cloud\fs-webserver-shared-pages` as the single source of truth for both **AsyncFsWebServer** and **FSWebServer** built-in pages. Current workflow: * edit `/setup` sources in `C:\Cloud\fs-webserver-shared-pages\setup` * edit `/edit` sources in `C:\Cloud\fs-webserver-shared-pages\edit` * open a terminal in `C:\Cloud\fs-webserver-shared-pages` * run `npm install` once * run `npm run build` to regenerate `setup_htm.h`, `logo_svg.h`, and `edit_htm.h` in both library `src/assets` folders Target resolution order in the shared repo: * `--target ` CLI arguments * `targets.json` * autodiscovery of the Arduino sketchbook `libraries` folder This folder is kept only as historical reference and should not be used as the primary build entrypoint anymore. ================================================ FILE: docs/API.md ================================================ # FSWebServer – API (overview) This page summarizes the main methods exposed by `FSWebServer` and when to use them. > Note: some APIs are available only when the related features are enabled via macros (e.g. `ESP_FS_WS_SETUP`, `ESP_FS_WS_EDIT`). ## Constructor ```cpp FSWebServer(uint16_t port, fs::FS &fs, const char* hostname = ""); ``` - `port`: HTTP port (typically `80`) - `fs`: filesystem (`LittleFS`, `SPIFFS`, `FFat`, …) - `hostname`: optional (also used for mDNS) ## Server start ```cpp void begin(WebSocketsServer::WebSocketServerEvent wsEventHandler = nullptr); ``` - Registers built-in handlers (setup/edit if enabled), static file serving, and notFound. - If `wsEventHandler != nullptr`, creates and starts a websocket server. - Starts the webserver. ## Runtime info ```cpp IPAddress getServerIP(); bool isAccessPointMode() const; ``` ## Authentication (/setup page) ```cpp void setAuthentication(const char* user, const char* pswd); ``` When set, the `/setup` page requires basic-auth. ## Filesystem listing ```cpp void printFileList(fs::FS &fs, const char * dirname, uint8_t levels); void printFileList(fs::FS &fs, const char * dirname, uint8_t levels, Print& out); ``` - The `Print& out` overload lets you send output to streams other than `Serial`. Example: ```cpp server.printFileList(FILESYSTEM, "/", 1); // default -> Serial server.printFileList(FILESYSTEM, "/", 1, Serial); // explicit ``` ## WiFi + captive portal ```cpp bool startWiFi(uint32_t timeout, CallbackF fn = nullptr); bool startCaptivePortal(const char* ssid, const char* pass, const char* redirectTargetURL); ``` - `startWiFi()` tries to connect using already-saved credentials. - If it fails, you typically start `startCaptivePortal()` and use `/setup` to configure. ## WebSocket (runtime) ```cpp WebSocketsServer* getWebSocketServer(); bool broadcastWebSocket(const String &payload); bool broadcastWebSocket(const uint8_t *payload, size_t length); bool sendWebSocket(uint8_t num, const String &payload); ``` ## Setup page (only if `ESP_FS_WS_SETUP`) Config file and callback: ```cpp File getConfigFile(const char* mode); const char* getConfiFileName(); bool clearConfigFile(); void setConfigSavedCallback(ConfigSavedCallbackF callback); ``` Options and setup UI: ```cpp void setSetupPageTitle(const char* title); void addOptionBox(const char* title); // attach a comment string to an existing option element // For most controls the comment is rendered in a separate line under the input. // Boolean (checkbox) fields use a so the text stays on the same line. void addComment(const char *lbl, const char *comment); // boolean option: grouped by default, pass fourth argument false to keep declaration order // the third parameter specifies `hidden` exactly like the generic template void addOption(const char *lbl, bool val, bool hidden = false, bool grouped = true); // boolean overload that accepts a comment void addOption(const char *lbl, bool val, const char *comment, bool hidden = false, bool grouped = true); // generic templated version (bool excluded via SFINAE below) template void addOption(const char *lbl, T val, bool hidden=false, double min=MIN_F, double max=MAX_F, double st=1.0); // convenience comment overload for non-bool types // use the bool-specific variant to add a comment to a checkbox template ::value, int>::type = 0> void addOption(const char *lbl, T val, const char *comment); template void addOption(const char *lbl, T val, bool hidden=false, double min=MIN_F, double max=MAX_F, double st=1.0); template bool getOptionValue(const char *lbl, T &var); template bool saveOptionValue(const char *lbl, T val); ``` Boolean options are now controlled per-option; the third parameter (or the `grouped` argument) determines whether the switch/check is collected with other booleans or left inline. Hidden behaviour still works via `hidden` argument. Dropdown/Slider: ```cpp using DropdownList = FSWebServer::DropdownList; using Slider = FSWebServer::Slider; void addDropdownList(DropdownList &def); void addSlider(Slider &def); bool getDropdownSelection(DropdownList &def); bool getSliderValue(Slider &def); ``` ## Web file editor (only if `ESP_FS_WS_EDIT`) ```cpp void enableFsCodeEditor(FsInfoCallbackF fsCallback = nullptr); void setFsInfoCallback(FsInfoCallbackF fsCallback); ``` See also: `SetupAndWiFi.md`, `FileEditorAndFS.md`, `WebSocket.md`. ================================================ FILE: docs/FileEditorAndFS.md ================================================ # Filesystem + Web File Editor (/edit) The library serves static files from the filesystem and, optionally, includes a web editor (ACE) to manage files directly from the browser. ## Static file serving By default, files are served from the filesystem with: - root URL `/` -> filesystem path `/` - default file: `index.htm` In code (internally): - `serveStatic("/", fs, "/").setDefaultFile("index.htm")` ## Stampa contenuto filesystem (debug) ## Print filesystem contents (debug) ```cpp server.printFileList(FILESYSTEM, "/", 1); // default -> Serial server.printFileList(FILESYSTEM, "/", 1, Serial); // or to a chosen stream ``` The `Print&` overload is useful to log to streams other than `Serial`. ## Enable /edit ```cpp server.enableFsCodeEditor(); ``` Main endpoints: - `GET /edit` editor page - `GET /list?dir=/` directory listing - `POST /edit` upload - `PUT /edit` create/rename - `DELETE /edit` delete ## Provide filesystem info (recommended on ESP32) On ESP32, to show correct “total/used bytes” in the UI: ```cpp server.setFsInfoCallback([](fsInfo_t* fsInfo) { fsInfo->fsName = "LittleFS"; fsInfo->totalBytes = LittleFS.totalBytes(); fsInfo->usedBytes = LittleFS.usedBytes(); }); ``` ================================================ FILE: docs/SetupAndWiFi.md ================================================ # Setup + WiFi (startWiFi / captive portal / config) This library can: - try to connect to a previously saved WiFi network (STA) - if it fails, start an Access Point + captive portal and serve `/setup` - persist **application options** into `config.json` on the filesystem (WiFi credentials are now handled separately by `CredentialManager` and **never** stored in `config.json`) ## Typical flow ```cpp if (!server.startWiFi(10000)) { server.startCaptivePortal("ESP_AP", "123456789", "/setup"); } server.init(onWsEvent); ``` 1. `startWiFi(timeout)` attempts to connect using saved credentials. 2. If it fails, `startCaptivePortal(ssid, pass, "/setup")` switches the ESP to AP mode and redirects requests to `/setup` (or a custom endpoint) 3. On `/setup` the user selects the WiFi network (managed by `CredentialManager`) and any extra application options; the library saves **only the application options** to `config.json` and stores WiFi credentials in encrypted form via `CredentialManager`. It's possible also change the setting of IP address and mask for the captive portal passing a `WiFiConnectParams` structs to `startCaptivePortal()` method. ```cpp if (!server.startWiFi(10000)) { Serial.println("\nWiFi not connected! Starting AP mode..."); WiFiConnectParams params ("ESP_AP", "123456789"); params.config.local_ip = IPAddress(192, 168, 1, 1); params.config.gateway = IPAddress(192, 168, 1, 1); params.config.subnet = IPAddress(255, 255, 255, 0); server.startCaptivePortal(params, "/setup"); } ``` ## Application options on /setup Example (see also `withWebSocket.ino`): ```cpp server.addOptionBox("My Options"); server.addOption("LED Pin", ledPin); server.addOption("Option 1", option1.c_str()); server.addOption("Option 2", option2); // you can also add a comment for a control that will appear beneath it server.addComment("Option 2", "Explanation or hint text goes here"); ``` Read your application options at boot (from `config.json`, WiFi excluded): ```cpp uint32_t option2; server.getOptionValue("Option 2", option2); ``` ## Config file: read/write - Full path: `server.getConfiFileName()` - File access: `server.getConfigFile("r")` / `server.getConfigFile("w")` - Reset config: `server.clearConfigFile()` “Config saved” callback (useful when saving via `/edit` or upload): ```cpp server.setConfigSavedCallback([](const char* filename){ Serial.printf("Config saved: %s\n", filename); }); ``` ## WiFi credentials storage (CredentialManager) - WiFi SSID, password, DHCP/static IP and related data are **not** stored in `config.json`. - They are managed and stored (encrypted) by the internal `CredentialManager`: - ESP32: NVS - ESP8266: filesystem (e.g. LittleFS) - The `/setup` page talks directly with the WiFi APIs (`/wifi/credentials`, `/connect`, etc.), so your sketch usually does **not** need to read or write WiFi data manually. See also: `pwd_encrypt.md` for a deeper overview of `CredentialManager` and encrypted WiFi storage. ## Protect /setup with basic-auth ```cpp server.setAuthentication("admin", "admin"); ``` When set, the `/setup` page requires authentication. ================================================ FILE: docs/WebSocket.md ================================================ # WebSocket Support The library includes and uses [WebSockets](https://github.com/Links2004/arduino-WebSockets) by Markus Sattler for bidirectional communication with web clients. Support is enabled by default via the `ESP_FS_WS_WEBSOCKET` macro. ## 1. Create the Event Handler First, you need to define a function that will handle WebSocket events. This function must match the `WebSocketServerEvent` signature. ```cpp // WebSocket event handler void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { switch (type) { case WStype_DISCONNECTED: Serial.printf("[%u] Disconnected!\n", num); break; case WStype_CONNECTED: { IPAddress ip = server.getWebSocketServer()->remoteIP(num); Serial.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); // Send a welcome message to the newly connected client server.getWebSocketServer()->sendTXT(num, "Hello from FSWebServer!"); } break; case WStype_TEXT: Serial.printf("[%u] Received text: %s\n", num, payload); // Echo the message back to the client server.getWebSocketServer()->sendTXT(num, payload); break; case WStype_BIN: Serial.printf("[%u] Received binary data of length %u\n", num, length); // hexdump(payload, length); // Example for printing binary data break; default: Serial.printf("Unhandled event type: %d\n", type); break; } } ``` - `num`: The client ID (an integer from 0 up to the maximum number of clients). - `type`: The type of event (e.g., `WStype_CONNECTED`, `WStype_DISCONNECTED`, `WStype_TEXT`). - `payload`: A pointer to the received data. - `length`: The length of the received data. ## 2. Start the Server with the Handler Pass your event handler function to the `server.begin()` method. This will automatically start the WebSocket server. ```cpp void setup() { // ... other setup code ... // Start the web server and enable WebSockets server.begin(webSocketEvent); } ``` ## 3. Sending Messages from the ESP You have two ways to send messages to clients. ### Simple Broadcast (Recommended) Use the helper methods built into `FSWebServer` to easily broadcast messages to all connected clients. ```cpp // Broadcast a text message server.broadcastWebSocket("This is a message for everyone."); // Broadcast binary data uint8_t binaryPayload[] = {0xDE, 0xAD, 0xBE, 0xEF}; server.broadcastWebSocket(binaryPayload, sizeof(binaryPayload)); ``` ### Advanced Control For more advanced scenarios, like sending a message to a specific client, you can get a pointer to the underlying `WebSocketsServer` object. ```cpp // Get the WebSocket server instance WebSocketsServer* ws = server.getWebSocketServer(); if (ws) { // Send a text message to client number 2 ws->sendTXT(2, "This is a private message for you."); // Disconnect client number 3 ws->disconnect(3); } ``` This gives you full access to the underlying WebSocket library's API. ================================================ FILE: docs/pwd_encrypt.md ================================================ # FSWebServer CredentialManager Integration ✅ **Encrypted Password Storage** - AES-256-CBC encryption ✅ **Automatic Persistence** - NVS (ESP32) or Filesystem (ESP8266) ✅ **Multi-SSID Support** - Store up to 5 WiFi networks (configurable) ✅ **FIFO Management** - Automatically remove oldest when full ✅ **RSSI-Based Selection** - Connect to strongest signal ✅ **Zero Configuration** - Works transparently with FSWebServer ✅ **Cross-Platform** - ESP32 and ESP8266 compatible ## Quick Start ### Set Encryption Key ```cpp // In CredentialManager.h, change this: #define CREDENTIAL_MANAGER_ENCRYPTION_KEY "YOUR_SECRET_KEY_16_CHARS!" // To something like: #define CREDENTIAL_MANAGER_ENCRYPTION_KEY "MyCompany2024Key!" ``` Then: 1. Open `/setup` to add WiFi credentials 2. Reboot device 3. Check serial logs for RSSI-based connection --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────┐ │ User's Arduino Sketch │ │ │ │ - Calls: server.begin() │ │ - Calls: server.run() │ │ - No credential management code needed │ └─────────────────────────┬───────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ Modified FSWebServer Library │ │ │ │ • Constructor: Load credentials from storage │ │ • startWiFi(): RSSI-based WiFi selection │ │ • doWifiConnection(): FIFO credential mgmt │ └─────────────────────────┬───────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ CredentialManager (New Class) │ │ │ │ • AES-256-CBC encryption/decryption │ │ • NVS persistence (ESP32) │ │ • Filesystem persistence (ESP8266) │ │ • Credential list management │ │ • FIFO removal when full │ └─────────────────────────┬───────────────────────────┘ │ ┌─────────────┴─────────────┐ ▼ ▼ ┌──────────────┐ ┌──────────────────┐ │ NVS Flash │ │ Filesystem │ │ (ESP32) │ │ (ESP8266/ESP32) │ │ Encrypted │ │ Encrypted │ │ Passwords │ │ Passwords │ └──────────────┘ └──────────────────┘ ``` --- ## Key Features Explained ### 1. Transparent Credential Management - User adds credentials via `/setup` web page - Credentials automatically saved to encrypted storage - No code changes needed in user sketch - Credentials persist across power cycles ### 2. FIFO (First In, First Out) Management ``` When list is full (5 credentials): Add new credential → Automatically remove oldest → Keep 5 total ``` ### 3. RSSI-Based WiFi Selection ``` On startup: 1. Scan available networks 2. Find stored credentials that match 3. Select the one with best (highest) signal strength 4. Connect to that network ``` **Example**: - HomeWiFi: -65 dBm (good) - OfficeWiFi: -42 dBm (excellent) → Connect to OfficeWiFi (higher value = better signal) ### 4. Encryption ``` User input: "MyPassword123" ↓ (AES-256-CBC encrypt) Stored: "aX7k3jQ9pL2nM8vX..." ↓ (AES-256-CBC decrypt) Retrieved: "MyPassword123" ``` ### 5. Cross-Platform Support ``` ESP32: Uses NVS (secure, encrypted by hardware) ESP8266: Uses LittleFS/SPIFFS (plaintext storage file) Both: Passwords encrypted by CredentialManager ``` ## Security Considerations ### Password Encryption - **Algorithm**: AES-256-CBC (256-bit encryption) - **Padding**: PKCS7 (standard padding) - **Mode**: Cipher Block Chaining (secure against pattern attacks) ### Encryption Key Storage - **ESP32**: Ideally in eFuse BLOCK_KEY0 (hardware protected) ```bash # Generate and set key: espefuse.py key_set BLOCK_KEY0 <32-byte-hex-key> ``` - **ESP8266**: Compile-time constant (less secure) ```cpp #define CREDENTIAL_MANAGER_ENCRYPTION_KEY "MySecretKey12345" ``` ### Additional Security Measures - Use **Secure Boot** to prevent firmware tampering - Use **eFuse** to lock down critical settings - Keep encryption key confidential and consistent - Change key only if you can re-encrypt stored credentials ### Threat Model - **Protected Against**: Flash dump reading (passwords encrypted) - **Protected Against**: Casual inspection of storage - **Not Protected Against**: Brute-force attacks on weak keys - **Not Protected Against**: Side-channel attacks (e.g., timing) --- ## Customization Options ### Change Maximum Credentials In FSWebServer.h: ```cpp static constexpr uint8_t MAX_CREDENTIALS = 5; // Change to 10 for more ``` ### Change Encryption Key In CredentialManager.h: ```cpp #define CREDENTIAL_MANAGER_ENCRYPTION_KEY "YourCustomKey12!" ``` --- ## Performance ### Startup Time Impact - WiFi scan: 500-2000ms (depends on RF environment) - Credential matching: 10-50ms - Connection: 2-5 seconds - **Total**: ~3-7 seconds (typical) ### Memory Usage - CredentialManager class: ~500 bytes - Per credential: ~100 bytes - Vector overhead: ~20 bytes - **Total for 5 credentials**: ~1.2 KB ### Storage Usage - Per credential in NVS/FS: ~60 bytes encrypted - For 5 credentials: ~300 bytes - **Impact**: Negligible on ESP32 (512KB NVS) --- ================================================ FILE: docs/readme.md ================================================ # Documentation - [API](API.md) – available methods and what they do - [Setup + WiFi](SetupAndWiFi.md) – `startWiFi()`, captive portal, `/setup`, config - [Filesystem + Editor](FileEditorAndFS.md) – static file serving, `/edit`, FS info - [WebSocket](WebSocket.md) – enablement, handler, broadcast ================================================ FILE: examples/csvLogger/.gitignore ================================================ .pio .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: examples/csvLogger/csvLogger.ino ================================================ #include #include #include #include FSWebServer server(LittleFS, 80, "esphost"); // Timezone definition to get properly time from NTP server #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" #include struct tm ntpTime; const char* basePath = "/csv"; //////////////////////////////// Filesystem //////////////////////////////////////////// bool startFilesystem() { if (LittleFS.begin()) { server.printFileList(LittleFS, "/", 2, Serial); return true; } else { Serial.println("ERROR on mounting filesystem. It will be formmatted!"); LittleFS.format(); ESP.restart(); } return false; } //////////////////////////// Append a row to csv file ////////////////////////////////////// bool appenRow() { getLocalTime(&ntpTime, 10); char filename[32]; snprintf(filename, sizeof(filename), "%s/%04d_%02d_%02d.csv", basePath, ntpTime.tm_year + 1900, ntpTime.tm_mon + 1, ntpTime.tm_mday); File file; if (LittleFS.exists(filename)) { file = LittleFS.open(filename, "a"); // Append to existing file } else { file = LittleFS.open(filename, "w"); // Create a new file file.println("timestamp, free heap, largest free block, connected, wifi strength"); } if (file) { char timestamp[25]; strftime(timestamp, sizeof(timestamp), "%c", &ntpTime); char row[64]; #ifdef ESP32 snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", timestamp, heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0), (WiFi.status() == WL_CONNECTED) ? "true" : "false", WiFi.RSSI()); #elif defined(ESP8266) uint32_t free; uint32_t max; ESP.getHeapStats(&free, &max, nullptr); snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", timestamp, free, max, (WiFi.status() == WL_CONNECTED) ? "true" : "false", WiFi.RSSI()); #endif Serial.println(row); file.println(row); file.close(); return true; } return false; } void setup() { Serial.begin(115200); delay(1000); startFilesystem(); // Try to connect to WiFi (will start AP if not connected after timeout) if (!server.startWiFi(10000)) { Serial.println("\nWiFi not connected! Starting AP mode..."); server.startCaptivePortal("ESP32_LOGGER", "123456789", "/setup"); } // Enable ACE FS file web editor and add FS info callback fucntion server.enableFsCodeEditor(); // Start server server.begin(); Serial.print(F("Async ESP Web Server started on IP Address: ")); Serial.println(server.getServerIP()); Serial.println( F("This is \"scvLogger.ino\" example.\n" "Open /setup page to configure optional parameters.\n" "Open /edit page to view, edit or upload example or your custom " "webserver source files.")); // Set NTP servers #ifdef ESP8266 configTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); #elif defined(ESP32) configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); #endif // Wait for NTP sync (with timeout) getLocalTime(&ntpTime, 5000); // Create csv logs folder if not exists if (!LittleFS.exists(basePath)) { LittleFS.mkdir(basePath); } Serial.println("Setup completed."); } void loop() { server.handleClient(); if (server.isAccessPointMode()) server.updateDNS(); static uint32_t updateTime; if (millis() - updateTime > 30000) { updateTime = millis(); appenRow(); } } ================================================ FILE: examples/csvLogger/data/assets/css/index.css ================================================ /* Misc */ html, body { font-family: "Helvetica","Arial",sans-serif; font-size: 14px; font-weight: 400; line-height: 20px; } body { margin: 0; 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"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } h1 { margin: 5px; text-align: center; } p { font-size: 0.9rem; margin: 0.5rem 0 1.5rem 0; } a, a:visited { color: #08C; text-decoration: none; } a:hover, a:focus { color: #69c773; cursor: pointer; } a.delete-file, a.delete-file:visited { color: #CC0000; margin-left: 0.5rem; vertical-align: middle; } button { display: inline-block; border-radius: 3px; border: none; font-size: 0.9rem; padding: 0.5rem 1em; background: #86b32d; border-bottom: 1px solid #5d7d1f; color: white; margin: 5px 0; text-align: center; } button:hover { opacity: 0.75; cursor: pointer; } #page-wrapper { width: 95%; background: #FFF; padding: 1.25rem; margin: 1rem auto; min-height: 800px; border-top: 5px solid #69c773; box-shadow: 0 2px 10px rgba(0,0,0,0.8); } #content { width: 85%; overflow: auto; height: 86vh; } #files { width: 15%; } #files ul { margin: 20px 0; padding: 0.5rem 1rem; overflow-y: auto; list-style: square; background: #F7F7F7; border: 1px solid #D9D9D9; border-radius: 5px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); max-height: 75vh; } #files li { margin-left: 8px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; } .container { display: flex; flex-direction: row; align-content: flex-start; justify-content: space-between; align-items: flex-start; column-gap: 10px; height: 86vh; } .delete { font-size: 24px; transition: 0.3s; margin-top:5px; } .delete-all{ color: #f44336; font-size: 12px; background-color: transparent; background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; } /* Tables */ .table-holder { margin-top: 20px; border: 1px solid lightgray; border-radius: 5px; border-bottom: 0px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .tables { margin-bottom: 50px; } table { width: 100%; border-bottom: 0px; } table, th, td { border: 1px solid lightgrey; border-collapse: collapse; padding-left: 5px; } table tr:nth-child(even) { background-color: white; } table tr:nth-child(odd) { background-color: #f2f2f2; } table th { background-color: #e1e1e1; color: black; } /* Sections */ .section { box-shadow: 10px 10px 10px 10px; background-color: #e5e5e5; padding: 10px; padding-top: 20px; font-size: 18px; } .section-lightgrey { background-color: #f9f9f9; } /* 100% Image Width on Smaller Screens */ @media only screen and (max-width: 700px){ .modal-content { width: 100%; } } ================================================ FILE: examples/csvLogger/data/assets/css/style.css ================================================ /* Misc */ html, body { font-family: "Helvetica","Arial",sans-serif; font-size: 14px; font-weight: 400; line-height: 20px; } body { margin: 0; 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"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } h1 { margin: 5px; text-align: center; } p { font-size: 0.9rem; margin: 0.5rem 0 1.5rem 0; } a, a:visited { color: #08C; text-decoration: none; } a:hover, a:focus { color: #69c773; cursor: pointer; } a.delete-file, a.delete-file:visited { color: #CC0000; margin-left: 0.5rem; vertical-align: middle; } button { display: inline-block; border-radius: 3px; border: none; font-size: 0.9rem; padding: 0.5rem 1em; background: #86b32d; border-bottom: 1px solid #5d7d1f; color: white; margin: 5px 0; text-align: center; } button:hover { opacity: 0.75; cursor: pointer; } #page-wrapper { width: 95%; background: #FFF; padding: 1.25rem; margin: 1rem auto; min-height: 800px; border-top: 5px solid #69c773; box-shadow: 0 2px 10px rgba(0,0,0,0.8); } #content { width: 85%; overflow: auto; height: 86vh; } #files { width: 15%; line-height: 1; } #files ul { margin: 20px 0; padding: 0.5rem 1rem; overflow-y: auto; list-style: square; background: #F7F7F7; border: 1px solid #D9D9D9; border-radius: 5px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); max-height: 75vh; } #files li { font-size: 14px; display: flex; justify-content: space-between; align-items: center; } .container { display: flex; flex-direction: row; align-content: flex-start; justify-content: space-between; align-items: flex-start; column-gap: 10px; height: 86vh; } .delete { font-size: 24px; transition: 0.3s; margin-top:5px; } .delete-all{ color: #f44336; font-size: 12px; background-color: transparent; background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; } /* Tables */ .table-holder { margin-top: 20px; border: 1px solid lightgray; border-radius: 5px; border-bottom: 0px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .tables { margin-bottom: 50px; } table { width: 100%; border-bottom: 0px; } table, th, td { border: 1px solid lightgrey; border-collapse: collapse; padding-left: 5px; } table tr:nth-child(even) { background-color: white; } table tr:nth-child(odd) { background-color: #f2f2f2; } table th { background-color: #e1e1e1; color: black; } /* Sections */ .section { box-shadow: 10px 10px 10px 10px; background-color: #e5e5e5; padding: 10px; padding-top: 20px; font-size: 18px; } .section-lightgrey { background-color: #f9f9f9; } /* 100% Image Width on Smaller Screens */ @media only screen and (max-width: 700px){ .modal-content { width: 100%; } } ================================================ FILE: examples/csvLogger/data/assets/js/csv.js ================================================ // Default file to be loaded with no parameter in url var filename = ''; // JQuery-like selector var $ = function(el) { return document.getElementById(el); }; /** * @returns {getUserInput.userInput} an object * containing all the user input at the time * of the method call. */ function getUserInput() { var userInput = {}; userInput.fileName = filename; userInput.maxRows = "0"; userInput.encoding = 'UTF-8'; userInput.columnSeparator = ','; userInput.useQuotes = true; userInput.firstRowHeaders = true; userInput.firstRowInlcude = false; return userInput; } /* Tables */ var tableCount = 1; /** * Creates a table holder with the * given table in it. * * @param {type} title * @param {type} tableHtml * @returns {String|getTableUnit.tableHolder} */ function getTableUnit(title, tableHtml){ var id = "table-" + tableCount; var tableHolder = "
{@name}{@table}
"; tableHolder = tableHolder.replace("{@name}", title); tableHolder = tableHolder.replace("{@table}", tableHtml); tableCount++; return tableHolder; } /** * Clears all tables from the page. */ function clearTables(){ tableCount = 1; $('csv-table').innerHTML = ''; } /** * Adds (appends) a table holder to the page. * * @param {type} unit * @returns {undefined} */ function addTableUnit(unit){ $('csv-table').innerHTML = unit ; } function saveTable(filename, text) { var myblob = new Blob([text]); var formData = new FormData(); formData.append("data", myblob, filename); // POST data using the Fetch API fetch('/edit', { method: 'POST', headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Max-Age': '600', 'Access-Control-Allow-Methods': 'PUT,POST,GET,OPTIONS', 'Access-Control-Allow-Headers': '*', 'filename': filename }, body: formData }) // Handle the server response .then(response => response.text()) .then(text => { console.log(text); }); } /* Table downloading */ /** * Download the specified table to * the users computer. * * @param {type} table table number * @param {save} save file to host memory * @returns {undefined} */ function downloadTable(table, save = false) { var tableId = "table-" + table; var csvArray = []; var rows = document.querySelectorAll("#" + tableId + " > table tr"); for (var i = 0; i < rows.length; i++) { var row = [], cols = rows[i].querySelectorAll("td, th"); for (var j = 0; j < cols.length; j++) { var value = cols[j].innerText; if(customParseFloat(value) ) { row.push(value); } else { row.push('"' + value +'"'); } } csvArray.push(row.join(",")); csvArray.push("\n"); } var csvString = csvArray.join(""); if (save === false) download(filename, csvString); else { saveTable(filename, csvString); } } /** * Creates and downloads a file to the users computer. * * @param {type} filename * @param {type} text * @returns {undefined} */ function download(filename, text) { var element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } /** * Gets the users input and creates * a set of tables from it. */ function populate(csv){ var ui = getUserInput(); addTableUnit( getTableUnit( ui.fileName, getTable( csvTo2DArray( csv, ui.columnSeparator, ui.useQuotes, ui.maxRows ), ui.firstRowHeaders, ui.firstRowInlcude ) ) ); // Prevents to ad a new line inside cell var cells = document.querySelectorAll('td'); cells.forEach(item => { item.addEventListener('keypress', event => { if ( event.keyCode === 13 ){ if (window.event) { window.event.returnValue = false; } } }); }); } /** * Load the csv from webserver location and fit table with data */ function loadCsv(path) { clearTables(); filename = path; fetch(path) .then(response => response.text()) .then(textString => { populate(textString); }); } /** * Standard parseFloat don't handle properly "0", "0.0", "0.00" etc strings * @param {strNumber} the string representing a number to be parsed * @returns {float number} or NaN */ function customParseFloat(strNumber){ if (isNaN(parseFloat(strNumber)) === false){ let toFixedLength = 0; let arr = strNumber.split('.'); if (arr.length === 2 ){ toFixedLength = arr[1].length; } return parseFloat(strNumber).toFixed(toFixedLength); } return NaN; // Not a number } /** * Creates an unstyled, bare-bones html table * from the provided 2D(Multidimentional Array). * * @param {type} tableArray the 2D array. * @param {type} useHeaders will make the first row * in the table bold if true. * @param {type} dupeHeaders will duplicate the first row * in the table if true and useHeaders is true. * @param {type} tableId HTML ID for the table. * @returns {String} the constructed html table as text. */ function getTable(tableArray, useHeaders, dupeHeaders, tableId){ var tableOpen = ""; var tableClose = "
"; var headerCell = "{@val}"; var cell = "{@val}"; var rowOpen = ""; var rowClose = ""; var table = tableOpen; for(i = 0; i < tableArray.length; i++){ //Row if(i === 1 && useHeaders && dupeHeaders){ i = 0; useHeaders = false; dupeHeaders = false; } table += rowOpen; for(j = 0; j < tableArray[i].length; j++){ //Cell if(i === 0 && useHeaders){ table += headerCell.replace("{@val}", tableArray[i][j]); } else { table += cell.replace("{@val}", tableArray[i][j]); } } table += rowClose; } return table + tableClose; } /** * Creates a 2D (Multidimentional) array from * CSV data in string form. * * @param {type} csv the CSV data. * @param {type} separator the character used * to separate the columns/cells. * @param {type} quotes ignores the separator * in quoted text. * @param {type} maxRows the maximum rows * to scan. * @returns {Array|csvTo2DArray.table} the CSV data * as a 2D (Multidimentional) array. */ function csvTo2DArray(csv, separator, quotes, maxRows){ var table = []; var rows = 0; csv.split("\n").map(function(row){ if(maxRows !== "0") if(rows >= maxRows) return; var tableRow = getRow(row, separator, quotes); if(tableRow === null) return table; table.push(tableRow); rows++; }); return table; } /** * Creates an array from a CSV row (line) * * @param {type} row the CSV row. * @param {type} separator character used to separate * cells/columns * @param {type} quotes ignores the separator * in quoted text. * @returns {Array|getRow.trow} the CSV row as an array. */ function getRow(row, separator, quotes){ if(row.length === 0) return null; isQuoted = false; var trow = []; var cell = ""; for(var i = 0; i < row.length; i++){ var char = row.charAt(i); if(quotes){ if(char === '\"' || char === '\''){ isQuoted = !isQuoted; continue; } } if(char === separator && !isQuoted){ trow.push(cell); cell = ""; continue; } cell += char; } trow.push(cell); return trow; } ================================================ FILE: examples/csvLogger/data/assets/js/index.js ================================================ var dataFolder = document.getElementById("csv-path").value; var fileList = document.getElementById('file-list'); var currentFile = ""; // Fetch the list of files and fill the filelist function listFiles() { var url = '/list?dir=' + dataFolder; if (url.charAt(url.length - 1) === '/') url = url.slice(0, -1); // Remove the last character fetch(url) // Do the request .then(response => response.json()) // Parse the response .then(obj => { // DO something with response fileList.innerHTML = ''; obj.forEach(function(entry, i) { addEntry(entry.name); }); // Load last file loadCsv(dataFolder + obj[obj.length -1].name); }); } // Load selected image inside the preview content function loadFile(filename) { loadCsv(filename); } // Delete selected file in SD async function deleteFile(filename) { var isExecuted = confirm("Are you sure to delete "+ filename + "?"); if(isExecuted){ const data = new URLSearchParams(); data.append('path', filename); fetch('/edit', { method: 'DELETE', body: data }); // Update the file browser. listFiles(); } } async function deleteAll() { var isExecuted = confirm("Are you sure to delete all files in "+ dataFolder + " folder?"); if(isExecuted){ var ul = document.getElementById("file-list"); var items = ul.getElementsByClassName("edit-file"); for (var i=0; i ESP32 CSV List

CSV list web interface





    ================================================ FILE: examples/csvLogger/partitions.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x140000, app1, app, ota_1, 0x150000,0x140000, spiffs, data, spiffs, 0x290000,0x160000, coredump, data, coredump,0x3F0000,0x10000, ================================================ FILE: examples/csvLogger/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [platformio] src_dir = . [env:esp32-s3-devkitc1-n4r2] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32-s3-devkitc1-n4r2 framework = arduino upload_speed = 921600 monitor_speed = 115200 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples [env:esp32dev] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32dev framework = arduino upload_speed = 921600 monitor_speed = 115200 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples [env:esp8266-nodemcuv2] platform = espressif8266 board = nodemcuv2 framework = arduino upload_speed = 921600 monitor_speed = 115200 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples ================================================ FILE: examples/csvLogger/readme.md ================================================ An example for logging to a CSV file and viewing the content with the browser. It is also possible to modify or download the file. ![image](https://github.com/cotestatnt/esp-fs-webserver/assets/27758688/a776a217-f634-480c-873c-8914e82f87e3) ================================================ FILE: examples/csvLoggerSD/csvLoggerSD.ino ================================================ #include #include // Timezone definition to get properly time from NTP server #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" #include #define PIN_CS 14 #define PIN_SCK 13 #define PIN_MOSI 12 #define PIN_MISO 11 FSWebServer server(SD, 80, "myServer"); bool captiveRun = false; struct tm ntpTime; const char* basePath = "/csv"; //////////////////////////////// NTP Time ///////////////////////////////////////// void getUpdatedtime(const uint32_t timeout) { uint32_t start = millis(); do { time_t now = time(nullptr); ntpTime = *localtime(&now); delay(1); } while (millis() - start < timeout && ntpTime.tm_year <= (1970 - 1900)); } //////////////////////////////// Filesystem ///////////////////////////////////////// bool startFilesystem(){ if (SD.begin(PIN_CS)){ server.printFileList(SD, "/", 2); return true; } else { Serial.println("ERROR on mounting filesystem. It will be formmatted!"); ESP.restart(); } return false; } //////////////////////////// Append a row to csv file /////////////////////////////////// bool appenRow() { getUpdatedtime(10); char filename[24]; snprintf(filename, sizeof(filename), "%s/%04d_%02d_%02d.csv", basePath, ntpTime.tm_year + 1900, ntpTime.tm_mon + 1, ntpTime.tm_mday ); File file; if (SD.exists(filename)) { file = SD.open(filename, "a"); // Append to existing file } else { file = SD.open(filename, "w"); // Create a new file file.println("timestamp, free heap, largest free block, connected, wifi strength"); } if (file) { char timestamp[25]; strftime(timestamp, sizeof(timestamp), "%c", &ntpTime); char row[64]; #ifdef ESP32 snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", timestamp, heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0), (WiFi.status() == WL_CONNECTED) ? "true" : "false", WiFi.RSSI() ); #elif defined(ESP8266) uint32_t free; uint16_t max; ESP.getHeapStats(&free, &max, nullptr); snprintf(row, sizeof(row), "%s, %d, %d, %s, %d", timestamp, free, max, (WiFi.status() == WL_CONNECTED) ? "true" : "false", WiFi.RSSI() ); #endif Serial.println(row); file.println(row); file.close(); return true; } return false; } void setup() { SPI.begin(PIN_SCK, PIN_MISO, PIN_MOSI, PIN_CS); Serial.begin(115200); delay(1000); startFilesystem(); // Create csv logs folder if not exists if (!SD.exists(basePath)) { SD.mkdir(basePath); } // Try to connect to WiFi (will start AP if not connected after timeout) if (!server.startWiFi(10000)) { Serial.println("\nWiFi not connected! Starting AP mode..."); server.startCaptivePortal("ESP_AP", "123456789", "/setup"); captiveRun = true; } // Enable ACE FS file web editor and add FS info callback fucntion server.enableFsCodeEditor(); #ifdef ESP32 server.setFsInfoCallback([](fsInfo_t* fsInfo) { fsInfo->totalBytes = SD.totalBytes(); fsInfo->usedBytes = SD.usedBytes(); fsInfo->fsName = "SD"; }); #endif // Start server server.begin(); Serial.print(F("\nAsync ESP Web Server started on IP Address: ")); Serial.println(server.getServerIP()); Serial.println(F( "This is \"scvLoggerSdFat.ino\" example.\n" "Open /setup page to configure optional parameters.\n" "Open /edit page to view, edit or upload example or your custom webserver source files." )); // Set NTP servers #ifdef ESP8266 configTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); #elif defined(ESP32) configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); #endif // Wait for NTP sync (with timeout) getUpdatedtime(5000); appenRow(); } void loop() { server.handleClient(); if (captiveRun) server.updateDNS(); static uint32_t updateTime; if (millis()- updateTime > 30000) { updateTime = millis(); appenRow(); } } ================================================ FILE: examples/csvLoggerSD/data/assets/css/index.css ================================================ /* Misc */ html, body { font-family: "Helvetica","Arial",sans-serif; font-size: 14px; font-weight: 400; line-height: 20px; } body { margin: 0; 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"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } h1 { margin: 5px; text-align: center; } p { font-size: 0.9rem; margin: 0.5rem 0 1.5rem 0; } a, a:visited { color: #08C; text-decoration: none; } a:hover, a:focus { color: #69c773; cursor: pointer; } a.delete-file, a.delete-file:visited { color: #CC0000; margin-left: 0.5rem; vertical-align: middle; } button { display: inline-block; border-radius: 3px; border: none; font-size: 0.9rem; padding: 0.5rem 1em; background: #86b32d; border-bottom: 1px solid #5d7d1f; color: white; margin: 5px 0; text-align: center; } button:hover { opacity: 0.75; cursor: pointer; } #page-wrapper { width: 95%; background: #FFF; padding: 1.25rem; margin: 1rem auto; min-height: 800px; border-top: 5px solid #69c773; box-shadow: 0 2px 10px rgba(0,0,0,0.8); } #content { width: 85%; overflow: auto; height: 86vh; } #files { width: 15%; } #files ul { margin: 20px 0; padding: 0.5rem 1rem; overflow-y: auto; list-style: square; background: #F7F7F7; border: 1px solid #D9D9D9; border-radius: 5px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); max-height: 75vh; } #files li { margin-left: 8px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; } .container { display: flex; flex-direction: row; align-content: flex-start; justify-content: space-between; align-items: flex-start; column-gap: 10px; height: 86vh; } .delete { font-size: 24px; transition: 0.3s; margin-top:5px; } .delete-all{ color: #f44336; font-size: 12px; background-color: transparent; background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; } /* Tables */ .table-holder { margin-top: 20px; border: 1px solid lightgray; border-radius: 5px; border-bottom: 0px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .tables { margin-bottom: 50px; } table { width: 100%; border-bottom: 0px; } table, th, td { border: 1px solid lightgrey; border-collapse: collapse; padding-left: 5px; } table tr:nth-child(even) { background-color: white; } table tr:nth-child(odd) { background-color: #f2f2f2; } table th { background-color: #e1e1e1; color: black; } /* Sections */ .section { box-shadow: 10px 10px 10px 10px; background-color: #e5e5e5; padding: 10px; padding-top: 20px; font-size: 18px; } .section-lightgrey { background-color: #f9f9f9; } /* 100% Image Width on Smaller Screens */ @media only screen and (max-width: 700px){ .modal-content { width: 100%; } } ================================================ FILE: examples/csvLoggerSD/data/assets/css/style.css ================================================ /* Misc */ html, body { font-family: "Helvetica","Arial",sans-serif; font-size: 14px; font-weight: 400; line-height: 20px; } body { margin: 0; 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"; font-size: 1rem; font-weight: 400; line-height: 1.5; color: #212529; text-align: left; background-color: #fff; } h1 { margin: 5px; text-align: center; } p { font-size: 0.9rem; margin: 0.5rem 0 1.5rem 0; } a, a:visited { color: #08C; text-decoration: none; } a:hover, a:focus { color: #69c773; cursor: pointer; } a.delete-file, a.delete-file:visited { color: #CC0000; margin-left: 0.5rem; vertical-align: middle; } button { display: inline-block; border-radius: 3px; border: none; font-size: 0.9rem; padding: 0.5rem 1em; background: #86b32d; border-bottom: 1px solid #5d7d1f; color: white; margin: 5px 0; text-align: center; } button:hover { opacity: 0.75; cursor: pointer; } #page-wrapper { width: 95%; background: #FFF; padding: 1.25rem; margin: 1rem auto; min-height: 800px; border-top: 5px solid #69c773; box-shadow: 0 2px 10px rgba(0,0,0,0.8); } #content { width: 85%; overflow: auto; height: 86vh; } #files { width: 15%; line-height: 1; } #files ul { margin: 20px 0; padding: 0.5rem 1rem; overflow-y: auto; list-style: square; background: #F7F7F7; border: 1px solid #D9D9D9; border-radius: 5px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); max-height: 75vh; } #files li { font-size: 14px; display: flex; justify-content: space-between; align-items: center; } .container { display: flex; flex-direction: row; align-content: flex-start; justify-content: space-between; align-items: flex-start; column-gap: 10px; height: 86vh; } .delete { font-size: 24px; transition: 0.3s; margin-top:5px; } .delete-all{ color: #f44336; font-size: 12px; background-color: transparent; background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; } /* Tables */ .table-holder { margin-top: 20px; border: 1px solid lightgray; border-radius: 5px; border-bottom: 0px; border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; } .tables { margin-bottom: 50px; } table { width: 100%; border-bottom: 0px; } table, th, td { border: 1px solid lightgrey; border-collapse: collapse; padding-left: 5px; } table tr:nth-child(even) { background-color: white; } table tr:nth-child(odd) { background-color: #f2f2f2; } table th { background-color: #e1e1e1; color: black; } /* Sections */ .section { box-shadow: 10px 10px 10px 10px; background-color: #e5e5e5; padding: 10px; padding-top: 20px; font-size: 18px; } .section-lightgrey { background-color: #f9f9f9; } /* 100% Image Width on Smaller Screens */ @media only screen and (max-width: 700px){ .modal-content { width: 100%; } } ================================================ FILE: examples/csvLoggerSD/data/assets/js/csv.js ================================================ // Default file to be loaded with no parameter in url var filename = ''; // JQuery-like selector var $ = function(el) { return document.getElementById(el); }; /** * @returns {getUserInput.userInput} an object * containing all the user input at the time * of the method call. */ function getUserInput() { var userInput = {}; userInput.fileName = filename; userInput.maxRows = "0"; userInput.encoding = 'UTF-8'; userInput.columnSeparator = ','; userInput.useQuotes = true; userInput.firstRowHeaders = true; userInput.firstRowInlcude = false; return userInput; } /* Tables */ var tableCount = 1; /** * Creates a table holder with the * given table in it. * * @param {type} title * @param {type} tableHtml * @returns {String|getTableUnit.tableHolder} */ function getTableUnit(title, tableHtml){ var id = "table-" + tableCount; var tableHolder = "
    {@name}{@table}
    "; tableHolder = tableHolder.replace("{@name}", title); tableHolder = tableHolder.replace("{@table}", tableHtml); tableCount++; return tableHolder; } /** * Clears all tables from the page. */ function clearTables(){ tableCount = 1; $('csv-table').innerHTML = ''; } /** * Adds (appends) a table holder to the page. * * @param {type} unit * @returns {undefined} */ function addTableUnit(unit){ $('csv-table').innerHTML = unit ; } function saveTable(filename, text) { var myblob = new Blob([text]); var formData = new FormData(); formData.append("data", myblob, filename); // POST data using the Fetch API fetch('/edit', { method: 'POST', headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Max-Age': '600', 'Access-Control-Allow-Methods': 'PUT,POST,GET,OPTIONS', 'Access-Control-Allow-Headers': '*', 'filename': filename }, body: formData }) // Handle the server response .then(response => response.text()) .then(text => { console.log(text); }); } /* Table downloading */ /** * Download the specified table to * the users computer. * * @param {type} table table number * @param {save} save file to host memory * @returns {undefined} */ function downloadTable(table, save = false) { var tableId = "table-" + table; var csvArray = []; var rows = document.querySelectorAll("#" + tableId + " > table tr"); for (var i = 0; i < rows.length; i++) { var row = [], cols = rows[i].querySelectorAll("td, th"); for (var j = 0; j < cols.length; j++) { var value = cols[j].innerText; if(customParseFloat(value) ) { row.push(value); } else { row.push('"' + value +'"'); } } csvArray.push(row.join(",")); csvArray.push("\n"); } var csvString = csvArray.join(""); if (save === false) download(filename, csvString); else { saveTable(filename, csvString); } } /** * Creates and downloads a file to the users computer. * * @param {type} filename * @param {type} text * @returns {undefined} */ function download(filename, text) { var element = document.createElement('a'); element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); element.setAttribute('download', filename); element.style.display = 'none'; document.body.appendChild(element); element.click(); document.body.removeChild(element); } /** * Gets the users input and creates * a set of tables from it. */ function populate(csv){ var ui = getUserInput(); addTableUnit( getTableUnit( ui.fileName, getTable( csvTo2DArray( csv, ui.columnSeparator, ui.useQuotes, ui.maxRows ), ui.firstRowHeaders, ui.firstRowInlcude ) ) ); // Prevents to ad a new line inside cell var cells = document.querySelectorAll('td'); cells.forEach(item => { item.addEventListener('keypress', event => { if ( event.keyCode === 13 ){ if (window.event) { window.event.returnValue = false; } } }); }); } /** * Load the csv from webserver location and fit table with data */ function loadCsv(path) { clearTables(); filename = path; fetch(path) .then(response => response.text()) .then(textString => { populate(textString); }); } /** * Standard parseFloat don't handle properly "0", "0.0", "0.00" etc strings * @param {strNumber} the string representing a number to be parsed * @returns {float number} or NaN */ function customParseFloat(strNumber){ if (isNaN(parseFloat(strNumber)) === false){ let toFixedLength = 0; let arr = strNumber.split('.'); if (arr.length === 2 ){ toFixedLength = arr[1].length; } return parseFloat(strNumber).toFixed(toFixedLength); } return NaN; // Not a number } /** * Creates an unstyled, bare-bones html table * from the provided 2D(Multidimentional Array). * * @param {type} tableArray the 2D array. * @param {type} useHeaders will make the first row * in the table bold if true. * @param {type} dupeHeaders will duplicate the first row * in the table if true and useHeaders is true. * @param {type} tableId HTML ID for the table. * @returns {String} the constructed html table as text. */ function getTable(tableArray, useHeaders, dupeHeaders, tableId){ var tableOpen = ""; var tableClose = "
    "; var headerCell = "{@val}"; var cell = "{@val}"; var rowOpen = ""; var rowClose = ""; var table = tableOpen; for(i = 0; i < tableArray.length; i++){ //Row if(i === 1 && useHeaders && dupeHeaders){ i = 0; useHeaders = false; dupeHeaders = false; } table += rowOpen; for(j = 0; j < tableArray[i].length; j++){ //Cell if(i === 0 && useHeaders){ table += headerCell.replace("{@val}", tableArray[i][j]); } else { table += cell.replace("{@val}", tableArray[i][j]); } } table += rowClose; } return table + tableClose; } /** * Creates a 2D (Multidimentional) array from * CSV data in string form. * * @param {type} csv the CSV data. * @param {type} separator the character used * to separate the columns/cells. * @param {type} quotes ignores the separator * in quoted text. * @param {type} maxRows the maximum rows * to scan. * @returns {Array|csvTo2DArray.table} the CSV data * as a 2D (Multidimentional) array. */ function csvTo2DArray(csv, separator, quotes, maxRows){ var table = []; var rows = 0; csv.split("\n").map(function(row){ if(maxRows !== "0") if(rows >= maxRows) return; var tableRow = getRow(row, separator, quotes); if(tableRow === null) return table; table.push(tableRow); rows++; }); return table; } /** * Creates an array from a CSV row (line) * * @param {type} row the CSV row. * @param {type} separator character used to separate * cells/columns * @param {type} quotes ignores the separator * in quoted text. * @returns {Array|getRow.trow} the CSV row as an array. */ function getRow(row, separator, quotes){ if(row.length === 0) return null; isQuoted = false; var trow = []; var cell = ""; for(var i = 0; i < row.length; i++){ var char = row.charAt(i); if(quotes){ if(char === '\"' || char === '\''){ isQuoted = !isQuoted; continue; } } if(char === separator && !isQuoted){ trow.push(cell); cell = ""; continue; } cell += char; } trow.push(cell); return trow; } ================================================ FILE: examples/csvLoggerSD/data/assets/js/index.js ================================================ var dataFolder = document.getElementById("csv-path").value; var fileList = document.getElementById('file-list'); var currentFile = ""; // Fetch the list of files and fill the filelist function listFiles() { var url = '/list?dir=' + dataFolder; if (url.charAt(url.length - 1) === '/') url = url.slice(0, -1); // Remove the last character fetch(url) // Do the request .then(response => response.json()) // Parse the response .then(obj => { // DO something with response fileList.innerHTML = ''; obj.forEach(function(entry, i) { addEntry(entry.name); }); // Load last file loadCsv(dataFolder + obj[obj.length -1].name); }); } // Load selected image inside the preview content function loadFile(filename) { loadCsv(filename); } // Delete selected file in SD async function deleteFile(filename) { var isExecuted = confirm("Are you sure to delete "+ filename + "?"); if(isExecuted){ const data = new URLSearchParams(); data.append('path', filename); fetch('/edit', { method: 'DELETE', body: data }); // Update the file browser. listFiles(); } } async function deleteAll() { var isExecuted = confirm("Are you sure to delete all files in "+ dataFolder + " folder?"); if(isExecuted){ var ul = document.getElementById("file-list"); var items = ul.getElementsByClassName("edit-file"); for (var i=0; i ESP32 CSV List

    CSV list web interface





      ================================================ FILE: examples/csvLoggerSD/partitions.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x140000, app1, app, ota_1, 0x150000,0x140000, spiffs, data, spiffs, 0x290000,0x160000, coredump, data, coredump,0x3F0000,0x10000, ================================================ FILE: examples/csvLoggerSD/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [platformio] src_dir = . [env:esp32-s3-devkitc1-n4r2] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32-s3-devkitc1-n4r2 framework = arduino upload_speed = 921600 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples [env:esp32dev] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32dev framework = arduino upload_speed = 921600 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples [env:esp8266-nodemcuv2] platform = espressif8266 board = nodemcuv2 framework = arduino upload_speed = 921600 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples ================================================ FILE: examples/csvLoggerSD/readme.md ================================================ An example for logging to a CSV file and viewing the content with the browser. It is also possible to modify or download the file. ![image](https://github.com/cotestatnt/esp-fs-webserver/assets/27758688/a776a217-f634-480c-873c-8914e82f87e3) ================================================ FILE: examples/customHTML/customElements.h ================================================ #pragma once #include /* * This HTML code will be injected in /setup webpage using a
      element as parent * The parent element will have the HTML id properties equal to 'raw-html-' * where the id value will be equal to the id parameter passed to the function addHTML(html_code, id). */ inline const char custom_html[] PROGMEM = R"EOF(

      Fecth url
      
      )EOF";
      
      
      /*
      * In this example, a style sections is added in order to render properly the new
      * 
                  
                
              
          
              

      Select image

        ================================================ FILE: examples/esp32-cam/data/www/styles.css ================================================ *, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; } html { font-family: Helvetica, Arial, sans-serif; font-size: 100%; background: #333; color: #33383D; -webkit-font-smoothing: antialiased; } #page-wrapper { width: 960px; background: #FFF; padding: 1.25rem; margin: 1rem auto; min-height: 300px; border-top: 5px solid #69c773; box-shadow: 0 2px 10px rgba(0,0,0,0.8); } h2 { margin-top: 0; font-size: 0.9rem; letter-spacing: 1px; color: #999; } p { font-size: 0.9rem; margin: 0.5rem 0 1.5rem 0; } a, a:visited { color: #08C; text-decoration: none; } a:hover, a:focus { color: #69c773; cursor: pointer; } a.delete-file, a.delete-file:visited { color: #CC0000; margin-left: 0.5rem; vertical-align: middle; } .field { margin-bottom: 1rem; } button { width: 150px; display: inline-block; border-radius: 3px; border: none; font-size: 0.9rem; padding: 0.6rem 1em; background: #86b32d; border-bottom: 1px solid #5d7d1f; color: white; margin: 0 0.25rem; text-align: center; } button:hover { opacity: 0.75; cursor: pointer; } #file-form { width: 75%; float: left; } #files { width: 23%; float: right; } #files ul { margin: 0; padding: 0.5rem 1rem; max-height: 600px; overflow-y: auto; list-style: square; background: #F7F7F7; border: 1px solid #D9D9D9; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.1); } #files li { margin-left: 8px; font-size: 14px; } /* Clearfix Utils */ .clearfix { *zoom: 1; } .clearfix:before, .clearfix:after { display: table; line-height: 0; content: ""; } .clearfix:after { clear: both; } #image-content { border-radius: 5px; cursor: pointer; transition: 0.3s; } #image-content:hover {opacity: 0.7;} /* The Modal (background) */ .modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1; /* Sit on top */ padding-top: 100px; /* Location of the box */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.9); /* Black w/ opacity */ } /* Modal Content (image) */ .modal-content { margin: auto; display: block; width: 90%; } /* Caption of Modal Image */ #caption { margin: auto; display: block; text-align: center; color: #ccc; padding: 10px 0; } /* Add Animation */ .modal-content, #caption { -webkit-animation-name: zoom; -webkit-animation-duration: 0.6s; animation-name: zoom; animation-duration: 0.6s; } @-webkit-keyframes zoom { from {-webkit-transform:scale(0)} to {-webkit-transform:scale(1)} } @keyframes zoom { from {transform:scale(0)} to {transform:scale(1)} } /* The Close Button */ .close { position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; transition: 0.3s; } .close:hover, .close:focus { color: #bbb; text-decoration: none; cursor: pointer; } .delete { font-size: 30px; font-weight: bold; transition: 0.3s; } .delete-all{ color: #f44336; font-size: 12px; background-color: transparent; background-repeat: no-repeat; border: none; cursor: pointer; overflow: hidden; outline: none; } /* 100% Image Width on Smaller Screens */ @media only screen and (max-width: 700px){ .modal-content { width: 100%; } } ================================================ FILE: examples/esp32-cam/esp32-cam.ino ================================================ #include #include #include #include // https://github.com/cotestatnt/esp-fs-webserver/ #include "esp_camera.h" #include "soc/soc.h" // Brownout error fix #include "soc/rtc_cntl_reg.h" // Brownout error fix #if ESP_ARDUINO_VERSION_MAJOR >= 3 #include "soc/soc_caps.h" #endif // Local include files with camera pin definition and configuration #include "camera_pins.h" // Timezone definition to get properly time from NTP server #define MYTZ "CET-1CEST,M3.5.0,M10.5.0/3" // Set default file system type #define USE_LITTLEFS 1 #define USE_SDMMC 2 #define FILESYSTEM_TYPE USE_LITTLEFS #if (FILESYSTEM_TYPE == USE_SDMMC) #define FILESYSTEM SD_MMC #elif (FILESYSTEM_TYPE == USE_LITTLEFS) #define FILESYSTEM LittleFS #endif FSWebServer server(FILESYSTEM, 80, "esp32cam"); // Functions prototype void setInterval(); void getPicture(); // Struct for saving time datas (needed for time-naming the image files) struct tm tInfo; uint16_t grabInterval = 0; // Grab a picture every x seconds uint32_t lastGrabTime = 0; const char* getFolder = "/img"; /////////////////////////////////// SETUP /////////////////////////////////////// void setup() { WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // disable brownout detect // Flash LED setup pinMode(LAMP_PIN, OUTPUT); // set the lamp pin as output #if ESP_ARDUINO_VERSION_MAJOR >= 3 // Nuova API: ledcAttach(pin, freq, resolution) ledcAttach(LAMP_PIN, pwmfreq, pwmresolution); #else ledcSetup(lampChannel, pwmfreq, pwmresolution); // configure LED PWM channel ledcAttachPin(LAMP_PIN, lampChannel); #endif setLamp(0); // set default value Serial.begin(115200); Serial.println(); // Try to connect to WiFi (will start AP if not connected after timeout) if (!server.startWiFi(10000)) { Serial.println("\nWiFi not connected! Starting AP mode..."); server.startCaptivePortal("ESP32CAM_AP", "123456789", "/setup"); } // Sync time with NTP configTzTime(MYTZ, "time.google.com", "time.windows.com", "pool.ntp.org"); #if (FILESYSTEM_TYPE == USE_SDMMC) /* Init onboard SD filesystem (format if necessary) SD_MMC.begin(const char * mountpoint, bool mode1bit, bool format_if_mount_failed, int sdmmc_frequency, uint8_t maxOpenFiles) To avoid led glowing, set mode1bit = true (SD HS_DATA1 is tied to GPIO4, the same of on-board flash led) */ if (!SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_HIGHSPEED, 5)) { Serial.println("\nSD Mount Failed.\n"); } else if (!SD_MMC.exists(getFolder)) { if(SD_MMC.mkdir(getFolder)) Serial.println("Dir created"); else Serial.println("mkdir failed"); } #elif (FILESYSTEM_TYPE == USE_LITTLEFS) if (!LittleFS.begin()) { Serial.println("ERROR on mounting LittleFS. It will be formmatted!"); LittleFS.format(); ESP.restart(); } else if (!LittleFS.exists(getFolder)) { if(LittleFS.mkdir(getFolder)) Serial.println("Dir created"); else Serial.println("mkdir failed"); } Serial.println("LittleFS mounted successfully"); #endif // List all files stored in filesystem server.printFileList(FILESYSTEM, "/", 1, Serial); // Enable ACE FS file web editor and add FS info callback function server.enableFsCodeEditor(); // Add custom handlers to webserver server.on("/getPicture", getPicture); server.on("/setInterval", setInterval); // Start server with built-in websocket event handler server.begin(); Serial.print(F("\nESP Web Server started on IP Address: ")); Serial.println(server.getServerIP()); Serial.println(F( "This is \"remoteOTA.ino\" example.\n" "Open /setup page to configure optional parameters.\n" "Open /edit page to view, edit or upload example or your custom webserver source files." )); // Init the camera module (according the camera_config_t defined) init_camera(); } /////////////////////////////////// LOOP /////////////////////////////////////// void loop() { server.run(); if (grabInterval) { if (millis() - lastGrabTime > grabInterval *1000) { lastGrabTime = millis(); getPicture(); } } } ////////////////////////////////// FUNCTIONS////////////////////////////////////// void setInterval() { if (server.hasArg("val")) { grabInterval = server.arg("val").toInt(); Serial.printf("Set grab interval every %d seconds\n", grabInterval); } server.send(200, "text/plain", "OK"); } // Lamp Control void setLamp(int newVal) { if (newVal < 0) return; // Apply exponential scaling to have a better visual effect int brightness = round((pow(2, (1 + (newVal * 0.02))) - 2) / 6 * pwmMax); #if ESP_ARDUINO_VERSION_MAJOR >= 3 // ledcWrite(pin, value) with new API ledcWrite(LAMP_PIN, brightness); #else ledcWrite(lampChannel, brightness); #endif Serial.print("Lamp: "); Serial.print(newVal); Serial.print("%, pwm = "); Serial.println(brightness); } // Send a picture taken from CAM to a Telegram chat void getPicture() { // Take Picture with Camera; Serial.println("Camera capture requested"); // Take Picture with Camera and store in ram buffer fb setLamp(100); delay(100); camera_fb_t *fb = esp_camera_fb_get(); setLamp(0); if (!fb) { Serial.println("Camera capture failed"); server.send(500, "text/plain", "ERROR. Image grab failed"); return; } // Keep files on SD memory, filename is time based (YYYYMMDD_HHMMSS.jpg) // Embedded filesystem is too small to keep all images, overwrite the same file char filename[20]; time_t now = time(nullptr); tInfo = *localtime(&now); strftime(filename, sizeof(filename), "%Y%m%d_%H%M%S.jpg", &tInfo); char filePath[30]; strcpy(filePath, getFolder); strcat(filePath, "/"); strcat(filePath, filename); File file = FILESYSTEM.open(filePath, "w"); if (!file) { Serial.println("Failed to open file in writing mode"); server.send(500, "text/plain", "ERROR. Image grab failed"); return; } // size_t _jpg_buf_len = 0; // uint8_t *_jpg_buf = NULL; // bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len); file.write(fb->buf, fb->len); file.close(); Serial.printf("Saved file to path: %s - %zu bytes\n", filePath, fb->len); // Clear buffer esp_camera_fb_return(fb); server.send(200, "text/plain", filename); } ================================================ FILE: examples/esp32-cam/partitions.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x140000, app1, app, ota_1, 0x150000,0x140000, spiffs, data, spiffs, 0x290000,0x160000, coredump, data, coredump,0x3F0000,0x10000, ================================================ FILE: examples/esp32-cam/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [platformio] src_dir = . # Questo esempio è compatibile solo con schede ESP32 dotate di CAM (ad es. AI Thinker, ESP32-CAM, TTGO T-Journal, ecc.) # Scegli la board corretta e decommenta la sezione corrispondente, oppure aggiungi la tua board compatibile. [env:esp32cam] platform = espressif32 board = esp32cam framework = arduino upload_speed = 921600 board_build.partitions = partitions.csv lib_extra_dirs = ../../ lib_ignore = pio_examples # Esempio per AI Thinker (decommenta se necessario) # [env:esp32cam-ai-thinker] # platform = espressif32 # board = esp32cam # framework = arduino # upload_speed = 921600 # board_build.partitions = partitions.csv # lib_extra_dirs = ../../ # lib_ignore = pio_examples ================================================ FILE: examples/gpio_list/.gitignore ================================================ .pio .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: examples/gpio_list/data/index.htm ================================================ ESP GPIO dinamic list

        GPIO status list

        Pin Name Pin number Type Level
        ================================================ FILE: examples/gpio_list/data/script.js ================================================ const svgLightOn = '' ; const svgLightOff = ''; /** * Custom selector "JQuery style", but in plain "Vanilla JS" */ var $ = function(el) { return document.getElementById(el); }; /** * Start a websocket client and set event callback functions */ function ws_connect() { var ws = new WebSocket('ws://' + location.hostname + ':81/'); ws.onopen = function() { ws.send('Connected - ' + new Date()); getGpioList(); }; ws.onmessage = function(e) { parseMessage(e.data); }; ws.onclose = function(e) { setTimeout(function() { ws_connect(); }, 1000); }; ws.onerror = function(err) { ws.close(); }; return ws; } /** * Send data "cmds" to ESP */ function sendCommand(cmd, pin, level) { var data = { cmd: cmd, pin: parseInt(pin), level: level }; console.log(data); connection.send(JSON.stringify(data)); } /** * Parse messages receveid via websocket */ function parseMessage(msg) { const obj = JSON.parse(msg); if (typeof obj === 'object' && obj !== null) { if (obj.esptime !== null) { var date = new Date(0); // The 0 sets the date to epoch if( date.setUTCSeconds(obj.esptime)) document.getElementById("esp-time").innerHTML = date; } updateGpiosList(obj); } } /** * Read GPIO list status */ function getGpioList() { fetch('/getGpioList') // Do the request .then(response => response.json()) // Parse the response .then(obj => { // DO something with response updateGpiosList(obj); }); } /** * Iterate to the gpio list passed as parameter anc create DOMs dinamically */ function updateGpiosList(elems) { // Get reference to gpio-list element and clear content const list = document.querySelector('#gpio-list'); list.innerHTML = ""; // Draw all input rows const inputs = Object.entries(elems).filter((item) => item[1].type === 'input'); inputs.forEach(el => { const obj = el[1]; var lbl = obj.level ? `${svgLightOn} HIGH` : `${svgLightOff} LOW`; // Create a single row with all columns var row = document.createElement('tr'); row.innerHTML = '' + obj.label + ''; row.innerHTML += '' + obj.pin + ''; row.innerHTML += '' + obj.type + ''; row.innerHTML += `` ; // Append this row to list list.appendChild(row); }); // Draw all output rows const outputs = Object.entries(elems).filter((item) => item[1].type === 'output'); outputs.forEach(el => { const obj = el[1]; var lbl = obj.level ? ` checked>Turn OFF` : `>Turn ON`; // Create a single row with all columns var row = document.createElement('tr'); row.innerHTML = '' + obj.label + ''; row.innerHTML += '' + obj.pin + ''; row.innerHTML += '' + obj.type + ''; row.innerHTML += `