From a39a3fbaecc2f022854f1a7bb211444a9c26ebb6 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 20 Jul 2025 13:25:55 +0000 Subject: [PATCH 01/34] save network config on nvs --- components/configuration/configuration.c | 51 +++++++++++++++++++ .../configuration/include/configuration.h | 15 ++++++ .../mqtt5_connection/mqtt5_connection.c | 7 +-- components/wifi_utils/wifi_utils.c | 16 +++--- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/components/configuration/configuration.c b/components/configuration/configuration.c index 9083544..1658cf4 100644 --- a/components/configuration/configuration.c +++ b/components/configuration/configuration.c @@ -10,6 +10,11 @@ #define CONFIG_PUMP_CYCLES_PUMP_TIME_S_NAME "PcPts" #define CONFIG_PUMP_CYCLES_NR_PUMP_CYCLES_NAME "PcNpc" #define CONFIG_PUMP_CYCLES_TIMES_MINUTES_PER_DAY_NAME "PcTmpd" +#define CONFIG_WIFI_SSID_NAME "NetSsid" +#define CONFIG_WIFI_PASSWORD_NAME "NetPass" +#define CONFIG_MQTT_BROKER_NAME "NetMqttB" +#define CONFIG_MQTT_USERNAME_NAME "NetMqttU" +#define CONFIG_MQTT_PASSWORD_NAME "NetMqttP" // Maximum number of task to be notified if the config changes #define CONFIG_MAX_NUMBER_TASK_TO_NOTIFY 20 @@ -24,6 +29,14 @@ struct configuration_t configuration = { .nr_pump_cycles = 3, .times_minutes_per_day = {6 * 60, 12 * 60, 20 * 60}, }, + .network = + { + .ssid = CONFIG_WIFI_SSID, + .password = CONFIG_WIFI_PASSWORD, + .mqtt_broker = CONFIG_MQTT_BROKER_URI, + .mqtt_username = CONFIG_MQTT_USERNAME, + .mqtt_password = CONFIG_MQTT_PASSWORD, + }, }; // Array containing all task handles which need to be notified @@ -56,6 +69,28 @@ void load_configuration() { nvs_get_blob(my_handle, CONFIG_PUMP_CYCLES_TIMES_MINUTES_PER_DAY_NAME, configuration.pump_cycles.times_minutes_per_day, &size)); + // Load network configuration + size_t wifi_ssid_length = sizeof(configuration.network.ssid); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_get_str(my_handle, CONFIG_WIFI_SSID_NAME, + configuration.network.ssid, + &wifi_ssid_length)); + size_t wifi_password_length = sizeof(configuration.network.password); + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_get_str(my_handle, CONFIG_WIFI_PASSWORD_NAME, + configuration.network.password, &wifi_password_length)); + size_t mqtt_broker_length = sizeof(configuration.network.mqtt_broker); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_get_str(my_handle, CONFIG_MQTT_BROKER_NAME, + configuration.network.mqtt_broker, + &mqtt_broker_length)); + size_t mqtt_username_length = sizeof(configuration.network.mqtt_username); + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_get_str(my_handle, CONFIG_MQTT_USERNAME_NAME, + configuration.network.mqtt_username, &mqtt_username_length)); + size_t mqtt_password_length = sizeof(configuration.network.mqtt_password); + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_get_str(my_handle, CONFIG_MQTT_PASSWORD_NAME, + configuration.network.mqtt_password, &mqtt_password_length)); + nvs_close(my_handle); } @@ -67,6 +102,8 @@ void save_configuration() { ESP_ERROR_CHECK_WITHOUT_ABORT( nvs_set_u8(my_handle, CONFIG_ID_NAME, configuration.id)); + + // Pump cycles ESP_ERROR_CHECK_WITHOUT_ABORT( nvs_set_u16(my_handle, CONFIG_PUMP_CYCLES_PUMP_TIME_S_NAME, configuration.pump_cycles.pump_time_s)); @@ -79,6 +116,20 @@ void save_configuration() { nvs_set_blob(my_handle, CONFIG_PUMP_CYCLES_TIMES_MINUTES_PER_DAY_NAME, configuration.pump_cycles.times_minutes_per_day, size)); + // Network configuration + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_str(my_handle, CONFIG_WIFI_SSID_NAME, + configuration.network.ssid)); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_str( + my_handle, CONFIG_WIFI_PASSWORD_NAME, configuration.network.password)); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_str(my_handle, CONFIG_MQTT_BROKER_NAME, + configuration.network.mqtt_broker)); + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_set_str(my_handle, CONFIG_MQTT_USERNAME_NAME, + configuration.network.mqtt_username)); + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_set_str(my_handle, CONFIG_MQTT_PASSWORD_NAME, + configuration.network.mqtt_password)); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_commit(my_handle)); nvs_close(my_handle); } diff --git a/components/configuration/include/configuration.h b/components/configuration/include/configuration.h index e657cfc..ae48392 100644 --- a/components/configuration/include/configuration.h +++ b/components/configuration/include/configuration.h @@ -20,6 +20,20 @@ struct pump_cycle_configuration_t { // start the pump }; +#define WIFI_SSID_MAX_LENGTH 32 +#define WIFI_PASSWORD_MAX_LENGTH 64 +#define MQTT_BROKER_MAX_LENGTH 128 +#define MQTT_USERNAME_MAX_LENGTH 64 +#define MQTT_PASSWORD_MAX_LENGTH 64 + +struct network_configuration_t { + char ssid[WIFI_SSID_MAX_LENGTH]; // SSID of WiFi network + char password[WIFI_PASSWORD_MAX_LENGTH]; // Password of WiFi network + char mqtt_broker[MQTT_BROKER_MAX_LENGTH]; // MQTT broker address + char mqtt_username[MQTT_USERNAME_MAX_LENGTH]; // MQTT username + char mqtt_password[MQTT_PASSWORD_MAX_LENGTH]; // MQTT password +}; + /** * @brief Define the global configuration of the application. * @@ -27,6 +41,7 @@ struct pump_cycle_configuration_t { struct configuration_t { unsigned char id; struct pump_cycle_configuration_t pump_cycles; + struct network_configuration_t network; }; /** diff --git a/components/mqtt5_connection/mqtt5_connection.c b/components/mqtt5_connection/mqtt5_connection.c index 5cb4f12..4accb02 100644 --- a/components/mqtt5_connection/mqtt5_connection.c +++ b/components/mqtt5_connection/mqtt5_connection.c @@ -277,12 +277,13 @@ void mqtt5_conn_init() { configuration.id, VERSION_STRING); esp_mqtt_client_config_t mqtt5_cfg = { - .broker.address.uri = CONFIG_MQTT_BROKER_URI, + .broker.address.uri = configuration.network.mqtt_broker, .session.protocol_ver = MQTT_PROTOCOL_V_5, .network.disable_auto_reconnect = false, .network.reconnect_timeout_ms = CONFIG_MQTT_TIMEOUT_RECONNECT_MS, - .credentials.username = CONFIG_MQTT_USERNAME, - .credentials.authentication.password = CONFIG_MQTT_PASSWORD, + .credentials.username = configuration.network.mqtt_username, + .credentials.authentication.password = + configuration.network.mqtt_password, .session.last_will.topic = CONFIG_MQTT_STATUS_TOPIC, .session.last_will.msg = last_will_message, .session.last_will.msg_len = last_will_message_count, diff --git a/components/wifi_utils/wifi_utils.c b/components/wifi_utils/wifi_utils.c index ad7554c..2f9c7b1 100644 --- a/components/wifi_utils/wifi_utils.c +++ b/components/wifi_utils/wifi_utils.c @@ -11,6 +11,7 @@ #include "freertos/event_groups.h" #include "freertos/task.h" #include "sdkconfig.h" +#include #include #include @@ -79,13 +80,14 @@ void wifi_utils_init(void) { IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); // Start Wifi connection - wifi_config_t wifi_config = { - .sta = - { - .ssid = CONFIG_WIFI_SSID, - .password = CONFIG_WIFI_PASSWORD, - }, - }; + wifi_config_t wifi_config = {0}; + strncpy((char *)wifi_config.sta.ssid, configuration.network.ssid, + sizeof(wifi_config.sta.ssid)); + wifi_config.sta.ssid[sizeof(wifi_config.sta.ssid) - 1] = + '\0'; // Ensure null-termination + strncpy((char *)wifi_config.sta.password, configuration.network.password, + sizeof(wifi_config.sta.password)); + wifi_config.sta.password[sizeof(wifi_config.sta.password) - 1] = '\0'; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); From fe138bf2bd7eeaf4f625f5dd5d9453961e208ceb Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 9 Aug 2025 16:38:13 +0000 Subject: [PATCH 02/34] build factory app and releases --- .github/workflows/release.yml | 68 +++++++++++++++++++++++++++++++++++ .gitignore | 2 ++ CMakeLists.txt | 2 ++ README.md | 9 +++++ build_all.sh | 14 ++++++++ main/CMakeLists.txt | 6 +++- main/Kconfig.projbuild | 7 ++++ main/main_factory.c | 44 +++++++++++++++++++++++ partitions.csv | 11 +++--- profiles/app | 1 + profiles/factory | 1 + sdkconfig.factory | 1 + 12 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100755 build_all.sh create mode 100644 main/main_factory.c create mode 100644 profiles/app create mode 100644 profiles/factory create mode 100644 sdkconfig.factory diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4ff0864 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Create Release + +on: + push: + # Sequence of patterns matched against refs/tags + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + +jobs: + create_release: + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Get version from tag + id: tag_name + run: | + echo "current_version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + shell: bash + - name: Checkout code + uses: actions/checkout@v4 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and run Dev Container task + uses: devcontainers/ci@v0.3 + with: + # Change this to point to your image name + imageName: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer + # Change this to be your CI task/script + runCmd: | + ./build_all.sh + + ## https://github.com/tj-actions/docker-cp + ## Copy the build files from the dev container to the host + - name: Copy build files + uses: tj-actions/docker-cp@v1 + with: + src: /workspace/build_app/EbbFlowControl.bin + dest: ./workspace/app.bin + container: ebbflowcontrol-devcontainer + - name: Copy build files + uses: tj-actions/docker-cp@v1 + with: + src: /workspace/build_factory/EbbFlowControl.bin + dest: ./workspace/factory.bin + container: ebbflowcontrol-devcontainer + + - name: Get Changelog Entry + id: changelog_reader + uses: mindsers/changelog-reader-action@v2 + with: + validation_level: warn + version: ${{ steps.tag_name.outputs.current_version }} + path: ./CHANGELOG.md + - name: Create Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.changelog_reader.outputs.version }} + name: Release ${{ steps.changelog_reader.outputs.version }} + body: ${{ steps.changelog_reader.outputs.changes }} + prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} + draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} + token: ${{ secrets.GITHUB_TOKEN }} + artifacts: ./workspace/app.bin,./workspace/factory.bin diff --git a/.gitignore b/.gitignore index d3c3ecf..fb49d64 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,8 @@ Mkfile.old dkms.conf build/ +build_factory/ +build_app/ sdkconfig sdkconfig.old node_modules diff --git a/CMakeLists.txt b/CMakeLists.txt index 16ca892..b313fd3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,5 +4,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.5) +set(SDKCONFIG "${CMAKE_BINARY_DIR}/sdkconfig") + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(EbbFlowControl) diff --git a/README.md b/README.md index 0324291..75aea55 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,15 @@ This repository hold the software for a controller for an automated ebb flow hydroponic grow system. The controller runs on an ESP32 and can be configured via MQTT. The MQTT connection is additionally used to send status information and data for monitoring to an overall system. +## Over the Air (OTA) updates + +## Factory vs Application + +- Build application: `idf.py @profiles/app build` +- Flash application: `idf.py -B build_app flash` +- Build factory: `idf.py @profiles/factory build` +- Flash factory: `idf.py -B build_app flash` + ## Build and Flash The easiest way to build the software is to run the Docker devcontainer. diff --git a/build_all.sh b/build_all.sh new file mode 100755 index 0000000..126e850 --- /dev/null +++ b/build_all.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Build script for two apps + +set -e + +echo "Building Application" +idf.py @profiles/app build || { echo "Application build failed"; exit -1; } + +echo "Building Factory" +idf.py @profiles/factory build || { echo "Factory build failed"; exit -1; } + +echo "Build finished.." +exit 0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 52a3b94..b30d436 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1 +1,5 @@ -idf_component_register(SRCS "main.c" INCLUDE_DIRS ".") +if(CONFIG_BUILD_FACTORY) +idf_component_register(SRCS "main_factory.c" INCLUDE_DIRS ".") +else() +idf_component_register(SRCS "main.c" INCLUDE_DIRS ".") +endif() diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index c458886..7b31e35 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -23,5 +23,12 @@ menu "Application Configuration" help Set the URI to the MQTT broker. + config BUILD_FACTORY + bool "Build factory firmware" + default n + help + If enabled, the factory firmware will be built instead of the normal application. + This is useful for initial setup or testing purposes. + endmenu diff --git a/main/main_factory.c b/main/main_factory.c new file mode 100644 index 0000000..1b346b0 --- /dev/null +++ b/main/main_factory.c @@ -0,0 +1,44 @@ +#include "esp_log.h" +// #include "esp_spiffs.h" +// #include "esp_vfs.h" +#include +#include +#include + +#include "configuration.h" +// #include "data_logging.h" +// #include "mqtt5_connection.h" +// #include "pump_control.h" +// #include "wifi_utils.h" + +void initialize_nvs() { + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); +} + +void app_main(void) { + // Initialize storage + initialize_nvs(); + + load_configuration(); + + // Create Event Loop + // ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // // Initialize and connect to Wifi + // wifi_utils_init(); + // wifi_utils_init_sntp(); + // wifi_utils_create_connection_checker_task(); + // // MQTT Setup + // mqtt5_conn_init(); + // mqtt5_create_connection_checker_task(); + // // Data Logging Setup + // create_data_logging_task(); + // // Create control tasks + // ESP_ERROR_CHECK(add_notify_for_new_config(create_pump_control_task())); +} diff --git a/partitions.csv b/partitions.csv index 22625ae..a10c1a8 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,6 +1,9 @@ # ESP-IDF Partition Table # Name, Type, SubType, Offset, Size, Flags -nvs,data,nvs,0x9000,24K, -phy_init,data,phy,0xf000,4K, -factory,app,factory,0x10000,1M, -storage,data,spiffs,,1M, +nvs, data, nvs, 0x9000, 0x4000, +otadata, data, ota, 0xd000, 0x2000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +ota_0, app, ota_0, , 1M, +ota_1, app, ota_1, , 1M, +storage, data, spiffs, , 900k diff --git a/profiles/app b/profiles/app new file mode 100644 index 0000000..4b75f6c --- /dev/null +++ b/profiles/app @@ -0,0 +1 @@ +-B build_app -DSDKCONFIG=build_app/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults" diff --git a/profiles/factory b/profiles/factory new file mode 100644 index 0000000..dd7d334 --- /dev/null +++ b/profiles/factory @@ -0,0 +1 @@ +-B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.factory" diff --git a/sdkconfig.factory b/sdkconfig.factory new file mode 100644 index 0000000..c78fbfe --- /dev/null +++ b/sdkconfig.factory @@ -0,0 +1 @@ +CONFIG_BUILD_FACTORY=y From 4c9e98747e81d072d43be8c81f2287b3637d5948 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 9 Aug 2025 16:45:32 +0000 Subject: [PATCH 03/34] correct name --- .github/workflows/release.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4ff0864..8e8ae37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,16 +36,16 @@ jobs: ## https://github.com/tj-actions/docker-cp ## Copy the build files from the dev container to the host - name: Copy build files - uses: tj-actions/docker-cp@v1 + uses: tj-actions/docker-cp@v2 with: - src: /workspace/build_app/EbbFlowControl.bin - dest: ./workspace/app.bin + source: /workspace/build_app/EbbFlowControl.bin + destination: ./workspace/app.bin container: ebbflowcontrol-devcontainer - name: Copy build files - uses: tj-actions/docker-cp@v1 + uses: tj-actions/docker-cp@v2 with: - src: /workspace/build_factory/EbbFlowControl.bin - dest: ./workspace/factory.bin + source: /workspace/build_factory/EbbFlowControl.bin + destination: ./workspace/factory.bin container: ebbflowcontrol-devcontainer - name: Get Changelog Entry From 71124e333aa2bddd2ca48283dc996c6ce160781c Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 9 Aug 2025 16:55:26 +0000 Subject: [PATCH 04/34] correct path --- .github/workflows/build.yml | 2 +- .github/workflows/release.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03d9132..4e10170 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,4 +27,4 @@ jobs: imageName: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer # Change this to be your CI task/script runCmd: | - idf.py build + ./build_all.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e8ae37..5b3a0ee 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,13 +39,13 @@ jobs: uses: tj-actions/docker-cp@v2 with: source: /workspace/build_app/EbbFlowControl.bin - destination: ./workspace/app.bin + destination: app.bin container: ebbflowcontrol-devcontainer - name: Copy build files uses: tj-actions/docker-cp@v2 with: source: /workspace/build_factory/EbbFlowControl.bin - destination: ./workspace/factory.bin + destination: factory.bin container: ebbflowcontrol-devcontainer - name: Get Changelog Entry @@ -65,4 +65,4 @@ jobs: prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} token: ${{ secrets.GITHUB_TOKEN }} - artifacts: ./workspace/app.bin,./workspace/factory.bin + artifacts: app.bin,factory.bin From fa0392dbe6c2232e021bf5a0b14a2c8e12bc2410 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 9 Aug 2025 17:08:28 +0000 Subject: [PATCH 05/34] correct path --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5b3a0ee..205bc5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,13 +40,13 @@ jobs: with: source: /workspace/build_app/EbbFlowControl.bin destination: app.bin - container: ebbflowcontrol-devcontainer + container: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer - name: Copy build files uses: tj-actions/docker-cp@v2 with: source: /workspace/build_factory/EbbFlowControl.bin destination: factory.bin - container: ebbflowcontrol-devcontainer + container: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer - name: Get Changelog Entry id: changelog_reader From 556d699b7008f9570f315ec2b4aebcb233f7dd0c Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 10 Aug 2025 07:26:10 +0000 Subject: [PATCH 06/34] tree test --- .github/workflows/release.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 205bc5b..9830ee9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,11 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Print Folder Tree + uses: jaywcjlove/github-action-folder-tree@main + with: + path: . + depth: 5 - name: Build and run Dev Container task uses: devcontainers/ci@v0.3 with: @@ -32,6 +37,11 @@ jobs: # Change this to be your CI task/script runCmd: | ./build_all.sh + - name: Print Folder Treeafter + uses: jaywcjlove/github-action-folder-tree@main + with: + path: . + depth: 5 ## https://github.com/tj-actions/docker-cp ## Copy the build files from the dev container to the host From 70323badacb71fc954e956941f4e78411b0a0e43 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 10 Aug 2025 07:37:23 +0000 Subject: [PATCH 07/34] move correct files --- .github/workflows/release.yml | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9830ee9..21f02b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,11 +24,6 @@ jobs: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Print Folder Tree - uses: jaywcjlove/github-action-folder-tree@main - with: - path: . - depth: 5 - name: Build and run Dev Container task uses: devcontainers/ci@v0.3 with: @@ -37,26 +32,22 @@ jobs: # Change this to be your CI task/script runCmd: | ./build_all.sh - - name: Print Folder Treeafter + + - name: Print Folder Tree uses: jaywcjlove/github-action-folder-tree@main with: path: . depth: 5 + - name: Move build files + run: | + mv ./EbbFlowControl/build_app/EbbFlowControl.bin ./app.bin + mv ./EbbFlowControl/build_factory/EbbFlowControl.bin ./factory.bin - ## https://github.com/tj-actions/docker-cp - ## Copy the build files from the dev container to the host - - name: Copy build files - uses: tj-actions/docker-cp@v2 - with: - source: /workspace/build_app/EbbFlowControl.bin - destination: app.bin - container: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer - - name: Copy build files - uses: tj-actions/docker-cp@v2 + - name: Print Folder Treeafter + uses: jaywcjlove/github-action-folder-tree@main with: - source: /workspace/build_factory/EbbFlowControl.bin - destination: factory.bin - container: ghcr.io/${{ github.repository_owner }}/ebbflowcontrol-devcontainer + path: . + depth: 5 - name: Get Changelog Entry id: changelog_reader @@ -75,4 +66,4 @@ jobs: prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} token: ${{ secrets.GITHUB_TOKEN }} - artifacts: app.bin,factory.bin + artifacts: ./app.bin,./factory.bin From ca7b3c8d50a41dd949373d21630dd34599e93df9 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 10 Aug 2025 07:49:45 +0000 Subject: [PATCH 08/34] move correct files --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21f02b5..38402f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,8 +40,8 @@ jobs: depth: 5 - name: Move build files run: | - mv ./EbbFlowControl/build_app/EbbFlowControl.bin ./app.bin - mv ./EbbFlowControl/build_factory/EbbFlowControl.bin ./factory.bin + mv ./build_app/EbbFlowControl.bin ./app.bin + mv ./build_factory/EbbFlowControl.bin ./factory.bin - name: Print Folder Treeafter uses: jaywcjlove/github-action-folder-tree@main From b6079150e2532b49ea54cfb55aea5a8330a8a226 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 10 Aug 2025 07:58:49 +0000 Subject: [PATCH 09/34] pretty release --- .github/workflows/release.yml | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38402f8..972f8a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,23 +32,10 @@ jobs: # Change this to be your CI task/script runCmd: | ./build_all.sh - - - name: Print Folder Tree - uses: jaywcjlove/github-action-folder-tree@main - with: - path: . - depth: 5 - name: Move build files run: | mv ./build_app/EbbFlowControl.bin ./app.bin mv ./build_factory/EbbFlowControl.bin ./factory.bin - - - name: Print Folder Treeafter - uses: jaywcjlove/github-action-folder-tree@main - with: - path: . - depth: 5 - - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2 From c5f1a5d10053a3ebdf2e46c03c5a2b5c8dc673ef Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 23 Nov 2025 13:45:46 +0000 Subject: [PATCH 10/34] first ota implementation --- .devcontainer/Dockerfile | 2 +- CMakeLists.txt | 5 + components/ota_updater/CMakeLists.txt | 5 + components/ota_updater/Kconfig | 12 ++ components/ota_updater/include/ota_updater.h | 25 +++ components/ota_updater/ota_updater.c | 180 +++++++++++++++++++ dependencies.lock | 2 +- main/main_factory.c | 29 ++- 8 files changed, 240 insertions(+), 20 deletions(-) create mode 100644 components/ota_updater/CMakeLists.txt create mode 100644 components/ota_updater/Kconfig create mode 100644 components/ota_updater/include/ota_updater.h create mode 100644 components/ota_updater/ota_updater.c diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ddbc441..c663023 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM espressif/idf:v5.4.2 +FROM espressif/idf:v5.5.1 ARG DEBIAN_FRONTEND=nointeractive ARG CONTAINER_USER=esp diff --git a/CMakeLists.txt b/CMakeLists.txt index b313fd3..e82ce7b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,4 +7,9 @@ cmake_minimum_required(VERSION 3.5) set(SDKCONFIG "${CMAKE_BINARY_DIR}/sdkconfig") include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if(CONFIG_BUILD_FACTORY) +project(EbbFlowControl-factory) +else() project(EbbFlowControl) +endif() diff --git a/components/ota_updater/CMakeLists.txt b/components/ota_updater/CMakeLists.txt new file mode 100644 index 0000000..f5513b6 --- /dev/null +++ b/components/ota_updater/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "ota_updater.c" + INCLUDE_DIRS "include" + REQUIRES esp_http_client app_update esp_https_ota + # Embed the server root certificate into the final binary + EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem) diff --git a/components/ota_updater/Kconfig b/components/ota_updater/Kconfig new file mode 100644 index 0000000..3c1cd84 --- /dev/null +++ b/components/ota_updater/Kconfig @@ -0,0 +1,12 @@ +menu "OTA Updater Configuration" + config OTA_FIRMWARE_UPGRADE_URL + string "URL for firmware upgrade." + default "https://example.com/firmware.bin" + help + Set the URL from which to download the firmware upgrade. + config OTA_USE_CERT_BUNDLE + bool "Use certificate bundle for HTTPS." + default n + help + Enable this option to use the built-in certificate bundle for HTTPS connections. If disabled, a custom server certificate must be provided. +endmenu diff --git a/components/ota_updater/include/ota_updater.h b/components/ota_updater/include/ota_updater.h new file mode 100644 index 0000000..3310123 --- /dev/null +++ b/components/ota_updater/include/ota_updater.h @@ -0,0 +1,25 @@ +#ifndef COMPONENTS_OTA_UPDATER_INCLUDE_OTA_UPDATER +#define COMPONENTS_OTA_UPDATER_INCLUDE_OTA_UPDATER +/*** + * @file ota_updater.h + * @brief Header file for OTA updater component + * This component handles over-the-air firmware updates. + */ + +/*** + * @brief Mark the currently running application version as valid to + * prevent rollback + */ +void mark_running_app_version_valid(); + +/*** + * @brief Task to perform OTA update + */ +void ota_updater_task(void *pvParameter); + +/*** + * @brief Initialize the OTA updater + */ +void initialize_ota_updater(); + +#endif /* COMPONENTS_OTA_UPDATER_INCLUDE_OTA_UPDATER */ diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c new file mode 100644 index 0000000..40d2de9 --- /dev/null +++ b/components/ota_updater/ota_updater.c @@ -0,0 +1,180 @@ +#include "ota_updater.h" +#include + +#include "esp_http_client.h" +#include "esp_https_ota.h" +#include "esp_log.h" +#include "esp_ota_ops.h" +#ifdef CONFIG_OTA_USE_CERT_BUNDLE +#include "esp_crt_bundle.h" +#endif + +static const char *TAG = "ota_updater"; + +extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start"); + +/* Event handler for catching system events */ +static void ota_updater_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + if (event_base == ESP_HTTPS_OTA_EVENT) { + switch (event_id) { + case ESP_HTTPS_OTA_START: + ESP_LOGI(TAG, "OTA started"); + break; + case ESP_HTTPS_OTA_CONNECTED: + ESP_LOGI(TAG, "Connected to server"); + break; + case ESP_HTTPS_OTA_GET_IMG_DESC: + ESP_LOGI(TAG, "Reading Image Description"); + break; + case ESP_HTTPS_OTA_VERIFY_CHIP_ID: + ESP_LOGI(TAG, "Verifying chip id of new image: %d", + *(esp_chip_id_t *)event_data); + break; + case ESP_HTTPS_OTA_VERIFY_CHIP_REVISION: + ESP_LOGI(TAG, "Verifying chip revision of new image: %d", + *(esp_chip_id_t *)event_data); + break; + case ESP_HTTPS_OTA_DECRYPT_CB: + ESP_LOGI(TAG, "Callback to decrypt function"); + break; + case ESP_HTTPS_OTA_WRITE_FLASH: + ESP_LOGD(TAG, "Writing to flash: %d written", *(int *)event_data); + break; + case ESP_HTTPS_OTA_UPDATE_BOOT_PARTITION: + ESP_LOGI(TAG, "Boot partition updated. Next Partition: %d", + *(esp_partition_subtype_t *)event_data); + break; + case ESP_HTTPS_OTA_FINISH: + ESP_LOGI(TAG, "OTA finish"); + break; + case ESP_HTTPS_OTA_ABORT: + ESP_LOGI(TAG, "OTA abort"); + break; + } + } +} + +/*** + * @brief Validate the new image header before proceeding with OTA + */ +static esp_err_t validate_image_header(const esp_app_desc_t *new_app_info) { + if (new_app_info == NULL) { + return ESP_ERR_INVALID_ARG; + } + + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_app_desc_t running_app_info; + if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) { + ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version); + } + + // Allow update if factory app is running + if (strncmp(running_app_info.project_name, "EbbFlowControl-factory", + sizeof(running_app_info.project_name)) == 0) { + ESP_LOGI(TAG, "Factory app is running, allowing update to proceed."); + return ESP_OK; + } + + if (memcmp(new_app_info->version, running_app_info.version, + sizeof(new_app_info->version)) == 0) { + ESP_LOGW(TAG, "Current running version is the same as a new. We will not " + "continue the update."); + return ESP_FAIL; + } + + return ESP_OK; +} + +void initialize_ota_updater() { + ESP_LOGI(TAG, "OTA Updater initialized"); + ESP_ERROR_CHECK(esp_event_handler_register( + ESP_HTTPS_OTA_EVENT, ESP_EVENT_ANY_ID, &ota_updater_event_handler, NULL)); +} + +void ota_updater_task(void *pvParameter) { + ESP_LOGI(TAG, "Starting OTA example task"); + esp_http_client_config_t config = { + .url = CONFIG_OTA_FIRMWARE_UPGRADE_URL, +#ifdef CONFIG_OTA_USE_CERT_BUNDLE + .crt_bundle_attach = esp_crt_bundle_attach, +#else + .cert_pem = (char *)server_cert_pem_start, +#endif /* CONFIG_OTA_USE_CERT_BUNDLE */ + .keep_alive_enable = true, + }; + + esp_https_ota_config_t ota_config = { + .http_config = &config, + }; + + esp_https_ota_handle_t https_ota_handle = NULL; + esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ESP HTTPS OTA Begin failed"); + vTaskDelete(NULL); + } + + esp_app_desc_t app_desc = {}; + err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_https_ota_get_img_desc failed"); + goto ota_end; + } + err = validate_image_header(&app_desc); + if (err != ESP_OK) { + ESP_LOGE(TAG, "image header verification failed"); + goto ota_end; + } + + while (1) { + err = esp_https_ota_perform(https_ota_handle); + if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { + break; + } + // esp_https_ota_perform returns after every read operation which gives user + // the ability to monitor the status of OTA upgrade by calling + // esp_https_ota_get_image_len_read, which gives length of image data read + // so far. + const size_t len = esp_https_ota_get_image_len_read(https_ota_handle); + ESP_LOGD(TAG, "Image bytes read: %d", len); + } + + if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) { + // the OTA image was not completely received and user can customise the + // response to this situation. + ESP_LOGE(TAG, "Complete data was not received."); + } else { + esp_err_t ota_finish_err = esp_https_ota_finish(https_ota_handle); + if ((err == ESP_OK) && (ota_finish_err == ESP_OK)) { + ESP_LOGI(TAG, "ESP_HTTPS_OTA upgrade successful. Rebooting ..."); + vTaskDelay(1000 / portTICK_PERIOD_MS); + esp_restart(); + } else { + if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) { + ESP_LOGE(TAG, "Image validation failed, image is corrupted"); + } + ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed 0x%x", ota_finish_err); + vTaskDelete(NULL); + } + } + +ota_end: + esp_https_ota_abort(https_ota_handle); + ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed"); + vTaskDelete(NULL); +} + +void mark_running_app_version_valid() { + const esp_partition_t *running = esp_ota_get_running_partition(); + esp_ota_img_states_t ota_state; + if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { + if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { + if (esp_ota_mark_app_valid_cancel_rollback() == ESP_OK) { + ESP_LOGI(TAG, "App is valid, rollback cancelled successfully"); + } else { + ESP_LOGE(TAG, "Failed to cancel rollback"); + } + } + } +} diff --git a/dependencies.lock b/dependencies.lock index 84551fb..d9fe808 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -10,7 +10,7 @@ dependencies: idf: source: type: idf - version: 5.4.2 + version: 5.5.1 mqtt5_connection: dependencies: - name: idf diff --git a/main/main_factory.c b/main/main_factory.c index 1b346b0..28d7dad 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -1,15 +1,12 @@ +#include "esp_event.h" #include "esp_log.h" -// #include "esp_spiffs.h" -// #include "esp_vfs.h" #include #include #include #include "configuration.h" -// #include "data_logging.h" -// #include "mqtt5_connection.h" -// #include "pump_control.h" -// #include "wifi_utils.h" +#include "ota_updater.h" +#include "wifi_utils.h" void initialize_nvs() { esp_err_t ret = nvs_flash_init(); @@ -28,17 +25,13 @@ void app_main(void) { load_configuration(); // Create Event Loop - // ESP_ERROR_CHECK(esp_event_loop_create_default()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); - // // Initialize and connect to Wifi - // wifi_utils_init(); - // wifi_utils_init_sntp(); - // wifi_utils_create_connection_checker_task(); - // // MQTT Setup - // mqtt5_conn_init(); - // mqtt5_create_connection_checker_task(); - // // Data Logging Setup - // create_data_logging_task(); - // // Create control tasks - // ESP_ERROR_CHECK(add_notify_for_new_config(create_pump_control_task())); + // Initialize and connect to Wifi + wifi_utils_init(); + wifi_utils_init_sntp(); + + // run ota updater task + initialize_ota_updater(); + xTaskCreate(&ota_updater_task, "ota_updater_task", 1024 * 8, NULL, 5, NULL); } From 00c48df0e9a3502204ec7d5f7676d0bfc36f7d4f Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 23 Nov 2025 13:53:35 +0000 Subject: [PATCH 11/34] use cert bundle by default --- components/ota_updater/CMakeLists.txt | 6 ++++++ components/ota_updater/Kconfig | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/components/ota_updater/CMakeLists.txt b/components/ota_updater/CMakeLists.txt index f5513b6..bdff755 100644 --- a/components/ota_updater/CMakeLists.txt +++ b/components/ota_updater/CMakeLists.txt @@ -1,5 +1,11 @@ +if(CONFIG_OTA_USE_CERT_BUNDLE) +idf_component_register(SRCS "ota_updater.c" + INCLUDE_DIRS "include" + REQUIRES esp_http_client app_update esp_https_ota) +else() idf_component_register(SRCS "ota_updater.c" INCLUDE_DIRS "include" REQUIRES esp_http_client app_update esp_https_ota # Embed the server root certificate into the final binary EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem) +endif() diff --git a/components/ota_updater/Kconfig b/components/ota_updater/Kconfig index 3c1cd84..4694dbe 100644 --- a/components/ota_updater/Kconfig +++ b/components/ota_updater/Kconfig @@ -6,7 +6,7 @@ menu "OTA Updater Configuration" Set the URL from which to download the firmware upgrade. config OTA_USE_CERT_BUNDLE bool "Use certificate bundle for HTTPS." - default n + default y help Enable this option to use the built-in certificate bundle for HTTPS connections. If disabled, a custom server certificate must be provided. endmenu From 4489fd26891132b76ef9e74a0fbb924913ff4944 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 23 Nov 2025 14:14:23 +0000 Subject: [PATCH 12/34] add correct requierments --- components/ota_updater/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ota_updater/CMakeLists.txt b/components/ota_updater/CMakeLists.txt index bdff755..0cafa02 100644 --- a/components/ota_updater/CMakeLists.txt +++ b/components/ota_updater/CMakeLists.txt @@ -1,11 +1,11 @@ if(CONFIG_OTA_USE_CERT_BUNDLE) idf_component_register(SRCS "ota_updater.c" INCLUDE_DIRS "include" - REQUIRES esp_http_client app_update esp_https_ota) + PRIV_REQUIRES mbedtls esp_http_client app_update esp_https_ota) else() idf_component_register(SRCS "ota_updater.c" INCLUDE_DIRS "include" - REQUIRES esp_http_client app_update esp_https_ota + PRIV_REQUIRES mbedtls esp_http_client app_update esp_https_ota # Embed the server root certificate into the final binary EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem) endif() From 704e41730fbd254de5d7fc1137a0c36e990f6af0 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 23 Nov 2025 15:17:07 +0000 Subject: [PATCH 13/34] update after restart --- components/ota_updater/Kconfig | 1 + main/main.c | 8 ++++++++ partitions.csv | 8 ++++---- sdkconfig.defaults | 2 ++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/components/ota_updater/Kconfig b/components/ota_updater/Kconfig index 4694dbe..cdc27e9 100644 --- a/components/ota_updater/Kconfig +++ b/components/ota_updater/Kconfig @@ -6,6 +6,7 @@ menu "OTA Updater Configuration" Set the URL from which to download the firmware upgrade. config OTA_USE_CERT_BUNDLE bool "Use certificate bundle for HTTPS." + depends on MBEDTLS_CERTIFICATE_BUNDLE default y help Enable this option to use the built-in certificate bundle for HTTPS connections. If disabled, a custom server certificate must be provided. diff --git a/main/main.c b/main/main.c index 5cafc65..2e88b56 100644 --- a/main/main.c +++ b/main/main.c @@ -8,6 +8,7 @@ #include "configuration.h" #include "data_logging.h" #include "mqtt5_connection.h" +#include "ota_updater.h" #include "pump_control.h" #include "wifi_utils.h" @@ -56,6 +57,11 @@ void app_main(void) { // Initialize and connect to Wifi wifi_utils_init(); wifi_utils_init_sntp(); + + // run ota updater task + initialize_ota_updater(); + xTaskCreate(&ota_updater_task, "ota_updater_task", 1024 * 8, NULL, 5, NULL); + wifi_utils_create_connection_checker_task(); // MQTT Setup mqtt5_conn_init(); @@ -64,4 +70,6 @@ void app_main(void) { create_data_logging_task(); // Create control tasks ESP_ERROR_CHECK(add_notify_for_new_config(create_pump_control_task())); + + mark_running_app_version_valid(); } diff --git a/partitions.csv b/partitions.csv index a10c1a8..674c425 100644 --- a/partitions.csv +++ b/partitions.csv @@ -3,7 +3,7 @@ nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, -factory, app, factory, 0x10000, 1M, -ota_0, app, ota_0, , 1M, -ota_1, app, ota_1, , 1M, -storage, data, spiffs, , 900k +factory, app, factory, 0x10000, 1100k, +ota_0, app, ota_0, , 1100k, +ota_1, app, ota_1, , 1100k, +storage, data, spiffs, , 600k diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 9daf685..432e68f 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -10,3 +10,5 @@ CONFIG_MQTT_PROTOCOL_311=n CONFIG_MQTT_PROTOCOL_5=y CONFIG_MQTT_REPORT_DELETED_MESSAGES=y CONFIG_NEWLIB_NANO_FORMAT=y +CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y +CONFIG_COMPILER_OPTIMIZATION_SIZE=y From fd0a57b2622b2f07a7133a109a465cb7f6a6f8f5 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 16:46:12 +0000 Subject: [PATCH 14/34] Add feature for OTA update --- .devcontainer/devcontainer.json | 3 +- CHANGELOG.md | 2 + CMakeLists.txt | 10 ++- README.md | 2 +- components/ota_updater/CMakeLists.txt | 2 +- components/ota_updater/ota_updater.c | 5 ++ components/wifi_utils/wifi_utils.c | 8 +-- profiles/factory | 2 +- tools/ota_server/create_certificates.sh | 8 +++ tools/ota_server/ota_server.py | 85 +++++++++++++++++++++++++ 10 files changed, 117 insertions(+), 10 deletions(-) create mode 100755 tools/ota_server/create_certificates.sh create mode 100644 tools/ota_server/ota_server.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4266bc3..4d824c3 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -22,7 +22,8 @@ }, "runArgs": [ "--privileged", - "--device=/dev/ttyUSB0" + "--device=/dev/ttyUSB0", + "-p=8070:8070" ], "postCreateCommand": "pip install --upgrade pip && pip install pre-commit && pre-commit install --install-hooks" } diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5ba9f..d939962 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Use the following labels: ## [UNRELEASED] +- [Minor] Add over the air update feature. + ## [0.0.1] - 2025-07-11 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index e82ce7b..f1b31f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,12 +4,18 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.5) +option(BUILD_FACTORY "Build the factory app" OFF) +message(STATUS "Build Factory Flag: ${BUILD_FACTORY}") + set(SDKCONFIG "${CMAKE_BINARY_DIR}/sdkconfig") include($ENV{IDF_PATH}/tools/cmake/project.cmake) -if(CONFIG_BUILD_FACTORY) +if(BUILD_FACTORY) project(EbbFlowControl-factory) -else() +message(STATUS "Build Factory") +else() project(EbbFlowControl) +message(STATUS "Build Normal Application") endif() + diff --git a/README.md b/README.md index 75aea55..7b54b55 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This repository hold the software for a controller for an automated ebb flow hyd - Build application: `idf.py @profiles/app build` - Flash application: `idf.py -B build_app flash` - Build factory: `idf.py @profiles/factory build` -- Flash factory: `idf.py -B build_app flash` +- Flash factory: `idf.py -B build_factory flash` ## Build and Flash diff --git a/components/ota_updater/CMakeLists.txt b/components/ota_updater/CMakeLists.txt index 0cafa02..76e7fd6 100644 --- a/components/ota_updater/CMakeLists.txt +++ b/components/ota_updater/CMakeLists.txt @@ -7,5 +7,5 @@ idf_component_register(SRCS "ota_updater.c" INCLUDE_DIRS "include" PRIV_REQUIRES mbedtls esp_http_client app_update esp_https_ota # Embed the server root certificate into the final binary - EMBED_TXTFILES ${project_dir}/server_certs/ca_cert.pem) + EMBED_TXTFILES ${project_dir}/tools/ota_server/ca_cert.pem) endif() diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index 40d2de9..e02b2a3 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -94,6 +94,11 @@ void initialize_ota_updater() { void ota_updater_task(void *pvParameter) { ESP_LOGI(TAG, "Starting OTA example task"); +#ifndef CONFIG_OTA_USE_CERT_BUNDLE + ESP_LOGI(TAG, "Server Certificate: \n%s", server_cert_pem_start); +#endif + ESP_LOGI(TAG, "Connecting to %s", CONFIG_OTA_FIRMWARE_UPGRADE_URL); + esp_http_client_config_t config = { .url = CONFIG_OTA_FIRMWARE_UPGRADE_URL, #ifdef CONFIG_OTA_USE_CERT_BUNDLE diff --git a/components/wifi_utils/wifi_utils.c b/components/wifi_utils/wifi_utils.c index 2f9c7b1..76b7218 100644 --- a/components/wifi_utils/wifi_utils.c +++ b/components/wifi_utils/wifi_utils.c @@ -115,14 +115,14 @@ esp_err_t wifi_utils_connect_wifi_blocking() { /* xEventGroupWaitBits() returns the bits before the call returned, hence we * can test which event actually happened. */ if (bits & WIFI_CONNECTED_BIT) { - ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", CONFIG_WIFI_SSID, - CONFIG_WIFI_PASSWORD); + ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", + configuration.network.ssid, configuration.network.password); return ESP_OK; } if (bits & WIFI_FAIL_BIT) { - ESP_LOGW(TAG, "Failed to connect to SSID:%s, password:%s", CONFIG_WIFI_SSID, - CONFIG_WIFI_PASSWORD); + ESP_LOGW(TAG, "Failed to connect to SSID:%s, password:%s", + configuration.network.ssid, configuration.network.password); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } diff --git a/profiles/factory b/profiles/factory index dd7d334..eb7949c 100644 --- a/profiles/factory +++ b/profiles/factory @@ -1 +1 @@ --B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.factory" +-B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.factory" -DBUILD_FACTORY=ON diff --git a/tools/ota_server/create_certificates.sh b/tools/ota_server/create_certificates.sh new file mode 100755 index 0000000..111eb3e --- /dev/null +++ b/tools/ota_server/create_certificates.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +echo "Use the IP address of the server as the Common Name (CN) for the certificate." +hostname=${1:-"$(hostname -I | awk '{print $1}')"} + +echo "Using hostname/IP: $hostname" + +openssl req -x509 -newkey rsa:2048 -keyout ca_key.pem -out ca_cert.pem -days 365 -nodes -subj /CN=$hostname diff --git a/tools/ota_server/ota_server.py b/tools/ota_server/ota_server.py new file mode 100644 index 0000000..89e7162 --- /dev/null +++ b/tools/ota_server/ota_server.py @@ -0,0 +1,85 @@ +import argparse +import http.server +import os +import ssl + +def start_https_server( + ota_image_dir: str, + server_ip: str, + server_port: int, + server_file: str = None, + key_file: str = None, +) -> None: + """Start an HTTPS server to serve OTA images. + Args: + ota_image_dir (str): Directory containing OTA images. + server_ip (str): IP address to bind the server. + server_port (int): Port to bind the server. + server_file (str, optional): Path to the server certificate file. Defaults to None. + key_file (str, optional): Path to the server key file. Defaults to None. +""" + + current_dir = os.path.abspath(".") + server_address = (server_ip, server_port) + + os.chdir(ota_image_dir) + httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) + + if server_file is not None and key_file is not None: + server_file_path = current_dir + "/" + server_file + key_file_path = current_dir + "/" + key_file + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(certfile=server_file_path, keyfile=key_file_path) + + httpd.socket = ssl_context.wrap_socket(httpd.socket, server_side=True) + + httpd.serve_forever() + +if __name__ == "__main__": + arg_parser = argparse.ArgumentParser(description="Runs an HTTPS server to serve Images for Over the Air (OTA) updates.") + arg_parser.add_argument( + "--ota_image_dir", + type=str, + required=False, + default="../../build_app/", + help="Directory containing OTA images.", + ) + arg_parser.add_argument( + "--server_ip", + type=str, + required=False, + default="0.0.0.0", + help="IP address to bind the server.", + ) + arg_parser.add_argument( + "--server_port", + type=int, + required=False, + default=8070, + help="Port to bind the server.", + ) + arg_parser.add_argument( + "--server_cert", + type=str, + nargs="?", + required=False, + default="./ca_cert.pem", + help="Path to the server certificate file.",) + + arg_parser.add_argument( + "--server_key", + type=str, + nargs="?", + required=False, + default="./ca_key.pem", + help="Path to the server key file.", + ) + args = arg_parser.parse_args() + print(f"Start Server with {args}") + start_https_server( + ota_image_dir=args.ota_image_dir, + server_ip=args.server_ip, + server_port=args.server_port, + server_file=args.server_cert, + key_file=args.server_key, + ) \ No newline at end of file From 9a8de9f7c38a7fef303c9c1419f61afd2ae7d7b2 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 16:54:53 +0000 Subject: [PATCH 15/34] fix pre-commit --- .devcontainer/devcontainer.json | 2 +- CMakeLists.txt | 3 +-- tools/ota_server/ota_server.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4d824c3..7de3569 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -25,5 +25,5 @@ "--device=/dev/ttyUSB0", "-p=8070:8070" ], - "postCreateCommand": "pip install --upgrade pip && pip install pre-commit && pre-commit install --install-hooks" + "postCreateCommand": "pip install --upgrade pip && pip install setuptools -U && pip install pre-commit && pre-commit install --install-hooks" } diff --git a/CMakeLists.txt b/CMakeLists.txt index f1b31f7..f5e3a3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,8 +14,7 @@ include($ENV{IDF_PATH}/tools/cmake/project.cmake) if(BUILD_FACTORY) project(EbbFlowControl-factory) message(STATUS "Build Factory") -else() +else() project(EbbFlowControl) message(STATUS "Build Normal Application") endif() - diff --git a/tools/ota_server/ota_server.py b/tools/ota_server/ota_server.py index 89e7162..1b23fbb 100644 --- a/tools/ota_server/ota_server.py +++ b/tools/ota_server/ota_server.py @@ -21,7 +21,7 @@ def start_https_server( current_dir = os.path.abspath(".") server_address = (server_ip, server_port) - + os.chdir(ota_image_dir) httpd = http.server.HTTPServer(server_address, http.server.SimpleHTTPRequestHandler) @@ -82,4 +82,4 @@ def start_https_server( server_port=args.server_port, server_file=args.server_cert, key_file=args.server_key, - ) \ No newline at end of file + ) From b6b71dc1282ffa47186d7d1b64bbd8f231dfa550 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 17:13:06 +0000 Subject: [PATCH 16/34] print new version --- components/ota_updater/ota_updater.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index e02b2a3..7d0cf16 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -75,7 +75,7 @@ static esp_err_t validate_image_header(const esp_app_desc_t *new_app_info) { ESP_LOGI(TAG, "Factory app is running, allowing update to proceed."); return ESP_OK; } - + ESP_LOGI(TAG, "New Application Version: %s", new_app_info->version); if (memcmp(new_app_info->version, running_app_info.version, sizeof(new_app_info->version)) == 0) { ESP_LOGW(TAG, "Current running version is the same as a new. We will not " From 5905c6cfb36d80f8e5b82bc7aa39fc40bb1a313a Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 18:14:41 +0000 Subject: [PATCH 17/34] update release script --- .github/workflows/release.yml | 6 +++--- CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 972f8a6..06c0620 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,8 +34,8 @@ jobs: ./build_all.sh - name: Move build files run: | - mv ./build_app/EbbFlowControl.bin ./app.bin - mv ./build_factory/EbbFlowControl.bin ./factory.bin + mv ./build_app/EbbFlowControl.bin ./EbbFlowControl.bin + mv ./build_factory/EbbFlowControl-factory.bin ./EbbFlowControl-factory.bin - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2 @@ -53,4 +53,4 @@ jobs: prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} token: ${{ secrets.GITHUB_TOKEN }} - artifacts: ./app.bin,./factory.bin + artifacts: ./EbbFlowControl.bin,./EbbFlowControl-factory.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index d939962..a4d79ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Use the following labels: - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. -## [UNRELEASED] +## [0.1.0] - 2026-01-02 - [Minor] Add over the air update feature. From a0416bf469ba3e1b8f2d5c9c0ce665303f646645 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 19:28:53 +0000 Subject: [PATCH 18/34] update release workflow --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 06c0620..9813c43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,4 +53,6 @@ jobs: prerelease: ${{ steps.changelog_reader.outputs.status == 'prereleased' }} draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} token: ${{ secrets.GITHUB_TOKEN }} - artifacts: ./EbbFlowControl.bin,./EbbFlowControl-factory.bin + files: | + EbbFlowControl.bin + EbbFlowControl-factory.bin From be58f7c8c209f3e078dae5c0e1dc99ec23660f82 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Fri, 2 Jan 2026 19:55:54 +0000 Subject: [PATCH 19/34] set default ota update url --- components/ota_updater/Kconfig | 2 +- components/ota_updater/ota_updater.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/components/ota_updater/Kconfig b/components/ota_updater/Kconfig index cdc27e9..774b8ab 100644 --- a/components/ota_updater/Kconfig +++ b/components/ota_updater/Kconfig @@ -1,7 +1,7 @@ menu "OTA Updater Configuration" config OTA_FIRMWARE_UPGRADE_URL string "URL for firmware upgrade." - default "https://example.com/firmware.bin" + default "https://github.com/phofmeier/EbbFlowControl/releases/latest/download/EbbFlowControl.bin" help Set the URL from which to download the firmware upgrade. config OTA_USE_CERT_BUNDLE diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index 7d0cf16..67ff974 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -107,6 +107,7 @@ void ota_updater_task(void *pvParameter) { .cert_pem = (char *)server_cert_pem_start, #endif /* CONFIG_OTA_USE_CERT_BUNDLE */ .keep_alive_enable = true, + .buffer_size_tx = 1024, }; esp_https_ota_config_t ota_config = { From 6c805ffa4ecfdf52165cfb01088a0f4d51174075 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 3 Jan 2026 15:37:07 +0000 Subject: [PATCH 20/34] add scheduler for automatic ota updates --- .gitignore | 3 + components/ota_updater/CMakeLists.txt | 4 +- .../ota_updater/include/ota_scheduler.h | 11 ++++ components/ota_updater/ota_scheduler.c | 59 +++++++++++++++++++ components/ota_updater/ota_updater.c | 5 +- main/CMakeLists.txt | 6 +- main/init_utils.c | 39 ++++++++++++ main/main.c | 50 ++++------------ main/main_factory.c | 15 +---- profiles/factory | 2 +- sdkconfig.factory | 1 - tools/ota_server/create_certificates.sh | 8 +++ tools/ota_server/ota_server.py | 10 +++- 13 files changed, 149 insertions(+), 64 deletions(-) create mode 100644 components/ota_updater/include/ota_scheduler.h create mode 100644 components/ota_updater/ota_scheduler.c create mode 100644 main/init_utils.c delete mode 100644 sdkconfig.factory diff --git a/.gitignore b/.gitignore index fb49d64..3ca0a09 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ sdkconfig sdkconfig.old node_modules warnings.txt +tools/ota_server/ca_cert.pem +tools/ota_server/ca_key.pem +log.* diff --git a/components/ota_updater/CMakeLists.txt b/components/ota_updater/CMakeLists.txt index 76e7fd6..07787b9 100644 --- a/components/ota_updater/CMakeLists.txt +++ b/components/ota_updater/CMakeLists.txt @@ -1,9 +1,9 @@ if(CONFIG_OTA_USE_CERT_BUNDLE) -idf_component_register(SRCS "ota_updater.c" +idf_component_register(SRCS "ota_updater.c" "ota_scheduler.c" INCLUDE_DIRS "include" PRIV_REQUIRES mbedtls esp_http_client app_update esp_https_ota) else() -idf_component_register(SRCS "ota_updater.c" +idf_component_register(SRCS "ota_updater.c" "ota_scheduler.c" INCLUDE_DIRS "include" PRIV_REQUIRES mbedtls esp_http_client app_update esp_https_ota # Embed the server root certificate into the final binary diff --git a/components/ota_updater/include/ota_scheduler.h b/components/ota_updater/include/ota_scheduler.h new file mode 100644 index 0000000..721afda --- /dev/null +++ b/components/ota_updater/include/ota_scheduler.h @@ -0,0 +1,11 @@ +#ifndef COMPONENTS_OTA_UPDATER_INCLUDE_OTA_SCHEDULER +#define COMPONENTS_OTA_UPDATER_INCLUDE_OTA_SCHEDULER + +/** + * @brief Create a task which schedules an over the air (ota) update always + * somewhere between 0 and 1 o clock. + * + */ +void create_ota_scheduler_task(); + +#endif /* COMPONENTS_OTA_UPDATER_INCLUDE_OTA_SCHEDULER */ diff --git a/components/ota_updater/ota_scheduler.c b/components/ota_updater/ota_scheduler.c new file mode 100644 index 0000000..3cc8d58 --- /dev/null +++ b/components/ota_updater/ota_scheduler.c @@ -0,0 +1,59 @@ +#include "ota_scheduler.h" + +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "sdkconfig.h" +#include + +#include "ota_updater.h" + +static const char *TAG = "ota_scheduler"; + +void ota_scheduler_task(void *pvParameter) { + // Note: If we restart at midnight we wait for 24h for the next update. So if + // we restart due to any error after an update we do not immediately update + // again. + for (;;) { + // Schedule next update somewhere between 0 and 1 o clock. + setenv("TZ", CONFIG_LOCAL_TIME_ZONE, 1); + tzset(); + time_t now; + time(&now); + struct tm timeinfo; + localtime_r(&now, &timeinfo); + const int hours_until_midnight = 24 - timeinfo.tm_hour; + ESP_LOGD(TAG, "Stack high water mark %d", + uxTaskGetStackHighWaterMark(NULL)); + ESP_LOGI(TAG, "Wait for %i hours before next update.", + hours_until_midnight); + vTaskDelay(pdMS_TO_TICKS(hours_until_midnight * 60 * 60 * 1e3)); + + // Try to update now. + xTaskCreate(&ota_updater_task, "ota_updater_task", 1024 * 8, NULL, 5, NULL); + } +} + +/* Stack Size for the connection check task*/ +#define STACK_SIZE 1300 + +/* Structure that will hold the TCB of the task being created. */ +static StaticTask_t xTaskBuffer; + +/* Buffer that the task being created will use as its stack. Note this is + an array of StackType_t variables. The size of StackType_t is dependent on + the RTOS port. */ +static StackType_t xStack[STACK_SIZE]; + +void create_ota_scheduler_task() { + initialize_ota_updater(); + // Static task without dynamic memory allocation + xTaskCreateStatic( + ota_scheduler_task, "OTAScheduler", /* Task Name */ + STACK_SIZE, /* Number of indexes in the xStack array. */ + NULL, /* No Parameter */ + tskIDLE_PRIORITY + 1, /* Priority at which the task is created. */ + xStack, /* Array to use as the task's stack. */ + &xTaskBuffer); /* Variable to hold the task's data structure. */ +} diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index 67ff974..f0001ab 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -93,11 +93,10 @@ void initialize_ota_updater() { } void ota_updater_task(void *pvParameter) { - ESP_LOGI(TAG, "Starting OTA example task"); #ifndef CONFIG_OTA_USE_CERT_BUNDLE - ESP_LOGI(TAG, "Server Certificate: \n%s", server_cert_pem_start); + ESP_LOGD(TAG, "Server Certificate: \n%s", server_cert_pem_start); #endif - ESP_LOGI(TAG, "Connecting to %s", CONFIG_OTA_FIRMWARE_UPGRADE_URL); + ESP_LOGD(TAG, "Connecting to %s", CONFIG_OTA_FIRMWARE_UPGRADE_URL); esp_http_client_config_t config = { .url = CONFIG_OTA_FIRMWARE_UPGRADE_URL, diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b30d436..d16d0a5 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,5 +1,5 @@ -if(CONFIG_BUILD_FACTORY) -idf_component_register(SRCS "main_factory.c" INCLUDE_DIRS ".") +if(BUILD_FACTORY) +idf_component_register(SRCS "main_factory.c" "init_utils.c" INCLUDE_DIRS ".") else() -idf_component_register(SRCS "main.c" INCLUDE_DIRS ".") +idf_component_register(SRCS "main.c" "init_utils.c" INCLUDE_DIRS ".") endif() diff --git a/main/init_utils.c b/main/init_utils.c new file mode 100644 index 0000000..8ed8024 --- /dev/null +++ b/main/init_utils.c @@ -0,0 +1,39 @@ +#include "esp_log.h" +#include "esp_spiffs.h" +#include "esp_vfs.h" +#include +#include + +/** + * @brief Initialize the nvs storage for configurations. + * + */ +static inline void initialize_nvs() { + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); +} + +/** + * @brief Initialize th SPIFFS filesystem for storing logging data. + * + */ +static inline void initialize_spiffs_storage() { + const char *base_path = "/store"; + const esp_vfs_spiffs_conf_t mount_config = {.base_path = base_path, + .partition_label = "storage", + .max_files = 2, + .format_if_mount_failed = true}; + esp_err_t ret = esp_vfs_spiffs_register(&mount_config); + + if (ret != ESP_OK) { + ESP_LOGE("SPIFFS", "Failed to mount SPIFFS filesystem: %s", + esp_err_to_name(ret)); + } else { + ESP_LOGD("SPIFFS", "SPIFFS filesystem mounted successfully"); + } +} diff --git a/main/main.c b/main/main.c index 2e88b56..1d777ee 100644 --- a/main/main.c +++ b/main/main.c @@ -1,47 +1,16 @@ -#include "esp_log.h" -#include "esp_spiffs.h" -#include "esp_vfs.h" +#include "esp_event.h" #include -#include -#include + +#include "init_utils.c" #include "configuration.h" #include "data_logging.h" #include "mqtt5_connection.h" +#include "ota_scheduler.h" #include "ota_updater.h" #include "pump_control.h" #include "wifi_utils.h" -void initialize_nvs() { - esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || - ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_ERROR_CHECK(nvs_flash_erase()); - ret = nvs_flash_init(); - } - ESP_ERROR_CHECK(ret); -} - -/** - * @brief Initialize th SPIFFS filesystem for storing logging data. - * - */ -void initialize_spiffs_storage() { - const char *base_path = "/store"; - const esp_vfs_spiffs_conf_t mount_config = {.base_path = base_path, - .partition_label = "storage", - .max_files = 2, - .format_if_mount_failed = true}; - esp_err_t ret = esp_vfs_spiffs_register(&mount_config); - - if (ret != ESP_OK) { - ESP_LOGE("SPIFFS", "Failed to mount SPIFFS filesystem: %s", - esp_err_to_name(ret)); - } else { - ESP_LOGD("SPIFFS", "SPIFFS filesystem mounted successfully"); - } -} - void app_main(void) { // Configure GPIOS configure_pump_output(); @@ -57,12 +26,8 @@ void app_main(void) { // Initialize and connect to Wifi wifi_utils_init(); wifi_utils_init_sntp(); - - // run ota updater task - initialize_ota_updater(); - xTaskCreate(&ota_updater_task, "ota_updater_task", 1024 * 8, NULL, 5, NULL); - wifi_utils_create_connection_checker_task(); + // MQTT Setup mqtt5_conn_init(); mqtt5_create_connection_checker_task(); @@ -71,5 +36,10 @@ void app_main(void) { // Create control tasks ESP_ERROR_CHECK(add_notify_for_new_config(create_pump_control_task())); + // Might wait up to 24 hours for the first update. + create_ota_scheduler_task(); + + // After running for 25 hours without any errors we can mark it valid. + vTaskDelay(pdMS_TO_TICKS(25 * 60 * 60 * 1e3)); mark_running_app_version_valid(); } diff --git a/main/main_factory.c b/main/main_factory.c index 28d7dad..8f5b0e5 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -1,23 +1,12 @@ #include "esp_event.h" -#include "esp_log.h" #include -#include -#include + +#include "init_utils.c" #include "configuration.h" #include "ota_updater.h" #include "wifi_utils.h" -void initialize_nvs() { - esp_err_t ret = nvs_flash_init(); - if (ret == ESP_ERR_NVS_NO_FREE_PAGES || - ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { - ESP_ERROR_CHECK(nvs_flash_erase()); - ret = nvs_flash_init(); - } - ESP_ERROR_CHECK(ret); -} - void app_main(void) { // Initialize storage initialize_nvs(); diff --git a/profiles/factory b/profiles/factory index eb7949c..87886e6 100644 --- a/profiles/factory +++ b/profiles/factory @@ -1 +1 @@ --B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.factory" -DBUILD_FACTORY=ON +-B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults" -DBUILD_FACTORY=ON diff --git a/sdkconfig.factory b/sdkconfig.factory deleted file mode 100644 index c78fbfe..0000000 --- a/sdkconfig.factory +++ /dev/null @@ -1 +0,0 @@ -CONFIG_BUILD_FACTORY=y diff --git a/tools/ota_server/create_certificates.sh b/tools/ota_server/create_certificates.sh index 111eb3e..a573f8e 100755 --- a/tools/ota_server/create_certificates.sh +++ b/tools/ota_server/create_certificates.sh @@ -1,6 +1,14 @@ #!/bin/bash +# This script creates a server certificate for an own secure https server. +# The certificate needs to be added to the Application to work properly. +# Set `CONFIG_OTA_USE_CERT_BUNDLE=n` and +#`CONFIG_OTA_FIRMWARE_UPGRADE_URL="https://192.168.2.106:8070/EbbFlowControl.bin` to use +# the local certificate. To change the path to the added certificate update the +# CMAKE file `components/ota_updater/CMakeLists.txt` + echo "Use the IP address of the server as the Common Name (CN) for the certificate." +echo "Add IP as first argument if you need to change it." hostname=${1:-"$(hostname -I | awk '{print $1}')"} echo "Using hostname/IP: $hostname" diff --git a/tools/ota_server/ota_server.py b/tools/ota_server/ota_server.py index 1b23fbb..cc81ea5 100644 --- a/tools/ota_server/ota_server.py +++ b/tools/ota_server/ota_server.py @@ -42,7 +42,7 @@ def start_https_server( type=str, required=False, default="../../build_app/", - help="Directory containing OTA images.", + help="Directory containing OTA images. Pointing to the build folder.", ) arg_parser.add_argument( "--server_ip", @@ -76,6 +76,14 @@ def start_https_server( ) args = arg_parser.parse_args() print(f"Start Server with {args}") + cwd = os.path.realpath(os.getcwd()) + file_path = os.path.realpath(__file__) + if cwd is not file_path: + print(f"Warning: The script was not started from the correct directory. " + f"This might lead to not working properly. " + f"Start from {file_path} to not get this warning." + ) + start_https_server( ota_image_dir=args.ota_image_dir, server_ip=args.server_ip, From ceef347a0b5de39241e39cdac3e87a126d145efa Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sat, 3 Jan 2026 16:12:02 +0000 Subject: [PATCH 21/34] enable app rollback --- CHANGELOG.md | 4 +++- sdkconfig.defaults | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4d79ca..acd7572 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,11 @@ Use the following labels: - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. -## [0.1.0] - 2026-01-02 +## [0.1.0] - 2026-01-03 - [Minor] Add over the air update feature. +- [Minor] Add factory build to load latest version. +- [Minor] Add automatic update scheduler. ## [0.0.1] - 2025-07-11 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 432e68f..3f98f3d 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -12,3 +12,4 @@ CONFIG_MQTT_REPORT_DELETED_MESSAGES=y CONFIG_NEWLIB_NANO_FORMAT=y CONFIG_MBEDTLS_TLS_CLIENT_ONLY=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y +CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y From 8025e90015c2ddf7954c243da809eec25f2b8e9e Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 4 Jan 2026 16:09:44 +0000 Subject: [PATCH 22/34] correct cumputation of wait time --- components/ota_updater/ota_scheduler.c | 4 +++- main/main.c | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/ota_updater/ota_scheduler.c b/components/ota_updater/ota_scheduler.c index 3cc8d58..fdd9a22 100644 --- a/components/ota_updater/ota_scheduler.c +++ b/components/ota_updater/ota_scheduler.c @@ -28,7 +28,9 @@ void ota_scheduler_task(void *pvParameter) { uxTaskGetStackHighWaterMark(NULL)); ESP_LOGI(TAG, "Wait for %i hours before next update.", hours_until_midnight); - vTaskDelay(pdMS_TO_TICKS(hours_until_midnight * 60 * 60 * 1e3)); + const uint32_t delay_ticks = + hours_until_midnight * 60 * 60 * configTICK_RATE_HZ; + vTaskDelay(delay_ticks); // Try to update now. xTaskCreate(&ota_updater_task, "ota_updater_task", 1024 * 8, NULL, 5, NULL); diff --git a/main/main.c b/main/main.c index 1d777ee..27a1b7a 100644 --- a/main/main.c +++ b/main/main.c @@ -1,4 +1,5 @@ #include "esp_event.h" +#include "freertos/FreeRTOS.h" #include #include "init_utils.c" @@ -40,6 +41,7 @@ void app_main(void) { create_ota_scheduler_task(); // After running for 25 hours without any errors we can mark it valid. - vTaskDelay(pdMS_TO_TICKS(25 * 60 * 60 * 1e3)); + static const uint32_t delay_ticks = 25 * 60 * 60 * configTICK_RATE_HZ; + vTaskDelay(delay_ticks); mark_running_app_version_valid(); } From 7ce7d918ebcc26e638f74fd6615099457199070f Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 4 Jan 2026 17:27:16 +0000 Subject: [PATCH 23/34] version extractor --- components/ota_updater/ota_updater.c | 42 ++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index f0001ab..5411e8d 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -13,6 +13,43 @@ static const char *TAG = "ota_updater"; extern const uint8_t server_cert_pem_start[] asm("_binary_ca_cert_pem_start"); +struct application_version_t { + int major; + int minor; + int patch; + int build; + bool dirty; + // char description[16]; +}; + +void extract_application_version(const char *app_version_str, + struct application_version_t *app_version) { + if (app_version_str == NULL || app_version == NULL) { + return; + } + + int major = 0, minor = 0, patch = 0, build = 0; + bool dirty = false; + + sscanf(app_version_str, "v%d.%d.%d", &major, &minor, &patch); + + if (strstr(app_version_str, "-dirty") != NULL) { + dirty = true; + } + + // char *dash = strchr(app_version_str, '-'); + // if (dash != NULL) { + // } + + app_version->major = major; + app_version->minor = minor; + app_version->patch = patch; + app_version->build = build; + app_version->dirty = dirty; + // snprintf(app_version->description, sizeof(app_version->description), "%s", + // app_desc->version); +} + /* Event handler for catching system events */ static void ota_updater_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { @@ -76,6 +113,7 @@ static esp_err_t validate_image_header(const esp_app_desc_t *new_app_info) { return ESP_OK; } ESP_LOGI(TAG, "New Application Version: %s", new_app_info->version); + if (memcmp(new_app_info->version, running_app_info.version, sizeof(new_app_info->version)) == 0) { ESP_LOGW(TAG, "Current running version is the same as a new. We will not " @@ -137,8 +175,8 @@ void ota_updater_task(void *pvParameter) { if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) { break; } - // esp_https_ota_perform returns after every read operation which gives user - // the ability to monitor the status of OTA upgrade by calling + // esp_https_ota_perform returns after every read operation which gives + // user the ability to monitor the status of OTA upgrade by calling // esp_https_ota_get_image_len_read, which gives length of image data read // so far. const size_t len = esp_https_ota_get_image_len_read(https_ota_handle); From 9f77f0187e326f6b34e1e5442de2c97e9cbeadf8 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 4 Jan 2026 18:41:36 +0000 Subject: [PATCH 24/34] correct version handling for updates --- components/ota_updater/ota_updater.c | 118 +++++++++++++++++++++++---- 1 file changed, 102 insertions(+), 16 deletions(-) diff --git a/components/ota_updater/ota_updater.c b/components/ota_updater/ota_updater.c index 5411e8d..1edea2e 100644 --- a/components/ota_updater/ota_updater.c +++ b/components/ota_updater/ota_updater.c @@ -19,35 +19,111 @@ struct application_version_t { int patch; int build; bool dirty; - // char description[16]; + char description[9]; }; +void log_application_version(const char *prefix, + const struct application_version_t *app_version) { + if (app_version == NULL) { + return; + } + ESP_LOGI(TAG, "%s Version: %d.%d.%d", prefix, app_version->major, + app_version->minor, app_version->patch); + if (app_version->build > 0) { + ESP_LOGI(TAG, "Build: %d", app_version->build); + } + if (app_version->description[0] != '\0') { + ESP_LOGI(TAG, "Description: %s", app_version->description); + } + if (app_version->dirty) { + ESP_LOGI(TAG, "Source tree is dirty"); + } +} + +/*** + * @brief Extract application version information from version string + * + * Assumes version string is in the format: + * v..[--][-dirty] + * Examples: v1.2.3 + * v1.2.3-4-alpha + * v1.2.3-dirty + * v1.2.3-4-alpha-dirty + * + * @param app_version_str Version string to parse + * @param app_version Pointer to application_version_t struct to fill + * + */ void extract_application_version(const char *app_version_str, struct application_version_t *app_version) { if (app_version_str == NULL || app_version == NULL) { return; } - int major = 0, minor = 0, patch = 0, build = 0; - bool dirty = false; - + int major = 0; + int minor = 0; + int patch = 0; sscanf(app_version_str, "v%d.%d.%d", &major, &minor, &patch); + bool dirty = false; if (strstr(app_version_str, "-dirty") != NULL) { dirty = true; } - // char *dash = strchr(app_version_str, '-'); - // if (dash != NULL) { - // } + int build = 0; + char desc[9] = ""; + const char *first_dash = strchr(app_version_str, '-'); + if (first_dash != NULL && first_dash[1] != '\0' && first_dash[1] != 'd') { + sscanf(first_dash + 1, "%d-%8s", &build, desc); + } app_version->major = major; app_version->minor = minor; app_version->patch = patch; - app_version->build = build; app_version->dirty = dirty; - // snprintf(app_version->description, sizeof(app_version->description), "%s", - // app_desc->version); + app_version->build = build; + snprintf(app_version->description, sizeof(app_version->description), "%s", + desc); +} + +/*** + * @brief Compare two application versions. + * + * Dirty versions are considered newer (bigger) than clean versions. + * If both versions are dirty, the first version is considered newer. + * + * @param v1 First application version + * @param v2 Second application version + * @return int 1 if v1 > v2, -1 if v1 < v2, 0 if equal or invalid + */ +int compare_application_versions(const struct application_version_t *v1, + const struct application_version_t *v2) { + if (v1 == NULL || v2 == NULL) { + return 0; + } + + if (v1->major != v2->major) { + return (v1->major > v2->major) ? 1 : -1; + } + if (v1->minor != v2->minor) { + return (v1->minor > v2->minor) ? 1 : -1; + } + if (v1->patch != v2->patch) { + return (v1->patch > v2->patch) ? 1 : -1; + } + if (v1->build != v2->build) { + return (v1->build > v2->build) ? 1 : -1; + } + // Assume dirty version is newer than clean version + // if both are dirty the first version is considered newer + if (v1->dirty != v2->dirty) { + return (v1->dirty) ? 1 : -1; + } + if (v1->dirty) { + return 1; + } + + return 0; } /* Event handler for catching system events */ @@ -102,21 +178,31 @@ static esp_err_t validate_image_header(const esp_app_desc_t *new_app_info) { const esp_partition_t *running = esp_ota_get_running_partition(); esp_app_desc_t running_app_info; + struct application_version_t running_app_version; + if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK) { - ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version); + extract_application_version(running_app_info.version, &running_app_version); + log_application_version("Running", &running_app_version); } + struct application_version_t new_app_version; + extract_application_version(new_app_info->version, &new_app_version); + log_application_version("New", &new_app_version); + // Allow update if factory app is running if (strncmp(running_app_info.project_name, "EbbFlowControl-factory", sizeof(running_app_info.project_name)) == 0) { ESP_LOGI(TAG, "Factory app is running, allowing update to proceed."); return ESP_OK; } - ESP_LOGI(TAG, "New Application Version: %s", new_app_info->version); - - if (memcmp(new_app_info->version, running_app_info.version, - sizeof(new_app_info->version)) == 0) { - ESP_LOGW(TAG, "Current running version is the same as a new. We will not " + const int cmp_result = + compare_application_versions(&new_app_version, &running_app_version); + if (cmp_result < 0) { + ESP_LOGW(TAG, "New version is older than the running version. We will not " + "continue the update."); + return ESP_FAIL; + } else if (cmp_result == 0) { + ESP_LOGW(TAG, "New version is the same as the running version. We will not " "continue the update."); return ESP_FAIL; } From 64146c125ed3bccd48c229a67886ec70035a2781 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 11 Jan 2026 18:27:14 +0000 Subject: [PATCH 25/34] add config page --- .devcontainer/Dockerfile | 2 + .github/workflows/release.yml | 14 +- .gitignore | 1 + .pre-commit-config.yaml | 1 + CMakeLists.txt | 16 ++ EbbFlowControl-Setup_wifi_qr.png | Bin 0 -> 400 bytes README.md | 7 + build_all.sh | 2 + components/config_page/CMakeLists.txt | 4 + components/config_page/config_page.c | 205 ++++++++++++++++++ components/config_page/html/config_page.html | 23 ++ components/config_page/idf_component.yml | 18 ++ components/config_page/include/config_page.h | 9 + components/configuration/configuration.c | 9 + .../configuration/include/configuration.h | 1 + .../mqtt5_connection/mqtt5_connection.c | 2 +- components/wifi_utils/CMakeLists.txt | 2 +- components/wifi_utils/Kconfig | 18 ++ .../wifi_utils/include/wifi_utils_sntp.h | 10 + .../wifi_utils/include/wifi_utils_softap.h | 29 +++ .../{wifi_utils.h => wifi_utils_sta.h} | 12 +- components/wifi_utils/wifi_utils_sntp.c | 53 +++++ components/wifi_utils/wifi_utils_softap.c | 98 +++++++++ .../{wifi_utils.c => wifi_utils_sta.c} | 46 +--- dependencies.lock | 2 +- downloads/EbbFlowControl-Setup_wifi_qr.png | Bin 0 -> 400 bytes main/main.c | 7 +- main/main_factory.c | 31 ++- profiles/app | 2 +- profiles/factory | 2 +- scripts/download_and_flash_release.sh | 104 +++++++++ scripts/generate_wifi_qr_code.sh | 49 +++++ 32 files changed, 713 insertions(+), 66 deletions(-) create mode 100644 EbbFlowControl-Setup_wifi_qr.png create mode 100644 components/config_page/CMakeLists.txt create mode 100644 components/config_page/config_page.c create mode 100644 components/config_page/html/config_page.html create mode 100644 components/config_page/idf_component.yml create mode 100644 components/config_page/include/config_page.h create mode 100644 components/wifi_utils/include/wifi_utils_sntp.h create mode 100644 components/wifi_utils/include/wifi_utils_softap.h rename components/wifi_utils/include/{wifi_utils.h => wifi_utils_sta.h} (80%) create mode 100644 components/wifi_utils/wifi_utils_sntp.c create mode 100644 components/wifi_utils/wifi_utils_softap.c rename components/wifi_utils/{wifi_utils.c => wifi_utils_sta.c} (81%) create mode 100644 downloads/EbbFlowControl-Setup_wifi_qr.png create mode 100755 scripts/download_and_flash_release.sh create mode 100755 scripts/generate_wifi_qr_code.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c663023..42e3135 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -19,6 +19,8 @@ RUN apt update \ cppcheck \ # iwyu \ # cpplint \ + qrencode \ + wget \ && rm -rf /var/lib/apt/lists/* # QEMU diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9813c43..e13e315 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,10 +32,14 @@ jobs: # Change this to be your CI task/script runCmd: | ./build_all.sh - - name: Move build files + - name: Zip factory build files run: | - mv ./build_app/EbbFlowControl.bin ./EbbFlowControl.bin - mv ./build_factory/EbbFlowControl-factory.bin ./EbbFlowControl-factory.bin + # Extract filenames from flash_project_args (keeping paths) + FILES_TO_ZIP=$(awk 'NR > 1 {print "./build_factory/"$2}' ./build_factory/flash_project_args | tr '\n' ' ') + # Add additional files + FILES_TO_ZIP="$FILES_TO_ZIP EbbFlowControl-Setup_wifi_qr.png ./build_factory/flash_project_args" + echo "Files to zip: $FILES_TO_ZIP" + zip FactoryBuildFiles.zip $FILES_TO_ZIP - name: Get Changelog Entry id: changelog_reader uses: mindsers/changelog-reader-action@v2 @@ -54,5 +58,5 @@ jobs: draft: ${{ steps.changelog_reader.outputs.status == 'unreleased' }} token: ${{ secrets.GITHUB_TOKEN }} files: | - EbbFlowControl.bin - EbbFlowControl-factory.bin + ./build_app/EbbFlowControl.bin + FactoryBuildFiles.zip diff --git a/.gitignore b/.gitignore index 3ca0a09..02f5f2d 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ warnings.txt tools/ota_server/ca_cert.pem tools/ota_server/ca_key.pem log.* +FactoryBuildFiles.zip diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0326b44..4df56c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,6 +38,7 @@ repos: "--suppress=missingIncludeSystem", "--suppress=unusedFunction", "--suppress=missingInclude", + "--inline-suppr" ] # - id: cpplint # - id: include-what-you-use diff --git a/CMakeLists.txt b/CMakeLists.txt index f5e3a3d..83c593b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,3 +18,19 @@ else() project(EbbFlowControl) message(STATUS "Build Normal Application") endif() + +# Custom command to generate QR code for WiFi credentials +set(QR_CODE_FILE "${CMAKE_SOURCE_DIR}/${CONFIG_WIFI_SOFT_AP_SSID}_wifi_qr.png") +add_custom_command( + OUTPUT ${QR_CODE_FILE} + COMMAND ${CMAKE_COMMAND} -E echo "Generating WiFi QR code..." + COMMAND ./scripts/generate_wifi_qr_code.sh "${CONFIG_WIFI_SOFT_AP_SSID}" "${CONFIG_WIFI_SOFT_AP_PASSWORD}" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + DEPENDS ${SDKCONFIG} + COMMENT "Generating QR code for SoftAP WiFi credentials" +) + +# Custom target to generate QR code +add_custom_target(generate_wifi_qr + DEPENDS ${QR_CODE_FILE} +) diff --git a/EbbFlowControl-Setup_wifi_qr.png b/EbbFlowControl-Setup_wifi_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..2435b4aeeccf5f8a6f8213b73d5d7294879d6fb5 GIT binary patch literal 400 zcmV;B0dM|^P)* z)Uj>FAP@%NKXO55FMz}vbgX1AVC)6RN_4DYi3M;+fqXefQJzw~cQmia$<2pk3(o%m z`cwEHEPzki*9I7oC$(7(8?>hmM(tzX6fbshcDbOJKI+l-k#_|OXpc=kC!cponxEg} zzu;Z5P0>6W`Fu(>FV=wQvo za@&u&gJ%LqWsd_T5?O2x(A>$*$vkVDYUVGD9V}|Na7SgSM&wyBrGs +#include +#include + +#include "configuration.h" + +extern const char config_page_start[] asm("_binary_config_page_html_start"); +extern const char config_page_end[] asm("_binary_config_page_html_end"); + +static const char *TAG = "config_page"; +TaskHandle_t configuration_task_handle_ = NULL; + +// Helper function to replace placeholder in HTML +static void replace_placeholder(char *html, const char *placeholder, + const char *value) { + char *pos = strstr(html, placeholder); + if (pos) { + size_t placeholder_len = strlen(placeholder); + size_t value_len = strlen(value); + memmove(pos + value_len, pos + placeholder_len, + strlen(pos + placeholder_len) + 1); + memcpy(pos, value, value_len); + } +} + +// HTTP GET Handler +static esp_err_t root_get_handler(httpd_req_t *req) { + const u_int32_t configuration_length = + 3 + // Board ID length + strlen(configuration.network.ssid) + + strlen(configuration.network.password) + + strlen(configuration.network.mqtt_broker) + + strlen(configuration.network.mqtt_username) + + strlen(configuration.network + .mqtt_password); // Estimated length for config values + const uint32_t root_len = + config_page_end - config_page_start; // cppcheck-suppress comparePointers + char *html = malloc(root_len + configuration_length + 1); + if (!html) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Memory allocation failed"); + return ESP_FAIL; + } + memcpy(html, config_page_start, root_len); + html[root_len] = '\0'; + + // Replace placeholders with configuration values + char board_id_str[4]; + sprintf(board_id_str, "%d", configuration.id); + replace_placeholder(html, "{{board_id}}", board_id_str); + replace_placeholder(html, "{{ssid}}", configuration.network.ssid); + replace_placeholder(html, "{{wifi_password}}", + configuration.network.password); + replace_placeholder(html, "{{mqtt}}", configuration.network.mqtt_broker); + replace_placeholder(html, "{{mqtt_username}}", + configuration.network.mqtt_username); + replace_placeholder(html, "{{mqtt_password}}", + configuration.network.mqtt_password); + + ESP_LOGI(TAG, "Serve root"); + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, html, strlen(html)); + + free(html); + return ESP_OK; +} + +static const httpd_uri_t root = { + .uri = "/", .method = HTTP_GET, .handler = root_get_handler}; + +static esp_err_t set_config_post_handler(httpd_req_t *req) { + char buf[1024]; + int ret, remaining = req->content_len; + + if (remaining > sizeof(buf) - 1) { + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Content too long"); + return ESP_FAIL; + } + + ret = httpd_req_recv(req, buf, remaining); + if (ret <= 0) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, + "Failed to receive data"); + return ESP_FAIL; + } + buf[ret] = '\0'; + + ESP_LOGI(TAG, "Received POST data: %s", buf); + + // Parse the form data + char *token = strtok(buf, "&"); + while (token != NULL) { + const char *key = token; + char *value = strchr(token, '='); + if (value) { + *value = '\0'; + value++; + // URL decode value if needed, but for simplicity assume no special chars + if (strcmp(key, "board_id") == 0) { + configuration.id = atoi(value); + } else if (strcmp(key, "ssid") == 0) { + strncpy(configuration.network.ssid, value, + sizeof(configuration.network.ssid) - 1); + } else if (strcmp(key, "wifi_password") == 0) { + strncpy(configuration.network.password, value, + sizeof(configuration.network.password) - 1); + } else if (strcmp(key, "mqtt") == 0) { + strncpy(configuration.network.mqtt_broker, value, + sizeof(configuration.network.mqtt_broker) - 1); + } else if (strcmp(key, "mqtt_username") == 0) { + strncpy(configuration.network.mqtt_username, value, + sizeof(configuration.network.mqtt_username) - 1); + } else if (strcmp(key, "mqtt_password") == 0) { + strncpy(configuration.network.mqtt_password, value, + sizeof(configuration.network.mqtt_password) - 1); + } + } + token = strtok(NULL, "&"); + } + + // Save the configuration + save_configuration(); + + // Respond with success + httpd_resp_set_type(req, "text/html"); + httpd_resp_send(req, + "Configuration updated successfully. Back", + -1); + xTaskNotifyGive(configuration_task_handle_); + + return ESP_OK; +} + +static const httpd_uri_t set_config = {.uri = "/setConfig", + .method = HTTP_POST, + .handler = set_config_post_handler}; + +// HTTP Error (404) Handler - Redirects all requests to the root page +esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) { + // Set status + httpd_resp_set_status(req, "303 See Other"); + // Redirect to the "/" root directory + httpd_resp_set_hdr(req, "Location", "/"); + // iOS requires content in the response to detect a captive portal, simply + // redirecting is not sufficient. + httpd_resp_send(req, "Redirect to the captive portal", HTTPD_RESP_USE_STRLEN); + + ESP_LOGI(TAG, "Redirecting to root"); + return ESP_OK; +} + +static httpd_handle_t start_webserver(void) { + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_open_sockets = 13; + config.lru_purge_enable = true; + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) { + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &root); + httpd_register_uri_handler(server, &set_config); + httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, + http_404_error_handler); + } + return server; +} + +void serve_config_page(TaskHandle_t configuration_task_handle) { + configuration_task_handle_ = configuration_task_handle; + /* + Turn of warnings from HTTP server as redirecting traffic will yield + lots of invalid requests + */ + esp_log_level_set("httpd_uri", ESP_LOG_ERROR); + esp_log_level_set("httpd_txrx", ESP_LOG_ERROR); + esp_log_level_set("httpd_parse", ESP_LOG_ERROR); + + // // Initialize networking stack + // ESP_ERROR_CHECK(esp_netif_init()); + + // // Create default event loop needed by the main app + // ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // // Initialize NVS needed by Wi-Fi + // ESP_ERROR_CHECK(nvs_flash_init()); + + // // Initialise ESP32 in SoftAP mode + // wifi_init_softap(); + + // // Configure DNS-based captive portal, if configured + // dhcp_set_captiveportal_url(); + + // Start the server for the first time + start_webserver(); +} diff --git a/components/config_page/html/config_page.html b/components/config_page/html/config_page.html new file mode 100644 index 0000000..03fabec --- /dev/null +++ b/components/config_page/html/config_page.html @@ -0,0 +1,23 @@ + + + + + WiFi & MQTT Config + + + +
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/components/config_page/idf_component.yml b/components/config_page/idf_component.yml new file mode 100644 index 0000000..f1a00ca --- /dev/null +++ b/components/config_page/idf_component.yml @@ -0,0 +1,18 @@ +## IDF Component Manager Manifest File +dependencies: + ## Required IDF version + idf: + version: ">=4.1.0" + # # Put list of dependencies here + # # For components maintained by Espressif: + # component: "~1.0.0" + # # For 3rd party components: + # username/component: ">=1.0.0,<2.0.0" + # username2/component2: + # version: "~1.0.0" + # # For transient dependencies `public` flag can be set. + # # `public` flag doesn't have an effect dependencies of the `main` component. + # # All dependencies of `main` are public by default. + # public: true + configuration: + path: "../configuration/" diff --git a/components/config_page/include/config_page.h b/components/config_page/include/config_page.h new file mode 100644 index 0000000..9fcc9e7 --- /dev/null +++ b/components/config_page/include/config_page.h @@ -0,0 +1,9 @@ +#ifndef CONFIG_PAGE_INCLUDE_CONFIG_PAGE +#define CONFIG_PAGE_INCLUDE_CONFIG_PAGE +#include "freertos/FreeRTOS.h" +/** + * @brief Serve the configuration page by starting the web server + */ +void serve_config_page(TaskHandle_t configuration_task_handle); + +#endif /* CONFIG_PAGE_INCLUDE_CONFIG_PAGE */ diff --git a/components/configuration/configuration.c b/components/configuration/configuration.c index 1658cf4..8582cca 100644 --- a/components/configuration/configuration.c +++ b/components/configuration/configuration.c @@ -15,6 +15,7 @@ #define CONFIG_MQTT_BROKER_NAME "NetMqttB" #define CONFIG_MQTT_USERNAME_NAME "NetMqttU" #define CONFIG_MQTT_PASSWORD_NAME "NetMqttP" +#define CONFIG_NETWORK_CONFIG_VALID_NAME "NetConfV" // Maximum number of task to be notified if the config changes #define CONFIG_MAX_NUMBER_TASK_TO_NOTIFY 20 @@ -36,6 +37,7 @@ struct configuration_t configuration = { .mqtt_broker = CONFIG_MQTT_BROKER_URI, .mqtt_username = CONFIG_MQTT_USERNAME, .mqtt_password = CONFIG_MQTT_PASSWORD, + .valid = false, }, }; @@ -90,6 +92,10 @@ void load_configuration() { ESP_ERROR_CHECK_WITHOUT_ABORT( nvs_get_str(my_handle, CONFIG_MQTT_PASSWORD_NAME, configuration.network.mqtt_password, &mqtt_password_length)); + uint8_t valid = 0; + ESP_ERROR_CHECK_WITHOUT_ABORT( + nvs_get_u8(my_handle, CONFIG_NETWORK_CONFIG_VALID_NAME, &valid)); + configuration.network.valid = valid; nvs_close(my_handle); } @@ -129,6 +135,9 @@ void save_configuration() { ESP_ERROR_CHECK_WITHOUT_ABORT( nvs_set_str(my_handle, CONFIG_MQTT_PASSWORD_NAME, configuration.network.mqtt_password)); + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_u8(my_handle, + CONFIG_NETWORK_CONFIG_VALID_NAME, + configuration.network.valid)); ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_commit(my_handle)); nvs_close(my_handle); diff --git a/components/configuration/include/configuration.h b/components/configuration/include/configuration.h index ae48392..f65d6e0 100644 --- a/components/configuration/include/configuration.h +++ b/components/configuration/include/configuration.h @@ -32,6 +32,7 @@ struct network_configuration_t { char mqtt_broker[MQTT_BROKER_MAX_LENGTH]; // MQTT broker address char mqtt_username[MQTT_USERNAME_MAX_LENGTH]; // MQTT username char mqtt_password[MQTT_PASSWORD_MAX_LENGTH]; // MQTT password + bool valid; // is the config valid }; /** diff --git a/components/mqtt5_connection/mqtt5_connection.c b/components/mqtt5_connection/mqtt5_connection.c index 4accb02..f80b594 100644 --- a/components/mqtt5_connection/mqtt5_connection.c +++ b/components/mqtt5_connection/mqtt5_connection.c @@ -8,7 +8,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/task.h" -#include "wifi_utils.h" +#include "wifi_utils_sta.h" #include #include #include diff --git a/components/wifi_utils/CMakeLists.txt b/components/wifi_utils/CMakeLists.txt index 2e12d03..99fba24 100644 --- a/components/wifi_utils/CMakeLists.txt +++ b/components/wifi_utils/CMakeLists.txt @@ -1,2 +1,2 @@ -idf_component_register(SRCS "wifi_utils.c" INCLUDE_DIRS +idf_component_register(SRCS "wifi_utils_sta.c" "wifi_utils_sntp.c" "wifi_utils_softap.c" INCLUDE_DIRS "include" REQUIRES esp_wifi) diff --git a/components/wifi_utils/Kconfig b/components/wifi_utils/Kconfig index 93d97b1..bc30ab2 100644 --- a/components/wifi_utils/Kconfig +++ b/components/wifi_utils/Kconfig @@ -23,5 +23,23 @@ menu "Wifi Application Config" help Set the time to wait for the initial SNTP server response. (Default 1 min) + config WIFI_SOFT_AP_SSID + string "SSID for the WiFi SoftAP mode." + default "EbbFlowControl-Setup" + help + SSID for the WiFi SoftAP mode used for initial device configuration. + + config WIFI_SOFT_AP_PASSWORD + string "Password for the WiFi SoftAP mode." + default "ebbflowcontrol" + help + Password for the WiFi SoftAP mode used for initial device configuration. + + config WIFI_SOFT_AP_MAX_CONNECTIONS + int "Maximum number of connections for the WiFi SoftAP mode." + default 2 + help + Maximum number of connections for the WiFi SoftAP mode used for initial device configuration. + endmenu diff --git a/components/wifi_utils/include/wifi_utils_sntp.h b/components/wifi_utils/include/wifi_utils_sntp.h new file mode 100644 index 0000000..74a9e84 --- /dev/null +++ b/components/wifi_utils/include/wifi_utils_sntp.h @@ -0,0 +1,10 @@ +#ifndef COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SNTP_H +#define COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SNTP_H + +/** + * @brief Initialize the connection to the SNTP Server to synchronize the time. + * + */ +void wifi_utils_init_sntp(void); + +#endif /* COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SNTP_H */ diff --git a/components/wifi_utils/include/wifi_utils_softap.h b/components/wifi_utils/include/wifi_utils_softap.h new file mode 100644 index 0000000..a44d494 --- /dev/null +++ b/components/wifi_utils/include/wifi_utils_softap.h @@ -0,0 +1,29 @@ +#ifndef COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SOFTAP_H +#define COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SOFTAP_H + +#include "esp_err.h" +#include "esp_event.h" + +/** + * @brief Initialize the WiFi in SoftAP mode. + */ +void wifi_init_softap(void); + +/** + * @brief Set the captive portal URL for DHCP. + */ +void dhcp_set_captiveportal_url(void); + +/** + * @brief Destroy the SoftAP and stop WiFi in AP mode. + */ +void destroy_softap(); + +/** + * @brief Get the number of connected stations to the SoftAP. + * + * @return int number of connected stations + */ +int get_wifi_softap_connections(); + +#endif /* COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_SOFTAP_H */ diff --git a/components/wifi_utils/include/wifi_utils.h b/components/wifi_utils/include/wifi_utils_sta.h similarity index 80% rename from components/wifi_utils/include/wifi_utils.h rename to components/wifi_utils/include/wifi_utils_sta.h index f905b94..d362d27 100644 --- a/components/wifi_utils/include/wifi_utils.h +++ b/components/wifi_utils/include/wifi_utils_sta.h @@ -1,5 +1,5 @@ -#ifndef COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS -#define COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS +#ifndef COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_STA +#define COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_STA #include "esp_err.h" #include "freertos/FreeRTOS.h" @@ -11,12 +11,6 @@ */ void wifi_utils_init(void); -/** - * @brief Initialize the connection to the SNTP Server to synchronize the time. - * - */ -void wifi_utils_init_sntp(void); - /** * @brief Get the wifi connection strength. * @@ -55,4 +49,4 @@ void wifi_utils_check_connection_task(void *pvParameters); */ TaskHandle_t wifi_utils_create_connection_checker_task(); -#endif /* COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS */ +#endif /* COMPONENTS_WIFI_UTILS_INCLUDE_WIFI_UTILS_STA */ diff --git a/components/wifi_utils/wifi_utils_sntp.c b/components/wifi_utils/wifi_utils_sntp.c new file mode 100644 index 0000000..44ddcca --- /dev/null +++ b/components/wifi_utils/wifi_utils_sntp.c @@ -0,0 +1,53 @@ +#include "wifi_utils_sntp.h" + +#include "configuration.h" +#include "esp_log.h" +#include "esp_netif_sntp.h" +#include "freertos/FreeRTOS.h" +#include +#include +#include + +static const char *TAG = "wifi_sntp"; + +void wifi_utils_init_sntp(void) { + setenv("TZ", CONFIG_LOCAL_TIME_ZONE, 1); + tzset(); + + struct tm timeinfo = { + .tm_sec = 0, + .tm_min = 0, + .tm_hour = 0, + .tm_mday = 1, + .tm_mon = 1, + .tm_year = 2025, + }; + // initialize time as 10 min before the first pump cycle. + // just in case the time is not set over SNTP we start pumping soon. + const unsigned short nr_pump_cycles = + configuration.pump_cycles.nr_pump_cycles; + if (nr_pump_cycles > 0) { + const int first_pump_time_minutes_per_day = + ((24 * 60) + + (configuration.pump_cycles.times_minutes_per_day[0] - 10)) % + (24 * 60); + const int hours = first_pump_time_minutes_per_day / 60; + const int minutes = first_pump_time_minutes_per_day % 60; + timeinfo.tm_hour = hours; + timeinfo.tm_min = minutes; + } + + struct timeval initial_time = {.tv_sec = mktime(&timeinfo), .tv_usec = 0}; + settimeofday(&initial_time, NULL); + + esp_sntp_config_t config = + ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_WIFI_SNTP_POOL_SERVER); + esp_netif_sntp_init(&config); + while (esp_netif_sntp_sync_wait(CONFIG_WIFI_SNTP_INIT_WAIT_TIME_MS / + portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT) { + ESP_LOGW(TAG, "Could not set time over SNTP. Tried for %d ms", + CONFIG_WIFI_SNTP_INIT_WAIT_TIME_MS); + return; + } + ESP_LOGD(TAG, "System time synced."); +} diff --git a/components/wifi_utils/wifi_utils_softap.c b/components/wifi_utils/wifi_utils_softap.c new file mode 100644 index 0000000..31d83d0 --- /dev/null +++ b/components/wifi_utils/wifi_utils_softap.c @@ -0,0 +1,98 @@ +#include "wifi_utils_softap.h" + +#include "esp_event.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "lwip/inet.h" + +static const char *TAG = "wifi_softap"; +static int number_of_connections_ = 0; + +static void wifi_event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + if (event_id == WIFI_EVENT_AP_STACONNECTED) { + wifi_event_ap_staconnected_t *event = + (wifi_event_ap_staconnected_t *)event_data; + ESP_LOGI(TAG, "station " MACSTR " join, AID=%d", MAC2STR(event->mac), + event->aid); + number_of_connections_++; + } else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) { + wifi_event_ap_stadisconnected_t *event = + (wifi_event_ap_stadisconnected_t *)event_data; + ESP_LOGI(TAG, "station " MACSTR " leave, AID=%d, reason=%d", + MAC2STR(event->mac), event->aid, event->reason); + number_of_connections_--; + } +} + +void wifi_init_softap(void) { + esp_netif_create_default_wifi_ap(); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, + &wifi_event_handler, NULL)); + + wifi_config_t wifi_config = { + .ap = {.ssid = CONFIG_WIFI_SOFT_AP_SSID, + .ssid_len = strlen(CONFIG_WIFI_SOFT_AP_SSID), + .password = CONFIG_WIFI_SOFT_AP_PASSWORD, + .max_connection = CONFIG_WIFI_SOFT_AP_MAX_CONNECTIONS, + .authmode = WIFI_AUTH_WPA_WPA2_PSK}, + }; + if (strlen(CONFIG_WIFI_SOFT_AP_PASSWORD) == 0) { + wifi_config.ap.authmode = WIFI_AUTH_OPEN; + } + + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); + + esp_netif_ip_info_t ip_info; + esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), + &ip_info); + + char ip_addr[16]; + inet_ntoa_r(ip_info.ip.addr, ip_addr, 16); + ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr); + + ESP_LOGI(TAG, "wifi_init_softap finished. SSID:'%s' password:'%s'", + CONFIG_WIFI_SOFT_AP_SSID, CONFIG_WIFI_SOFT_AP_PASSWORD); +} + +void dhcp_set_captiveportal_url(void) { + // get the IP of the access point to redirect to + esp_netif_ip_info_t ip_info; + esp_netif_get_ip_info(esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"), + &ip_info); + + char ip_addr[16]; + inet_ntoa_r(ip_info.ip.addr, ip_addr, 16); + ESP_LOGI(TAG, "Set up softAP with IP: %s", ip_addr); + + // turn the IP into a URI + char *captiveportal_uri = (char *)malloc(32 * sizeof(char)); + assert(captiveportal_uri && "Failed to allocate captiveportal_uri"); + strcpy(captiveportal_uri, "http://"); + strcat(captiveportal_uri, ip_addr); + + // get a handle to configure DHCP with + esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF"); + + // set the DHCP option 114 + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_stop(netif)); + ESP_ERROR_CHECK(esp_netif_dhcps_option( + netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captiveportal_uri, + strlen(captiveportal_uri))); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_netif_dhcps_start(netif)); +} + +void destroy_softap() { + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_wifi_stop()); + ESP_ERROR_CHECK_WITHOUT_ABORT(esp_event_handler_unregister( + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler)); +} + +int get_wifi_softap_connections() { return number_of_connections_; } diff --git a/components/wifi_utils/wifi_utils.c b/components/wifi_utils/wifi_utils_sta.c similarity index 81% rename from components/wifi_utils/wifi_utils.c rename to components/wifi_utils/wifi_utils_sta.c index 76b7218..93a9a0d 100644 --- a/components/wifi_utils/wifi_utils.c +++ b/components/wifi_utils/wifi_utils_sta.c @@ -1,4 +1,5 @@ -#include "wifi_utils.h" +#include "wifi_utils_sta.h" +#include "wifi_utils_sntp.h" #include "esp_bit_defs.h" @@ -66,7 +67,6 @@ static void event_handler(void *arg, esp_event_base_t event_base, void wifi_utils_init(void) { // Configure and initialize wifi - ESP_ERROR_CHECK(esp_netif_init()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); @@ -145,48 +145,6 @@ void wifi_utils_check_connection_task(void *pvParameters) { } } -void wifi_utils_init_sntp(void) { - setenv("TZ", CONFIG_LOCAL_TIME_ZONE, 1); - tzset(); - - struct tm timeinfo = { - .tm_sec = 0, - .tm_min = 0, - .tm_hour = 0, - .tm_mday = 1, - .tm_mon = 1, - .tm_year = 2025, - }; - // initialize time as 10 min before the first pump cycle. - // just in case the time is not set over SNTP we start pumping soon. - const unsigned short nr_pump_cycles = - configuration.pump_cycles.nr_pump_cycles; - if (nr_pump_cycles > 0) { - const int first_pump_time_minutes_per_day = - ((24 * 60) + - (configuration.pump_cycles.times_minutes_per_day[0] - 10)) % - (24 * 60); - const int hours = first_pump_time_minutes_per_day / 60; - const int minutes = first_pump_time_minutes_per_day % 60; - timeinfo.tm_hour = hours; - timeinfo.tm_min = minutes; - } - - struct timeval initial_time = {.tv_sec = mktime(&timeinfo), .tv_usec = 0}; - settimeofday(&initial_time, NULL); - - esp_sntp_config_t config = - ESP_NETIF_SNTP_DEFAULT_CONFIG(CONFIG_WIFI_SNTP_POOL_SERVER); - esp_netif_sntp_init(&config); - while (esp_netif_sntp_sync_wait(CONFIG_WIFI_SNTP_INIT_WAIT_TIME_MS / - portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT) { - ESP_LOGW(TAG, "Could not set time over SNTP. Tried for %d ms", - CONFIG_WIFI_SNTP_INIT_WAIT_TIME_MS); - return; - } - ESP_LOGD(TAG, "System time synced."); -} - esp_err_t wifi_utils_get_connection_strength(int *rssi_level) { return esp_wifi_sta_get_rssi(rssi_level); } diff --git a/dependencies.lock b/dependencies.lock index d9fe808..6f8a69d 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -38,6 +38,6 @@ direct_dependencies: - idf - mqtt5_connection - wifi_utils -manifest_hash: 44ed24fb213c76fac8024f62bd8a302819da1bf219e91e2bfa7f71b9c9528fce +manifest_hash: bebf590e1eae53fca6caaeb6b9e9b835d0401dad874932fc6c681f296c171d8b target: esp32 version: 2.0.0 diff --git a/downloads/EbbFlowControl-Setup_wifi_qr.png b/downloads/EbbFlowControl-Setup_wifi_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..2435b4aeeccf5f8a6f8213b73d5d7294879d6fb5 GIT binary patch literal 400 zcmV;B0dM|^P)* z)Uj>FAP@%NKXO55FMz}vbgX1AVC)6RN_4DYi3M;+fqXefQJzw~cQmia$<2pk3(o%m z`cwEHEPzki*9I7oC$(7(8?>hmM(tzX6fbshcDbOJKI+l-k#_|OXpc=kC!cponxEg} zzu;Z5P0>6W`Fu(>FV=wQvo za@&u&gJ%LqWsd_T5?O2x(A>$*$vkVDYUVGD9V}|Na7SgSM&wyBrGs @@ -10,7 +11,8 @@ #include "ota_scheduler.h" #include "ota_updater.h" #include "pump_control.h" -#include "wifi_utils.h" +#include "wifi_utils_sntp.h" +#include "wifi_utils_sta.h" void app_main(void) { // Configure GPIOS @@ -25,6 +27,7 @@ void app_main(void) { ESP_ERROR_CHECK(esp_event_loop_create_default()); // Initialize and connect to Wifi + ESP_ERROR_CHECK(esp_netif_init()); wifi_utils_init(); wifi_utils_init_sntp(); wifi_utils_create_connection_checker_task(); @@ -40,6 +43,8 @@ void app_main(void) { // Might wait up to 24 hours for the first update. create_ota_scheduler_task(); + // Mark config as valid after successful wifi and mqtt connection + // After running for 25 hours without any errors we can mark it valid. static const uint32_t delay_ticks = 25 * 60 * 60 * configTICK_RATE_HZ; vTaskDelay(delay_ticks); diff --git a/main/main_factory.c b/main/main_factory.c index 8f5b0e5..95a4f6d 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -1,11 +1,15 @@ #include "esp_event.h" +#include "esp_netif.h" #include #include "init_utils.c" +#include "config_page.h" #include "configuration.h" #include "ota_updater.h" -#include "wifi_utils.h" +#include "wifi_utils_sntp.h" +#include "wifi_utils_softap.h" +#include "wifi_utils_sta.h" void app_main(void) { // Initialize storage @@ -16,7 +20,30 @@ void app_main(void) { // Create Event Loop ESP_ERROR_CHECK(esp_event_loop_create_default()); - // Initialize and connect to Wifi + // Initialize Wifi + ESP_ERROR_CHECK(esp_netif_init()); + + // Start SoftAP and serve configuration page + wifi_init_softap(); + dhcp_set_captiveportal_url(); + serve_config_page(xTaskGetCurrentTaskHandle()); + + if (configuration.network.valid) { + // if config valid wait for one minute and check if somebody connected to + // the ap + const u_int32_t wait_time = pdMS_TO_TICKS(60 * 1e3); + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(wait_time)); + } + + if (configuration.network.valid == false || + get_wifi_softap_connections() > 0) { + // wait indefinitely for configuration + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + } + + // if configured destroy soft ap and start sta + destroy_softap(); + wifi_utils_init(); wifi_utils_init_sntp(); diff --git a/profiles/app b/profiles/app index 4b75f6c..a34f339 100644 --- a/profiles/app +++ b/profiles/app @@ -1 +1 @@ --B build_app -DSDKCONFIG=build_app/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults" +-B build_app -DSDKCONFIG=build_app/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.private" diff --git a/profiles/factory b/profiles/factory index 87886e6..82d3fb4 100644 --- a/profiles/factory +++ b/profiles/factory @@ -1 +1 @@ --B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults" -DBUILD_FACTORY=ON +-B build_factory -DSDKCONFIG=build_factory/sdkconfig -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.private" -DBUILD_FACTORY=ON diff --git a/scripts/download_and_flash_release.sh b/scripts/download_and_flash_release.sh new file mode 100755 index 0000000..d4a9dc2 --- /dev/null +++ b/scripts/download_and_flash_release.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Script to download factory app from GitHub releases and flash to ESP32 +# Usage: ./download_and_flash_release.sh [OPTIONS] [VERSION] +# Options: +# -p, --port PORT Serial port (default: /dev/ttyUSB0) +# -h, --help Show this help +# If VERSION is not specified, uses the latest tag + +set -e + +REPO="phofmeier/EbbFlowControl" +ZIP_NAME="EbbFlowControl-factory.zip" # Base name, will be versioned +DEFAULT_PORT="/dev/ttyUSB0" + +# Parse arguments +PORT="$DEFAULT_PORT" +VERSION="" + +while [[ $# -gt 0 ]]; do + case $1 in + -p|--port) + PORT="$2" + shift 2 + ;; + -h|--help) + echo "Usage: $0 [OPTIONS] [VERSION]" + echo "Download and flash factory app from GitHub releases" + echo "" + echo "Options:" + echo " -p, --port PORT Serial port (default: $DEFAULT_PORT)" + echo " -h, --help Show this help" + echo "" + echo "If VERSION is not specified, uses the latest tag" + exit 0 + ;; + *) + if [ -z "$VERSION" ]; then + VERSION="$1" + else + echo "Unknown option: $1" + echo "Use -h for help" + exit 1 + fi + shift + ;; + esac +done + +# Get version if not specified +if [ -z "$VERSION" ]; then + echo "No version specified, getting latest tag..." + VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "latest") + if [ "$VERSION" = "latest" ]; then + echo "Could not determine latest tag from git, using 'latest'" + fi +fi + +echo "Using version: $VERSION" +echo "Serial port: $PORT" + +# Construct download URL for zip + +ZIP_FILE="FactoryBuildFiles.zip" +DOWNLOAD_URL="https://github.com/$REPO/releases/download/$VERSION/$ZIP_FILE" + +echo "Downloading from: $DOWNLOAD_URL" + +# Create downloads directory +DOWNLOAD_DIR=$(pwd)/downloads +mkdir -p "$DOWNLOAD_DIR" + +ZIP_PATH="$DOWNLOAD_DIR/$ZIP_FILE" +EXTRACT_DIR="$DOWNLOAD_DIR/extracted_$VERSION" + +echo "Downloading $ZIP_FILE..." +if ! curl -L -o "$ZIP_PATH" "$DOWNLOAD_URL"; then + echo "Failed to download $ZIP_FILE from $DOWNLOAD_URL" + exit 1 +fi + +echo "Download complete." + +# Extract zip +echo "Extracting $ZIP_FILE..." +mkdir -p "$EXTRACT_DIR" +if ! unzip -q "$ZIP_PATH" -d "$EXTRACT_DIR"; then + echo "Failed to extract $ZIP_FILE" + exit 1 +fi + +echo "Extracted successfully." + +# Flash all components +echo "Flashing to ESP32..." + +cd "$EXTRACT_DIR/build_factory" || exit 1 +python -m esptool --chip esp32 --port "$PORT" -b 460800 --before default_reset --after hard_reset write_flash @"flash_project_args" || { + echo "Flashing failed!" + exit 1 +} +echo "Flash complete!" + +echo "Factory app $VERSION flashed successfully to $PORT." diff --git a/scripts/generate_wifi_qr_code.sh b/scripts/generate_wifi_qr_code.sh new file mode 100755 index 0000000..0647e05 --- /dev/null +++ b/scripts/generate_wifi_qr_code.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +# Script to generate WiFi QR code using qrencode +# Usage: ./generate_wifi_qr_code.sh [SECURITY_TYPE] +# SECURITY_TYPE defaults to WPA2 + +if [ $# -lt 2 ]; then + echo "Usage: $0 [SECURITY_TYPE]" + echo "SECURITY_TYPE: WPA, WPA2, WEP, or nopass (default: WPA2)" + exit 1 +fi + +SSID="$1" +PASSWORD="$2" +SECURITY="${3:-WPA2}" + +# Validate security type +case "$SECURITY" in + WPA|WPA2|WEP) + ;; + nopass) + SECURITY="" + ;; + *) + echo "Invalid security type. Use WPA, WPA2, WEP, or nopass" + exit 1 + ;; +esac + +# Generate the WiFi QR code string +if [ -z "$SECURITY" ]; then + QR_STRING="WIFI:S:$SSID;;" +else + QR_STRING="WIFI:S:$SSID;T:$SECURITY;P:$PASSWORD;;" +fi + +echo "Generating QR code for WiFi network: $SSID" +echo "QR String: $QR_STRING" + +# Generate QR code and save as PNG +OUTPUT_FILE="${SSID}_wifi_qr.png" +qrencode -o "$OUTPUT_FILE" "$QR_STRING" + +if [ $? -eq 0 ]; then + echo "QR code generated successfully: $OUTPUT_FILE" +else + echo "Failed to generate QR code" + exit 1 +fi From 789d4568ffa480e29e83bb68632cf8ea72e642fa Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 11 Jan 2026 18:36:46 +0000 Subject: [PATCH 26/34] fix build --- components/wifi_utils/wifi_utils_softap.c | 5 +++++ sdkconfig.private | 4 ++++ 2 files changed, 9 insertions(+) create mode 100644 sdkconfig.private diff --git a/components/wifi_utils/wifi_utils_softap.c b/components/wifi_utils/wifi_utils_softap.c index 31d83d0..79a989f 100644 --- a/components/wifi_utils/wifi_utils_softap.c +++ b/components/wifi_utils/wifi_utils_softap.c @@ -7,6 +7,11 @@ #include "esp_wifi.h" #include "lwip/inet.h" +#ifndef MAC2STR +#define MAC2STR(a) (a)[0], (a)[1], (a)[2], (a)[3], (a)[4], (a)[5] +#define MACSTR "%02x:%02x:%02x:%02x:%02x:%02x" +#endif + static const char *TAG = "wifi_softap"; static int number_of_connections_ = 0; diff --git a/sdkconfig.private b/sdkconfig.private new file mode 100644 index 0000000..eb32759 --- /dev/null +++ b/sdkconfig.private @@ -0,0 +1,4 @@ +CONFIG_WIFI_SSID="WIFI_SSID" +CONFIG_WIFI_PASSWORD="WIFI_PASS" +CONFIG_DEVICE_ID=0 +CONFIG_MQTT_BROKER_URI="mqtt://example" From c40dc51ac0e76a9499d07e92f4b094e84aad04a9 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 18:33:41 +0000 Subject: [PATCH 27/34] fix config page --- components/config_page/config_page.c | 68 +++++++++++++++---- components/config_page/html/config_page.html | 2 + components/configuration/configuration.c | 11 ++- .../configuration/include/configuration.h | 6 +- .../mqtt5_connection/mqtt5_connection.c | 2 + components/wifi_utils/wifi_utils_sta.c | 3 +- main/main.c | 9 ++- main/main_factory.c | 14 +++- 8 files changed, 89 insertions(+), 26 deletions(-) diff --git a/components/config_page/config_page.c b/components/config_page/config_page.c index 6df73e1..cf992a6 100644 --- a/components/config_page/config_page.c +++ b/components/config_page/config_page.c @@ -4,6 +4,7 @@ #include "esp_http_server.h" #include "esp_log.h" #include "nvs_flash.h" +#include #include #include #include @@ -16,6 +17,36 @@ extern const char config_page_end[] asm("_binary_config_page_html_end"); static const char *TAG = "config_page"; TaskHandle_t configuration_task_handle_ = NULL; +/** URL decode a string in place */ +void urldecode2(char *dst, const char *src) { + while (*src) { + char a, b; + if ((*src == '%') && ((a = src[1]) && (b = src[2])) && + (isxdigit(a) && isxdigit(b))) { + if (a >= 'a') + a -= 'a' - 'A'; + if (a >= 'A') + a -= ('A' - 10); + else + a -= '0'; + if (b >= 'a') + b -= 'a' - 'A'; + if (b >= 'A') + b -= ('A' - 10); + else + b -= '0'; + *dst++ = 16 * a + b; + src += 3; + } else if (*src == '+') { + *dst++ = ' '; + src++; + } else { + *dst++ = *src++; + } + } + *dst++ = '\0'; +} + // Helper function to replace placeholder in HTML static void replace_placeholder(char *html, const char *placeholder, const char *value) { @@ -31,14 +62,16 @@ static void replace_placeholder(char *html, const char *placeholder, // HTTP GET Handler static esp_err_t root_get_handler(httpd_req_t *req) { + // Estimated length for config values const u_int32_t configuration_length = 3 + // Board ID length strlen(configuration.network.ssid) + - strlen(configuration.network.password) + + strlen(configuration.network.password) + // + 40 + // Wifi status length strlen(configuration.network.mqtt_broker) + strlen(configuration.network.mqtt_username) + - strlen(configuration.network - .mqtt_password); // Estimated length for config values + strlen(configuration.network.mqtt_password) + // + 40; // MQTT status length const uint32_t root_len = config_page_end - config_page_start; // cppcheck-suppress comparePointers char *html = malloc(root_len + configuration_length + 1); @@ -57,11 +90,23 @@ static esp_err_t root_get_handler(httpd_req_t *req) { replace_placeholder(html, "{{ssid}}", configuration.network.ssid); replace_placeholder(html, "{{wifi_password}}", configuration.network.password); + + const char *wifi_status = + (configuration.network.valid_bits & NETWORK_WIFI_VALID_BIT) + ? "OK" + : "FAILED"; + replace_placeholder(html, "{{wifi_status}}", wifi_status); + replace_placeholder(html, "{{mqtt}}", configuration.network.mqtt_broker); replace_placeholder(html, "{{mqtt_username}}", configuration.network.mqtt_username); replace_placeholder(html, "{{mqtt_password}}", configuration.network.mqtt_password); + const char *mqtt_status = + (configuration.network.valid_bits & NETWORK_MQTT_VALID_BIT) + ? "OK" + : "FAILED"; + replace_placeholder(html, "{{mqtt_status}}", mqtt_status); ESP_LOGI(TAG, "Serve root"); httpd_resp_set_type(req, "text/html"); @@ -105,20 +150,15 @@ static esp_err_t set_config_post_handler(httpd_req_t *req) { if (strcmp(key, "board_id") == 0) { configuration.id = atoi(value); } else if (strcmp(key, "ssid") == 0) { - strncpy(configuration.network.ssid, value, - sizeof(configuration.network.ssid) - 1); + urldecode2(configuration.network.ssid, value); } else if (strcmp(key, "wifi_password") == 0) { - strncpy(configuration.network.password, value, - sizeof(configuration.network.password) - 1); + urldecode2(configuration.network.password, value); } else if (strcmp(key, "mqtt") == 0) { - strncpy(configuration.network.mqtt_broker, value, - sizeof(configuration.network.mqtt_broker) - 1); + urldecode2(configuration.network.mqtt_broker, value); } else if (strcmp(key, "mqtt_username") == 0) { - strncpy(configuration.network.mqtt_username, value, - sizeof(configuration.network.mqtt_username) - 1); + urldecode2(configuration.network.mqtt_username, value); } else if (strcmp(key, "mqtt_password") == 0) { - strncpy(configuration.network.mqtt_password, value, - sizeof(configuration.network.mqtt_password) - 1); + urldecode2(configuration.network.mqtt_password, value); } } token = strtok(NULL, "&"); @@ -159,7 +199,7 @@ esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) { static httpd_handle_t start_webserver(void) { httpd_handle_t server = NULL; httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.max_open_sockets = 13; + config.max_open_sockets = 5; config.lru_purge_enable = true; // Start the httpd server diff --git a/components/config_page/html/config_page.html b/components/config_page/html/config_page.html index 03fabec..e664981 100644 --- a/components/config_page/html/config_page.html +++ b/components/config_page/html/config_page.html @@ -11,11 +11,13 @@


