diff --git a/platformio.ini b/platformio.ini index 3ce99f6..493b24d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -22,8 +22,10 @@ monitor_filters = esp8266_exception_decoder lib_deps = fastled/FastLED@^3.6.0 me-no-dev/ESPAsyncTCP@^1.2.2 - me-no-dev/ESP Async WebServer@^1.2.3 + #me-no-dev/ESP Async WebServer@^1.2.4 TODO: doesn't work well + https://github.com/yubox-node-org/ESPAsyncWebServer.git arduino-libraries/NTPClient@^3.2.1 + bblanchon/ArduinoJson@^7.1.0 [env:debug] build_type = debug diff --git a/src/constants.h b/src/constants.h index c71e8b0..fa07fa2 100644 --- a/src/constants.h +++ b/src/constants.h @@ -1,16 +1,22 @@ #pragma once #include "misc/led.h" + +#include "credentials.h" #include "sys_constants.h" #define WIFI_MODE (WIFI_AP_MODE) -#define WIFI_SSID "ESP_LED" -#define WIFI_PASSWORD "12345678" +#define WIFI_SSID CREDENTIAL_WIFI_SSID +#define WIFI_PASSWORD CREDENTIAL_WIFI_PASSWORD #define WIFI_CONNECTION_CHECK_INTERVAL (5000u) // Interval (ms) between Wi-Fi connection check #define WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL (0u) // Max time (ms) to wait for Wi-Fi connection before switch to AP mode // 0 - Newer switch to AP mode +#define WEB_AUTH +#define AUTH_USER CREDENTIAL_AUTH_USER +#define AUTH_PASSWORD CREDENTIAL_AUTH_PASSWORD + #define TIME_ZONE (5.f) // GMT +5:00 #define MDNS_NAME "esp_lamp" diff --git a/src/credentials.h b/src/credentials.h new file mode 100644 index 0000000..28f9a79 --- /dev/null +++ b/src/credentials.h @@ -0,0 +1,7 @@ +#pragma once + +#define CREDENTIAL_WIFI_SSID "ESP_LED" +#define CREDENTIAL_WIFI_PASSWORD "12345678" + +#define CREDENTIAL_AUTH_USER "esp_lamp" +#define CREDENTIAL_AUTH_PASSWORD "password" diff --git a/src/main.cpp b/src/main.cpp index de3f699..e0c97b5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include "network/wifi.h" #include "network/web.h" +#include "network/protocol/server/api.h" #include "network/protocol/server/udp.h" #include "network/protocol/server/ws.h" @@ -80,6 +81,7 @@ Application app(config_storage, preset_names_storage, preset_configs_storage, cu WifiManager wifi_manager; WebServer web_server(WEB_PORT); +ApiWebServer api_server(app); UdpServer udp_server(app); WebSocketServer ws_server(app); @@ -302,6 +304,11 @@ void service_loop(void *) { break; case 2: +#ifdef WEB_AUTH + web_server.add_handler(new WebAuthHandler()); +#endif + + api_server.begin(web_server); udp_server.begin(UDP_PORT); ws_server.begin(web_server); diff --git a/src/network/protocol/server/api.cpp b/src/network/protocol/server/api.cpp new file mode 100644 index 0000000..38cd565 --- /dev/null +++ b/src/network/protocol/server/api.cpp @@ -0,0 +1,95 @@ +#include "api.h" + +#include + +#include "utils/math.h" + + +ApiWebServer::ApiWebServer(Application &application, const char *path) : _app(application), _path(path) {} + +void ApiWebServer::begin(WebServer &server) { + server.on((_path + String("/power")).c_str(), HTTP_GET, [this](AsyncWebServerRequest *request) { + if (!request->hasArg("value")) { + D_PRINT("Request power status"); + return response_with_json(request, JsonPropListT{ + {"status", "ok"}, + {"value", _app.config.power ? 1 : 0}, + }); + } + + bool enabled = request->arg("value") == "1"; + _app.set_power(enabled); + + response_with_json_status(request, "ok"); + }); + + server.on((_path + String("/brightness")).c_str(), HTTP_GET, [this](AsyncWebServerRequest *request) { + if (!request->hasArg("value")) { + D_PRINT("Request brightness status"); + return response_with_json(request, JsonPropListT{ + {"status", "ok"}, + {"value", map16(_app.config.max_brightness, 255, 100)}, + }); + } + + auto new_brightness = map16(request->arg("value").toInt(), 100, 255); + + _app.config.max_brightness = new_brightness; + _app.load(); + + response_with_json_status(request, "ok"); + }); + + server.on((_path + String("/color")).c_str(), HTTP_GET, [this](AsyncWebServerRequest *request) { + if (!request->hasArg("value")) { + D_PRINT("Request color status"); + auto hsv = _app.preset().color_effect == ColorEffectEnum::SOLID + ? CHSV(_app.preset().speed, _app.preset().scale, 255) + : CHSV(255, 255, 225); + + CRGB color{}; + hsv2rgb_rainbow(hsv, color); + + return response_with_json(request, JsonPropListT{ + {"status", "ok"}, + {"value", static_cast(color) & 0xffffff}, + + {"hue", hsv.hue}, + {"sat", hsv.sat}, + {"bri", hsv.val}, + }); + } + + PresetConfig *preset = nullptr; + if (_app.preset().color_effect == ColorEffectEnum::SOLID) { + preset = &_app.preset(); + } else { + for (int i = 0; i < _app.preset_configs.count; ++i) { + if (_app.preset_configs.presets[i].color_effect == ColorEffectEnum::SOLID) { + _app.change_preset(i); + preset = &_app.preset_configs.presets[i]; + break; + } + } + + if (preset == nullptr) { + return response_with_json_status(request, "error"); + } + } + + + auto new_color = CRGB(request->arg("value").toInt()); + auto hsv = rgb2hsv_approximate(new_color); + preset->speed = hsv.hue; + preset->scale = hsv.sat; + + _app.load(); + + response_with_json(request, { + {"status", "ok"}, + {"hue", hsv.hue}, + {"sat", hsv.sat}, + {"bri", hsv.val}, + }); + }); +} diff --git a/src/network/protocol/server/api.h b/src/network/protocol/server/api.h new file mode 100644 index 0000000..4fe4e7e --- /dev/null +++ b/src/network/protocol/server/api.h @@ -0,0 +1,18 @@ +#pragma once + +#include "constants.h" +#include "application.h" + +#include "network/web.h" + +#include "utils/network.h" + +class ApiWebServer { + Application &_app; + const char *_path; + +public: + ApiWebServer(Application &application, const char *path = "/api"); + + void begin(WebServer &server); +}; \ No newline at end of file diff --git a/src/network/web.cpp b/src/network/web.cpp index 45232a9..7328314 100644 --- a/src/network/web.cpp +++ b/src/network/web.cpp @@ -28,4 +28,17 @@ void WebServer::begin(FS *fs) { D_WRITE("Web server listening on port: "); D_PRINT(_port); -} \ No newline at end of file +} + +void WebAuthHandler::handleRequest(AsyncWebServerRequest *request) { + D_PRINTF("Reject request from: %s\n", request->client()->remoteIP().toString().c_str()); + + request->redirect("https://google.com"); +} + +bool WebAuthHandler::canHandle(AsyncWebServerRequest *request) { + bool is_local = (request->client()->getRemoteAddress() & PP_HTONL(0xffff0000UL)) == PP_HTONL(0xc0a80000UL); + bool auth_required = _allow_local ? !is_local : true; + + return auth_required && !request->authenticate(AUTH_USER, AUTH_PASSWORD); +} diff --git a/src/network/web.h b/src/network/web.h index 8924fa0..60da450 100644 --- a/src/network/web.h +++ b/src/network/web.h @@ -3,6 +3,9 @@ #include "ESPAsyncWebServer.h" #include "FS.h" +#include "constants.h" +#include "debug.h" + class WebServer { uint16_t _port; AsyncWebServer _server; @@ -19,4 +22,15 @@ class WebServer { } inline void add_handler(AsyncWebHandler *handler) { _server.addHandler(handler); } +}; + +class WebAuthHandler : public AsyncWebHandler { +private: + bool _allow_local = true; + +public: + inline void set_allow_local(bool value) { _allow_local = value; } + + virtual bool canHandle(AsyncWebServerRequest *request) override final; + virtual void handleRequest(AsyncWebServerRequest *request) override final; }; \ No newline at end of file diff --git a/src/utils/math.h b/src/utils/math.h new file mode 100644 index 0000000..65c27a2 --- /dev/null +++ b/src/utils/math.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +inline uint16_t map16(uint16_t value, uint16_t limit_src, uint16_t limit_dst) { + value = std::max((uint16_t) 0, std::min(limit_src, value)); + return (int32_t) value * limit_dst / limit_src; +} \ No newline at end of file diff --git a/src/utils/network.h b/src/utils/network.h new file mode 100644 index 0000000..c1e6b55 --- /dev/null +++ b/src/utils/network.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include + +typedef std::variant JsonPropVariantT; +typedef std::initializer_list> JsonPropListT; + +inline void response_with_json(AsyncWebServerRequest *request, JsonDocument &doc) { + auto *response = request->beginResponseStream("application/json"); + serializeJson(doc, *response); + request->send(response); +} + + +inline void response_with_json(AsyncWebServerRequest *request, JsonPropListT props) { + JsonDocument doc; + for (auto prop: props) { + std::visit([&](auto &&arg) { doc[prop.first] = arg; }, prop.second); + } + + response_with_json(request, doc); +} + +inline void response_with_json_status(AsyncWebServerRequest *request, const char *status) { + JsonDocument doc; + doc["status"] = status; + + response_with_json(request, doc); +} \ No newline at end of file