From 9074d9837a8aa47d36fb2bc93030364f315c47c8 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 21 Sep 2024 20:19:15 +0500 Subject: [PATCH] Migrate to the new esp-iot-framework version --- .clang-tidy | 1 - README.md | 8 +- README_RU.MD | 2 +- platformio.ini | 2 +- src/app/application.cpp | 249 +++++++++++++++++++++--------- src/app/application.h | 62 ++++---- src/app/config.h | 68 +++----- src/app/metadata.h | 234 ++++++++++++++++++++++++++++ src/app/type.h | 158 ------------------- src/constants.h | 4 +- src/lib | 2 +- src/main.cpp | 227 ++------------------------- src/misc/led.cpp | 88 +++++++++++ src/misc/led.h | 38 +++++ src/{app => misc}/night_mode.cpp | 0 src/{app => misc}/night_mode.h | 2 +- src/network/{server => }/api.cpp | 5 +- src/network/{server => }/api.h | 3 +- src/network/server/handler.cpp | 14 -- src/network/server/handler.h | 13 -- src/network/server/mqtt.cpp | 29 ---- src/network/server/mqtt.h | 16 -- src/sys_constants.h | 7 +- www/package.json | 2 +- www/src/config.js | 10 +- www/src/index.html | 2 +- www/src/lib/application.js | 70 +++++++-- www/src/lib/config.js | 7 +- www/src/lib/index.d.ts | 14 +- www/src/lib/index.js | 1 + www/src/lib/misc/event_emitter.js | 12 +- www/src/lib/network/cmd.js | 3 + www/src/lib/network/packet.js | 2 +- www/src/lib/network/ws.js | 5 +- www/src/lib/style.css | 17 +- www/src/lib/utils/common.js | 52 +++++++ 36 files changed, 775 insertions(+), 654 deletions(-) create mode 100644 src/app/metadata.h delete mode 100644 src/app/type.h create mode 100644 src/misc/led.cpp create mode 100644 src/misc/led.h rename src/{app => misc}/night_mode.cpp (100%) rename src/{app => misc}/night_mode.h (97%) rename src/network/{server => }/api.cpp (93%) rename src/network/{server => }/api.h (93%) delete mode 100644 src/network/server/handler.cpp delete mode 100644 src/network/server/handler.h delete mode 100644 src/network/server/mqtt.cpp delete mode 100644 src/network/server/mqtt.h create mode 100644 www/src/lib/utils/common.js diff --git a/.clang-tidy b/.clang-tidy index c23d2aa..28da22b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -166,6 +166,5 @@ CheckOptions: readability-identifier-naming.PublicMemberCase: lower_case readability-identifier-naming.ProtectedMemberCase: lower_case readability-identifier-naming.PrivateMemberCase: lower_case - readability-identifier-naming.ClassMemberPrefix: readability-identifier-naming.PrivateMemberPrefix: _ readability-identifier-naming.PrivateMethodPrefix: _ diff --git a/README.md b/README.md index f5fff0b..0c0927f 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ pio run -t upload -e $PLATFORM-$ENV --upload-port "$ADDRESS" | Endpoint | Method | Parameters | Response | Description | |----------------------|-----------|--------------------------|-----------------------------------------------------------|---------------------------------------------------------| | `/api/status` | `GET` | None | `{"status": "ok", "value": number, "brightness": number}` | Retrieves the current power and brightness values. | -| `/api/power` | `GET` | `value` (1 or 0) | {"status": "ok"} | Sets the power state (on/off). | +| `/api/power` | `GET` | `value` (1 or 0) | {"status": "ok"} | Sets the power _state (on/off). | | `/api/brightness` | `GET` | `value` (0-100) | {"status": "ok"} | Updates the brightness level. | | `/api/debug` | `GET` | None | Plain Text | Provides debugging information. | | `/api/restart` | `GET` | None | Plain Text: "OK" | Restarts the server and saves configuration. | @@ -86,10 +86,10 @@ pio run -t upload -e $PLATFORM-$ENV --upload-port "$ADDRESS" | Topic In * | Topic Out * | Type | Values | Comments | |---------------------------|-------------------------------|-------------|----------------------|---------------------------------------| -| `MQTT_TOPIC_POWER` | `MQTT_OUT_TOPIC_POWER` | `uint8_t` | 0..1 | Power state: ON (1) / OFF (0) | -| `MQTT_TOPIC_BRIGHTNESS` | `MQTT_OUT_TOPIC_BRIGHTNESS` | `uint16_t` | 0..`DAC_MAX_VALUE` | Brightness level, can switch to 0..100 (`MQTT_CONVERT_BRIGHTNESS`) | +| `MQTT_TOPIC_POWER` | `MQTT_OUT_TOPIC_POWER` | `uint8_t` | 0..1 | Power _state: ON (1) / OFF (0) | +| `MQTT_TOPIC_BRIGHTNESS` | `MQTT_OUT_TOPIC_BRIGHTNESS` | `uint16_t` | 0..`PWM_MAX_VALUE` | Brightness level, can switch to 0..100 (`MQTT_CONVERT_BRIGHTNESS`) | | `MQTT_TOPIC_COLOR` | `MQTT_OUT_TOPIC_COLOR` | `uint32_t` | 0..0xFFFFFF | Color value (ARGB or RGB format) | -| `MQTT_TOPIC_NIGHT_MODE` | `MQTT_OUT_TOPIC_NIGHT_MODE` | `uint8_t` | 0..1 | Night mode state: ON (1) / OFF (0) | +| `MQTT_TOPIC_NIGHT_MODE` | `MQTT_OUT_TOPIC_NIGHT_MODE` | `uint8_t` | 0..1 | Night mode _state: ON (1) / OFF (0) | \* Actual topic values decalred in `constants.h` diff --git a/README_RU.MD b/README_RU.MD index a6c7ef1..9cec763 100644 --- a/README_RU.MD +++ b/README_RU.MD @@ -79,7 +79,7 @@ pio run -t upload -e $PLATFORM-$ENV --upload-port "$ADDRESS" | Топик Команд * | Топик Уведомлений * | Тип | Значения | Комментарии | |------------------------|---------------------------|-------------|-----------------------|--------------------------------------| | `MQTT_TOPIC_POWER` | `MQTT_OUT_TOPIC_POWER` | `uint8_t` | 0..1 | Состояние питания: ВКЛ (1) / ВЫКЛ (0) | -| `MQTT_TOPIC_BRIGHTNESS`| `MQTT_OUT_TOPIC_BRIGHTNESS`| `uint16_t` | 0..`DAC_MAX_VALUE` | Уровень яркости, можно переключать на диапазон 0..100 (`MQTT_CONVERT_BRIGHTNESS`) | +| `MQTT_TOPIC_BRIGHTNESS`| `MQTT_OUT_TOPIC_BRIGHTNESS`| `uint16_t` | 0..`PWM_MAX_VALUE` | Уровень яркости, можно переключать на диапазон 0..100 (`MQTT_CONVERT_BRIGHTNESS`) | | `MQTT_TOPIC_COLOR` | `MQTT_OUT_TOPIC_COLOR` | `uint32_t` | 0..0xFFFFFF | Значение цвета (формат RGB) | | `MQTT_TOPIC_NIGHT_MODE`| `MQTT_OUT_TOPIC_NIGHT_MODE`| `uint8_t` | 0..1 | Состояние ночного режима: ВКЛ (1) / ВЫКЛ (0) | diff --git a/platformio.ini b/platformio.ini index ab9a2fc..bd5932c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ framework = arduino board_build.f_cpu = 160000000L board_build.filesystem = littlefs -monitor_speed = 74880 +monitor_speed = 115200 lib_deps = me-no-dev/ESPAsyncTCP@^1.2.2 diff --git a/src/app/application.cpp b/src/app/application.cpp index 6fe0f92..e784956 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -1,30 +1,98 @@ #include "application.h" -#include "lib/utils/meta.h" -#include "utils/math.h" void Application::begin() { - _rgb_mode = sys_config().rgb_mode; - if (_rgb_mode) { - pinMode(sys_config().led_r_pin, OUTPUT); - pinMode(sys_config().led_g_pin, OUTPUT); - pinMode(sys_config().led_b_pin, OUTPUT); + D_PRINT("Starting application..."); + + if (!LittleFS.begin()) { + D_PRINT("Unable to initialize FS"); + } + + _bootstrap = std::make_unique>(&LittleFS); + + auto &sys_config = _bootstrap->config().sys_config; + _bootstrap->begin({ + .mdns_name = sys_config.mdns_name, + .wifi_mode = sys_config.wifi_mode, + .wifi_ssid = sys_config.wifi_ssid, + .wifi_password = sys_config.wifi_password, + .wifi_connection_timeout = sys_config.wifi_max_connection_attempt_interval, + .mqtt_enabled = sys_config.mqtt, + .mqtt_host = sys_config.mqtt_host, + .mqtt_port = sys_config.mqtt_port, + .mqtt_user = sys_config.mqtt_user, + .mqtt_password = sys_config.mqtt_password, + }); + + if (sys_config.rgb_mode) { + _led = std::make_unique( + sys_config.led_r_pin, sys_config.led_g_pin, sys_config.led_b_pin); } else { - pinMode(sys_config().led_pin, OUTPUT); + _led = std::make_unique(sys_config.led_r_pin); } - event_property_changed().subscribe(this, - [this](auto sender, auto type, auto) { - if (sender != this) _handle_property_change(type); - }); + _led->begin(); + _night_mode_manager = std::make_unique(_bootstrap->config()); + _ntp_time = std::make_unique(); - load(); + _api = std::make_unique(*this); + _api->begin(_bootstrap->web_server()); + + _bootstrap->event_state_changed().subscribe(this, [this](auto sender, auto state, auto arg) { + _bootstrap_state_changed(sender, state, arg); + }); + _bootstrap->timer().add_interval([this](auto) { _app_loop(); }, APP_LOOP_INTERVAL); + + _setup(); + change_state(AppState::INITIALIZATION); +} + +void Application::_setup() { + NotificationBus::get().subscribe([this](auto sender, auto param) { + if (sender != this) _handle_property_change(param); + }); + + auto &ws_server = _bootstrap->ws_server(); + auto &mqtt_server = _bootstrap->mqtt_server(); + + _metadata = std::make_unique(build_metadata(config())); + _metadata->visit([this, &ws_server, &mqtt_server](AbstractPropertyMeta *meta) { + auto binary_protocol = (BinaryProtocolMeta *) meta->get_binary_protocol(); + if (binary_protocol->packet_type.has_value()) { + ws_server->register_parameter(*binary_protocol->packet_type, meta->get_parameter()); + VERBOSE(D_PRINTF("WebSocket: Register property %s\r\n", __debug_enum_str(*binary_protocol->packet_type))); + } + + auto mqtt_protocol = meta->get_mqtt_protocol(); + if (mqtt_protocol->topic_in && mqtt_protocol->topic_out) { + mqtt_server->register_parameter(mqtt_protocol->topic_in, mqtt_protocol->topic_out, meta->get_parameter()); + VERBOSE(D_PRINTF("MQTT: Register property %s <-> %s\r\n", mqtt_protocol->topic_in, mqtt_protocol->topic_out)); + } else if (mqtt_protocol->topic_out) { + mqtt_server->register_notification(mqtt_protocol->topic_out, meta->get_parameter()); + VERBOSE(D_PRINTF("MQTT: Register notification -> %s\r\n", mqtt_protocol->topic_out)); + } + + if (binary_protocol->packet_type.has_value()) { + _parameter_to_packet[meta->get_parameter()] = binary_protocol->packet_type.value(); + } + }); + + ws_server->register_data_request(PacketType::GET_CONFIG, _metadata->data.config); + ws_server->register_command(PacketType::RESTART, [this] { _bootstrap->restart(); }); } -void Application::_handle_property_change(NotificationProperty type) { - if (type == NotificationProperty::POWER) { +void Application::event_loop() { + _bootstrap->event_loop(); +} + +void Application::_handle_property_change(const AbstractParameter *parameter) { + auto it = _parameter_to_packet.find(parameter); + if (it == _parameter_to_packet.end()) return; + + auto type = it->second; + if (type == PacketType::POWER) { set_power(config().power); - } else if (type >= NotificationProperty::NIGHT_MODE_ENABLED && type <= NotificationProperty::NIGHT_MODE_BRIGHTNESS) { - _night_mode_manager.reset(); + } else if (type >= PacketType::NIGHT_MODE_ENABLED && type <= PacketType::NIGHT_MODE_BRIGHTNESS) { + _night_mode_manager->reset(); update(); } else { update(); @@ -32,93 +100,124 @@ void Application::_handle_property_change(NotificationProperty type) { } void Application::load() { - if (_rgb_mode) _load_calibration(); - set_brightness(config().power ? brightness() : PIN_DISABLED); -} + _led->set_brightness(config().power ? config().brightness : PIN_DISABLED); -void Application::_load_calibration() { - _color_r = _convert_color(config().color, config().calibration, 16); - _color_g = _convert_color(config().color, config().calibration, 8); - _color_b = _convert_color(config().color, config().calibration, 0); + if (_led->rgb_mode()) { + _led->set_calibration(config().calibration); + _led->set_color(config().color); + } } void Application::update() { - _config_storage.save(); + _bootstrap->save_changes(); load(); } void Application::change_state(AppState s) { - state_change_time = millis(); - state = s; - D_PRINTF("Change app state: %u\n", (uint8_t) s); + _state_change_time = millis(); + _state = s; + D_PRINTF("Change app state: %s\r\n", __debug_enum_str(s)); } void Application::set_power(bool on) { config().power = on; - D_PRINTF("Turning Power: %s\n", on ? "ON" : "OFF"); - if (state != AppState::INITIALIZATION) { + D_PRINTF("Turning Power: %s\r\n", on ? "ON" : "OFF"); + if (_state != AppState::INITIALIZATION) { change_state(on ? AppState::TURNING_ON : AppState::TURNING_OFF); } - _config_storage.save(); + _bootstrap->save_changes(); } -void Application::restart() { - D_PRINTF("Received restart signal. Restarting after %u ms.\r\n", RESTART_DELAY); - - if (_config_storage.is_pending_commit()) _config_storage.force_save(); - - _timer.add_timeout([](auto) { ESP.restart(); }, RESTART_DELAY); -} - -void Application::set_brightness(uint16_t value) { - auto brightness = DAC_MAX_VALUE - (uint16_t) floor( - log10f(10 - (float) value * 9 / DAC_MAX_VALUE) * DAC_MAX_VALUE); - - if (_rgb_mode) { - _apply_rgb_brightness(brightness); - } else { - analogWrite(sys_config().led_pin, brightness); - } -} - -void Application::_apply_rgb_brightness(uint16_t brightness) { - analogWrite(sys_config().led_r_pin, static_cast(_color_r * brightness / DAC_MAX_VALUE)); - analogWrite(sys_config().led_g_pin, static_cast(_color_g * brightness / DAC_MAX_VALUE)); - analogWrite(sys_config().led_b_pin, static_cast(_color_b * brightness / DAC_MAX_VALUE)); -} - -uint16_t Application::brightness() { +uint16_t Application::_brightness() { uint16_t result; - if (_night_mode_manager.is_night_time()) { - result = _night_mode_manager.get_brightness(); + if (_night_mode_manager->is_night_time()) { + result = _night_mode_manager->get_brightness(); } else { result = std::max(sys_config().led_min_brightness, config().brightness); } - - return std::min(DAC_MAX_VALUE, result); + return std::min(PWM_MAX_VALUE, result); } -uint16_t Application::_convert_color(uint32_t color_data, uint32_t calibration_data, uint8_t bit) { - uint8_t color = (color_data >> bit) & 0xff; - uint8_t calibration = (calibration_data >> bit) & 0xff; - - uint8_t calibrated_color = (uint16_t) color * calibration / 255; - return map16(_apply_gamma(calibrated_color), 255, DAC_MAX_VALUE); +void Application::_app_loop() { +#if defined(DEBUG) && DEBUG_LEVEL <= __DEBUG_LEVEL_VERBOSE + static unsigned long t = 0; + static unsigned long ii = 0; + if (ii % 10 == 0) D_PRINTF("Animation latency: %lu\r\n", millis() - t); + + t = millis(); + ++ii; +#endif + + switch (_state) { + case AppState::UNINITIALIZED: + break; + + case AppState::INITIALIZATION: { + if (config().power) { + const auto factor = map16( + (millis() - _state_change_time) % sys_config().wifi_connect_flash_timeout, + sys_config().wifi_connect_flash_timeout, + PWM_MAX_VALUE + ); + + uint16_t brightness = _brightness() * cubic_wave16(factor, PWM_MAX_VALUE) / PWM_MAX_VALUE; + _led->set_brightness(brightness); + } + } + break; + + case AppState::TURNING_ON: { + uint16_t factor = std::min(PWM_MAX_VALUE, + (millis() - _state_change_time) * PWM_MAX_VALUE / sys_config().power_change_timeout); + uint16_t brightness = (uint16_t) _brightness() * ease_cubic16(factor, PWM_MAX_VALUE) / PWM_MAX_VALUE; + _led->set_brightness(brightness); + + if (factor == PWM_MAX_VALUE) change_state(AppState::STAND_BY); + break; + } + + case AppState::TURNING_OFF: { + uint16_t factor = PWM_MAX_VALUE - std::min(PWM_MAX_VALUE, + (millis() - _state_change_time) * PWM_MAX_VALUE / sys_config().power_change_timeout); + uint16_t brightness = (uint16_t) _brightness() * ease_cubic16(factor, PWM_MAX_VALUE) / PWM_MAX_VALUE; + _led->set_brightness(brightness); + + if (factor == 0) change_state(AppState::STAND_BY); + break; + } + + case AppState::STAND_BY: + if (config().power && _night_mode_manager->is_night_time()) { + auto brightness = _night_mode_manager->get_brightness(); + _led->set_brightness(brightness); + } + break; + } } -uint8_t Application::_apply_gamma(uint8_t color, float gamma) { - if (gamma == 1.0f) return color; +void Application::_service_loop() { + _ntp_time->update(); + _night_mode_manager->handle_night(*_ntp_time); +} - auto orig = (float) color / 255.0f; - auto adj = pow(orig, gamma) * 255.0f; +void Application::_bootstrap_state_changed(void *sender, BootstrapState state, void *arg) { + if (state == BootstrapState::INITIALIZING) { + _ntp_time->begin(TIME_ZONE); - auto result = (uint8_t) adj; + change_state(AppState::INITIALIZATION); + load(); + } else if (state == BootstrapState::READY && !_initialized) { + _initialized = true; - // Avoid gamma-adjusting a positive number to zero - if (color > 0 && result == 0) return 1; + change_state(AppState::STAND_BY); + load(); - return result; + _bootstrap->timer().add_interval([this](auto) { + _ntp_time->update(); + _night_mode_manager->handle_night(*_ntp_time); + }, BOOTSTRAP_SERVICE_LOOP_INTERVAL); + } } diff --git a/src/app/application.h b/src/app/application.h index 45d217c..39af0fa 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -1,52 +1,54 @@ #pragma once -#include "lib/base/application.h" -#include "lib/misc/storage.h" +#include "lib/bootstrap.h" +#include "lib/misc/ntp_time.h" #include "config.h" -#include "constants.h" -#include "night_mode.h" -#include "type.h" +#include "metadata.h" +#include "network/api.h" +#include "misc/night_mode.h" +#include "misc/led.h" +#include "utils/math.h" -class Application : public AbstractApplication { - Storage &_config_storage; - NightModeManager &_night_mode_manager; - Timer &_timer; +class Application { + std::unique_ptr> _bootstrap = nullptr; + std::unique_ptr _metadata = nullptr; + std::unique_ptr _night_mode_manager = nullptr; + std::unique_ptr _ntp_time = nullptr; + std::unique_ptr _api = nullptr; + std::unique_ptr _led = nullptr; - bool _rgb_mode {false}; - uint16_t _color_r {0}, _color_g {0}, _color_b {0}; + bool _initialized = false; -public: - unsigned long state_change_time = 0; - AppState state = AppState::UNINITIALIZED; + unsigned long _state_change_time = 0; + AppState _state = AppState::UNINITIALIZED; - explicit Application(Storage &config_storage, NightModeManager &night_mode_manager, Timer &timer) - : AbstractApplication(PacketTypeMetadataMap), - _config_storage(config_storage), - _night_mode_manager(night_mode_manager), - _timer(timer) {} + std::map _parameter_to_packet{}; - inline ConfigT &config() override { return _config_storage.get(); } +public: + inline Config &config() { return _bootstrap->config(); } inline SysConfig &sys_config() { return config().sys_config; } void begin(); + void event_loop(); void change_state(AppState s); - - uint16_t brightness(); - void set_brightness(uint16_t value); - void set_power(bool on); - void restart(); void load(); void update(); + inline void restart() { _bootstrap->restart(); } + private: - void _handle_property_change(NotificationProperty type); - void _apply_rgb_brightness(uint16_t brightness); - void _load_calibration(); + void _setup(); + + void _bootstrap_state_changed(void *sender, BootstrapState state, void *arg); + + void _app_loop(); + void _service_loop(); + + uint16_t _brightness(); - uint16_t _convert_color(uint32_t color_data, uint32_t calibration_data, uint8_t bit); - uint8_t _apply_gamma(uint8_t color, float gamma = GAMMA); + void _handle_property_change(const AbstractParameter *param); }; diff --git a/src/app/config.h b/src/app/config.h index 16da1eb..ceeaa2b 100644 --- a/src/app/config.h +++ b/src/app/config.h @@ -2,48 +2,35 @@ #include #include +#include -#include "constants.h" #include "credentials.h" +#include "constants.h" -enum class AppState { - UNINITIALIZED, - INITIALIZATION, - STAND_BY, - TURNING_ON, - TURNING_OFF -}; +MAKE_ENUM_AUTO(AppState, uint8_t, + UNINITIALIZED, + INITIALIZATION, + STAND_BY, + TURNING_ON, + TURNING_OFF +); -enum class ServiceState { - UNINITIALIZED, - WIFI_CONNECT, - INITIALIZATION, - STAND_BY -}; +typedef char ConfigString[CONFIG_STRING_SIZE]; struct __attribute ((packed)) SysConfig { - char mdns_name[16] {MDNS_NAME}; + ConfigString mdns_name{MDNS_NAME}; WifiMode wifi_mode = WIFI_MODE; - char wifi_ssid[32] {WIFI_SSID}; - char wifi_password[32] {WIFI_PASSWORD}; + ConfigString wifi_ssid{WIFI_SSID}; + ConfigString wifi_password{WIFI_PASSWORD}; uint32_t wifi_connection_check_interval = WIFI_CONNECTION_CHECK_INTERVAL; uint32_t wifi_max_connection_attempt_interval = WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL; bool rgb_mode = RGB_MODE_ENABLED; - - union { - struct { - uint8_t led_r_pin; - uint8_t led_g_pin; - uint8_t led_b_pin; - }; - - struct { - uint8_t led_pin; - }; - }; + uint8_t led_r_pin = LED_R_PIN; + uint8_t led_g_pin = LED_G_PIN; + uint8_t led_b_pin = LED_B_PIN; uint16_t led_min_brightness = LED_MIN_BRIGHTNESS; uint32_t power_change_timeout = POWER_CHANGE_TIMEOUT; @@ -52,23 +39,16 @@ struct __attribute ((packed)) SysConfig { float time_zone = TIME_ZONE; bool web_auth = WEB_AUTH; - char web_auth_user[16] = WEB_AUTH_USER; - char web_auth_password[16] = WEB_AUTH_PASSWORD; + ConfigString web_auth_user = WEB_AUTH_USER; + ConfigString web_auth_password = WEB_AUTH_PASSWORD; bool mqtt = MQTT; - char mqtt_host[32] = MQTT_HOST; + ConfigString mqtt_host = MQTT_HOST; uint16_t mqtt_port = MQTT_PORT; - char mqtt_user[16] = MQTT_USER; - char mqtt_password[16] = MQTT_PASSWORD; - - uint8_t mqtt_convert_brightness = MQTT_CONVERT_BRIGHTNESS; + ConfigString mqtt_user = MQTT_USER; + ConfigString mqtt_password = MQTT_PASSWORD; - SysConfig() { - led_pin = LED_PIN; - led_r_pin = LED_R_PIN; - led_g_pin = LED_G_PIN; - led_b_pin = LED_B_PIN; - } + bool mqtt_convert_brightness = MQTT_CONVERT_BRIGHTNESS; }; struct __attribute ((packed)) NightModeConfig { @@ -86,7 +66,7 @@ struct __attribute ((packed)) Config { uint32_t color = ~0u; // All colors uint32_t calibration = ~0u; // No calibration - NightModeConfig night_mode {}; + NightModeConfig night_mode{}; - SysConfig sys_config {}; + SysConfig sys_config{}; }; diff --git a/src/app/metadata.h b/src/app/metadata.h new file mode 100644 index 0000000..325e0b4 --- /dev/null +++ b/src/app/metadata.h @@ -0,0 +1,234 @@ +#pragma once + +#include +#include + +#include "app/config.h" +#include "network/cmd.h" +#include "utils/math.h" + +class BrightnessParameter : public Parameter { + const SysConfig &_config; + +public: + BrightnessParameter(uint16_t *value, const SysConfig &config) : Parameter(value), _config(config) {} + + bool parse(const String &data) override { + if (_config.mqtt_convert_brightness) { + if (data.length() == 0) return false; + + uint16_t value = map16(data.toInt(), 100, PWM_MAX_VALUE); + Parameter::set_value(&value, sizeof(value)); + return true; + } + + return Parameter::parse(data); + } + + [[nodiscard]] String to_string() const override { + if (_config.mqtt_convert_brightness) { + uint16_t value; + memcpy(&value, Parameter::get_value(), sizeof(value)); + auto converted = map16(value, PWM_MAX_VALUE, 100); + return String(converted); + } + + return Parameter::to_string(); + } +}; + +DECLARE_META_TYPE(AppMetaProperty, PacketType) + +DECLARE_META(NightModeConfigMeta, AppMetaProperty, + MEMBER(Parameter, enabled), + MEMBER(Parameter, brightness), + MEMBER(Parameter, start_time), + MEMBER(Parameter, end_time), + MEMBER(Parameter, switch_interval) +) + +DECLARE_META(SysConfigMeta, AppMetaProperty, + MEMBER(FixedString, mdns_name), + MEMBER(Parameter, wifi_mode), + MEMBER(FixedString, wifi_ssid), + MEMBER(FixedString, wifi_password), + MEMBER(Parameter, wifi_connection_check_interval), + MEMBER(Parameter, wifi_max_connection_attempt_interval), + MEMBER(Parameter, rgb_mode), + MEMBER(Parameter, led_r_pin), + MEMBER(Parameter, led_g_pin), + MEMBER(Parameter, led_b_pin), + MEMBER(Parameter, led_min_brightness), + MEMBER(Parameter, power_change_timeout), + MEMBER(Parameter, wifi_connect_flash_timeout), + MEMBER(Parameter, time_zone), + MEMBER(Parameter, web_auth), + MEMBER(FixedString, web_auth_user), + MEMBER(FixedString, web_auth_password), + MEMBER(Parameter, mqtt), + MEMBER(FixedString, mqtt_host), + MEMBER(Parameter, mqtt_port), + MEMBER(FixedString, mqtt_user), + MEMBER(FixedString, mqtt_password), + MEMBER(Parameter, mqtt_convert_brightness) +) + +DECLARE_META(DataConfigMeta, AppMetaProperty, + MEMBER(ComplexParameter, config), +) + +DECLARE_META(ConfigMetadata, AppMetaProperty, + MEMBER(Parameter, power), + MEMBER(BrightnessParameter, brightness), + MEMBER(Parameter, color), + MEMBER(Parameter, calibration), + SUB_TYPE(NightModeConfigMeta, night_mode), + SUB_TYPE(SysConfigMeta, sys_config), + + SUB_TYPE(DataConfigMeta, data), +) + +inline ConfigMetadata build_metadata(Config &config) { + return { + .power = { + PacketType::POWER, + MQTT_TOPIC_POWER, MQTT_OUT_TOPIC_POWER, + &config.power + }, + .brightness = { + PacketType::BRIGHTNESS, + MQTT_TOPIC_BRIGHTNESS, MQTT_OUT_TOPIC_BRIGHTNESS, + BrightnessParameter(&config.brightness, config.sys_config) + }, + .color = { + PacketType::COLOR, + MQTT_TOPIC_COLOR, MQTT_OUT_TOPIC_COLOR, + &config.color + }, + .calibration = { + PacketType::CALIBRATION, + &config.calibration + }, + .night_mode = { + .enabled = { + PacketType::NIGHT_MODE_ENABLED, + MQTT_TOPIC_NIGHT_MODE, MQTT_OUT_TOPIC_NIGHT_MODE, + &config.night_mode.enabled + }, + .brightness = { + PacketType::NIGHT_MODE_BRIGHTNESS, + &config.night_mode.brightness + }, + .start_time = { + PacketType::NIGHT_MODE_START, + &config.night_mode.start_time + }, + .end_time = { + PacketType::NIGHT_MODE_END, + &config.night_mode.end_time + }, + .switch_interval = { + PacketType::NIGHT_MODE_INTERVAL, + &config.night_mode.switch_interval + } + }, + .sys_config = { + .mdns_name = { + PacketType::SYS_CONFIG_MDNS_NAME, + {config.sys_config.mdns_name, CONFIG_STRING_SIZE} + }, + .wifi_mode = { + PacketType::SYS_CONFIG_WIFI_MODE, + (uint8_t *) &config.sys_config.wifi_mode + }, + .wifi_ssid = { + PacketType::SYS_CONFIG_WIFI_SSID, + {config.sys_config.wifi_ssid, CONFIG_STRING_SIZE} + }, + .wifi_password = { + PacketType::SYS_CONFIG_WIFI_PASSWORD, + {config.sys_config.wifi_password, CONFIG_STRING_SIZE} + }, + .wifi_connection_check_interval = { + PacketType::SYS_CONFIG_WIFI_CONNECTION_CHECK_INTERVAL, + &config.sys_config.wifi_connection_check_interval + }, + .wifi_max_connection_attempt_interval = { + PacketType::SYS_CONFIG_WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL, + &config.sys_config.wifi_max_connection_attempt_interval + }, + .rgb_mode = { + PacketType::SYS_RGB_MODE, + &config.sys_config.rgb_mode + }, + .led_r_pin = { + PacketType::SYS_CONFIG_LED_R_PIN, + &config.sys_config.led_r_pin + }, + .led_g_pin = { + PacketType::SYS_CONFIG_LED_G_PIN, + &config.sys_config.led_g_pin + }, + .led_b_pin = { + PacketType::SYS_CONFIG_LED_B_PIN, + &config.sys_config.led_b_pin + }, + .led_min_brightness = { + PacketType::SYS_CONFIG_LED_MIN_BRIGHTNESS, + &config.sys_config.led_min_brightness + }, + .power_change_timeout = { + PacketType::SYS_CONFIG_POWER_CHANGE_TIMEOUT, + &config.sys_config.power_change_timeout + }, + .wifi_connect_flash_timeout = { + PacketType::SYS_CONFIG_WIFI_CONNECT_FLASH_TIMEOUT, + &config.sys_config.wifi_connect_flash_timeout + }, + .time_zone = { + PacketType::SYS_CONFIG_TIME_ZONE, + &config.sys_config.time_zone + }, + .web_auth = { + PacketType::SYS_CONFIG_WEB_AUTH_ENABLED, + &config.sys_config.web_auth + }, + .web_auth_user = { + PacketType::SYS_CONFIG_WEB_AUTH_USER, + {config.sys_config.web_auth_user, CONFIG_STRING_SIZE} + }, + .web_auth_password = { + PacketType::SYS_CONFIG_WEB_AUTH_PASSWORD, + {config.sys_config.web_auth_password, CONFIG_STRING_SIZE} + }, + .mqtt = { + PacketType::SYS_CONFIG_MQTT_ENABLED, + &config.sys_config.mqtt + }, + .mqtt_host = { + PacketType::SYS_CONFIG_MQTT_HOST, + {config.sys_config.mqtt_host, CONFIG_STRING_SIZE} + }, + .mqtt_port = { + PacketType::SYS_CONFIG_MQTT_PORT, + &config.sys_config.mqtt_port + }, + .mqtt_user = { + PacketType::SYS_CONFIG_MQTT_USER, + {config.sys_config.mqtt_user, CONFIG_STRING_SIZE} + }, + .mqtt_password = { + PacketType::SYS_CONFIG_MQTT_PASSWORD, + {config.sys_config.mqtt_password, CONFIG_STRING_SIZE} + }, + .mqtt_convert_brightness = { + PacketType::SYS_CONFIG_MQTT_CONVERT_BRIGHTNESS, + &config.sys_config.mqtt_convert_brightness + } + }, + + .data { + .config = ComplexParameter(&config), + }, + }; +} diff --git a/src/app/type.h b/src/app/type.h deleted file mode 100644 index 828d767..0000000 --- a/src/app/type.h +++ /dev/null @@ -1,158 +0,0 @@ -#pragma once - -#include - -#include -#include "lib/utils/enum.h" -#include - -#include "constants.h" -#include "config.h" -#include "network/cmd.h" - -MAKE_ENUM(NotificationProperty, uint8_t, - POWER, 0, - BRIGHTNESS, 1, - COLOR, 2, - CALIBRATION, 3, - NIGHT_MODE_ENABLED, 4, - NIGHT_MODE_START, 5, - NIGHT_MODE_END, 6, - NIGHT_MODE_INTERVAL, 7, - NIGHT_MODE_BRIGHTNESS, 8, - - SYS_CONFIG_MDNS_NAME, 9, - - SYS_CONFIG_WIFI_MODE, 10, - SYS_CONFIG_WIFI_SSID, 11, - SYS_CONFIG_WIFI_PASSWORD, 12, - SYS_CONFIG_WIFI_CONNECTION_CHECK_INTERVAL, 13, - SYS_CONFIG_WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL, 14, - - SYS_RGB_MODE, 15, - SYS_CONFIG_LED_R_PIN, 16, - SYS_CONFIG_LED_G_PIN, 17, - SYS_CONFIG_LED_B_PIN, 18, - - SYS_CONFIG_POWER_CHANGE_TIMEOUT, 19, - SYS_CONFIG_WIFI_CONNECT_FLASH_TIMEOUT, 20, - - SYS_CONFIG_TIME_ZONE, 21, - - SYS_CONFIG_WEB_AUTH_ENABLED, 22, - SYS_CONFIG_WEB_AUTH_USER, 23, - SYS_CONFIG_WEB_AUTH_PASSWORD, 24, - - SYS_CONFIG_MQTT_ENABLED, 25, - SYS_CONFIG_MQTT_HOST, 26, - SYS_CONFIG_MQTT_PORT, 27, - SYS_CONFIG_MQTT_USER, 28, - SYS_CONFIG_MQTT_PASSWORD, 29, - SYS_CONFIG_MQTT_CONVERT_BRIGHTNESS, 30, - - SYS_CONFIG_LED_MIN_BRIGHTNESS, 31, -) - -using AppPropertyMetadata = PropertyMetadata; - -inline std::map PacketTypeMetadataMap = { - define_meta_entry(NotificationProperty::POWER, PacketType::POWER, - MQTT_TOPIC_POWER, MQTT_OUT_TOPIC_POWER, - &Config::power), - - define_meta_entry(NotificationProperty::BRIGHTNESS, PacketType::BRIGHTNESS, - MQTT_TOPIC_BRIGHTNESS, MQTT_OUT_TOPIC_BRIGHTNESS, - &Config::brightness), - - define_meta_entry(NotificationProperty::COLOR, PacketType::COLOR, - MQTT_TOPIC_COLOR, MQTT_OUT_TOPIC_COLOR, - &Config::color), - - define_meta_entry(NotificationProperty::CALIBRATION, PacketType::CALIBRATION, - &Config::calibration), - - define_meta_entry(NotificationProperty::NIGHT_MODE_ENABLED, PacketType::NIGHT_MODE_ENABLED, - MQTT_TOPIC_NIGHT_MODE, MQTT_OUT_TOPIC_NIGHT_MODE, - &Config::night_mode, &NightModeConfig::enabled), - - define_meta_entry(NotificationProperty::NIGHT_MODE_START, PacketType::NIGHT_MODE_START, - &Config::night_mode, &NightModeConfig::start_time), - - define_meta_entry(NotificationProperty::NIGHT_MODE_END, PacketType::NIGHT_MODE_END, - &Config::night_mode, &NightModeConfig::end_time), - - define_meta_entry(NotificationProperty::NIGHT_MODE_INTERVAL, PacketType::NIGHT_MODE_INTERVAL, - &Config::night_mode, &NightModeConfig::switch_interval), - - define_meta_entry(NotificationProperty::NIGHT_MODE_BRIGHTNESS, PacketType::NIGHT_MODE_BRIGHTNESS, - &Config::night_mode, &NightModeConfig::brightness), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MDNS_NAME, PacketType::SYS_CONFIG_MDNS_NAME, - &Config::sys_config, &SysConfig::mdns_name), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_MODE, PacketType::SYS_CONFIG_WIFI_MODE, - &Config::sys_config, &SysConfig::wifi_mode), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_SSID, PacketType::SYS_CONFIG_WIFI_SSID, - &Config::sys_config, &SysConfig::wifi_ssid), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_PASSWORD, PacketType::SYS_CONFIG_WIFI_PASSWORD, - &Config::sys_config, &SysConfig::wifi_password), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_CONNECTION_CHECK_INTERVAL, PacketType::SYS_CONFIG_WIFI_CONNECTION_CHECK_INTERVAL, - &Config::sys_config, &SysConfig::wifi_connection_check_interval), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL, PacketType::SYS_CONFIG_WIFI_MAX_CONNECTION_ATTEMPT_INTERVAL, - &Config::sys_config, &SysConfig::wifi_max_connection_attempt_interval), - - define_meta_entry(NotificationProperty::SYS_RGB_MODE, PacketType::SYS_RGB_MODE, - &Config::sys_config, &SysConfig::rgb_mode), - - define_meta_entry(NotificationProperty::SYS_CONFIG_LED_R_PIN, PacketType::SYS_CONFIG_LED_R_PIN, - &Config::sys_config, &SysConfig::led_r_pin), - - define_meta_entry(NotificationProperty::SYS_CONFIG_LED_G_PIN, PacketType::SYS_CONFIG_LED_G_PIN, - &Config::sys_config, &SysConfig::led_g_pin), - - define_meta_entry(NotificationProperty::SYS_CONFIG_LED_B_PIN, PacketType::SYS_CONFIG_LED_B_PIN, - &Config::sys_config, &SysConfig::led_b_pin), - - define_meta_entry(NotificationProperty::SYS_CONFIG_POWER_CHANGE_TIMEOUT, PacketType::SYS_CONFIG_POWER_CHANGE_TIMEOUT, - &Config::sys_config, &SysConfig::power_change_timeout), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WIFI_CONNECT_FLASH_TIMEOUT, PacketType::SYS_CONFIG_WIFI_CONNECT_FLASH_TIMEOUT, - &Config::sys_config, &SysConfig::wifi_connect_flash_timeout), - - define_meta_entry(NotificationProperty::SYS_CONFIG_TIME_ZONE, PacketType::SYS_CONFIG_TIME_ZONE, - &Config::sys_config, &SysConfig::time_zone), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WEB_AUTH_ENABLED, PacketType::SYS_CONFIG_WEB_AUTH_ENABLED, - &Config::sys_config, &SysConfig::web_auth), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WEB_AUTH_USER, PacketType::SYS_CONFIG_WEB_AUTH_USER, - &Config::sys_config, &SysConfig::web_auth_user), - - define_meta_entry(NotificationProperty::SYS_CONFIG_WEB_AUTH_PASSWORD, PacketType::SYS_CONFIG_WEB_AUTH_PASSWORD, - &Config::sys_config, &SysConfig::web_auth_password), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_ENABLED, PacketType::SYS_CONFIG_MQTT_ENABLED, - &Config::sys_config, &SysConfig::mqtt), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_HOST, PacketType::SYS_CONFIG_MQTT_HOST, - &Config::sys_config, &SysConfig::mqtt_host), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_PORT, PacketType::SYS_CONFIG_MQTT_PORT, - &Config::sys_config, &SysConfig::mqtt_port), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_USER, PacketType::SYS_CONFIG_MQTT_USER, - &Config::sys_config, &SysConfig::mqtt_user), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_PASSWORD, PacketType::SYS_CONFIG_MQTT_PASSWORD, - &Config::sys_config, &SysConfig::mqtt_password), - - define_meta_entry(NotificationProperty::SYS_CONFIG_MQTT_CONVERT_BRIGHTNESS, PacketType::SYS_CONFIG_MQTT_CONVERT_BRIGHTNESS, - &Config::sys_config, &SysConfig::mqtt_convert_brightness), - - define_meta_entry(NotificationProperty::SYS_CONFIG_LED_MIN_BRIGHTNESS, PacketType::SYS_CONFIG_LED_MIN_BRIGHTNESS, - &Config::sys_config, &SysConfig::led_min_brightness), -}; diff --git a/src/constants.h b/src/constants.h index 98151d5..9e1f67b 100644 --- a/src/constants.h +++ b/src/constants.h @@ -24,7 +24,7 @@ #define LED_B_PIN (5u) // D1 #endif -#define LED_PIN LED_R_PIN +#define LED_MIN_BRIGHTNESS (1u) #define POWER_CHANGE_TIMEOUT (1000u) // Timeout for power change animation #define WIFI_CONNECT_FLASH_TIMEOUT (3000u) @@ -39,7 +39,7 @@ #define MQTT_CONVERT_BRIGHTNESS (0u) // Convert brightness from internal range to [0..100] -#define MQTT_PREFIX MDNS_NAME +#define MQTT_PREFIX "" #define MQTT_TOPIC_BRIGHTNESS MQTT_PREFIX "/brightness" #define MQTT_TOPIC_POWER MQTT_PREFIX "/power" #define MQTT_TOPIC_COLOR MQTT_PREFIX "/color" diff --git a/src/lib b/src/lib index 6534bfd..b09342c 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 6534bfd60c3b146ae7aa9d9aeb569ec8c1ed1482 +Subproject commit b09342c4be7c5bf2e36ac9ea03fea2f23367abca diff --git a/src/main.cpp b/src/main.cpp index 184a6be..7415d64 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,231 +1,26 @@ -#include -#include -#include #include -#include "credentials.h" -#include "constants.h" - -#include "lib/misc/event_topic.h" -#include "lib/misc/ntp_time.h" -#include "lib/misc/storage.h" -#include "lib/misc/timer.h" -#include "lib/network/web.h" -#include "lib/network/wifi.h" -#include "lib/network/server/ws.h" -#include "lib/utils/qr.h" -#include "lib/debug.h" - -#include "app/config.h" #include "app/application.h" -#include "app/night_mode.h" - -#include "network/server/api.h" -#include "network/server/handler.h" -#include "network/server/mqtt.h" - -#include "utils/math.h" - -Timer global_timer; - -Storage config_storage(global_timer, "config", STORAGE_CONFIG_VERSION); - -NightModeManager night_mode_manager(config_storage.get()); -Application app(config_storage, night_mode_manager, global_timer); - -WifiManager *wifi_manager; -WebServer web_server(WEB_PORT); -ApiWebServer api_server(app); -AppMqttServer mqtt_server(app); - -AppPacketHandler packet_handler(app); -WebSocketServer ws_server(app, packet_handler); - -NtpTime ntp_time; - -SysConfig *sys_config; -DNSServer *dns_server; - -void animation_loop(void *); -void service_loop(void *); +Application ApplicationInstance; void setup() { -#if defined(DEBUG) - Serial.begin(74880); - D_PRINT(); - - delay(2000); -#endif +#ifdef DEBUG + Serial.begin(115200); - if (!LittleFS.begin()) { - D_PRINT("Unable to initialize FS"); + { + auto start_t = millis(); + while (!Serial && (millis() - start_t) < 15000ul) delay(1); } - config_storage.begin(&LittleFS); - sys_config = &config_storage.get().sys_config; - - wifi_manager = new WifiManager(sys_config->wifi_ssid, sys_config->wifi_password, - sys_config->wifi_connection_check_interval); - - analogWriteResolution(DAC_RESOLUTION); - - global_timer.add_interval(animation_loop, 1000 / 60); - global_timer.add_interval(service_loop, 20); -} - -void loop() { - global_timer.handle_timers(); -} - -void animation_loop(void *) { -#if defined(DEBUG) && DEBUG_LEVEL <= __DEBUG_LEVEL_VERBOSE - static unsigned long t = 0; - static unsigned long ii = 0; - if (ii % 10 == 0) D_PRINTF("Animation latency: %lu\n", millis() - t); - - t = millis(); - ++ii; + delay(2000); #endif - switch (app.state) { - case AppState::UNINITIALIZED: - break; - - case AppState::INITIALIZATION: { - if (app.config().power) { - const auto factor = map16( - (millis() - app.state_change_time) % sys_config->wifi_connect_flash_timeout, - sys_config->wifi_connect_flash_timeout, - DAC_MAX_VALUE - ); - - uint16_t brightness = app.brightness() * cubic_wave16(factor, DAC_MAX_VALUE) / DAC_MAX_VALUE; - app.set_brightness(brightness); - } - } - break; - - case AppState::TURNING_ON: { - uint16_t factor = std::min(DAC_MAX_VALUE, - (millis() - app.state_change_time) * DAC_MAX_VALUE / sys_config->power_change_timeout); - uint16_t brightness = (uint16_t) app.brightness() * ease_cubic16(factor, DAC_MAX_VALUE) / DAC_MAX_VALUE; - app.set_brightness(brightness); - - if (factor == DAC_MAX_VALUE) app.change_state(AppState::STAND_BY); - break; - } - - case AppState::TURNING_OFF: { - uint16_t factor = DAC_MAX_VALUE - std::min(DAC_MAX_VALUE, - (millis() - app.state_change_time) * DAC_MAX_VALUE / sys_config->power_change_timeout); - uint16_t brightness = (uint16_t) app.brightness() * ease_cubic16(factor, DAC_MAX_VALUE) / DAC_MAX_VALUE; - app.set_brightness(brightness); + analogWriteResolution(PWM_RESOLUTION); - if (factor == 0) app.change_state(AppState::STAND_BY); - break; - } - case AppState::STAND_BY: - if (app.config().power && night_mode_manager.is_night_time()) { - auto brightness = night_mode_manager.get_brightness(); - app.set_brightness(brightness); - } - break; - } + ApplicationInstance.begin(); } -void service_loop(void *) { -#if defined(DEBUG) && DEBUG_LEVEL <= __DEBUG_LEVEL_VERBOSE - static unsigned long t = 0; - static unsigned long ii = 0; - if (ii % 10 == 0) D_PRINTF("Service latency: %lu\n", millis() - t); - - t = millis(); - ++ii; -#endif - - static auto state = ServiceState::UNINITIALIZED; - - switch (state) { - case ServiceState::UNINITIALIZED: - wifi_manager->connect(sys_config->wifi_mode, sys_config->wifi_max_connection_attempt_interval); - state = ServiceState::WIFI_CONNECT; - - app.change_state(AppState::INITIALIZATION); - break; - - case ServiceState::WIFI_CONNECT: - wifi_manager->handle_connection(); - - if (wifi_manager->state() == WifiManagerState::CONNECTED) { - state = ServiceState::INITIALIZATION; - } - - break; - - case ServiceState::INITIALIZATION: - if (sys_config->web_auth) { - web_server.add_handler(new WebAuthHandler(sys_config->web_auth_user, sys_config->web_auth_password)); - } - - if (sys_config->mqtt) { - mqtt_server.begin(sys_config->mqtt_host, sys_config->mqtt_port, - sys_config->mqtt_user, sys_config->mqtt_password); - } - - api_server.begin(web_server); - ws_server.begin(web_server); - - web_server.begin(&LittleFS); - ntp_time.begin(sys_config->time_zone); - - ArduinoOTA.setHostname(sys_config->mdns_name); - ArduinoOTA.begin(); - - app.begin(); - - D_PRINT("ESP Ready"); - - if (wifi_manager->mode() == WifiMode::AP) { - dns_server = new DNSServer(); - dns_server->start(53, "*", WiFi.softAPIP()); - - D_PRINT("Connect to WiFi:"); - qr_print_wifi_connection(wifi_manager->ssid(), wifi_manager->password()); - } else { - String url = "http://"; - url += sys_config->mdns_name; - url += ".local"; - - if (web_server.port() != 80) { - url += ":"; - url += web_server.port(); - } - - D_PRINT("Open WebUI:"); - qr_print_string(url.c_str()); - } - - app.change_state(AppState::STAND_BY); - state = ServiceState::STAND_BY; - break; - - case ServiceState::STAND_BY: { - wifi_manager->handle_connection(); - ArduinoOTA.handle(); - - if (dns_server) dns_server->processNextRequest(); - - night_mode_manager.handle_night(ntp_time); - if (wifi_manager->mode() == WifiMode::STA) ntp_time.update(); - - ws_server.handle_incoming_data(); - - if (sys_config->mqtt) mqtt_server.handle_connection(); - - break; - } - - default:; - } +void loop() { + ApplicationInstance.event_loop(); } diff --git a/src/misc/led.cpp b/src/misc/led.cpp new file mode 100644 index 0000000..5092a22 --- /dev/null +++ b/src/misc/led.cpp @@ -0,0 +1,88 @@ +#include "led.h" + +#include +#include "utils/math.h" + +LedController::LedController(uint8_t pin) : + _rgb_mode(false), _r_pin(pin), _g_pin(0), _b_pin(0) {} + +LedController::LedController(uint8_t r_pin, uint8_t g_pin, uint8_t b_pin) : + _rgb_mode(true), _r_pin(r_pin), _g_pin(g_pin), _b_pin(b_pin) {} + +void LedController::begin() { + pinMode(_r_pin, OUTPUT); + + if (_rgb_mode) { + pinMode(_g_pin, OUTPUT); + pinMode(_b_pin, OUTPUT); + + _load_calibration(_color, _calibration); + } +} + +void LedController::set_brightness(uint16_t value) { + _brightness = PWM_MAX_VALUE - (uint16_t) floor( + log10f(10 - (float) value * 9 / PWM_MAX_VALUE) * PWM_MAX_VALUE); + + _analog_write(); +} + +void LedController::set_color(uint32_t color) { + if (!_rgb_mode) return; + + _color = color; + _load_calibration(_color, _calibration); + + _analog_write(); +} + +void LedController::set_calibration(uint32_t calibration) { + if (!_rgb_mode) return; + + _calibration = calibration; + _load_calibration(_color, _calibration); + + _analog_write(); +} + +void LedController::_apply_rgb_brightness(uint16_t brightness) { + analogWrite(_r_pin, (uint32_t) _color_r * brightness / PWM_MAX_VALUE); + analogWrite(_g_pin, (uint32_t) _color_g * brightness / PWM_MAX_VALUE); + analogWrite(_b_pin, (uint32_t) _color_b * brightness / PWM_MAX_VALUE); +} + +void LedController::_load_calibration(uint32_t color, uint32_t calibration) { + _color_r = _convert_color(color, calibration, 16); + _color_g = _convert_color(color, calibration, 8); + _color_b = _convert_color(color, calibration, 0); +} + +uint16_t LedController::_convert_color(uint32_t color_data, uint32_t calibration_data, uint8_t bit) { + uint8_t color = (color_data >> bit) & 0xff; + uint8_t calibration = (calibration_data >> bit) & 0xff; + + uint8_t calibrated_color = (uint16_t) color * calibration / 255; + return map16(_apply_gamma(calibrated_color), 255, PWM_MAX_VALUE); +} + +uint8_t LedController::_apply_gamma(uint8_t color, float gamma) { + if (gamma == 1.0f) return color; + + auto orig = (float) color / 255.0f; + auto adj = pow(orig, gamma) * 255.0f; + + auto result = (uint8_t) adj; + + // Avoid gamma-adjusting a positive number to zero + if (color > 0 && result == 0) return 1; + + return result; +} + +void LedController::_analog_write() { + if (_rgb_mode) { + _apply_rgb_brightness(_brightness); + } else { + analogWrite(_r_pin, _brightness); + } +} diff --git a/src/misc/led.h b/src/misc/led.h new file mode 100644 index 0000000..8ae7ad6 --- /dev/null +++ b/src/misc/led.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "constants.h" + +class LedController { + bool _rgb_mode; + + uint8_t _r_pin, _g_pin, _b_pin; + + uint16_t _brightness = PWM_MAX_VALUE; + uint16_t _color_r{0}, _color_g{0}, _color_b{0}; + + uint32_t _color = 0xffffff; + uint32_t _calibration = 0xffffff; + +public: + LedController(uint8_t pin); + LedController(uint8_t r_pin, uint8_t g_pin, uint8_t b_pin); + + void begin(); + + void set_brightness(uint16_t value); + void set_color(uint32_t color); + void set_calibration(uint32_t calibration); + + inline bool rgb_mode() { return _rgb_mode; } + inline uint16_t brightness() { return _brightness; } + +private: + void _apply_rgb_brightness(uint16_t brightness); + void _analog_write(); + + void _load_calibration(uint32_t color, uint32_t calibration); + uint16_t _convert_color(uint32_t color_data, uint32_t calibration_data, uint8_t bit); + uint8_t _apply_gamma(uint8_t color, float gamma = GAMMA); +}; \ No newline at end of file diff --git a/src/app/night_mode.cpp b/src/misc/night_mode.cpp similarity index 100% rename from src/app/night_mode.cpp rename to src/misc/night_mode.cpp diff --git a/src/app/night_mode.h b/src/misc/night_mode.h similarity index 97% rename from src/app/night_mode.h rename to src/misc/night_mode.h index 4a7f3dd..52ca492 100644 --- a/src/app/night_mode.h +++ b/src/misc/night_mode.h @@ -1,6 +1,6 @@ #pragma once -#include "config.h" +#include "app/config.h" #include "lib/debug.h" class NtpTime; diff --git a/src/network/server/api.cpp b/src/network/api.cpp similarity index 93% rename from src/network/server/api.cpp rename to src/network/api.cpp index 6eef71e..2f7847a 100644 --- a/src/network/server/api.cpp +++ b/src/network/api.cpp @@ -2,12 +2,13 @@ #include "utils/math.h" +#include "app/application.h" ApiWebServer::ApiWebServer(Application &application, const char *path) : _app(application), _path(path) {} void ApiWebServer::begin(WebServer &server) { _on(server, "/status", HTTP_GET, [this](AsyncWebServerRequest *request) { - auto brightness = map16(_app.config().brightness, DAC_MAX_VALUE, 100); + auto brightness = map16(_app.config().brightness, PWM_MAX_VALUE, 100); response_with_json(request, JsonPropListT{ {"status", "ok"}, {"value", _app.config().power}, @@ -23,7 +24,7 @@ void ApiWebServer::begin(WebServer &server) { }); _on(server, "/brightness", HTTP_GET, [this](AsyncWebServerRequest *request) { - auto new_brightness = map16(request->arg("value").toInt(), 100, DAC_MAX_VALUE); + auto new_brightness = map16(request->arg("value").toInt(), 100, PWM_MAX_VALUE); _app.config().brightness = new_brightness; _app.load(); diff --git a/src/network/server/api.h b/src/network/api.h similarity index 93% rename from src/network/server/api.h rename to src/network/api.h index 47542f1..6a60e72 100644 --- a/src/network/server/api.h +++ b/src/network/api.h @@ -2,9 +2,10 @@ #include "lib/network/web.h" -#include "app/application.h" #include "utils/network.h" +class Application; + class ApiWebServer { Application &_app; String _path; diff --git a/src/network/server/handler.cpp b/src/network/server/handler.cpp deleted file mode 100644 index 84c7438..0000000 --- a/src/network/server/handler.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "handler.h" - -Response AppPacketHandler::handle_packet_data(uint32_t client_id, const Packet &packet) { - const auto [header, data] = packet; - - if (header->type == PacketType::GET_CONFIG) { - return protocol().serialize(app().config()); - } else if (packet.header->type == PacketType::RESTART) { - app().restart(); - return Response::ok(); - } - - return PacketHandler::handle_packet_data(client_id, packet); -} diff --git a/src/network/server/handler.h b/src/network/server/handler.h deleted file mode 100644 index 307714b..0000000 --- a/src/network/server/handler.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once - -#include "lib/network/protocol/packet_handler.h" - -#include "app/application.h" - -class AppPacketHandler : public PacketHandler { -public: - AppPacketHandler(Application &app) : PacketHandler(app) {} - -protected: - Response handle_packet_data(uint32_t client_id, const Packet &packet) override; -}; diff --git a/src/network/server/mqtt.cpp b/src/network/server/mqtt.cpp deleted file mode 100644 index 9fe7f62..0000000 --- a/src/network/server/mqtt.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include "mqtt.h" - -#include "utils/math.h" - -void AppMqttServer::_transform_topic_payload(const String &topic, String &payload) { - if (app().sys_config().mqtt_convert_brightness && topic == MQTT_TOPIC_BRIGHTNESS) { - payload = map16(payload.toInt(), 100, DAC_MAX_VALUE); - } - - MqttServer::_transform_topic_payload(topic, payload); -} - -void AppMqttServer::_transform_topic_notification(const PropertyMetadata &meta, String &payload) { - if (app().sys_config().mqtt_convert_brightness && meta.property == NotificationProperty::BRIGHTNESS) { - payload = map16(payload.toInt(), DAC_MAX_VALUE, 100); - } - - MqttServer::_transform_topic_notification(meta, payload); -} - -void AppMqttServer::_after_message_process(const PropertyMetadata &meta) { - if (meta.property == NotificationProperty::POWER) { - app().set_power(app().config().power); - } else { - app().load(); - } - - MqttServer::_after_message_process(meta); -} diff --git a/src/network/server/mqtt.h b/src/network/server/mqtt.h deleted file mode 100644 index 1f0f8b0..0000000 --- a/src/network/server/mqtt.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "lib/network/server/mqtt.h" - -#include "app/application.h" - -class AppMqttServer : public MqttServer { -public: - explicit AppMqttServer(Application &app) : MqttServer(app) {} - -protected: - void _transform_topic_payload(const String &topic, String &payload) override; - void _transform_topic_notification(const MetaPropT &meta, String &payload) override; - - void _after_message_process(const MetaPropT &meta) override; -}; diff --git a/src/sys_constants.h b/src/sys_constants.h index 2c3e1aa..d95b8d6 100644 --- a/src/sys_constants.h +++ b/src/sys_constants.h @@ -20,11 +20,14 @@ #define PIN_DISABLED (LOW) #define PIN_ENABLED (HIGH) -#define DAC_RESOLUTION (14u) -#define DAC_MAX_VALUE ((uint16_t)((1u << DAC_RESOLUTION) - 1)) +#define PWM_RESOLUTION (14u) +#define PWM_MAX_VALUE ((uint16_t)((1u << PWM_RESOLUTION) - 1)) #define NTP_UPDATE_INTERVAL (24ul * 3600 * 1000) #define GAMMA (2.2f) #define RESTART_DELAY (500u) +#define APP_LOOP_INTERVAL (10u) + +#define CONFIG_STRING_SIZE (32u) \ No newline at end of file diff --git a/www/package.json b/www/package.json index 3cb70d0..9662735 100644 --- a/www/package.json +++ b/www/package.json @@ -3,7 +3,7 @@ "author": "DrA1ex", "scripts": { "build": "rm -rf ../data/* && esbuild ./src/index.js ./src/service_worker.js --bundle --format=esm --outdir=../data --minify && npm run static", - "static": "cp ./src/index.html ../data/ && cp ./src/hotspot-detect.html ../data/ && cp ./src/lib/style.css ../data/ && cp -r ./favicons/* ../data/" + "static": "mkdir -p ../data/lib && cp ./src/index.html ../data/ && cp ./src/hotspot-detect.html ../data/ && cp ./src/lib/style.css ../data/lib/ && cp -r ./favicons/* ../data/" }, "devDependencies": { "esbuild": "^0.19.11" diff --git a/www/src/config.js b/www/src/config.js index 1c0107b..1d30c40 100644 --- a/www/src/config.js +++ b/www/src/config.js @@ -45,7 +45,7 @@ export class Config extends AppConfigBase { }; this.sysConfig = { - mdnsName: parser.readFixedString(16), + mdnsName: parser.readFixedString(32), wifiMode: parser.readUint8(), wifiSsid: parser.readFixedString(32), @@ -67,14 +67,14 @@ export class Config extends AppConfigBase { timeZone: parser.readFloat32(), webAuth: parser.readBoolean(), - webAuthUser: parser.readFixedString(16), - webAuthPassword: parser.readFixedString(16), + webAuthUser: parser.readFixedString(32), + webAuthPassword: parser.readFixedString(32), mqtt: parser.readBoolean(), mqttHost: parser.readFixedString(32), mqttPort: parser.readUint16(), - mqttUser: parser.readFixedString(16), - mqttPassword: parser.readFixedString(16), + mqttUser: parser.readFixedString(32), + mqttPassword: parser.readFixedString(32), mqttConvertBrightness: parser.readBoolean(), }; diff --git a/www/src/index.html b/www/src/index.html index 440e6c8..02b12ed 100644 --- a/www/src/index.html +++ b/www/src/index.html @@ -21,7 +21,7 @@ LED Control - + diff --git a/www/src/lib/application.js b/www/src/lib/application.js index 8dbdce8..b551aa8 100644 --- a/www/src/lib/application.js +++ b/www/src/lib/application.js @@ -106,7 +106,22 @@ export class ApplicationBase extends EventEmitter { this.subscribe(this, this.Event.Notification, (_, {key, value}) => { this.config.setProperty(key, value, false); - this.propertyMeta[key].control.setValue(this.config.getProperty(key)); + + const {control, title, prop} = this.propertyMeta[key]; + if (prop.type === "skip") return; + + const propValue = this.config.getProperty(key); + if ("setValue" in control) { + control.setValue(propValue); + } else if ("setText" in control) { + control.setText(propValue); + } + + if (prop.visibleIf) { + const visible = !!this.config.getProperty(prop.visibleIf); + control.setVisibility(visible); + title?.setVisibility(true); + } }); } @@ -178,29 +193,32 @@ export class ApplicationBase extends EventEmitter { if (prop.visibleIf) { if (config.getProperty(prop.visibleIf)) { control.setVisibility(true); - title.setVisibility(true); + title?.setVisibility(true); } else { control.setVisibility(false); - title.setVisibility(false); + title?.setVisibility(false); + + control.setAttribute("data-loading", false); continue; } } - // TODO: if (prop.type === "select") { control.setOptions(this.config.lists[prop.list].map(v => ({key: v.code, label: v.name}))); } - if (prop.type !== "button") { + if (control instanceof ButtonControl && prop.cmd) { + control.setOnClick(() => this.#sendCommand(prop)); + } else if ("setValue" in control) { const value = config.getProperty(prop.key); control.setValue(value); + } else if (prop.type === "label") { + const value = config.getProperty(prop.key); + control.setText(value); } - if (control.getAttribute("data-loading") === "true") { - if ("setOnChange" in control) control.setOnChange((value) => config.setProperty(prop.key, value)); - - control.setAttribute("data-loading", false); - } + control.setAttribute("data-loading", false); + if ("setOnChange" in control) control.setOnChange((value) => config.setProperty(prop.key, value)); } } } @@ -331,6 +349,11 @@ export class ApplicationBase extends EventEmitter { control.setText(prop.label); break; + case "label": + control = new TextControl(document.createElement("h4")); + control.addClass("label"); + break; + case "separator": control = new FrameControl(document.createElement("hr")); break; @@ -412,6 +435,24 @@ export class ApplicationBase extends EventEmitter { return titleElement; } + async #sendCommand(prop) { + if (prop.__busy) return; + + const {control} = this.propertyMeta[prop.key]; + + prop.__busy = true; + control.setAttribute("data-saving", true); + + try { + await this.#ws.request(prop.cmd); + } catch (e) { + console.error("Unable to send command", e); + } finally { + prop.__busy = false; + control.setAttribute("data-saving", false); + } + } + async #sendChangesImpl(config, prop, value, oldValue) { if (prop.__busy) { return FunctionUtils.ThrottleDelay; @@ -421,7 +462,7 @@ export class ApplicationBase extends EventEmitter { prop.__busy = true; try { - if (prop.type !== "wheel") control.setAttribute("data-saving", "true"); + if (!["wheel", "color"].includes(prop.type)) control.setAttribute("data-saving", "true"); if (Array.isArray(prop.cmd)) { await this.#ws.request(value ? prop.cmd[0] : prop.cmd[1]); @@ -479,10 +520,7 @@ export class ApplicationBase extends EventEmitter { serializeFn.call(view, 0, value, true); await this.#ws.request(prop.cmd, req.buffer); - - if (prop.type !== "wheel") { - control.setValue(value); - } + control.setValue(value); } console.log(`Changed '${prop.key}': '${oldValue}' -> '${value}'`); @@ -491,7 +529,7 @@ export class ApplicationBase extends EventEmitter { control.setValue(oldValue); } finally { prop.__busy = false; - if (prop.type !== "wheel") control.setAttribute("data-saving", "false"); + if (!["wheel", "color"].includes(prop.type)) control.setAttribute("data-saving", "false"); } } } \ No newline at end of file diff --git a/www/src/lib/config.js b/www/src/lib/config.js index 54884ed..a3bfb38 100644 --- a/www/src/lib/config.js +++ b/www/src/lib/config.js @@ -1,5 +1,6 @@ import {EventEmitter} from "./misc/event_emitter.js"; import {BinaryParser} from "./misc/binary_parser.js"; +import {SystemPacketType} from "./network/cmd.js"; /** @@ -19,11 +20,9 @@ export class AppConfigBase extends EventEmitter { get propertyMap() { return this.#propertyMap;} /** - * @abstract - * * @returns {number} */ - get cmd() {throw new Error("Not implemented");} + get cmd() {return SystemPacketType.GET_CONFIG;} /** * @abstract @@ -66,7 +65,7 @@ export class AppConfigBase extends EventEmitter { const {data} = await ws.request(this.cmd); const parser = new BinaryParser(data.buffer, data.byteOffset); - this.parse(parser); + await this.parse(parser); this.emitEvent(AppConfigBase.Event.Loaded); } diff --git a/www/src/lib/index.d.ts b/www/src/lib/index.d.ts index 0297ed1..4d3d9a3 100644 --- a/www/src/lib/index.d.ts +++ b/www/src/lib/index.d.ts @@ -139,6 +139,7 @@ declare module "misc/binary_parser.js" { // network/ws.js declare module "network/ws.js" { import {EventEmitter} from "misc/event_emitter.js"; + import {BinaryParser} from "misc/binary_parser.js"; export enum WebSocketState { uninitialized = "uninitialized", @@ -156,7 +157,12 @@ declare module "network/ws.js" { class Packet { requestId: number; type: PacketTypeT | SystemPacketType; + signature: number; + size: number; + data: Uint8Array; + parseString(): string; + parser(): BinaryParser; static parse(buffer: ArrayBuffer): Packet; } @@ -320,7 +326,7 @@ declare module "control/index.js" { // main module declare module "application.js" { - import {Control, FrameControl, InputControl, TextControl} from "control/index.js"; + import {Control, FrameControl, TextControl} from "control/index.js"; import {WebSocketInteraction, WebSocketConfig} from "network/ws.js"; import {EventEmitter} from "misc/event_emitter.js"; import {BinaryParser} from "misc/binary_parser.js"; @@ -331,13 +337,13 @@ declare module "application.js" { time = "time", select = "select", int = "int", - float = "float", text = "text", password = "password", color = "color", button = "button", skip = "skip", title = "title", + label = "label", separator = "separator", } @@ -392,12 +398,12 @@ declare module "application.js" { name: string; } - export type SelectListConfig = Record + export type SelectListConfig = Record export interface PropertyMeta { prop: PropertyConfig; title: TextControl; - control: Control | InputControl; + control: Control; } export interface PropertySectionMeta { diff --git a/www/src/lib/index.js b/www/src/lib/index.js index 2c91848..2344819 100644 --- a/www/src/lib/index.js +++ b/www/src/lib/index.js @@ -6,6 +6,7 @@ export { } from "./control/index.js" export {Packet} from "./network/packet.js" +export {SystemPacketType} from "./network/cmd.js" export {WebSocketInteraction, WebSocketState} from "./network/ws.js" export {ApplicationBase} from "./application.js" diff --git a/www/src/lib/misc/event_emitter.js b/www/src/lib/misc/event_emitter.js index 7eaeec5..57b9d8f 100644 --- a/www/src/lib/misc/event_emitter.js +++ b/www/src/lib/misc/event_emitter.js @@ -1,12 +1,12 @@ export class EventEmitter { - subscribers = new Map(); + #subscribers = new Map(); /** * @param {string} type * @param {*|null} data */ emitEvent(type, data = null) { - for (let subscriptions of this.subscribers.values()) { + for (let subscriptions of this.#subscribers.values()) { const handler = subscriptions[type]; if (handler) { try { @@ -24,11 +24,11 @@ export class EventEmitter { * @param {function} handler */ subscribe(subscriber, type, handler) { - if (!this.subscribers.has(subscriber)) { - this.subscribers.set(subscriber, {}); + if (!this.#subscribers.has(subscriber)) { + this.#subscribers.set(subscriber, {}); } - const subscription = this.subscribers.get(subscriber); + const subscription = this.#subscribers.get(subscriber); subscription[type] = handler; } @@ -37,7 +37,7 @@ export class EventEmitter { * @param {string} type */ unsubscribe(subscriber, type) { - const subscription = this.subscribers.has(subscriber) ? this.subscribers.get(subscriber) : null; + const subscription = this.#subscribers.has(subscriber) ? this.#subscribers.get(subscriber) : null; if (subscription && subscription.hasOwnProperty(type)) { delete subscription[type]; } diff --git a/www/src/lib/network/cmd.js b/www/src/lib/network/cmd.js index 98c71be..75b7cb8 100644 --- a/www/src/lib/network/cmd.js +++ b/www/src/lib/network/cmd.js @@ -4,4 +4,7 @@ export const SystemPacketType = { RESPONSE_STRING: 0xf0, RESPONSE_BINARY: 0xf1, + + GET_CONFIG: 0xfa, + RESTART: 0xfb, }; \ No newline at end of file diff --git a/www/src/lib/network/packet.js b/www/src/lib/network/packet.js index 9c2f275..5fdc3cb 100644 --- a/www/src/lib/network/packet.js +++ b/www/src/lib/network/packet.js @@ -15,7 +15,7 @@ export class Packet { packet.signature = parser.readUint16(); packet.requestId = parser.readUint16(); packet.type = parser.readUint8(); - packet.size = parser.readUint8(); + packet.size = parser.readUint16(); packet.data = new Uint8Array(buffer, parser.position); return packet; diff --git a/www/src/lib/network/ws.js b/www/src/lib/network/ws.js index cc273e1..1025745 100644 --- a/www/src/lib/network/ws.js +++ b/www/src/lib/network/ws.js @@ -130,9 +130,10 @@ export class WebSocketInteraction extends EventEmitter { if (buffer) { if (buffer.byteLength > 255) throw new Error("Request payload too long!"); - this.#ws.send(Uint8Array.of(...this.config.requestSignature, ...requestIdBytes, cmd, buffer.byteLength, ...new Uint8Array(buffer))); + const requestLengthBytes = new Uint8Array(Uint16Array.of(buffer.byteLength).buffer); + this.#ws.send(Uint8Array.of(...this.config.requestSignature, ...requestIdBytes, cmd, ...requestLengthBytes, ...new Uint8Array(buffer))); } else { - this.#ws.send(Uint8Array.of(...this.config.requestSignature, ...requestIdBytes, cmd, 0x00)); + this.#ws.send(Uint8Array.of(...this.config.requestSignature, ...requestIdBytes, cmd, 0x00, 0x00)); } return new Promise((resolve, reject) => { diff --git a/www/src/lib/style.css b/www/src/lib/style.css index 3c49aa6..1c9e5e0 100644 --- a/www/src/lib/style.css +++ b/www/src/lib/style.css @@ -179,6 +179,8 @@ h4 { margin: 0; width: 2rem; height: 2rem; + + user-select: none; } .section > .lock::after, @@ -423,6 +425,18 @@ input[type="color"]::-webkit-color-swatch { color: transparent; } +.label[data-loading=true] { + height: 1.5rem; + border-radius: 0.25rem; + + pointer-events: none; + background-image: linear-gradient(90deg, var(--control-color-1), var(--control-color-2), var(--control-color-1)); + background-color: var(--control-color-1); + background-repeat: no-repeat; + background-size: 90% 100%; + animation: background-shift linear infinite 2s; +} + p { font-size: 1rem; font-weight: 300; @@ -452,9 +466,6 @@ hr { color: white; background: var(--control-gradient-accent); - - pointer-events: none; - user-select: none; } :not(.m-top, .section-title) + .m-top { diff --git a/www/src/lib/utils/common.js b/www/src/lib/utils/common.js new file mode 100644 index 0000000..287312d --- /dev/null +++ b/www/src/lib/utils/common.js @@ -0,0 +1,52 @@ +function _formatUnit(value, unit, unitsConfig, fractionDigits) { + let sizeUnit = ""; + for (let i = 0; i < unitsConfig.length; i++) { + if (value >= unitsConfig[i].exp) { + value /= unitsConfig[i].exp; + sizeUnit = unitsConfig[i].unit; + break; + } + } + + sizeUnit = formatUnitSuffix(fractionDigits > 0 ? value : Math.round(value), sizeUnit); + return `${value.toFixed(fractionDigits)} ${sizeUnit}${unit}`; +} + +export function formatUnitSuffix(value, unit) { + if (unit instanceof Array) { + if (Math.abs(value) === 1) { + return unit[0]; + } else { + return unit[1]; + } + } + + return unit; +} + +export function formatUnit(value, unit, fractionDigits = 2, exp = 1000) { + const units = [ + {unit: "T", exp: Math.pow(exp, 4)}, + {unit: "G", exp: Math.pow(exp, 3)}, + {unit: "M", exp: Math.pow(exp, 2)}, + {unit: "K", exp: exp}, + ] + + return _formatUnit(value, unit, units, fractionDigits); +} + +export function formatByteSize(size) { + return formatUnit(size, "B", 2, 1024) +} + +export function formatTimeSpan(ms, fractionDigits = 0) { + const units = [ + {unit: ["day", "days"], exp: 60 * 60 * 24 * 1000}, + {unit: ["hour", "hours"], exp: 60 * 60 * 1000}, + {unit: ["minute", "minutes"], exp: 60 * 1000}, + {unit: ["second", "seconds"], exp: 1000}, + {unit: "ms", exp: 1}, + ] + + return _formatUnit(ms, "", units, fractionDigits); +} \ No newline at end of file