+
{{wifi_status}}




+
{{mqtt_status}}
diff --git a/components/configuration/configuration.c b/components/configuration/configuration.c index 8582cca..e1b9e85 100644 --- a/components/configuration/configuration.c +++ b/components/configuration/configuration.c @@ -37,7 +37,7 @@ struct configuration_t configuration = { .mqtt_broker = CONFIG_MQTT_BROKER_URI, .mqtt_username = CONFIG_MQTT_USERNAME, .mqtt_password = CONFIG_MQTT_PASSWORD, - .valid = false, + .valid_bits = 0x00, }, }; @@ -92,10 +92,9 @@ void load_configuration() { ESP_ERROR_CHECK_WITHOUT_ABORT( nvs_get_str(my_handle, CONFIG_MQTT_PASSWORD_NAME, configuration.network.mqtt_password, &mqtt_password_length)); - uint8_t valid = 0; - ESP_ERROR_CHECK_WITHOUT_ABORT( - nvs_get_u8(my_handle, CONFIG_NETWORK_CONFIG_VALID_NAME, &valid)); - configuration.network.valid = valid; + ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_get_u8(my_handle, + CONFIG_NETWORK_CONFIG_VALID_NAME, + &configuration.network.valid_bits)); nvs_close(my_handle); } @@ -137,7 +136,7 @@ void save_configuration() { configuration.network.mqtt_password)); ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_set_u8(my_handle, CONFIG_NETWORK_CONFIG_VALID_NAME, - configuration.network.valid)); + configuration.network.valid_bits)); ESP_ERROR_CHECK_WITHOUT_ABORT(nvs_commit(my_handle)); nvs_close(my_handle); diff --git a/components/configuration/include/configuration.h b/components/configuration/include/configuration.h index f65d6e0..66b5f1a 100644 --- a/components/configuration/include/configuration.h +++ b/components/configuration/include/configuration.h @@ -2,6 +2,7 @@ #define COMPONENTS_CONFIGURATION_INCLUDE_CONFIGURATION #include "cJSON.h" +#include "esp_bit_defs.h" #include "freertos/FreeRTOS.h" #include "freertos/event_groups.h" #include "freertos/task.h" @@ -26,13 +27,16 @@ struct pump_cycle_configuration_t { #define MQTT_USERNAME_MAX_LENGTH 64 #define MQTT_PASSWORD_MAX_LENGTH 64 +#define NETWORK_WIFI_VALID_BIT BIT0 +#define NETWORK_MQTT_VALID_BIT BIT1 + struct network_configuration_t { char ssid[WIFI_SSID_MAX_LENGTH]; // SSID of WiFi network char password[WIFI_PASSWORD_MAX_LENGTH]; // Password of WiFi network char mqtt_broker[MQTT_BROKER_MAX_LENGTH]; // MQTT broker address char mqtt_username[MQTT_USERNAME_MAX_LENGTH]; // MQTT username char mqtt_password[MQTT_PASSWORD_MAX_LENGTH]; // MQTT password - bool valid; // is the config valid + uint8_t valid_bits; // is the config valid }; /** diff --git a/components/mqtt5_connection/mqtt5_connection.c b/components/mqtt5_connection/mqtt5_connection.c index f80b594..1fecacf 100644 --- a/components/mqtt5_connection/mqtt5_connection.c +++ b/components/mqtt5_connection/mqtt5_connection.c @@ -147,6 +147,8 @@ static void mqtt5_event_handler(void *handler_args, esp_event_base_t base, send_status_connected(client); send_current_configuration(client); set_connected(); + configuration.network.valid_bits |= NETWORK_MQTT_VALID_BIT; + save_configuration(); break; case MQTT_EVENT_DISCONNECTED: mqtt5_connected = false; diff --git a/components/wifi_utils/wifi_utils_sta.c b/components/wifi_utils/wifi_utils_sta.c index 93a9a0d..ea12f1e 100644 --- a/components/wifi_utils/wifi_utils_sta.c +++ b/components/wifi_utils/wifi_utils_sta.c @@ -93,7 +93,6 @@ void wifi_utils_init(void) { ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_MIN_MODEM)); ESP_LOGD(TAG, "wifi_init_sta finished."); s_wifi_event_group = xEventGroupCreate(); - ESP_ERROR_CHECK_WITHOUT_ABORT(wifi_utils_connect_wifi_blocking()); } void wifi_utils_connect() { @@ -117,6 +116,8 @@ esp_err_t wifi_utils_connect_wifi_blocking() { if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", configuration.network.ssid, configuration.network.password); + configuration.network.valid_bits |= NETWORK_WIFI_VALID_BIT; + save_configuration(); return ESP_OK; } diff --git a/main/main.c b/main/main.c index 8b0769e..3aded5d 100644 --- a/main/main.c +++ b/main/main.c @@ -29,6 +29,7 @@ void app_main(void) { // Initialize and connect to Wifi ESP_ERROR_CHECK(esp_netif_init()); wifi_utils_init(); + ESP_ERROR_CHECK_WITHOUT_ABORT(wifi_utils_connect_wifi_blocking()); wifi_utils_init_sntp(); wifi_utils_create_connection_checker_task(); @@ -46,7 +47,11 @@ void app_main(void) { // Mark config as valid after successful wifi and mqtt connection // After running for 25 hours without any errors we can mark it valid. - static const uint32_t delay_ticks = 25 * 60 * 60 * configTICK_RATE_HZ; - vTaskDelay(delay_ticks); + static const uint32_t initial_delay_ticks = 25 * 60 * 60 * configTICK_RATE_HZ; + vTaskDelay(initial_delay_ticks); + while (configuration.network.valid_bits != + (NETWORK_WIFI_VALID_BIT | NETWORK_MQTT_VALID_BIT)) { + vTaskDelay(pdMS_TO_TICKS(1000 * 60 * 60)); // check every hour + } mark_running_app_version_valid(); } diff --git a/main/main_factory.c b/main/main_factory.c index 95a4f6d..e563b69 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -11,6 +11,8 @@ #include "wifi_utils_softap.h" #include "wifi_utils_sta.h" +static const char *TAG = "factory_main"; + void app_main(void) { // Initialize storage initialize_nvs(); @@ -28,14 +30,16 @@ void app_main(void) { dhcp_set_captiveportal_url(); serve_config_page(xTaskGetCurrentTaskHandle()); - if (configuration.network.valid) { + if (configuration.network.valid_bits == + (NETWORK_WIFI_VALID_BIT | NETWORK_MQTT_VALID_BIT)) { // if config valid wait for one minute and check if somebody connected to // the ap const u_int32_t wait_time = pdMS_TO_TICKS(60 * 1e3); ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(wait_time)); } - if (configuration.network.valid == false || + if (configuration.network.valid_bits != + (NETWORK_WIFI_VALID_BIT | NETWORK_MQTT_VALID_BIT) || get_wifi_softap_connections() > 0) { // wait indefinitely for configuration ulTaskNotifyTake(pdTRUE, portMAX_DELAY); @@ -45,6 +49,12 @@ void app_main(void) { destroy_softap(); wifi_utils_init(); + esp_err_t wifi_error = wifi_utils_connect_wifi_blocking(); + if (wifi_error != ESP_OK) { + configuration.network.valid_bits &= ~NETWORK_WIFI_VALID_BIT; + ESP_LOGE(TAG, "Could not connect to WiFi. Restarting..."); + esp_restart(); + } wifi_utils_init_sntp(); // run ota updater task From 04fa293f1ce10e9d2fe8f2fb6633e4bbf370715a Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 18:48:52 +0000 Subject: [PATCH 28/34] generate url qr code --- .github/workflows/release.yml | 2 +- CMakeLists.txt | 2 +- EbbFlowControl-Setup_connection_url_qr.png | Bin 0 -> 348 bytes scripts/generate_wifi_qr_code.sh | 21 ++++++++++++++++++--- 4 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 EbbFlowControl-Setup_connection_url_qr.png diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e13e315..85a1c97 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: # Extract filenames from flash_project_args (keeping paths) FILES_TO_ZIP=$(awk 'NR > 1 {print "./build_factory/"$2}' ./build_factory/flash_project_args | tr '\n' ' ') # Add additional files - FILES_TO_ZIP="$FILES_TO_ZIP EbbFlowControl-Setup_wifi_qr.png ./build_factory/flash_project_args" + FILES_TO_ZIP="$FILES_TO_ZIP EbbFlowControl-Setup_wifi_qr.png EbbFlowControl-Setup_connection_url_qr.png ./build_factory/flash_project_args" echo "Files to zip: $FILES_TO_ZIP" zip FactoryBuildFiles.zip $FILES_TO_ZIP - name: Get Changelog Entry diff --git a/CMakeLists.txt b/CMakeLists.txt index 83c593b..2216279 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ set(QR_CODE_FILE "${CMAKE_SOURCE_DIR}/${CONFIG_WIFI_SOFT_AP_SSID}_wifi_qr.png") add_custom_command( OUTPUT ${QR_CODE_FILE} COMMAND ${CMAKE_COMMAND} -E echo "Generating WiFi QR code..." - COMMAND ./scripts/generate_wifi_qr_code.sh "${CONFIG_WIFI_SOFT_AP_SSID}" "${CONFIG_WIFI_SOFT_AP_PASSWORD}" + COMMAND ./scripts/generate_wifi_qr_code.sh "${CONFIG_WIFI_SOFT_AP_SSID}" "${CONFIG_WIFI_SOFT_AP_PASSWORD}" "192.168.4.1" "80" "WPA2" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} DEPENDS ${SDKCONFIG} COMMENT "Generating QR code for SoftAP WiFi credentials" diff --git a/EbbFlowControl-Setup_connection_url_qr.png b/EbbFlowControl-Setup_connection_url_qr.png new file mode 100644 index 0000000000000000000000000000000000000000..c834706465e2b132171f6a2e89b13543e6d582f7 GIT binary patch literal 348 zcmeAS@N?(olHy`uVBq!ia0vp^$so+g3?#RH)wu?w*aCb)T!Hle|NocXoPQU{Vk!yp z3;zHA#Mb-AfjrIvkH}&M25un`X1sK_?hjD#ou`Xqh(+(&D;w7yRuEvguvz29AtsMC zj>o+c8Gpnbi0kY#oe;rb#ijkdQ0V3Ejs1_@PlfJbDk+y?kYnB#!Fu448k5fyt-NiX z^1qvcCX_2LzfsQLvAWWy&)ZPx&O4cp`kRB_es#^{zdA3oe?q^kO|anu<=N4*GQRq9 zT10DJohEZ!>R@nHMod-tVKtABDpg8vL+?-RI%m3S(N!(D$ zMTzkn_9e@&ES`}PsJOIl`?&{y-rbz~ZKqr2$6EeyuG$D8hS}^-0z=P~I`}o`XQ^)8 rzkg3g)cKuh+|z}Zp8s$Ae_{N&%j;LSDfo5)ea_(N>gTe~DWM4f-*u7D literal 0 HcmV?d00001 diff --git a/scripts/generate_wifi_qr_code.sh b/scripts/generate_wifi_qr_code.sh index 0647e05..297f7b6 100755 --- a/scripts/generate_wifi_qr_code.sh +++ b/scripts/generate_wifi_qr_code.sh @@ -1,18 +1,20 @@ #!/bin/bash # Script to generate WiFi QR code using qrencode -# Usage: ./generate_wifi_qr_code.sh [SECURITY_TYPE] +# Usage: ./generate_wifi_qr_code.sh [SECURITY_TYPE] # SECURITY_TYPE defaults to WPA2 if [ $# -lt 2 ]; then - echo "Usage: $0 [SECURITY_TYPE]" + echo "Usage: $0 [SECURITY_TYPE]" echo "SECURITY_TYPE: WPA, WPA2, WEP, or nopass (default: WPA2)" exit 1 fi SSID="$1" PASSWORD="$2" -SECURITY="${3:-WPA2}" +IP_ADDRESS="${3}" +PORT="${4}" +SECURITY="${5:-WPA2}" # Validate security type case "$SECURITY" in @@ -47,3 +49,16 @@ else echo "Failed to generate QR code" exit 1 fi + +# Generate connection URL QR code +CONNECTION_URL="http://${IP_ADDRESS}:${PORT}/" +URL_QR_FILE="${SSID}_connection_url_qr.png" +echo "Generating QR code for connection URL: $CONNECTION_URL" +qrencode -o "$URL_QR_FILE" "$CONNECTION_URL" + +if [ $? -eq 0 ]; then + echo "Connection URL QR code generated successfully: $URL_QR_FILE" +else + echo "Failed to generate connection URL QR code" + exit 1 +fi From e30bae42457550f1da76ba55b30a4f27ed5895d8 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 18:52:46 +0000 Subject: [PATCH 29/34] changelog adapted --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd7572..5beccd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ Use the following labels: - `Fixed` for any bug fixes. - `Security` in case of vulnerabilities. +## [0.1.1] - 2026-01-13 + +- [Patch] Add configuration webpage for network configuration. + ## [0.1.0] - 2026-01-03 - [Minor] Add over the air update feature. From 34d6c28fc663cc2099b4450cf3743705a231da7b Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 19:12:34 +0000 Subject: [PATCH 30/34] Update script to flash from download --- .gitignore | 1 + README.md | 6 +++++- downloads/EbbFlowControl-Setup_wifi_qr.png | Bin 400 -> 0 bytes scripts/download_and_flash_release.sh | 8 ++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) delete mode 100644 downloads/EbbFlowControl-Setup_wifi_qr.png diff --git a/.gitignore b/.gitignore index 02f5f2d..49bf48e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ tools/ota_server/ca_cert.pem tools/ota_server/ca_key.pem log.* FactoryBuildFiles.zip +downloads/ diff --git a/README.md b/README.md index a8cceaa..d548297 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,14 @@ This repository hold the software for a controller for an automated ebb flow hyd ## First configuration -Scan the QR Code. +Scan the QR Code to connect to the Wifi. ![Wifi QR Code](EbbFlowControl-Setup_wifi_qr.png?raw=True) +Scan the QR Code to show the Configuration Website. + +![Config Website URL](EbbFlowControl-Setup_connection_url_qr.png?raw=True) + ## Over the Air (OTA) updates diff --git a/downloads/EbbFlowControl-Setup_wifi_qr.png b/downloads/EbbFlowControl-Setup_wifi_qr.png deleted file mode 100644 index 2435b4aeeccf5f8a6f8213b73d5d7294879d6fb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmV;B0dM|^P)* z)Uj>FAP@%NKXO55FMz}vbgX1AVC)6RN_4DYi3M;+fqXefQJzw~cQmia$<2pk3(o%m z`cwEHEPzki*9I7oC$(7(8?>hmM(tzX6fbshcDbOJKI+l-k#_|OXpc=kC!cponxEg} zzu;Z5P0>6W`Fu(>FV=wQvo za@&u&gJ%LqWsd_T5?O2x(A>$*$vkVDYUVGD9V}|Na7SgSM&wyBrGs/dev/null 2>&1 exit 1 } echo "Flash complete!" echo "Factory app $VERSION flashed successfully to $PORT." +# move back to original directory +cd - >/dev/null 2>&1 +exit 0 From db39e0e4dd2cc43b607adacc442c7ddce1caafba Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 19:40:51 +0000 Subject: [PATCH 31/34] update flash script for hard reset --- main/main_factory.c | 4 ++++ scripts/download_and_flash_release.sh | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/main/main_factory.c b/main/main_factory.c index e563b69..2f4ca40 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -45,6 +45,10 @@ void app_main(void) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } + // Waiting here means we got configured. + ESP_LOGI(TAG, "Configuration received. Starting WiFi in STA mode."); + ulTaskDelay(pdMS_TO_TICKS(10 * 1e3)); + // if configured destroy soft ap and start sta destroy_softap(); diff --git a/scripts/download_and_flash_release.sh b/scripts/download_and_flash_release.sh index 0f56d91..a190c96 100755 --- a/scripts/download_and_flash_release.sh +++ b/scripts/download_and_flash_release.sh @@ -4,6 +4,7 @@ # Usage: ./download_and_flash_release.sh [OPTIONS] [VERSION] # Options: # -p, --port PORT Serial port (default: /dev/ttyUSB0) +# -r, --hard-reset Perform hard reset and erase NVS partition # -h, --help Show this help # If VERSION is not specified, uses the latest tag @@ -15,6 +16,7 @@ DEFAULT_PORT="/dev/ttyUSB0" # Parse arguments PORT="$DEFAULT_PORT" +HARD_RESET=false VERSION="" while [[ $# -gt 0 ]]; do @@ -23,12 +25,17 @@ while [[ $# -gt 0 ]]; do PORT="$2" shift 2 ;; + -r|--hard-reset) + HARD_RESET=true + shift + ;; -h|--help) echo "Usage: $0 [OPTIONS] [VERSION]" echo "Download and flash factory app from GitHub releases" echo "" echo "Options:" echo " -p, --port PORT Serial port (default: $DEFAULT_PORT)" + echo " -r, --hard-reset Perform hard reset and erase NVS partition" echo " -h, --help Show this help" echo "" echo "If VERSION is not specified, uses the latest tag" @@ -87,6 +94,7 @@ echo "Download complete." # Extract zip echo "Extracting $ZIP_FILE..." mkdir -p "$EXTRACT_DIR" +# Unzip always overwrites, no need to clean if ! unzip -q "$ZIP_PATH" -d "$EXTRACT_DIR"; then echo "Failed to extract $ZIP_FILE" exit 1 @@ -98,6 +106,18 @@ echo "Extracted successfully." echo "Flashing to ESP32..." cd "$EXTRACT_DIR/build_factory" || exit 1 + +if [ "$HARD_RESET" = true ]; then + echo "Performing hard reset and erasing NVS partition..." + python -m esptool --chip esp32 --port "$PORT" erase_flash || { + echo "Failed to erase flash!" + # move back to original directory + cd - >/dev/null 2>&1 + exit 1 + } + echo "Flash erased." +fi + python -m esptool --chip esp32 --port "$PORT" -b 460800 --before default_reset --after hard_reset write_flash @"flash_project_args" || { echo "Flashing failed!" # move back to original directory @@ -106,6 +126,10 @@ python -m esptool --chip esp32 --port "$PORT" -b 460800 --before default_reset - } echo "Flash complete!" +if [ "$HARD_RESET" = true ]; then + echo "Hard reset performed - NVS partition erased." +fi + echo "Factory app $VERSION flashed successfully to $PORT." # move back to original directory cd - >/dev/null 2>&1 From 7e7b59df27e5c656031a169bff28f2c406d01373 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Tue, 13 Jan 2026 19:41:54 +0000 Subject: [PATCH 32/34] using correct task delay function --- main/main_factory.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/main_factory.c b/main/main_factory.c index 2f4ca40..36fcae0 100644 --- a/main/main_factory.c +++ b/main/main_factory.c @@ -47,7 +47,7 @@ void app_main(void) { // Waiting here means we got configured. ESP_LOGI(TAG, "Configuration received. Starting WiFi in STA mode."); - ulTaskDelay(pdMS_TO_TICKS(10 * 1e3)); + vTaskDelay(pdMS_TO_TICKS(10 * 1e3)); // if configured destroy soft ap and start sta destroy_softap(); From bd7ebeff1797d4ee71b6415ba3500abc29ef9992 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 18 Jan 2026 12:39:31 +0000 Subject: [PATCH 33/34] add force download flag to flash script --- scripts/download_and_flash_release.sh | 84 +++++++++++++++------------ 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/scripts/download_and_flash_release.sh b/scripts/download_and_flash_release.sh index a190c96..883affe 100755 --- a/scripts/download_and_flash_release.sh +++ b/scripts/download_and_flash_release.sh @@ -3,20 +3,22 @@ # Script to download factory app from GitHub releases and flash to ESP32 # Usage: ./download_and_flash_release.sh [OPTIONS] [VERSION] # Options: -# -p, --port PORT Serial port (default: /dev/ttyUSB0) -# -r, --hard-reset Perform hard reset and erase NVS partition -# -h, --help Show this help +# -p, --port PORT Serial port (default: /dev/ttyUSB0) +# -r, --hard-reset Perform hard reset and erase NVS partition +# -f, --force-download Force download even if version already exists +# -h, --help Show this help # If VERSION is not specified, uses the latest tag set -e REPO="phofmeier/EbbFlowControl" -ZIP_NAME="EbbFlowControl-factory.zip" # Base name, will be versioned DEFAULT_PORT="/dev/ttyUSB0" # Parse arguments PORT="$DEFAULT_PORT" HARD_RESET=false +FORCE_DOWNLOAD=false +DOWNLOAD_DIR=$(pwd)/downloads VERSION="" while [[ $# -gt 0 ]]; do @@ -29,14 +31,19 @@ while [[ $# -gt 0 ]]; do HARD_RESET=true shift ;; + -f|--force-download) + FORCE_DOWNLOAD=true + shift + ;; -h|--help) echo "Usage: $0 [OPTIONS] [VERSION]" echo "Download and flash factory app from GitHub releases" echo "" echo "Options:" - echo " -p, --port PORT Serial port (default: $DEFAULT_PORT)" - echo " -r, --hard-reset Perform hard reset and erase NVS partition" - echo " -h, --help Show this help" + echo " -p, --port PORT Serial port (default: $DEFAULT_PORT)" + echo " -r, --hard-reset Perform hard reset and erase NVS partition" + echo " -f, --force-download Force download even if version already exists" + echo " -h, --help Show this help" echo "" echo "If VERSION is not specified, uses the latest tag" exit 0 @@ -59,7 +66,13 @@ if [ -z "$VERSION" ]; then echo "No version specified, getting latest tag..." VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "latest") if [ "$VERSION" = "latest" ]; then - echo "Could not determine latest tag from git, using 'latest'" + echo "Could not determine latest tag from local git, using 'latest'" + # get latest version from GitHub API + VERSION=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep -oP '"tag_name": "\K(.*)(?=")') + if [ -z "$VERSION" ]; then + echo "Failed to get latest version from GitHub API" + exit 1 + fi fi fi @@ -69,39 +82,42 @@ VERSION="${VERSION#v}" echo "Using version: $VERSION" echo "Serial port: $PORT" -# Construct download URL for zip +# Check if version already exists (unless force download is enabled) +if [ "$FORCE_DOWNLOAD" = false ] && [ -d "$DOWNLOAD_DIR/extracted_$VERSION" ]; then + echo "Version $VERSION already downloaded. Use --force-download to re-download." + echo "Proceeding with existing download..." +else + ZIP_FILE="FactoryBuildFiles.zip" + DOWNLOAD_URL="https://github.com/$REPO/releases/download/$VERSION/$ZIP_FILE" -ZIP_FILE="FactoryBuildFiles.zip" -DOWNLOAD_URL="https://github.com/$REPO/releases/download/$VERSION/$ZIP_FILE" + echo "Downloading from: $DOWNLOAD_URL" -echo "Downloading from: $DOWNLOAD_URL" + # Create downloads directory + mkdir -p "$DOWNLOAD_DIR" -# Create downloads directory -DOWNLOAD_DIR=$(pwd)/downloads -mkdir -p "$DOWNLOAD_DIR" + ZIP_PATH="$DOWNLOAD_DIR/$ZIP_FILE" + EXTRACT_DIR="$DOWNLOAD_DIR/extracted_$VERSION" -ZIP_PATH="$DOWNLOAD_DIR/$ZIP_FILE" -EXTRACT_DIR="$DOWNLOAD_DIR/extracted_$VERSION" + echo "Downloading $ZIP_FILE..." + if ! curl -L -o "$ZIP_PATH" "$DOWNLOAD_URL"; then + echo "Failed to download $ZIP_FILE from $DOWNLOAD_URL" + exit 1 + fi -echo "Downloading $ZIP_FILE..." -if ! curl -L -o "$ZIP_PATH" "$DOWNLOAD_URL"; then - echo "Failed to download $ZIP_FILE from $DOWNLOAD_URL" - exit 1 -fi + echo "Download complete." -echo "Download complete." + # Extract zip + echo "Extracting $ZIP_FILE..." + mkdir -p "$EXTRACT_DIR" + # Unzip always overwrites, no need to clean + if ! unzip -o -q "$ZIP_PATH" -d "$EXTRACT_DIR"; then + echo "Failed to extract $ZIP_FILE" + exit 1 + fi -# Extract zip -echo "Extracting $ZIP_FILE..." -mkdir -p "$EXTRACT_DIR" -# Unzip always overwrites, no need to clean -if ! unzip -q "$ZIP_PATH" -d "$EXTRACT_DIR"; then - echo "Failed to extract $ZIP_FILE" - exit 1 + echo "Extracted successfully." fi -echo "Extracted successfully." - # Flash all components echo "Flashing to ESP32..." @@ -126,10 +142,6 @@ python -m esptool --chip esp32 --port "$PORT" -b 460800 --before default_reset - } echo "Flash complete!" -if [ "$HARD_RESET" = true ]; then - echo "Hard reset performed - NVS partition erased." -fi - echo "Factory app $VERSION flashed successfully to $PORT." # move back to original directory cd - >/dev/null 2>&1 From d81eb43425ac8a8530e9c90478c1b2f6c8377236 Mon Sep 17 00:00:00 2001 From: phofmeier Date: Sun, 18 Jan 2026 13:19:44 +0000 Subject: [PATCH 34/34] update Readme --- README.md | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index d548297..b050817 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +![GitHub release (latest by date)](https://img.shields.io/github/v/release/phofmeier/EbbFlowControl?label=Current%20Release) [![build](https://github.com/phofmeier/EbbFlowControl/actions/workflows/build.yml/badge.svg)](https://github.com/phofmeier/EbbFlowControl/actions/workflows/build.yml) [![pre-commit](https://github.com/phofmeier/EbbFlowControl/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/phofmeier/EbbFlowControl/actions/workflows/pre-commit.yml) @@ -5,7 +6,11 @@ This repository hold the software for a controller for an automated ebb flow hydroponic grow system. The controller runs on an ESP32 and can be configured via MQTT. The MQTT connection is additionally used to send status information and data for monitoring to an overall system. -## First configuration +## Quick Start + +Use the script `scripts/download_and_flash_release.sh` to download and flash the newest released software version to your ESP32 Board. + +### First configuration Scan the QR Code to connect to the Wifi. @@ -18,23 +23,31 @@ Scan the QR Code to show the Configuration Website. ## Over the Air (OTA) updates -## Factory vs Application +There are two different apps built for this project. -- Build application: `idf.py @profiles/app build` -- Flash application: `idf.py -B build_app flash` -- Build factory: `idf.py @profiles/factory build` -- Flash factory: `idf.py -B build_factory flash` +### Factory application + +The factory application does not hold the normal application. It only serves for a first initial configuration and downloading the latests main application. It starts an Wifi Access point and host a webpage to configure the device for the first time. The Wifi and webpage can be joind by scanning the qr-codes shown [here](#first-configuration). After submitting the correct configuration it downloads the latest version of the main application and starts it. + +### Main application + +The main application is running always. It serves all the implemented features. It can be updated over the air by having always teo copies of the application. Always when a new version is released it is downloaded automatically at around midnight and is written on en extra partition. Be aware that it never overrides the factory app. After a successful update the new code needs to run for more than 24h to be considered valid. A restart during this timeframe would consider the update as invalid and the old app would be booted again. ## Build and Flash -The easiest way to build the software is to run the Docker devcontainer. -Inside the container you can use the espressif idf build environment. Run the following commands to build flash and monitor the device on the software. +If you do not need any special configuration you can just download and flash the prebuild release version with the script located `scripts/download_and_flash_release.sh`. -``` -idf.py build -idf.py flash -idf.py monitor -``` +If you need to change anything the easiest way to build the software is to run the Docker devcontainer. +Inside the container you can use the espressif idf build environment. + +### Factory vs Application build + +For building the factory or main application the profile files can be used. The following list shows the how to use them. + +- Build application: `idf.py @profiles/app build` +- Flash application: `idf.py @profiles/app flash` +- Build factory: `idf.py @profiles/factory build` +- Flash factory: `idf.py @profiles/factory flash` For configuration use the idf configuration environment. See the paragraph about the [configuration](#configuration) for more details. @@ -143,6 +156,7 @@ Data: | id | uint_8 | Id of the specific board Integer between 0 and 255 | | connection | string | Current connection status to the MQTT Broker. "connected" or "disconnected" | | rssi_level | int | Connection strength of the Wifi connection. -100 if an error occurs. | +| version | string | Version string of the current running version. | Example: