diff --git a/.gitmodules b/.gitmodules index 55daf02..92477d9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -43,3 +43,6 @@ [submodule "components/espasyncota"] path = components/espasyncota url = git@github.com:0xFEEDC0DE64/espasyncota.git +[submodule "components/esphttpdutils"] + path = components/esphttpdutils + url = git@github.com:0xFEEDC0DE64/esphttpdutils.git diff --git a/components/espasyncota b/components/espasyncota index bbc1f64..b76c6c7 160000 --- a/components/espasyncota +++ b/components/espasyncota @@ -1 +1 @@ -Subproject commit bbc1f643e2f4af41edc642f73c420edda8ea16cc +Subproject commit b76c6c7bf2c11ccf53f0c346b509848a59a0936b diff --git a/components/espcpputils b/components/espcpputils index 78fa43e..1ef4a9e 160000 --- a/components/espcpputils +++ b/components/espcpputils @@ -1 +1 @@ -Subproject commit 78fa43edcfc24dfd2faceb16396332a9f1b3c203 +Subproject commit 1ef4a9ea561947fa327c59e54a0cef79f6817c46 diff --git a/components/esphttpdutils b/components/esphttpdutils new file mode 160000 index 0000000..a787113 --- /dev/null +++ b/components/esphttpdutils @@ -0,0 +1 @@ +Subproject commit a78711317ba4a5d5ee0ebf9e047a3609919ef898 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index fd791c7..bd63976 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -37,8 +37,9 @@ set(sources set(dependencies freertos nvs_flash esp_http_server esp_https_ota mdns app_update esp_system mqtt - arduino-esp32 cpputils date esp-nimble-cpp espasyncota espchrono espcpputils espwifistack expected fmt + arduino-esp32 date esp-nimble-cpp expected fmt Adafruit_BMP085_Unified Adafruit_TSL2561 DHT-sensor-library + cpputils espasyncota espchrono espcpputils esphttpdutils espwifistack ) idf_component_register( diff --git a/main/webserver.cpp b/main/webserver.cpp index f350f5c..f984203 100644 --- a/main/webserver.cpp +++ b/main/webserver.cpp @@ -21,12 +21,14 @@ #include "mymqtt.h" #include "espwifistack.h" #include "espcppmacros.h" -#include "espstrutils.h" #include "strutils.h" #include "espchrono.h" #include "numberparsing.h" #include "myota.h" #include "espasyncota.h" +#include "esphttpdutils.h" + +using namespace esphttpdutils; namespace deckenlampe { httpd_handle_t httpdHandle; @@ -34,9 +36,6 @@ httpd_handle_t httpdHandle; namespace { constexpr const char * const TAG = "WEBSERVER"; -template T htmlentities(const T &val) { return val; } // TODO -template T htmlentities(T &&val) { return val; } // TODO - std::atomic shouldReboot; esp_err_t webserver_root_handler(httpd_req_t *req); @@ -44,6 +43,7 @@ esp_err_t webserver_on_handler(httpd_req_t *req); esp_err_t webserver_off_handler(httpd_req_t *req); esp_err_t webserver_toggle_handler(httpd_req_t *req); esp_err_t webserver_reboot_handler(httpd_req_t *req); +esp_err_t webserver_ota_handler(httpd_req_t *req); } // namespace void init_webserver() @@ -66,7 +66,8 @@ void init_webserver() httpd_uri_t { .uri = "/on", .method = HTTP_GET, .handler = webserver_on_handler, .user_ctx = NULL }, httpd_uri_t { .uri = "/off", .method = HTTP_GET, .handler = webserver_off_handler, .user_ctx = NULL }, httpd_uri_t { .uri = "/toggle", .method = HTTP_GET, .handler = webserver_toggle_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/reboot", .method = HTTP_GET, .handler = webserver_reboot_handler, .user_ctx = NULL } + httpd_uri_t { .uri = "/reboot", .method = HTTP_GET, .handler = webserver_reboot_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); @@ -89,12 +90,14 @@ void update_webserver() } namespace { +esp_err_t webserver_otainfo_display(httpd_req_t *req, std::string &body); esp_err_t webserver_dht_display(httpd_req_t *req, std::string &body); esp_err_t webserver_tsl_display(httpd_req_t *req, std::string &body); esp_err_t webserver_bmp_display(httpd_req_t *req, std::string &body); esp_err_t webserver_wifi_display(httpd_req_t *req, std::string &body); esp_err_t webserver_mqtt_display(httpd_req_t *req, std::string &body); esp_err_t webserver_config_display(httpd_req_t *req, std::string &body, std::string_view query); +esp_err_t webserver_otaform_display(httpd_req_t *req, std::string &body); esp_err_t webserver_root_handler(httpd_req_t *req) { @@ -108,6 +111,9 @@ esp_err_t webserver_root_handler(httpd_req_t *req) std::string body; + if (const auto result = webserver_otainfo_display(req, body); result != ESP_OK) + return result; + if (config::enable_lamp.value()) { body += "on
\n" @@ -144,8 +150,41 @@ esp_err_t webserver_root_handler(httpd_req_t *req) if (const auto result = webserver_config_display(req, body, query); result != ESP_OK) return result; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + if (const auto result = webserver_otaform_display(req, body); result != ESP_OK) + return result; + + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/html", body) +} + +esp_err_t webserver_otainfo_display(httpd_req_t *req, std::string &body) +{ + if (const auto otaStatus = asyncOta.status(); otaStatus != OtaCloudUpdateStatus::Idle) + { + 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"; + } + + return ESP_OK; } esp_err_t webserver_dht_display(httpd_req_t *req, std::string &body) @@ -210,13 +249,13 @@ esp_err_t webserver_wifi_display(httpd_req_t *req, std::string &body) if (const auto result = wifi_stack::get_sta_ap_info(); result) { body += fmt::format("STA rssi{}dB\n", result->rssi); - body += fmt::format("STA SSID{}\n", htmlentities(result->ssid)); + body += fmt::format("STA SSID{}\n", esphttpdutils::htmlentities(result->ssid)); body += fmt::format("STA channel{}\n", result->primary); body += fmt::format("STA BSSID{}\n", wifi_stack::toString(wifi_stack::mac_t{result->bssid})); } else { - body += fmt::format("get_sta_ap_info() failed: {}\n", htmlentities(result.error())); + body += fmt::format("get_sta_ap_info() failed: {}\n", esphttpdutils::htmlentities(result.error())); } if (const auto result = wifi_stack::get_ip_info(TCPIP_ADAPTER_IF_STA)) @@ -227,7 +266,7 @@ esp_err_t webserver_wifi_display(httpd_req_t *req, std::string &body) } else { - body += fmt::format("get_ip_info() failed: {}\n", htmlentities(result.error())); + body += fmt::format("get_ip_info() failed: {}\n", esphttpdutils::htmlentities(result.error())); } } } @@ -265,7 +304,7 @@ esp_err_t webserver_wifi_display(httpd_req_t *req, std::string &body) "{}\n" "{}\n" "\n", - htmlentities(entry.ssid), + esphttpdutils::htmlentities(entry.ssid), wifi_stack::toString(entry.authmode), wifi_stack::toString(entry.pairwise_cipher), wifi_stack::toString(entry.group_cipher), @@ -292,7 +331,7 @@ esp_err_t webserver_mqtt_display(httpd_req_t *req, std::string &body) body += "
\n" "

MQTT

\n" "\n"; - body += fmt::format("\n", htmlentities(config::broker_url.value())); + body += fmt::format("\n", esphttpdutils::htmlentities(config::broker_url.value())); body += fmt::format("\n", mqttClient ? "true" : "false"); if (mqttClient) { @@ -318,10 +357,10 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper &con { char valueBufEncoded[256]; - if (const auto result = httpd_query_key_value(query.data(), config.nvsKey(), valueBufEncoded, 256); result == ESP_OK) + if (const auto result = httpd_query_key_value(query.data(), config.nvsKey(), valueBufEncoded, sizeof(valueBufEncoded)); result == ESP_OK) { char valueBuf[257]; - espcpputils::urldecode(valueBuf, valueBufEncoded); + esphttpdutils::urldecode(valueBuf, valueBufEncoded); std::string_view value{valueBuf}; @@ -330,11 +369,11 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper &con if (const auto result = config.writeToFlash(value == "true")) str += "Successfully saved"; else - str += fmt::format("Error while saving: {}", htmlentities(result.error())); + str += fmt::format("Error while saving: {}", esphttpdutils::htmlentities(result.error())); } else { - str += fmt::format("Error while saving: Invalid value \"{}\"", htmlentities(value)); + str += fmt::format("Error while saving: Invalid value \"{}\"", esphttpdutils::htmlentities(value)); } } else if (result != ESP_ERR_NOT_FOUND) @@ -349,8 +388,8 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper &con "" "" "", - htmlentities(config.nvsKey()), - htmlentities(config.nvsKey()), + esphttpdutils::htmlentities(config.nvsKey()), + esphttpdutils::htmlentities(config.nvsKey()), config.value() ? " checked" : ""); return str; @@ -363,15 +402,15 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapperSuccessfully saved"; else - str += fmt::format("Error while saving: {}", htmlentities(result.error())); + str += fmt::format("Error while saving: {}", esphttpdutils::htmlentities(result.error())); } else if (result != ESP_ERR_NOT_FOUND) { @@ -384,8 +423,8 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper" "" "", - htmlentities(config.nvsKey()), - htmlentities(config.value())); + esphttpdutils::htmlentities(config.nvsKey()), + esphttpdutils::htmlentities(config.value())); return str; } @@ -397,10 +436,10 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapperSuccessfully saved"; else - str += fmt::format("Error while saving: {}", htmlentities(result.error())); + str += fmt::format("Error while saving: {}", esphttpdutils::htmlentities(result.error())); } else { - str += fmt::format("Error while saving: Invalid value \"{}\"", htmlentities(value)); + str += fmt::format("Error while saving: Invalid value \"{}\"", esphttpdutils::htmlentities(value)); } } else if (result != ESP_ERR_NOT_FOUND) @@ -426,7 +465,7 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper" "" "", - htmlentities(config.nvsKey()), + esphttpdutils::htmlentities(config.nvsKey()), config.value()); return str; @@ -439,10 +478,10 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapperSuccessfully saved"; else - str += fmt::format("Error while saving: {}", htmlentities(result.error())); + str += fmt::format("Error while saving: {}", esphttpdutils::htmlentities(result.error())); } else { - str += fmt::format("Error while saving: Invalid value \"{}\"", htmlentities(value)); + str += fmt::format("Error while saving: Invalid value \"{}\"", esphttpdutils::htmlentities(value)); } } else if (result != ESP_ERR_NOT_FOUND) @@ -468,7 +507,7 @@ std::string webserver_form_for_config(httpd_req_t *req, ConfigWrapper" "" "", - htmlentities(config.nvsKey()), + esphttpdutils::htmlentities(config.nvsKey()), config.value().count()); return str; @@ -499,19 +538,19 @@ esp_err_t webserver_config_display(httpd_req_t *req, std::string &body, std::str "\n" "\n" "\n", - htmlentities(config.name()), - //htmlentities(toString(config.value())), + esphttpdutils::htmlentities(config.name()), + //esphttpdutils::htmlentities(toString(config.value())), webserver_form_for_config(req, config, query), [&]() -> std::string { if (const auto result = config.readFromFlash()) { if (*result) - return htmlentities(toString(**result)); + return esphttpdutils::htmlentities(toString(**result)); else return "not set"; } else - return fmt::format("{}", htmlentities(result.error())); + return fmt::format("{}", esphttpdutils::htmlentities(result.error())); }()); }); @@ -521,12 +560,22 @@ esp_err_t webserver_config_display(httpd_req_t *req, std::string &body, std::str return ESP_OK; } +esp_err_t webserver_otaform_display(httpd_req_t *req, std::string &body) +{ + body += "\n" + "\n" + "\n" + "\n"; + return ESP_OK; +} + esp_err_t webserver_on_handler(httpd_req_t *req) { if (!config::enable_lamp.value()) { - ESP_LOGW(TAG, "lamp support not enabled!"); - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "lamp support not enabled!") + constexpr const char *msg = "lamp support not enabled!"; + ESP_LOGW(TAG, "%s", msg); + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", msg) } const bool state = (lampState = true); @@ -535,17 +584,16 @@ esp_err_t webserver_on_handler(httpd_req_t *req) if (mqttConnected) mqttVerbosePub(config::topic_lamp_status.value(), state ? "ON" : "OFF", 0, 1); - std::string_view body{"ON called..."}; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/plain", "ON called...") } esp_err_t webserver_off_handler(httpd_req_t *req) { if (!config::enable_lamp.value()) { - ESP_LOGW(TAG, "lamp support not enabled!"); - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "lamp support not enabled!") + constexpr const char *msg = "lamp support not enabled!"; + ESP_LOGW(TAG, "%s", msg); + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", msg) } const bool state = (lampState = false); @@ -554,17 +602,16 @@ esp_err_t webserver_off_handler(httpd_req_t *req) if (mqttConnected) mqttVerbosePub(config::topic_lamp_status.value(), state ? "ON" : "OFF", 0, 1); - std::string_view body{"OFF called..."}; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/plain", "OFF called...") } esp_err_t webserver_toggle_handler(httpd_req_t *req) { if (!config::enable_lamp.value()) { - ESP_LOGW(TAG, "lamp support not enabled!"); - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "lamp support not enabled!") + constexpr const char *msg = "lamp support not enabled!"; + ESP_LOGW(TAG, "%s", msg); + CALL_AND_EXIT(webserver_resp_send_err, req, HTTPD_400_BAD_REQUEST, "text/plain", msg) } const bool state = (lampState = !lampState); @@ -573,18 +620,50 @@ esp_err_t webserver_toggle_handler(httpd_req_t *req) if (mqttConnected) mqttVerbosePub(config::topic_lamp_status.value(), state ? "ON" : "OFF", 0, 1); - std::string_view body{"TOGGLE called..."}; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/plain", "TOGGLE called...") } esp_err_t webserver_reboot_handler(httpd_req_t *req) { shouldReboot = true; - std::string_view body{"REBOOT called..."}; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + CALL_AND_EXIT(webserver_resp_send_succ, req, "text/plain", "REBOOT called...") +} + +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...") } } // namespace } // namespace deckenlampe
client url{}
client url{}
client constructed{}
{}{}