diff --git a/components/cpputils b/components/cpputils index 4368243..47417e7 160000 --- a/components/cpputils +++ b/components/cpputils @@ -1 +1 @@ -Subproject commit 43682439fe86041f635c9c70e52bbf0b8085641c +Subproject commit 47417e77b70b9d37e16b8fb1f2ec3643c5c0bdab diff --git a/components/espcpputils b/components/espcpputils index 6274c10..234dbb2 160000 --- a/components/espcpputils +++ b/components/espcpputils @@ -1 +1 @@ -Subproject commit 6274c103b48e0e9130b0518047750c143ff0a346 +Subproject commit 234dbb23fbe73ddd43e59598e571187413c64ad5 diff --git a/components/esphttpdutils b/components/esphttpdutils index 4ebd651..148e02b 160000 --- a/components/esphttpdutils +++ b/components/esphttpdutils @@ -1 +1 @@ -Subproject commit 4ebd651a70a4fddd40b1bcf2fab5194639629276 +Subproject commit 148e02b6ca6c5580027d56c8288f328b782f5554 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 2015d47..5938567 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -23,7 +23,7 @@ set(headers actions/switchprofileaction.h actions/updateswapfrontbackaction.h bluetoothmode.h - bobby_ble.h + ble_bobby.h bletexthelpers.h bmsutils.h changevaluedisplay_bluetoothmode.h @@ -91,7 +91,6 @@ set(headers displays/statusdisplay.h displays/updatedisplay.h esp_websocket_client.h - htmltag.h icons/alert.h icons/bluetooth.h icons/bms.h @@ -134,7 +133,6 @@ set(headers dpad5wire.h feedbackparser.h globals.h - htmlutils.h macros_bobbycar.h modeinterface.h ota.h @@ -150,6 +148,9 @@ set(headers unifiedmodelmode.h utils.h webserver.h + webserver_displaycontrol.h + webserver_ota.h + webserver_settings.h wifitexthelpers.h wifi_bobbycar.h ) diff --git a/main/bobby_ble.h b/main/ble_bobby.h similarity index 100% rename from main/bobby_ble.h rename to main/ble_bobby.h diff --git a/main/bletexthelpers.h b/main/bletexthelpers.h index ad52814..ac8271f 100644 --- a/main/bletexthelpers.h +++ b/main/bletexthelpers.h @@ -2,7 +2,7 @@ // local includes #include "textinterface.h" -#include "bobby_ble.h" +#include "ble_bobby.h" namespace { #ifdef FEATURE_BLE diff --git a/main/htmltag.h b/main/htmltag.h deleted file mode 100644 index 6def81e..0000000 --- a/main/htmltag.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include - -namespace { -class HtmlTag { -public: - HtmlTag(AsyncResponseStream &stream, const char *tag); - - template - HtmlTag(AsyncResponseStream &stream, const char *tag, const T &x); - - ~HtmlTag(); - -private: - AsyncResponseStream &stream; - const char * const tag; -}; - -HtmlTag::HtmlTag(AsyncResponseStream &stream, const char *tag) : - stream{stream}, - tag{tag} -{ - stream.print("<"); - stream.print(tag); - stream.print(">"); -} - -template -HtmlTag::HtmlTag(AsyncResponseStream &stream, const char *tag, const T &x) : - stream{stream}, - tag{tag} -{ - stream.print("<"); - stream.print(tag); - stream.print(x); - stream.print(">"); -} - -HtmlTag::~HtmlTag() -{ - stream.print(""); -} -} diff --git a/main/htmlutils.h b/main/htmlutils.h deleted file mode 100644 index aeb7539..0000000 --- a/main/htmlutils.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "htmltag.h" - -namespace { -void breakLine(AsyncResponseStream &stream) -{ - stream.print("
"); -} - -void label(AsyncResponseStream &stream, const char *name, const char *text) -{ - HtmlTag label(stream, "label", std::string(" for=\"") + name + "\""); - stream.print(text); -} - -template -void input(AsyncResponseStream &stream, T value, const char *type, const char *id, const char *name, const char *additionalAttributes = nullptr) -{ - stream.print(""); -} - -template -void hiddenInput(AsyncResponseStream &stream, T value, const char *name) -{ - input(stream, value, "hidden", nullptr, name); -} - -template -void numberInput(AsyncResponseStream &stream, T value, const char *id, const char *name, const char *text) -{ - label(stream, id, text); - - breakLine(stream); - - input(stream, value, "number", id, name, " required"); -} - -template -void numberInput(AsyncResponseStream &stream, T value, const char *name, const char *text) -{ - numberInput(stream, value, name, name, text); -} - -void submitButton(AsyncResponseStream &stream) -{ - HtmlTag button(stream, "button", " type=\"submit\""); - stream.print("Submit"); -} - -void checkboxInput(AsyncResponseStream &stream, bool value, const char *id, const char *name, const char *text) -{ - label(stream, id, text); - - breakLine(stream); - - input(stream, "on", "checkbox", id, name, value?" checked":""); -} - -void checkboxInput(AsyncResponseStream &stream, bool value, const char *name, const char *text) -{ - checkboxInput(stream, value, name, name, text); -} - -void selectOption(AsyncResponseStream &stream, const char *value, const char *text, bool selected) -{ - std::string str{" value=\""}; - str += value; - str += "\""; - - if (selected) - str += " selected"; - - HtmlTag option(stream, "option", str); - stream.print(text); -} -} diff --git a/main/main.cpp b/main/main.cpp index 2c382bb..745aa63 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -131,7 +131,7 @@ using namespace std::chrono_literals; #endif #endif #ifdef FEATURE_BLE -#include "bobby_ble.h" +#include "ble_bobby.h" #endif #ifdef FEATURE_WEBSERVER #include "webserver.h" diff --git a/main/webserver.h b/main/webserver.h index 0c2d371..99b7d81 100644 --- a/main/webserver.h +++ b/main/webserver.h @@ -11,85 +11,28 @@ #include // 3rdparty lib includes -#include #include -#include -#include -#include #include // local includes -#include "screens.h" -#include "textinterface.h" -#include "menudisplay.h" -#include "changevaluedisplay.h" -#include "displays/updatedisplay.h" -//#include "esputils.h" -#include "buttons.h" -#include "esphttpdutils.h" +#include "webserver_displaycontrol.h" #ifdef FEATURE_OTA -#include "ota.h" +#include "webserver_ota.h" #endif -#include "globals.h" +#include "webserver_settings.h" -namespace { #ifdef FEATURE_WEBSERVER +namespace { httpd_handle_t httpdHandle; std::atomic shouldReboot; -class HtmlTag { -public: - HtmlTag(std::string_view tagName, std::string &body) : - m_tagName{tagName}, - m_body{body} - { - m_body += '<'; - m_body += m_tagName; - m_body += '>'; - } - - HtmlTag(std::string_view tagName, std::string_view attributes, std::string &body) : - m_tagName{tagName}, - m_body{body} - { - m_body += '<'; - m_body += m_tagName; - if (!attributes.empty()) - { - m_body += ' '; - m_body += attributes; - } - m_body += '>'; - } - - ~HtmlTag() - { - m_body += "'; - } - -private: - const std::string_view m_tagName; - std::string &m_body; -}; - -esp_err_t webserver_root_handler(httpd_req_t *req); -esp_err_t webserver_up_handler(httpd_req_t *req); -esp_err_t webserver_down_handler(httpd_req_t *req); -esp_err_t webserver_confirm_handler(httpd_req_t *req); -esp_err_t webserver_back_handler(httpd_req_t *req); -esp_err_t webserver_triggerItem_handler(httpd_req_t *req); -esp_err_t webserver_setValue_handler(httpd_req_t *req); +void initWebserver(); +void handleWebserver(); esp_err_t webserver_reboot_handler(httpd_req_t *req); -#ifdef FEATURE_OTA -esp_err_t webserver_ota_handler(httpd_req_t *req); -esp_err_t webserver_trigger_ota_handler(httpd_req_t *req); -#endif -esp_err_t webserver_settings_handler(httpd_req_t *req); -esp_err_t webserver_save_settings_handler(httpd_req_t *req); +} +namespace { void initWebserver() { shouldReboot = false; @@ -106,20 +49,17 @@ void initWebserver() } for (const httpd_uri_t &uri : { - httpd_uri_t { .uri = "/", .method = HTTP_GET, .handler = webserver_root_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/up", .method = HTTP_GET, .handler = webserver_up_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/down", .method = HTTP_GET, .handler = webserver_down_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/confirm", .method = HTTP_GET, .handler = webserver_confirm_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/back", .method = HTTP_GET, .handler = webserver_back_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/triggerItem", .method = HTTP_GET, .handler = webserver_triggerItem_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/setValue", .method = HTTP_GET, .handler = webserver_setValue_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/reboot", .method = HTTP_GET, .handler = webserver_reboot_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/", .method = HTTP_GET, .handler = webserver_root_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/triggerButton", .method = HTTP_GET, .handler = webserver_triggerButton_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/triggerItem", .method = HTTP_GET, .handler = webserver_triggerItem_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/setValue", .method = HTTP_GET, .handler = webserver_setValue_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/reboot", .method = HTTP_GET, .handler = webserver_reboot_handler, .user_ctx = NULL }, #ifdef FEATURE_OTA - 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 }, + 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 }, #endif - httpd_uri_t { .uri = "/settings", .method = HTTP_GET, .handler = webserver_settings_handler, .user_ctx = NULL }, - httpd_uri_t { .uri = "/saveSettings", .method = HTTP_GET, .handler = webserver_save_settings_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/settings", .method = HTTP_GET, .handler = webserver_settings_handler, .user_ctx = NULL }, + httpd_uri_t { .uri = "/saveSettings", .method = HTTP_GET, .handler = webserver_save_settings_handler, .user_ctx = NULL }, }) { const auto result = httpd_register_uri_handler(httpdHandle, &uri); @@ -138,387 +78,12 @@ void handleWebserver() } } -esp_err_t webserver_root_handler(httpd_req_t *req) -{ - std::string body; - - { - HtmlTag htmlTag{"html", body}; - - { - HtmlTag headTag{"head", body}; - - { - HtmlTag titleTag{"title", body}; - body += "Bobbycar remote"; - } - - body += ""; - } - - { - HtmlTag bodyTag{"body", body}; - - { - HtmlTag h1Tag{"h1", body}; - body += "Bobbycar remote"; - } - - { - HtmlTag pTag{"p", body}; - body += "Up " - "Down " - "Confirm " - "Back "; - -#ifdef FEATURE_OTA - body += "Update "; -#endif - - body += "Settings"; - } - - if (auto constCurrentDisplay = static_cast(currentDisplay.get())) - { - if (const auto *textInterface = constCurrentDisplay->asTextInterface()) - { - HtmlTag h2Tag{"h2", body}; - body += esphttpdutils::htmlentities(textInterface->text()); - } - - if (const auto *menuDisplay = constCurrentDisplay->asMenuDisplay()) - { - HtmlTag ulTag{"ul", body}; - - int i{0}; - menuDisplay->runForEveryMenuItem([&,selectedIndex=menuDisplay->selectedIndex()](const MenuItem &menuItem){ - HtmlTag liTag = i == selectedIndex ? - HtmlTag{"li", "style=\"border: 1px solid black;\"", body} : - HtmlTag{"li", body}; - - body += fmt::format("{}", i, esphttpdutils::htmlentities(menuItem.text())); - i++; - }); - } - else if (const auto *changeValueDisplay = constCurrentDisplay->asChangeValueDisplayInterface()) - { - HtmlTag formTag{"form", "action=\"/setValue\" method=\"GET\"", body}; - body += fmt::format("", changeValueDisplay->shownValue()); - body += ""; - } - else - { - body += "No web control implemented for current display."; - } - } - else - { - body += "Currently no screen instantiated."; - } - } - } - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_up_handler(httpd_req_t *req) -{ - InputDispatcher::rotate(-1); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_down_handler(httpd_req_t *req) -{ - InputDispatcher::rotate(1); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_confirm_handler(httpd_req_t *req) -{ - InputDispatcher::confirmButton(true); - InputDispatcher::confirmButton(false); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_back_handler(httpd_req_t *req) -{ - InputDispatcher::backButton(true); - InputDispatcher::backButton(false); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_triggerItem_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) - } - - std::size_t index; - - constexpr const std::string_view indexParamName{"index"}; - - { - char valueBufEncoded[256]; - if (const auto result = httpd_query_key_value(query.data(), indexParamName.data(), valueBufEncoded, 256); result == ESP_OK) - { - char valueBuf[257]; - esphttpdutils::urldecode(valueBuf, valueBufEncoded); - - std::string_view value{valueBuf}; - - if (auto parsed = cpputils::fromString(value)) - { - index = *parsed; - } - else - { - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, fmt::format("could not parse {} {}", indexParamName, value).c_str()); - } - } - else if (result != ESP_ERR_NOT_FOUND) - { - ESP_LOGE(TAG, "httpd_query_key_value() %.*s failed with %s", indexParamName.size(), indexParamName.data(), esp_err_to_name(result)); - return result; - } - else - { - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, fmt::format("{} not set", indexParamName).c_str()); - } - } - - if (!currentDisplay) - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "currentDisplay is null"); - - auto *menuDisplay = currentDisplay->asMenuDisplay(); - if (!menuDisplay) - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "currentDisplay is not a menu display"); - - if (/*index < 0 ||*/ index >= menuDisplay->menuItemCount()) - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, fmt::format("{} out of range", indexParamName).c_str()); - - menuDisplay->getMenuItem(index).triggered(); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_setValue_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) - } - - int newValue; - - constexpr const std::string_view valueParamName{"value"}; - - { - char valueBufEncoded[256]; - if (const auto result = httpd_query_key_value(query.data(), valueParamName.data(), valueBufEncoded, 256); result == ESP_OK) - { - char valueBuf[257]; - esphttpdutils::urldecode(valueBuf, valueBufEncoded); - - std::string_view value{valueBuf}; - - if (auto parsed = cpputils::fromString(value)) - { - newValue = *parsed; - } - else - { - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, fmt::format("could not parse {} {}", valueParamName, value).c_str()); - } - } - else if (result != ESP_ERR_NOT_FOUND) - { - ESP_LOGE(TAG, "httpd_query_key_value() %.*s failed with %s", valueParamName.size(), valueParamName.data(), esp_err_to_name(result)); - return result; - } - else - { - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, fmt::format("{} not set", valueParamName).c_str()); - } - } - - if (!currentDisplay) - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "currentDisplay is null"); - - auto *changeValueDisplay = currentDisplay->asChangeValueDisplayInterface(); - if (!changeValueDisplay) - CALL_AND_EXIT(httpd_resp_send_err, req, HTTPD_400_BAD_REQUEST, "currentDisplay is not a change value display"); - - changeValueDisplay->setShownValue(newValue); - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_status, req, "302 Moved Permanently") - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") - constexpr const std::string_view body{"Ok, continue at /"}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - esp_err_t webserver_reboot_handler(httpd_req_t *req) { shouldReboot = true; - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - std::string_view body{"REBOOT called..."}; - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "REBOOT called...") } -#ifdef FEATURE_OTA -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 += "Remote control " - "Settings"; - } - - { - HtmlTag formTag{"form", "action=\"/triggerOta\" method=\"GET\"", body}; - HtmlTag fieldsetTag{"fieldset", body}; - { - HtmlTag legendTag{"legend", body}; - body += "Trigger Update"; - } - - body += fmt::format("", esphttpdutils::htmlentities(stringSettings.otaUrl)); - - { - HtmlTag buttonTag{"button", "type=\"submit\"", body}; - body += "Go"; - } - } - } - } - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_trigger_ota_handler(httpd_req_t *req) -{ - return ESP_FAIL; } #endif - -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 bodyTag{"body", body}; - - { - HtmlTag h1Tag{"h1", body}; - body += "Settings"; - } - - { - HtmlTag pTag{"p", body}; - body += "Remote control " - "Update"; - } - - stringSettings.executeForEveryCommonSetting([&](const char *key, auto value){ - HtmlTag formTag{"form", "action=\"/saveSettings\" method=\"GET\"", body}; - HtmlTag fieldsetTag{"fieldset", body}; - { - HtmlTag legendTag{"legend", body}; - body += esphttpdutils::htmlentities(key); - } - - body += fmt::format("", - esphttpdutils::htmlentities(key), - esphttpdutils::htmlentities(value)); - - { - HtmlTag buttonTag{"button", "type=\"submit\"", body}; - body += "Save"; - } - }); - } - } - - CALL_AND_EXIT_ON_ERROR(httpd_resp_set_type, req, "text/html") - CALL_AND_EXIT(httpd_resp_send, req, body.data(), body.size()) -} - -esp_err_t webserver_save_settings_handler(httpd_req_t *req) -{ - return ESP_FAIL; -} - -#endif -} diff --git a/main/webserver_displaycontrol.h b/main/webserver_displaycontrol.h new file mode 100644 index 0000000..b8f5a8a --- /dev/null +++ b/main/webserver_displaycontrol.h @@ -0,0 +1,383 @@ +#pragma once + +// esp-idf includes +#ifdef FEATURE_WEBSERVER +#include +#endif +#include + +// 3rdparty lib includes +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "buttons.h" +#include "globals.h" + +#ifdef FEATURE_WEBSERVER +namespace { +esp_err_t webserver_root_handler(httpd_req_t *req); +esp_err_t webserver_triggerButton_handler(httpd_req_t *req); +esp_err_t webserver_triggerItem_handler(httpd_req_t *req); +esp_err_t webserver_setValue_handler(httpd_req_t *req); +} // namespace + +using esphttpdutils::HtmlTag; + +namespace { +esp_err_t webserver_root_handler(httpd_req_t *req) +{ + std::string body; + + { + HtmlTag htmlTag{"html", body}; + + { + HtmlTag headTag{"head", body}; + + { + HtmlTag titleTag{"title", body}; + body += "Display control"; + } + + body += ""; + } + + { + HtmlTag bodyTag{"body", body}; + + { + HtmlTag h1Tag{"h1", body}; + body += "Display control"; + } + + { + HtmlTag pTag{"p", body}; + body += "Display control " +#ifdef FEATURE_OTA + "Update " +#endif + + "Settings"; + } + + { + HtmlTag pTag{"p", body}; + body += "Up " + "Down " + "Confirm " + "Back " + "Profile0 " + "Profile1 " + "Profile2 " + "Profile3 "; + } + + if (auto constCurrentDisplay = static_cast(currentDisplay.get())) + { + if (const auto *textInterface = constCurrentDisplay->asTextInterface()) + { + HtmlTag h2Tag{"h2", body}; + body += esphttpdutils::htmlentities(textInterface->text()); + } + + if (const auto *menuDisplay = constCurrentDisplay->asMenuDisplay()) + { + HtmlTag ulTag{"ul", body}; + + int i{0}; + menuDisplay->runForEveryMenuItem([&,selectedIndex=menuDisplay->selectedIndex()](const MenuItem &menuItem){ + HtmlTag liTag = i == selectedIndex ? + HtmlTag{"li", "style=\"border: 1px solid black;\"", body} : + HtmlTag{"li", body}; + + body += fmt::format("{}", i, esphttpdutils::htmlentities(menuItem.text())); + i++; + }); + } + else if (const auto *changeValueDisplay = constCurrentDisplay->asChangeValueDisplayInterface()) + { + HtmlTag formTag{"form", "action=\"/setValue\" method=\"GET\"", body}; + body += fmt::format("", changeValueDisplay->shownValue()); + body += ""; + } + else + { + body += "No web control implemented for current display."; + } + } + else + { + body += "Currently no screen instantiated."; + } + } + } + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/html", body) +} + +esp_err_t webserver_triggerButton_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 button; + constexpr const std::string_view buttonParamName{"button"}; + + { + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), buttonParamName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result == ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{} not set", buttonParamName); + 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 {}", buttonParamName, 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); + + button = valueBuf; + } + + if (button == "up") + { + InputDispatcher::rotate(-1); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "down") + { + InputDispatcher::rotate(1); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "confirm") + { + InputDispatcher::confirmButton(true); + InputDispatcher::confirmButton(false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "back") + { + InputDispatcher::backButton(true); + InputDispatcher::backButton(false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "profile0") + { + InputDispatcher::profileButton(0, true); + InputDispatcher::profileButton(0, false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "profile1") + { + InputDispatcher::profileButton(1, true); + InputDispatcher::profileButton(1, false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "profile2") + { + InputDispatcher::profileButton(2, true); + InputDispatcher::profileButton(2, false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else if (button == "profile3") + { + InputDispatcher::profileButton(3, true); + InputDispatcher::profileButton(3, false); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") + } + else + { + const auto msg = fmt::format("invalid {} {}", buttonParamName, button); + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } +} + +esp_err_t webserver_triggerItem_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::size_t index; + + constexpr const std::string_view indexParamName{"index"}; + + { + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), indexParamName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result == ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{} not set", indexParamName); + 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 {}", indexParamName, 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); + + std::string_view value{valueBuf}; + + if (auto parsed = cpputils::fromString(value)) + { + index = *parsed; + } + else + { + const auto msg = fmt::format("could not parse {} {}", indexParamName, value); + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + } + + if (!currentDisplay) + { + constexpr const std::string_view msg = "currentDisplay is null"; + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + + auto *menuDisplay = currentDisplay->asMenuDisplay(); + if (!menuDisplay) + { + constexpr const std::string_view msg = "currentDisplay is not a menu display"; + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + + if (/*index < 0 ||*/ index >= menuDisplay->menuItemCount()) + { + const auto msg = fmt::format("{} {} out of range (must be smaller than {})", indexParamName, index, menuDisplay->menuItemCount()); + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + + menuDisplay->getMenuItem(index).triggered(); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") +} + +esp_err_t webserver_setValue_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()); + } + + int newValue; + + constexpr const std::string_view valueParamName{"value"}; + + { + char valueBufEncoded[256]; + if (const auto result = httpd_query_key_value(query.data(), valueParamName.data(), valueBufEncoded, 256); result != ESP_OK) + { + if (result == ESP_ERR_NOT_FOUND) + { + const auto msg = fmt::format("{} not set", valueParamName); + 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 {}", valueParamName, 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); + + std::string_view value{valueBuf}; + + if (auto parsed = cpputils::fromString(value)) + { + newValue = *parsed; + } + else + { + const auto msg = fmt::format("could not parse {} {}", valueParamName, value); + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + } + + if (!currentDisplay) + { + constexpr const std::string_view msg = "currentDisplay is null"; + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + + auto *changeValueDisplay = currentDisplay->asChangeValueDisplayInterface(); + if (!changeValueDisplay) + { + constexpr const std::string_view msg = "currentDisplay is not a change value display"; + ESP_LOGW(TAG, "%.*s", msg.size(), msg.data()); + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::BadRequest, "text/plain", msg); + } + + changeValueDisplay->setShownValue(newValue); + + CALL_AND_EXIT_ON_ERROR(httpd_resp_set_hdr, req, "Location", "/") + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::TemporaryRedirect, "text/html", "Ok, continue at /") +} +} // namespace + +#endif diff --git a/main/webserver_ota.h b/main/webserver_ota.h new file mode 100644 index 0000000..ff57bea --- /dev/null +++ b/main/webserver_ota.h @@ -0,0 +1,88 @@ +#pragma once + +// esp-idf includes +#ifdef FEATURE_WEBSERVER +#include +#endif +#include + +// 3rdparty lib includes +#include +#include +#include +#include + +// local includes +#ifdef FEATURE_OTA +#include "ota.h" +#endif + +#if defined(FEATURE_WEBSERVER) && defined(FEATURE_OTA) +namespace { +esp_err_t webserver_ota_handler(httpd_req_t *req); +esp_err_t webserver_trigger_ota_handler(httpd_req_t *req); +} // namespace + +using esphttpdutils::HtmlTag; + +namespace { +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 += "Display control " + "Update " + "Settings"; + } + + { + HtmlTag formTag{"form", "action=\"/triggerOta\" method=\"GET\"", body}; + HtmlTag fieldsetTag{"fieldset", body}; + { + HtmlTag legendTag{"legend", body}; + body += "Trigger Update"; + } + + body += fmt::format("", esphttpdutils::htmlentities(stringSettings.otaUrl)); + + { + HtmlTag buttonTag{"button", "type=\"submit\"", body}; + body += "Go"; + } + } + } + } + + 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) +{ + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "not yet implemented") +} +} +#endif diff --git a/main/webserver_settings.h b/main/webserver_settings.h new file mode 100644 index 0000000..671f8e3 --- /dev/null +++ b/main/webserver_settings.h @@ -0,0 +1,92 @@ +#pragma once + +// esp-idf includes +#ifdef FEATURE_WEBSERVER +#include +#endif +#include + +// 3rdparty lib includes +#include +#include +#include +#include + +// local includes +#include "globals.h" + +#ifdef FEATURE_WEBSERVER +namespace { +esp_err_t webserver_settings_handler(httpd_req_t *req); +esp_err_t webserver_save_settings_handler(httpd_req_t *req); +} // namespace + +using esphttpdutils::HtmlTag; + +namespace { +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 bodyTag{"body", body}; + + { + HtmlTag h1Tag{"h1", body}; + body += "Settings"; + } + + { + HtmlTag pTag{"p", body}; + body += "Display control " +#ifdef FEATURE_OTA + "Update " +#endif + "Settings"; + } + + stringSettings.executeForEveryCommonSetting([&](const char *key, auto value){ + HtmlTag formTag{"form", "action=\"/saveSettings\" method=\"GET\"", body}; + HtmlTag fieldsetTag{"fieldset", body}; + { + HtmlTag legendTag{"legend", body}; + body += esphttpdutils::htmlentities(key); + } + + body += fmt::format("", + esphttpdutils::htmlentities(key), + esphttpdutils::htmlentities(value)); + + { + HtmlTag buttonTag{"button", "type=\"submit\"", body}; + body += "Save"; + } + }); + } + } + + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/html", body) +} + +esp_err_t webserver_save_settings_handler(httpd_req_t *req) +{ + CALL_AND_EXIT(esphttpdutils::webserver_resp_send, req, esphttpdutils::ResponseStatus::Ok, "text/plain", "not yet implemented") +} +} // namespace + +#endif +