diff --git a/.gitmodules b/.gitmodules index bb648b8..8541c06 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,3 +37,6 @@ [submodule "components/esp-nimble-cpp"] path = components/esp-nimble-cpp url = git@github.com:0xFEEDC0DE64/esp-nimble-cpp.git +[submodule "components/espasyncota"] + path = components/espasyncota + url = git@github.com:0xFEEDC0DE64/espasyncota.git diff --git a/components/espasyncota b/components/espasyncota new file mode 160000 index 0000000..b76c6c7 --- /dev/null +++ b/components/espasyncota @@ -0,0 +1 @@ +Subproject commit b76c6c7bf2c11ccf53f0c346b509848a59a0936b diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index a1f91fd..e839778 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,6 +1,7 @@ set(headers ble.h led.h + ota.h webserver.h wifi.h ) @@ -9,13 +10,15 @@ set(sources ble.cpp led.cpp main.cpp + ota.cpp webserver.cpp wifi.cpp ) set(dependencies freertos nvs_flash esp_http_server esp_https_ota mdns app_update esp_system esp_websocket_client driver - arduino-esp32 ArduinoJson FastLED-idf cpputils date espchrono espcpputils esp-nimble-cpp esphttpdutils espwifistack expected fmt + arduino-esp32 ArduinoJson FastLED-idf cpputils date espchrono espasyncota espcpputils esp-nimble-cpp + esphttpdutils espwifistack expected fmt ) idf_component_register( diff --git a/main/led.cpp b/main/led.cpp index f81ba8d..5b20558 100644 --- a/main/led.cpp +++ b/main/led.cpp @@ -13,9 +13,8 @@ #include #include -#define LEDC_CHANNEL_0 0 -#define LEDC_CHANNEL_1 0 -#define LEDC_CHANNEL_2 0 +using namespace std::chrono_literals; + #define LEDC_TIMER_13_BIT 13 #define LEDC_BASE_FREQ 5000 @@ -25,10 +24,113 @@ std::array leds; CRGBPalette16 currentPalette; TBlendType currentBlending; -extern CRGBPalette16 myRedWhiteBluePalette; -extern const TProgmemPalette16 myRedWhiteBluePalette_p; +const TProgmemPalette16 myRedWhiteBluePalette_p = +{ + CRGB::Red, + CRGB::Gray, // 'white' is too bright compared to red and blue + CRGB::Blue, + CRGB::Black, + + CRGB::Red, + CRGB::Gray, + CRGB::Blue, + CRGB::Black, + + CRGB::Red, + CRGB::Red, + CRGB::Gray, + CRGB::Gray, + CRGB::Blue, + CRGB::Blue, + CRGB::Black, + CRGB::Black +}; void ledcAnalogWrite(uint8_t channel, uint32_t value, uint32_t valueMax = 255); +void ChangePalettePeriodically(); +void FillLEDsFromPaletteColors( uint8_t colorIndex); + +espchrono::millis_clock::time_point lastRedraw{}; +} + +void led_setup() +{ + currentPalette = RainbowColors_p; + currentBlending = LINEARBLEND; + + ledcSetup(0, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(1, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(2, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + + ledcSetup(3, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(4, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(5, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + + ledcSetup(6, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(7, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(8, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + + ledcSetup(9, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(10, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(11, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + + ledcSetup(12, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(13, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + ledcSetup(14, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); + + ledcAttachPin(27, 0); + ledcAttachPin(33, 1); + ledcAttachPin(32, 2); + + ledcAttachPin(23, 3); + ledcAttachPin(25, 4); + ledcAttachPin(26, 5); + + ledcAttachPin(19, 6); + ledcAttachPin(21, 7); + ledcAttachPin(22, 8); + + ledcAttachPin(18, 9); + ledcAttachPin(17, 10); + ledcAttachPin(16, 11); + + ledcAttachPin(13, 12); + ledcAttachPin(14, 13); + ledcAttachPin(15, 14); +} + +void led_update() +{ + if (espchrono::ago(lastRedraw) < 20ms) + return; + lastRedraw = espchrono::millis_clock::now(); + + ChangePalettePeriodically(); + + static uint8_t startIndex = 0; + startIndex = startIndex + 1; /* motion speed */ + + FillLEDsFromPaletteColors(startIndex); + + ledcAnalogWrite(0, leds[0].red); + ledcAnalogWrite(1, leds[0].green); + ledcAnalogWrite(2, leds[0].blue); + + ledcAnalogWrite(3, leds[1].red); + ledcAnalogWrite(4, leds[1].green); + ledcAnalogWrite(5, leds[1].blue); + + ledcAnalogWrite(6, leds[2].red); + ledcAnalogWrite(7, leds[2].green); + ledcAnalogWrite(8, leds[2].blue); + + ledcAnalogWrite(9, leds[3].red); + ledcAnalogWrite(10, leds[3].green); + ledcAnalogWrite(11, leds[3].blue); + + ledcAnalogWrite(12, leds[4].red); + ledcAnalogWrite(13, leds[4].green); + ledcAnalogWrite(14, leds[4].blue); } namespace { @@ -83,29 +185,6 @@ void SetupPurpleAndGreenPalette() purple, purple, black, black ); } -const TProgmemPalette16 myRedWhiteBluePalette_p = -{ - CRGB::Red, - CRGB::Gray, // 'white' is too bright compared to red and blue - CRGB::Blue, - CRGB::Black, - - CRGB::Red, - CRGB::Gray, - CRGB::Blue, - CRGB::Black, - - CRGB::Red, - CRGB::Red, - CRGB::Gray, - CRGB::Gray, - CRGB::Blue, - CRGB::Blue, - CRGB::Black, - CRGB::Black -}; - - void ChangePalettePeriodically() { constexpr auto millis = []() -> unsigned int { return espchrono::millis_clock::now().time_since_epoch().count(); }; @@ -128,32 +207,4 @@ void ChangePalettePeriodically() if( secondHand == 55) { currentPalette = myRedWhiteBluePalette_p; currentBlending = LINEARBLEND; } } } -} - - -void led_setup() -{ - currentPalette = RainbowColors_p; - currentBlending = LINEARBLEND; - - ledcSetup(LEDC_CHANNEL_0, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); - ledcSetup(LEDC_CHANNEL_1, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); - ledcSetup(LEDC_CHANNEL_2, LEDC_BASE_FREQ, LEDC_TIMER_13_BIT); - ledcAttachPin(27, LEDC_CHANNEL_0); - ledcAttachPin(33, LEDC_CHANNEL_1); - ledcAttachPin(32, LEDC_CHANNEL_2); -} - -void led_update() -{ - ChangePalettePeriodically(); - - static uint8_t startIndex = 0; - startIndex = startIndex + 1; /* motion speed */ - - FillLEDsFromPaletteColors( startIndex); - - ledcAnalogWrite(LEDC_CHANNEL_0, leds[0].red); - ledcAnalogWrite(LEDC_CHANNEL_1, leds[0].green); - ledcAnalogWrite(LEDC_CHANNEL_2, leds[0].blue); -} +} // namespace diff --git a/main/main.cpp b/main/main.cpp index 3116a1c..2c4b216 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -19,6 +19,7 @@ #include "wifi.h" #include "ble.h" #include "webserver.h" +#include "ota.h" #include "led.h" using namespace std::chrono_literals; @@ -65,6 +66,7 @@ extern "C" void app_main() wifi_setup(); ble_setup(); webserver_setup(); + ota_setup(); led_setup(); while (true) @@ -77,6 +79,7 @@ extern "C" void app_main() wifi_update(); ble_update(); webserver_update(); + ota_update(); led_update(); } } diff --git a/main/ota.cpp b/main/ota.cpp new file mode 100644 index 0000000..48342b1 --- /dev/null +++ b/main/ota.cpp @@ -0,0 +1,50 @@ +#include "ota.h" + +// esp-idf includes +#include + +// 3rdparty lib includes +#include +#include +#include + +// local includes +#include "wifi.h" + +namespace { +constexpr const char * const TAG = "OTA"; + +cpputils::DelayedConstruction _asyncOta; +} // namespace + +EspAsyncOta &asyncOta{_asyncOta.getUnsafe()}; + +void ota_setup() +{ + ESP_LOGI(TAG, "called"); + + _asyncOta.construct(); + + if (const auto result = _asyncOta->startTask(); !result) + { + ESP_LOGE(TAG, "starting OTA task failed: %.*s", result.error().size(), result.error().data()); + return; + } +} + +void ota_update() +{ + _asyncOta->update(); +} + +tl::expected triggerOta(std::string_view url) +{ + ESP_LOGI(TAG, "%.*s", url.size(), url.data()); + + if (const auto result = _asyncOta->trigger(url, {}, {}, {}); !result) + return tl::make_unexpected(std::move(result).error()); + + wifi_stack::delete_scan_result(); + + return {}; +} diff --git a/main/ota.h b/main/ota.h new file mode 100644 index 0000000..fe8fabd --- /dev/null +++ b/main/ota.h @@ -0,0 +1,18 @@ +#pragma once + +// system includes +#include +#include + +// 3rdparty lib includes +#include + +// forward declares +class EspAsyncOta; + +extern EspAsyncOta &asyncOta; + +void ota_setup(); +void ota_update(); + +tl::expected triggerOta(std::string_view url); diff --git a/main/webserver.cpp b/main/webserver.cpp index d606583..78cd051 100644 --- a/main/webserver.cpp +++ b/main/webserver.cpp @@ -7,8 +7,12 @@ // 3rdparty lib includes #include #include +#include +#include // local includes +#include "wifi.h" +#include "ota.h" using namespace esphttpdutils; @@ -18,6 +22,7 @@ constexpr const char * const TAG = "WEBSERVER"; httpd_handle_t httpdHandle; esp_err_t webserver_root_handler(httpd_req_t *req); +esp_err_t webserver_ota_handler(httpd_req_t *req); } // namespace void webserver_setup() @@ -34,7 +39,8 @@ void webserver_setup() } for (const httpd_uri_t &uri : { - httpd_uri_t { .uri = "/", .method = HTTP_GET, .handler = webserver_root_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/", .method = HTTP_GET, .handler = webserver_root_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/ota", .method = HTTP_GET, .handler = webserver_ota_handler, .user_ctx = NULL }, }) { const auto result = httpd_register_uri_handler(httpdHandle, &uri); @@ -52,8 +58,78 @@ void webserver_update() namespace { esp_err_t webserver_root_handler(httpd_req_t *req) { - std::string body = "hello world"; + std::string body = fmt::format("

{}

", htmlentities(deviceName)); + + if (const auto otaStatus = asyncOta.status(); otaStatus == OtaCloudUpdateStatus::Idle) + { + body += + "
" + "" + "" + "
"; + } + else + { + const auto progress = asyncOta.progress(); + const char *color; + switch (otaStatus) + { + case OtaCloudUpdateStatus::Updating: color = "#b8b800"; break; + case OtaCloudUpdateStatus::Succeeded: color = "#13c200"; break; + default: color = "#b80000"; + } + + body += fmt::format("

OTA: status={} progress={}", color, esphttpdutils::htmlentities(toString(otaStatus)), progress); + + if (const auto totalSize = asyncOta.totalSize()) + { + body += fmt::format(" totalSize={}", *totalSize); + if (*totalSize) + body += fmt::format(" percentage={:.1f}%", 100.f * progress / *totalSize); + } + + if (const auto &message = asyncOta.message(); !message.empty()) + body += fmt::format(" message={}", esphttpdutils::htmlentities(message)); + + body += "

\n"; + } CALL_AND_EXIT(webserver_resp_send_succ, req, "text/html", body) } + +esp_err_t webserver_ota_handler(httpd_req_t *req) +{ + std::string query; + + if (const size_t queryLength = httpd_req_get_url_query_len(req)) + { + query.resize(queryLength); + CALL_AND_EXIT_ON_ERROR(httpd_req_get_url_query_str, req, query.data(), query.size() + 1) + } + + char urlBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), "url", urlBufEncoded, sizeof(urlBufEncoded)); result == ESP_ERR_NOT_FOUND) + { + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", "url parameter missing") + } + else if (result != ESP_OK) + { + const auto msg = fmt::format("httpd_query_key_value() {} failed with {}", "url", esp_err_to_name(result)); + ESP_LOGE(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", msg) + } + + char urlBuf[257]; + esphttpdutils::urldecode(urlBuf, urlBufEncoded); + + std::string_view url{urlBuf}; + + if (const auto result = triggerOta(url); !result) + { + ESP_LOGE(TAG, "%.*s", result.error().size(), result.error().data()); + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", result.error()) + } + + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/plain", "OTA called...") +} } diff --git a/partitions.csv b/partitions.csv index 25d0f8d..ff5549b 100644 --- a/partitions.csv +++ b/partitions.csv @@ -1,4 +1,5 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x10000, 0x5000, otadata, data, ota, 0x15000, 0x2000, -app0, app, ota_0, 0x20000, 0x1F0000, +app0, app, ota_0, 0x20000, 0x190000, +app1, app, ota_1, 0x220000, 0x190000,