diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 0719b7c..d89fc9a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,6 +3,7 @@ set(headers debugconsole.h ota.h taskmanager.h + webserver.h wifi.h ) @@ -12,6 +13,7 @@ set(sources main.cpp ota.cpp taskmanager.cpp + webserver.cpp wifi.cpp ) diff --git a/main/config.cpp b/main/config.cpp index 701a84f..6a5206d 100644 --- a/main/config.cpp +++ b/main/config.cpp @@ -22,10 +22,10 @@ namespace { std::string defaultHostname() { if (const auto result = wifi_stack::get_default_mac_addr()) - return fmt::format("bobby_{:02x}{:02x}{:02x}", result->at(3), result->at(4), result->at(5)); + return fmt::format("dmx_{:02x}{:02x}{:02x}", result->at(3), result->at(4), result->at(5)); else ESP_LOGE(TAG, "get_default_mac_addr() failed: %.*s", result.error().size(), result.error().data()); - return "bobby"; + return "dmx"; } ConfigManager configs; diff --git a/main/config.h b/main/config.h index 1d132fa..0ae3434 100644 --- a/main/config.h +++ b/main/config.h @@ -107,6 +107,8 @@ public: ConfigWrapper timezoneOffset{espchrono::minutes32{60}, DoReset, {}, "timezoneOffset" }; // MinMaxValue ConfigWrappertimeDst{espchrono::DayLightSavingMode::EuropeanSummerTime, DoReset, {}, "time_dst" }; + ConfigWrapper otaUrl {std::string{}, DoReset, StringOr, "otaUrl" }; + template void callForEveryConfig(T &&callable) { @@ -161,6 +163,8 @@ public: REGISTER_CONFIG(timezoneOffset) REGISTER_CONFIG(timeDst) + REGISTER_CONFIG(otaUrl) + #undef REGISTER_API_VALUE } }; diff --git a/main/ota.cpp b/main/ota.cpp index 06b25c4..b3c5f9b 100644 --- a/main/ota.cpp +++ b/main/ota.cpp @@ -20,21 +20,21 @@ using namespace std::chrono_literals; namespace { -constexpr const char * const TAG = "OTA_CLOUD"; +constexpr const char * const TAG = "OTA_CLIENT"; cpputils::DelayedConstruction _otaClient; } // namespace EspAsyncOta &otaClient{_otaClient.getUnsafe()}; -void ota_cloud_init() +void ota_client_init() { ESP_LOGI(TAG, "called"); _otaClient.construct("asyncOtaTask", 8192u); } -void ota_cloud_update() +void ota_client_update() { _otaClient->update(); } diff --git a/main/ota.h b/main/ota.h index 8d01137..78faa64 100644 --- a/main/ota.h +++ b/main/ota.h @@ -13,8 +13,8 @@ class EspAsyncOta; extern EspAsyncOta &otaClient; -void ota_cloud_init(); -void ota_cloud_update(); +void ota_client_init(); +void ota_client_update(); -tl::expected otaCloudTrigger(std::string_view url); -tl::expected otaCloudAbort(); +tl::expected otaClientTrigger(std::string_view url); +tl::expected otaClientAbort(); diff --git a/main/taskmanager.cpp b/main/taskmanager.cpp index a104c22..506611e 100644 --- a/main/taskmanager.cpp +++ b/main/taskmanager.cpp @@ -14,6 +14,7 @@ #include "wifi.h" #include "debugconsole.h" #include "ota.h" +#include "webserver.h" using namespace std::chrono_literals; @@ -25,7 +26,8 @@ void not_needed() {} espcpputils::SchedulerTask schedulerTasksArr[] { espcpputils::SchedulerTask { "wifi", wifi_begin, wifi_update, 100ms }, espcpputils::SchedulerTask { "debugconsole", init_debugconsole, update_debugconsole, 50ms }, - espcpputils::SchedulerTask { "ota_cloud", ota_cloud_init, ota_cloud_update, 100ms }, + espcpputils::SchedulerTask { "ota_client", ota_client_init, ota_client_update, 100ms }, + espcpputils::SchedulerTask { "webserver", initWebserver, handleWebserver, 100ms }, }; } // namespace diff --git a/main/webserver.cpp b/main/webserver.cpp new file mode 100644 index 0000000..036b3ca --- /dev/null +++ b/main/webserver.cpp @@ -0,0 +1,822 @@ +#include "webserver.h" + +#include "sdkconfig.h" + +// system includes +#include + +// esp-idf includes +#include +#include +#include + +// 3rdparty lib includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "ota.h" +#include "config.h" + +using namespace std::chrono_literals; +using esphttpdutils::HtmlTag; + +httpd_handle_t httpdHandle; + +namespace { +constexpr const char * const TAG = "WEBSERVER"; + +esp_err_t webserver_reboot_handler(httpd_req_t *req); + +esp_err_t webserver_ota_handler(httpd_req_t *req); +esp_err_t webserver_trigger_ota_handler(httpd_req_t *req); + +esp_err_t webserver_settings_handler(httpd_req_t *req); +esp_err_t webserver_saveSettings_handler(httpd_req_t *req); +esp_err_t webserver_resetSettings_handler(httpd_req_t *req); +} // namespace + +void initWebserver() +{ + { + httpd_config_t httpConfig HTTPD_DEFAULT_CONFIG(); + httpConfig.core_id = 1; + httpConfig.max_uri_handlers = 16; + httpConfig.stack_size = 8192; + + const auto result = httpd_start(&httpdHandle, &httpConfig); + ESP_LOG_LEVEL_LOCAL((result == ESP_OK ? ESP_LOG_INFO : ESP_LOG_ERROR), TAG, "httpd_start(): %s", esp_err_to_name(result)); + if (result != ESP_OK) + return; + } + + for (const httpd_uri_t &uri : { + httpd_uri_t { .uri = "/", .method = HTTP_GET, .handler = webserver_settings_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/saveSettings", .method = HTTP_GET, .handler = webserver_saveSettings_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/resetSettings", .method = HTTP_GET, .handler = webserver_resetSettings_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 }, + httpd_uri_t { .uri = "/triggerOta", .method = HTTP_GET, .handler = webserver_trigger_ota_handler, .user_ctx = NULL }, + }) + { + const auto result = httpd_register_uri_handler(httpdHandle, &uri); + ESP_LOG_LEVEL_LOCAL((result == ESP_OK ? ESP_LOG_INFO : ESP_LOG_ERROR), TAG, "httpd_register_uri_handler() for %s: %s", uri.uri, esp_err_to_name(result)); + //if (result != ESP_OK) + // return result; + } +} + +void handleWebserver() +{ +} + +namespace { +esp_err_t webserver_reboot_handler(httpd_req_t *req) +{ + esp_restart(); + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "REBOOT called...") +} + +esp_err_t webserver_ota_handler(httpd_req_t *req) +{ + std::string body; + + HtmlTag htmlTag{"html", body}; + + { + HtmlTag headTag{"head", body}; + + { + HtmlTag titleTag{"title", body}; + body += "Update"; + } + + body += ""; + } + + { + HtmlTag bodyTag{"body", body}; + + { + HtmlTag h1Tag{"h1", body}; + body += "Update"; + } + + { + HtmlTag pTag{"p", body}; + body += "Settings - " + "Update"; + } + + if (const esp_app_desc_t *app_desc = esp_ota_get_app_description()) + { + HtmlTag tableTag{"table", "border=\"1\"", body}; + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current project_name"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(app_desc->project_name); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current version"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(app_desc->version); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current secure_version"; } + { HtmlTag tdTag{"td", body}; body += std::to_string(app_desc->secure_version); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current timestamp"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(fmt::format("{} {}", app_desc->date, app_desc->time)); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current idf_ver"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(app_desc->idf_ver); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Current sha256"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(espcpputils::toHexString({app_desc->app_elf_sha256, 8})); } + } + } + else + { + constexpr const std::string_view msg = "esp_ota_get_app_description() failed"; + ESP_LOGE(TAG, "%.*s", msg.size(), msg.data()); + HtmlTag pTag{"p", "style=\"color: red;\"", body}; + body += esphttpdutils::htmlentities(msg); + } + + { + HtmlTag tableTag{"table", "border=\"1\"", body}; + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Update status"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(toString(otaClient.status())); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Update progress"; } + { + HtmlTag tdTag{"td", body}; + const auto progress = otaClient.progress(); + const auto totalSize = otaClient.totalSize(); + body += fmt::format("{} / {}{}", + progress, + totalSize ? std::to_string(*totalSize) : "?", + (totalSize && *totalSize > 0) ? fmt::format(" ({:.02f}%)", float(progress) / *totalSize * 100) : ""); + } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "Update message"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(otaClient.message()); } + } + + if (const auto &appDesc = otaClient.appDesc()) + { + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New project_name"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(appDesc->project_name); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New version"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(appDesc->version); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New secure_version"; } + { HtmlTag tdTag{"td", body}; body += std::to_string(appDesc->secure_version); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New timestamp"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(fmt::format("{} {}", appDesc->date, appDesc->time)); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New idf_ver"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(appDesc->idf_ver); } + } + + { + HtmlTag trTag{"tr", body}; + { HtmlTag tdTag{"td", body}; body += "New sha256"; } + { HtmlTag tdTag{"td", body}; body += esphttpdutils::htmlentities(espcpputils::toHexString({appDesc->app_elf_sha256, 8})); } + } + } + } + + { + HtmlTag formTag{"form", "action=\"/triggerOta\" method=\"GET\"", body}; + HtmlTag fieldsetTag{"fieldset", body}; + { + HtmlTag legendTag{"legend", body}; + body += "Trigger Update"; + } + + body += fmt::format("", esphttpdutils::htmlentities(configs.otaUrl.value)); + + { + HtmlTag buttonTag{"button", "type=\"submit\"", body}; + body += "Go"; + } + + body += "url is only used temporarely and not persisted in flash"; + } + } + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/html", body) +} + +esp_err_t webserver_trigger_ota_handler(httpd_req_t *req) +{ + std::string query; + if (auto result = esphttpdutils::webserver_get_query(req)) + query = *result; + else + { + ESP_LOGE(TAG, "%.*s", result.error().size(), result.error().data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", result.error()); + } + + std::string url; + constexpr const std::string_view urlParamName{"url"}; + + { + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), urlParamName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result == ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{} not set", urlParamName); + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + else + { + const auto msg = fmt::format("httpd_query_key_value() {} failed with {}", urlParamName, esp_err_to_name(result)); + ESP_LOGE(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + } + + char valueBuf[257]; + esphttpdutils::urldecode(valueBuf, valueBufEncoded); + + url = valueBuf; + } + + if (const auto result = otaClientTrigger(url); !result) + { + ESP_LOGE(TAG, "%.*s", result.error().size(), result.error().data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", result.error()); + } + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/ota") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") +} + +template +struct is_duration : std::false_type {}; + +template +struct is_duration> : std::true_type {}; + +template +inline constexpr bool is_duration_v = is_duration<_Tp>::value; + +template +typename std::enable_if< + !std::is_same_v && + !std::is_integral_v && + !is_duration_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v> && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + HtmlTag spanTag{"span", "style=\"color: red;\"", body}; + body += "Unsupported config type"; +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("" + "", + esphttpdutils::htmlentities(key), + value ? "checked " : "", + esphttpdutils::htmlentities(key)); +} + +template +typename std::enable_if< + !std::is_same_v && + std::is_integral_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("", + esphttpdutils::htmlentities(key), + value, + std::numeric_limits::min(), + std::numeric_limits::max()); +} + +template +typename std::enable_if< + is_duration_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("", + esphttpdutils::htmlentities(key), + value.count()); +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("", + esphttpdutils::htmlentities(key), + esphttpdutils::htmlentities(value)); +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("", + esphttpdutils::htmlentities(key), + esphttpdutils::htmlentities(wifi_stack::toString(value))); +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format("", + esphttpdutils::htmlentities(key), + esphttpdutils::htmlentities(wifi_stack::toString(value))); +} + +template +typename std::enable_if< + std::is_same_v> +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + body += fmt::format(" ?", + esphttpdutils::htmlentities(key), + value ? esphttpdutils::htmlentities(wifi_stack::toString(*value)) : std::string{}); +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + HtmlTag select{"select", fmt::format("name=\"{}\"", esphttpdutils::htmlentities(key)), body}; + +#define HANDLE_ENUM_KEY(x) \ + { \ + HtmlTag option{"option", fmt::format("value=\"{}\"{}", std::to_underlying(x), value == x ? " selected" : ""), body}; \ + body += esphttpdutils::htmlentities(#x); \ + } + HANDLE_ENUM_KEY(WIFI_AUTH_OPEN) + HANDLE_ENUM_KEY(WIFI_AUTH_WEP) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA2_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA_WPA2_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA2_ENTERPRISE) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA3_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_WPA2_WPA3_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_WAPI_PSK) + HANDLE_ENUM_KEY(WIFI_AUTH_MAX) +#undef HANDLE_ENUM_KEY +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + HtmlTag select{"select", fmt::format("name=\"{}\"", esphttpdutils::htmlentities(key)), body}; + +#define HANDLE_ENUM_KEY(x) \ + { \ + HtmlTag option{"option", fmt::format("value=\"{}\"{}", std::to_underlying(x), value == x ? " selected" : ""), body}; \ + body += esphttpdutils::htmlentities(#x); \ + } + HANDLE_ENUM_KEY(SNTP_SYNC_MODE_IMMED) + HANDLE_ENUM_KEY(SNTP_SYNC_MODE_SMOOTH) +#undef HANDLE_ENUM_KEY +} + +template +typename std::enable_if< + std::is_same_v +, void>::type +showInputForSetting(std::string_view key, T value, std::string &body) +{ + HtmlTag select{"select", fmt::format("name=\"{}\"", esphttpdutils::htmlentities(key)), body}; + + espchrono::iterateDayLightSavingMode([&](T enumVal, std::string_view enumKey){ + HtmlTag option{"option", fmt::format("value=\"{}\"{}", std::to_underlying(enumVal), value == enumVal ? " selected" : ""), body}; + body += esphttpdutils::htmlentities(enumKey); + }); +} + +esp_err_t webserver_settings_handler(httpd_req_t *req) +{ + std::string body; + + { + HtmlTag htmlTag{"html", body}; + + { + HtmlTag headTag{"head", body}; + + { + HtmlTag titleTag{"title", body}; + body += "Settings"; + } + + body += ""; + + HtmlTag styleTag{"style", "type=\"text/css\"", body}; + body += + ".form-table {" + "display: table;" + "border-collapse: separate;" + "border-spacing: 10px 0;" + "}" + + ".form-table .form-table-row {" + "display: table-row;" + "}" + + ".form-table .form-table-row .form-table-cell {" + "display: table-cell;" + "}"; + } + + { + HtmlTag bodyTag{"body", body}; + + { + HtmlTag h1Tag{"h1", body}; + body += "Settings"; + } + + { + HtmlTag pTag{"p", body}; + body += "Settings - " + "Update"; + } + + HtmlTag divTag{"div", "class=\"form-table\"", body}; + + configs.callForEveryConfig([&](const auto &config){ + if (body.size() > 2048) + { + if (const auto result = httpd_resp_send_chunk(req, body.data(), body.size()); result != ESP_OK) + { + ESP_LOGE(TAG, "httpd_resp_send_chunk() failed with %s", esp_err_to_name(result)); + //return result; + } + body.clear(); + } + + const std::string_view nvsName{config.nvsName()}; + + HtmlTag formTag{"form", "class=\"form-table-row\" action=\"/saveSettings\" method=\"GET\"", body}; + + { + HtmlTag divTag{"div", "class=\"form-table\"", body}; + HtmlTag bTag{"b", body}; + body += esphttpdutils::htmlentities(nvsName); + } + + { + HtmlTag divTag{"div", "class=\"form-table-cell\"", body}; + showInputForSetting(nvsName, config.value, body); + } + + { + HtmlTag divTag{"div", "class=\"form-table-cell\"", body}; + HtmlTag buttonTag{"button", "type=\"submit\"", body}; + body += "Save"; + } + + { + HtmlTag divTag{"div", "class=\"form-table-cell\"", body}; + + if (config.touched()) + { + { + HtmlTag buttonTag{"span", "style=\"color: red;\"", body}; + body += "Touched"; + } + + body += ' '; + + if (config.allowReset()) + { + HtmlTag buttonTag{"a", fmt::format("href=\"/resetSettings?{}=1\"", esphttpdutils::htmlentities(nvsName)), body}; + body += "Reset"; + } + else + { + HtmlTag buttonTag{"span", "style=\"color: yellow;\"", body}; + body += "No Reset"; + } + } + } + + return false; // dont abort loop + }); + } + } + + CALL_AND_EXIT_ON_ERROR(httpd_resp_send_chunk, req, body.data(), body.size()); + body.clear(); + CALL_AND_EXIT(httpd_resp_send_chunk, req, nullptr, 0); + //CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/html", body) +} + +template +typename std::enable_if< + !std::is_same_v && + !std::is_integral_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v> && + !std::is_same_v && + !std::is_same_v && + !std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + return tl::make_unexpected("Unsupported config type"); +} + +template +typename std::enable_if< + std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (cpputils::is_in(newValue, "true", "false")) + return configs.write_config(config, newValue == "true"); + else + return tl::make_unexpected(fmt::format("only true and false allowed, not {}", newValue)); +} + +template +typename std::enable_if< + !std::is_same_v && + std::is_integral_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (auto parsed = cpputils::fromString(newValue)) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(fmt::format("could not parse {}", newValue)); +} + +template +typename std::enable_if< + std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + return configs.write_config(config, std::string{newValue}); +} + +template +typename std::enable_if< + std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v> +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (newValue.empty()) + return configs.write_config(config, std::nullopt); + else if (const auto parsed = wifi_stack::fromString(newValue); parsed) + return configs.write_config(config, *parsed); + else + return tl::make_unexpected(parsed.error()); +} + +template +typename std::enable_if< + std::is_same_v || + std::is_same_v || + std::is_same_v +, tl::expected>::type +saveSetting(ConfigWrapper &config, std::string_view newValue) +{ + if (auto parsed = cpputils::fromString>(newValue)) + return configs.write_config(config, T(*parsed)); + else + return tl::make_unexpected(fmt::format("could not parse {}", newValue)); +} + +esp_err_t webserver_saveSettings_handler(httpd_req_t *req) +{ + std::string query; + if (auto result = esphttpdutils::webserver_get_query(req)) + query = *result; + else + { + ESP_LOGE(TAG, "%.*s", result.error().size(), result.error().data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", result.error()); + } + + std::string body; + bool success{true}; + + configs.callForEveryConfig([&](auto &config){ + const std::string_view nvsName{config.nvsName()}; + + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), nvsName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result != ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{}: httpd_query_key_value() failed with {}", nvsName, esp_err_to_name(result)); + ESP_LOGE(TAG, "%.*s", msg.size(), msg.data()); + body += msg; + body += '\n'; + success = false; + } + return false; // dont abort loop + } + + char valueBuf[257]; + esphttpdutils::urldecode(valueBuf, valueBufEncoded); + + if (const auto result = saveSetting(config, valueBuf); result) + body += fmt::format("{} succeeded!\n", esphttpdutils::htmlentities(nvsName)); + else + { + body += fmt::format("{} failed: {}\n", esphttpdutils::htmlentities(nvsName), esphttpdutils::htmlentities(result.error())); + success = false; + } + + return false; // dont abort loop + }); + + if (body.empty()) + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "nothing changed?!") + + if (success) + { + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + body += "\nOk, continue at /"; + } + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, + req, + success ? esphttpdutils::ResponseStatus::TemporaryRedirect : esphttpdutils::ResponseStatus::BadRequest, + "text/plain", + body) +} + +esp_err_t webserver_resetSettings_handler(httpd_req_t *req) +{ + std::string query; + if (auto result = esphttpdutils::webserver_get_query(req)) + query = *result; + else + { + ESP_LOGE(TAG, "%.*s", result.error().size(), result.error().data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", result.error()); + } + + std::string body; + bool success{true}; + + configs.callForEveryConfig([&](auto &config){ + const std::string_view nvsName{config.nvsName()}; + + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), nvsName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result != ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{}: httpd_query_key_value() failed with {}", nvsName, esp_err_to_name(result)); + ESP_LOGE(TAG, "%.*s", msg.size(), msg.data()); + body += msg; + body += '\n'; + success = false; + } + return false; // dont abort loop + } + + body += nvsName; + body += ' '; + + if (const auto result = configs.reset_config(config); result) + body += "reset successful"; + else + { + body += result.error(); + success = false; + } + + body += '\n'; + + return false; // dont abort loop + }); + + if (body.empty()) + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "nothing changed?!") + + if (success) + { + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + body += "\nOk, continue at /"; + } + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, + req, + success ? esphttpdutils::ResponseStatus::TemporaryRedirect : esphttpdutils::ResponseStatus::BadRequest, + "text/plain", + body) +} +} // namespace diff --git a/main/webserver.h b/main/webserver.h new file mode 100644 index 0000000..adb0147 --- /dev/null +++ b/main/webserver.h @@ -0,0 +1,9 @@ +#pragma once + +// esp-idf includes +#include + +extern httpd_handle_t httpdHandle; + +void initWebserver(); +void handleWebserver(); diff --git a/main/wifi.cpp b/main/wifi.cpp index b321640..181d80f 100644 --- a/main/wifi.cpp +++ b/main/wifi.cpp @@ -13,6 +13,8 @@ #include "config.h" namespace { +constexpr const char * const TAG = "WIFI"; + wifi_stack::config createConfig(); std::optional createStaConfig(); wifi_stack::wifi_entry createWifiEntry(const WiFiConfig &wifi_config); @@ -34,13 +36,13 @@ esp_err_t wifi_scan() const auto &sta_config = createStaConfig(); if (!sta_config) { - ESP_LOGE("BOBBY", "no sta enabled"); + ESP_LOGE(TAG, "no sta enabled"); return ESP_FAIL; } if (const auto result = wifi_stack::begin_scan(*sta_config); !result) { - ESP_LOGE("BOBBY", "begin_scan() failed with %.*s", result.error().size(), result.error().data()); + ESP_LOGE(TAG, "begin_scan() failed with %.*s", result.error().size(), result.error().data()); return ESP_FAIL; }