From fb7a218f75fc76f06df092e2591e737df4c6b328 Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Tue, 16 Jan 2024 12:18:34 +0900 Subject: [PATCH 1/5] Reimplement button control use new ESPAsyncButton library for button handling implement event-based button<>lamp communication implement UI configuration for button, gpio, logic level, etc... implement UI configuration for button events, i.e. mapping actions to button events --- platformio.ini | 4 +- src/bencoder.cpp | 145 ++++++++++++++++ src/bencoder.hpp | 91 ++++++++++ src/buttons.cpp | 349 -------------------------------------- src/buttons.h | 136 --------------- src/char_const.h | 45 +++-- src/devices.cpp | 87 +++++++++- src/devices.h | 10 +- src/evtloop.cpp | 2 +- src/evtloop.h | 3 +- src/interface.cpp | 158 +++++++++++++++-- src/interface.h | 20 ++- src/interface_actions.cpp | 32 ++++ src/lamp.cpp | 3 + src/main.cpp | 4 +- src/main.h | 7 - 16 files changed, 555 insertions(+), 541 deletions(-) create mode 100644 src/bencoder.cpp create mode 100644 src/bencoder.hpp delete mode 100644 src/buttons.cpp delete mode 100644 src/buttons.h diff --git a/platformio.ini b/platformio.ini index f08e9037..70b81250 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,7 +36,7 @@ build_flags = [libs] common = https://github.com/DmytroKorniienko/DFRobotDFPlayerMini - GyverLibs/GyverButton@3.8 + ;GyverLibs/GyverButton@3.8 mrfaptastic/ESP32 HUB75 LED MATRIX PANEL DMA Display@3.0 https://github.com/toblum/TetrisAnimation ;GyverLibs/microDS18B20@3.10 @@ -45,6 +45,8 @@ vortigont = https://github.com/vortigont/EmbUI#v3.1 https://github.com/vortigont/LedFB https://github.com/vortigont/TM1637 + https://github.com/vortigont/ESPAsyncButton + [env] framework = arduino diff --git a/src/bencoder.cpp b/src/bencoder.cpp new file mode 100644 index 00000000..52ecc304 --- /dev/null +++ b/src/bencoder.cpp @@ -0,0 +1,145 @@ +/* + This file is a part of FireLamp_JeeUI project + https://github.com/vortigont/FireLamp_JeeUI + + Copyright © 2023 Emil Muratov (vortigont) + + FireLamp_JeeUI is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FireLamp_JeeUI is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FireLamp_JeeUI. If not, see . + + (Этот файл — часть FireLamp_JeeUI. + + FireLamp_JeeUI - свободная программа: вы можете перераспространять ее и/или + изменять ее на условиях Стандартной общественной лицензии GNU в том виде, + в каком она была опубликована Фондом свободного программного обеспечения; + либо версии 3 лицензии, либо (по вашему выбору) любой более поздней + версии. + + FireLamp_JeeUI распространяется в надежде, что она будет полезной, + но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА + или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Стандартной + общественной лицензии GNU. + + Вы должны были получить копию Стандартной общественной лицензии GNU + вместе с этой программой. Если это не так, см. + .) +*/ + +// this file contains implementation for Button/Encoder control devices bound to event bus +#include "Arduino.h" +#include "bencoder.hpp" +#include "embuifs.hpp" +#include "traits.hpp" +#include "char_const.h" +#include "constants.h" +#include "log.h" + + +void ButtonEventHandler::subscribe(esp_event_loop_handle_t loop){ + _loop = loop; + + // Register the handler for task iteration event; need to pass instance handle for later unregistration. + ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), LAMP_CHANGE_EVENTS, ESP_EVENT_ANY_ID, ButtonEventHandler::event_hndlr, this, &_lmp_einstance)); + + ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), EBTN_EVENTS, ESP_EVENT_ANY_ID, ButtonEventHandler::event_hndlr, this, &_btn_einstance)); +} + +void ButtonEventHandler::unsubscribe(){ + ESP_ERROR_CHECK(esp_event_handler_instance_unregister_with(_loop, LAMP_CHANGE_EVENTS, ESP_EVENT_ANY_ID, _lmp_einstance)); + ESP_ERROR_CHECK(esp_event_handler_instance_unregister_with(_loop, EBTN_EVENTS, ESP_EVENT_ANY_ID, _btn_einstance)); +}; + +void ButtonEventHandler::event_hndlr(void* handler, esp_event_base_t base, int32_t id, void* event_data){ + //LOG(printf, "ButtonEventHandler::event_hndlr %s:%d\n", base, id); + if (base == EBTN_EVENTS) + return static_cast(handler)->_btnEventHandler(ESPButton::int2event_t(id), reinterpret_cast(event_data)); + + if ( base == LAMP_CHANGE_EVENTS ) + return static_cast(handler)->_lmpEventHandler(base, id, event_data); + +} + + +void ButtonEventHandler::_btnEventHandler(ESPButton::event_t e, const EventMsg* msg){ + LOG(printf, "Button EventID:%u gpio:%d, ctr:%u\n", e, msg->gpio, msg->cntr); + + // static event longRelease will toggle brightness control direction + // I do not like it, maybe will rework it later somehow + if (e == ESPButton::event_t::longRelease){ + _brightness_direction *= -1; + return; + } + + for (auto &it : _event_map ){ + LOG(printf, "Lookup event: it_en:%u, it.e:%u, e:%u ilp:%u lp:%u\n", it.enabled, it.e, e, it.lamppwr, _lamp_pwr ); + if ( it.enabled && (it.e == e) && (it.lamppwr == _lamp_pwr) ){ + // check for multiclicks + if (e == ESPButton::event_t::multiClick && msg->cntr != it.clicks) + continue; + + // event matches + LOG(printf, "BTN Execute Event:%u\n", e2int( it.evt_lamp ) ); + + switch (it.evt_lamp){ + case evt::lamp_t::effSwitchTo: + EVT_POST_DATA(LAMP_SET_EVENTS, e2int(it.evt_lamp), &it.arg, sizeof(it.arg)); + break; + case evt::lamp_t::brightness_step: { + int step = it.arg * _brightness_direction; + EVT_POST_DATA(LAMP_SET_EVENTS, e2int(it.evt_lamp), &step, sizeof(int)); + break; + } + default: + EVT_POST(LAMP_SET_EVENTS, e2int(it.evt_lamp)); + } + return; + } + } +} + +void ButtonEventHandler::_lmpEventHandler(esp_event_base_t base, int32_t id, void* data){ + switch (static_cast(id)){ + // Power control + case evt::lamp_t::pwron : + _lamp_pwr = true; + break; + case evt::lamp_t::pwroff : + _lamp_pwr = false; + break; + } +} + +void ButtonEventHandler::load(JsonVariantConst cfg){ + JsonArrayConst array = cfg.as(); + if (!array){ + return; // bad document + } + + _event_map.clear(); + + for(JsonVariantConst v : array) { + LOG(printf, "Add cfg Event:%u\n", v[T_btn_event].as() ); + _event_map.emplace_back(ButtonAction(static_cast(v[T_btn_event].as()), static_cast(v[T_lamp_event].as()), v[T_clicks], v[T_arg], v[T_enabled], v[T_pwr] )); + //Serial.println(v.as()); + } + +} + +void ButtonEventHandler::save(){ + +} + + + + + diff --git a/src/bencoder.hpp b/src/bencoder.hpp new file mode 100644 index 00000000..14f510bd --- /dev/null +++ b/src/bencoder.hpp @@ -0,0 +1,91 @@ +/* + This file is a part of FireLamp_JeeUI project + https://github.com/vortigont/FireLamp_JeeUI + + Copyright © 2023 Emil Muratov (vortigont) + + FireLamp_JeeUI is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FireLamp_JeeUI is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FireLamp_JeeUI. If not, see . + + (Этот файл — часть FireLamp_JeeUI. + + FireLamp_JeeUI - свободная программа: вы можете перераспространять ее и/или + изменять ее на условиях Стандартной общественной лицензии GNU в том виде, + в каком она была опубликована Фондом свободного программного обеспечения; + либо версии 3 лицензии, либо (по вашему выбору) любой более поздней + версии. + + FireLamp_JeeUI распространяется в надежде, что она будет полезной, + но БЕЗО ВСЯКИХ ГАРАНТИЙ; даже без неявной гарантии ТОВАРНОГО ВИДА + или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННЫХ ЦЕЛЕЙ. Подробнее см. в Стандартной + общественной лицензии GNU. + + Вы должны были получить копию Стандартной общественной лицензии GNU + вместе с этой программой. Если это не так, см. + .) +*/ + +// this file contains implementation for Button/Encoder control devices bound to event bus + +#pragma once +#include "espasyncbutton.hpp" +#include "evtloop.h" +#include "ArduinoJson.h" + +#define BTN_EVENTS_CFG_JSIZE 4096 + +struct ButtonAction { + ESPButton::event_t e; + evt::lamp_t evt_lamp; + int32_t clicks; + int32_t arg; + bool enabled; + // lamp power state + bool lamppwr; + ButtonAction(ESPButton::event_t e, evt::lamp_t evt_lamp, int32_t clicks = 0, int32_t arg = 0, bool enabled = true, bool lamppwr = true) : e(e), evt_lamp(evt_lamp), clicks(clicks), arg(arg), enabled(enabled), lamppwr(lamppwr) {} +}; + +class ButtonEventHandler { + + esp_event_loop_handle_t _loop; + // button event instance + esp_event_handler_instance_t _btn_einstance = nullptr; + // lamp state events instance + esp_event_handler_instance_t _lmp_einstance = nullptr; + + std::list _event_map; + + // lamp power state + bool _lamp_pwr = false; + // incr/decr multiplicator + int _brightness_direction = 1; + + static void event_hndlr(void* handler, esp_event_base_t base, int32_t id, void* event_data); + + void _btnEventHandler(ESPButton::event_t e, const EventMsg* msg); + void _lmpEventHandler(esp_event_base_t base, int32_t id, void* data); + +public: + ~ButtonEventHandler(){ unsubscribe(); } + + void subscribe( esp_event_loop_handle_t loop ); + + void unsubscribe(); + + + void load(JsonVariantConst cfg); + + void save(); +}; + + diff --git a/src/buttons.cpp b/src/buttons.cpp deleted file mode 100644 index e89ce192..00000000 --- a/src/buttons.cpp +++ /dev/null @@ -1,349 +0,0 @@ -#include "lamp.h" -#include "buttons.h" -#include "actions.hpp" -#include "evtloop.h" - -const char *btn_get_desc(BA action){ - switch (action) { - case BA_BRIGHT: return PSTR("BRIGHT"); - case BA_SPEED: return PSTR("SPEED"); - case BA_SCALE: return PSTR("SCALE"); - case BA_ON: return PSTR("ON"); - case BA_OFF: return PSTR("OFF"); - case BA_DEMO: return PSTR("DEMO"); - case BA_AUX_TOGLE: return PSTR("AUX"); - case BA_OTA: return PSTR("OTA"); - case BA_EFF_NEXT: return PSTR("NEXT"); - case BA_EFF_PREV: return PSTR("PREV"); - case BA_SEND_TIME: return PSTR("TIME"); - case BA_SEND_IP: return PSTR("IP"); - case BA_WHITE_HI: return PSTR("WHITE HI"); - case BA_WHITE_LO: return PSTR("WHITE LO"); - case BA_WIFI_REC: return PSTR("WIFI REC"); - case BA_EFFECT: return PSTR("EFFECT"); - default:; - } - return PSTR(""); -} - -Task *tReverseTimeout = nullptr; // задержка переключения направления -bool Button::activate(btnflags& flg, bool reverse){ - uint8_t newval; - //RA ract = RA_UNKNOWN; - ra act = ra::end; // for transition period, let's make it a new var - bool ret = false; - if (reverse) flags.direction = !flags.direction; - switch (action) { - case BA_BRIGHT: - newval = constrain(myLamp.getBrightness() + (myLamp.getBrightness() / 25 + 1) * (flags.direction * 2 - 1), 1 , 255); - if ((newval == 1 || newval == 255) && tReverseTimeout==nullptr){ - tReverseTimeout = new Task(2 * TASK_SECOND, TASK_ONCE, - [this](){ flags.direction = !flags.direction; LOG(println,"reverse"); }, - &ts, false, nullptr, [](){ tReverseTimeout = nullptr;}, true - ); - tReverseTimeout->enableDelayed(); - } - // if (myLamp.getGaugeType()!=GAUGETYPE::GT_NONE){ - // GAUGE::GaugeShow(newval, 255, 10); - // } - run_action(ra::brt_nofade, newval); // change brightness without fade effect - return true; - case BA_SPEED: { - byte speed = (myLamp.effwrkr.getControls()[1]->getVal()).toInt(); - newval = constrain( speed + (speed / 25 + 1) * (flags.direction * 2 - 1), 1 , 255); - if ((newval == 1 || newval == 255) && tReverseTimeout==nullptr){ - tReverseTimeout = new Task(2 * TASK_SECOND, TASK_ONCE, - [this](){ flags.direction = !flags.direction; LOG(println,"reverse"); }, - &ts, false, nullptr, [](){ tReverseTimeout = nullptr;}, true - ); - tReverseTimeout->enableDelayed(); - } - /* if (myLamp.getGaugeType()!=GAUGETYPE::GT_NONE){ - GAUGE::GaugeShow(newval, 255, 100); - }*/ - run_action(String(T_effect_dynCtrl)+1, newval); - return true; - } - case BA_SCALE: { - byte scale = (myLamp.effwrkr.getControls()[2]->getVal()).toInt(); - newval = constrain(scale + (scale / 25 + 1) * (flags.direction * 2 - 1), 1 , 255); - if ((newval == 1 || newval == 255) && tReverseTimeout==nullptr){ - tReverseTimeout = new Task(2 * TASK_SECOND, TASK_ONCE, - [this](){ flags.direction = !flags.direction; LOG(println,"reverse"); }, - &ts, false, nullptr, [](){ tReverseTimeout = nullptr; }, true - ); - tReverseTimeout->enableDelayed(); - } - /* if (myLamp.getGaugeType()!=GAUGETYPE::GT_NONE){ - GAUGE::GaugeShow(newval, 255, 150); - }*/ - run_action(String(T_effect_dynCtrl)+2, newval); - return true; - } - default:; - } - - if((flg.onetime&2)) return ret; // если не установлен бит сработавшего однократного события - выходим - - // проверяем дальше - switch (action) { - case BA_ON: - EVT_POST(LAMP_SET_EVENTS, e2int(evt::lamp_t::pwron)); - return ret; - case BA_OFF: - EVT_POST(LAMP_SET_EVENTS, e2int(evt::lamp_t::pwron)); - return ret; - case BA_DEMO: run_action(ra::demo, !param.isEmpty()); return ret; - case BA_AUX_TOGLE: run_action(ra::aux_flip); return ret; // set AUX pin - case BA_EFF_NEXT: run_action(ra::eff_next); return ret; - case BA_EFF_PREV: run_action(ra::eff_prev); return ret; - //case BA_SEND_TIME: myLamp.showTimeOnScreen(NULL); return ret; // show time on screen - //case BA_SEND_IP: ract = RA_SEND_IP; break; - //case BA_WIFI_REC: ract = RA_WIFI_REC; break; - case BA_EFFECT: { run_action(ra::eff_switch, param.toInt()); return ret; } - default:; - } - - //LOG(printf_P,PSTR("Button send action: %d\n"), act != ra::end ? static_cast(act) : static_cast(ract)); - if (act != ra::end) return ret; // уже отработали, выходим -/* - if(param.isEmpty()) - remote_action(ract, NULL); - else - remote_action(ract, param.c_str(), NULL); -*/ - return ret; -} - -String Button::getName(){ - String buffer; - buffer.concat(flags.on? "ON: " : "OFF: "); - if (flags.hold) { - if (flags.click) { - buffer.concat(String(flags.click)); - buffer.concat(" Click + "); - } - buffer.concat("HOLD - "); - } else - if (flags.click) { - buffer.concat(String(flags.click)); - buffer.concat(" Click - "); - } - - buffer.concat(btn_get_desc(action)); - - return buffer; -}; - -Buttons::Buttons(uint8_t _pin, uint8_t _pullmode, uint8_t _state): buttons(), touch(_pin, _pullmode, _state){ - pin = _pin; - pullmode = _pullmode; - state = _state; - holding = false; - holded = false; - buttonEnabled = true; // кнопка обрабатывается если true, пока что обрабатывается всегда :) - pinTransition = true; - onoffLampState = myLamp.isLampOn(); - - clicks = 0; - - if (pullmode == LOW_PULL) - pinMode(pin, INPUT); - else - pinMode(pin, INPUT_PULLUP); - - touch.setType(pullmode); - touch.setTickMode(MANUAL); // мы сами говорим когда опрашивать пин - touch.setStepTimeout(BUTTON_STEP_TIMEOUT); - touch.setClickTimeout(BUTTON_CLICK_TIMEOUT); - touch.setTimeout(BUTTON_TIMEOUT); - touch.setDebounce(BUTTON_DEBOUNCE); // т.к. работаем с прерываниями, может пригодиться для железной кнопки - touch.resetStates(); - - attachInterrupt(pin, std::bind(&Buttons::isrPress,this), pullmode!=LOW_PULL ? RISING : FALLING ); - isrEnable(); -} - -void Buttons::buttonTick(){ - if (!buttonEnabled) return; - - touch.tick(); - bool reverse = false; - - if ((holding = touch.isHolded())) { - // начало удержания кнопки - byte tstclicks = touch.getHoldClicks(); - if(!tClicksClear || (tstclicks && tstclicks!=clicks)) // нажатия после удержания не сбрасываем!!! они сбросятся по tClicksClear или по смене кол-ва нажатий до удержания - clicks=tstclicks; - if(!tClicksClear){ - tClicksClear = new Task(NUMHOLD_TIME, TASK_ONCE, [this](){ holded = true; clicks=0; }, &ts, false, nullptr, [this](){tClicksClear=nullptr;}, true); - tClicksClear->enableDelayed(); - } - onoffLampState = myLamp.isLampOn(); // получить статус на начало удержания - reverse = true; - if(tReverseTimeout){ // сброс реверса, если он включен - LOG(println,"reverce canceled"); - tReverseTimeout->cancel(); - } - LOG(printf_P, PSTR("start hold - buttonEnabled=%d, onoffLampState=%d, holding=%d, holded=%d, clicks=%d, reverse=%d\n"), buttonEnabled, onoffLampState, holding, holded, clicks, reverse); - } else if ((holding = touch.isStep())) { - // кнопка удерживается - if(tClicksClear) - tClicksClear->restartDelayed(); // отсрочиваем сброс нажатий - } else if (!touch.hasClicks() || !(clicks = touch.getClicks())) { - if( (!touch.isHold() && holded) ) { // кнопку уже не трогают - LOG(println,"Сброс состояния кнопки после окончания удержания"); - resetStates(); - onoffLampState = myLamp.isLampOn(); // сменить статус после удержания - LOG(printf_P, PSTR("reset - buttonEnabled=%d, onoffLampState=%d, holding=%d, holded=%d, clicks=%d, reverse=%d\n"), buttonEnabled, onoffLampState, holding, holded, clicks, reverse); - for (unsigned i = 0; i < buttons.size(); i++) { - buttons[i]->flags.onetime&=1; - } - isrEnable(); // переключение на ленивый опрос - } - // здесь баг, этот выход часто перехватывает "одиночные" нажатия и превращает их в "клик" - return; - } - - if (myLamp.isAlarm()) { - // нажатие во время будильника - // ALARMTASK::stopAlarm(); - return; - } - - if(!holding){ - onoffLampState=myLamp.isLampOn(); // обновить статус, если не удерживается и это однократное нажатие - LOG(printf_P, PSTR("onetime click - buttonEnabled=%d, onoffLampState=%d, holding=%d, holded=%d, clicks=%d, reverse=%d\n"), buttonEnabled, onoffLampState, holding, holded, clicks, reverse); - } - - Button btn(onoffLampState, holding, clicks, true); // myLamp.isLampOn() - анализироваться будет состояние на начало нажимания кнопки - for (unsigned i = 0; i < buttons.size(); i++) { - if (btn == *buttons[i]) { - //if(buttons[i]->action==1) continue; // отладка, отключить действие увеличения яркости - if (!buttons[i]->activate(buttons[i]->flags, reverse)) { - //LOG(println,buttons[i]->action); // отладка - // действие не подразумевает повтора - if(buttons[i]->flags.onetime && touch.isHold()){ // в процессе удержания - buttons[i]->flags.onetime|=3; // установить старший бит сработавшего действия - } - } - // break; // Не выходим после первого найденного совпадения. Можем делать макросы из нажатий - } - } - - // Здесь уже все отработало, и кнопка точно не удерживается - if(!holding){ - LOG(println,"Сброс состояния кнопки"); - resetStates(); - onoffLampState = myLamp.isLampOn(); // обновить статус по итогу работы - isrEnable(); - } -} - -void Buttons::clear() { - while (buttons.size()) { - Button *btn = buttons.shift(); - delete btn; - } -} - -int Buttons::loadConfig(const char *cfg){ - if (LittleFS.begin()) { - File configFile; - if (cfg == nullptr) { - LOG(println, "Load default buttons config file"); - configFile = LittleFS.open("/buttons_config.json", "r"); // PSTR("r") использовать нельзя, будет исключение! - } else { - LOG(printf_P, PSTR("Load %s buttons config file\n"), cfg); - configFile = LittleFS.open(cfg, "r"); // PSTR("r") использовать нельзя, будет исключение! - } - String cfg_str = configFile.readString(); - if (cfg_str.isEmpty()){ - LOG(println, "Failed to open buttons config file"); - return 0; - } - - DynamicJsonDocument doc(2048); - DeserializationError error = deserializeJson(doc, cfg_str); - if (error) { - LOG(print, "deserializeJson error: "); - LOG(println, error.code()); - LOG(println, cfg_str); - return 0; - } - JsonArray arr = doc.as(); - for (size_t i = 0; i < arr.size(); i++) { - JsonObject item = arr[i]; - uint8_t mask = item["flg"].as(); - BA ac = (BA)item["ac"].as(); - if(item.containsKey("p")){ - String param = item["p"].as(); - buttons.add(new Button(mask, ac, param)); - } else { - buttons.add(new Button(mask, ac)); - } - } - doc.clear(); - } - return 1; -} - -void Buttons::saveConfig(const char *cfg){ - if (LittleFS.begin()) { - File configFile; - if (cfg == nullptr) { - LOG(println, "Save default buttons config file"); - configFile = LittleFS.open("/buttons_config.json", "w"); // PSTR("w") использовать нельзя, будет исключение! - } else { - LOG(printf_P, PSTR("Save %s buttons config file\n"), cfg); - configFile = LittleFS.open(cfg, "w"); // PSTR("w") использовать нельзя, будет исключение! - } - configFile.print("["); - - for (unsigned i = 0; i < buttons.size(); i++) { - Button *btn = buttons[i]; - configFile.printf_P(PSTR("%s{\"flg\":%u,\"ac\":%u,\"p\":\"%s\"}"), - (char*)(i? "," : ""), btn->flags.mask, btn->action, btn->getParam().c_str() - ); - LOG(printf_P, PSTR("%s{\"flg\":%u,\"ac\":%u,\"p\":\"%s\"}"), - (char*)(i? "," : ""), btn->flags.mask, btn->action, btn->getParam().c_str() - ); - } - configFile.print("]"); - configFile.flush(); - configFile.close(); - } -} - -void IRAM_ATTR Buttons::isrPress() { - detachInterrupt(pin); - // какой нерюх создает новый объект в ISR??? - // todo: убрать - if(tButton) - tButton->restartDelayed(); - else - tButton = new Task(20, TASK_FOREVER, std::bind(&Buttons::buttonTick, this), &ts, true, nullptr, [this](){ tButton=nullptr; }, true); // переключение в режим удержания кнопки -} - -void Buttons::isrEnable(){ - LOG(println,"Button switch to isr"); - attachInterrupt(pin, std::bind(&Buttons::isrPress,this), pullmode==LOW_PULL ? RISING : FALLING ); - if(tButton) - tButton->restartDelayed(); - else - tButton = new Task(TASK_SECOND, 5, std::bind(&Buttons::buttonTick, this), &ts, true, nullptr, [this](){ tButton=nullptr;}, true); // "ленивый" опрос 1 раз в сек в течение 5 секунд -} - -void Buttons::setButtonOn(bool flag) { - buttonEnabled = flag; - resetStates(); - if (flag){ - LOG(println,"Button watch enabled"); - isrEnable(); - } else { - detachInterrupt(pin); - if(tButton) - tButton->cancel(); - LOG(println,"Button watch disabled"); - } -} diff --git a/src/buttons.h b/src/buttons.h deleted file mode 100644 index 34692b83..00000000 --- a/src/buttons.h +++ /dev/null @@ -1,136 +0,0 @@ -#pragma once -#include "GyverButton.h" -#include "config.h" // подключаем эффекты, там же их настройки -#include -#include "ts.h" -#include "LList.h" - -#ifndef BUTTON_DEBOUNCE -#define BUTTON_DEBOUNCE (30U) // Button debounce time, ms -#endif -#ifndef PULL_MODE -#define PULL_MODE (LOW_PULL) // подтяжка кнопки к нулю (для сенсорных кнопок на TP223) - LOW_PULL, подтяжка кнопки к питанию (для механических кнопок НО, на массу) - HIGH_PULL -#endif -#ifndef BUTTON_STEP_TIMEOUT -#define BUTTON_STEP_TIMEOUT (75U) // каждые BUTTON_STEP_TIMEOUT мс будет генерироваться событие удержания кнопки (для регулировки яркости) -#endif -#ifndef BUTTON_CLICK_TIMEOUT -#define BUTTON_CLICK_TIMEOUT (500U) // максимальное время между нажатиями кнопки в мс, до достижения которого считается серия последовательных нажатий -#endif -#ifndef BUTTON_TIMEOUT -#define BUTTON_TIMEOUT (500U) // с какого момента начинает считаться, что кнопка удерживается в мс -#endif - -typedef enum _button_action { - BA_NONE, - BA_BRIGHT, - BA_SPEED, - BA_SCALE, - BA_ON, - BA_OFF, - BA_DEMO, - BA_AUX_TOGLE, - BA_OTA, - BA_EFF_NEXT, - BA_EFF_PREV, - BA_SEND_TIME, - BA_SEND_IP, - BA_WHITE_HI, - BA_WHITE_LO, - BA_WIFI_REC, - BA_EFFECT, - BA_END // признак конца enum -} BA; - -const char *btn_get_desc(BA action); - -class Button{ - typedef union _bflags { - uint8_t mask; - struct { - uint8_t on:1; - uint8_t hold:1; - uint8_t click:3; - uint8_t onetime:2; // признак однократного срабатывания при удержании и старший бит - то что срабатывание уже было - uint8_t direction:1; // направление изменения - }; - } btnflags; - - friend bool operator== (const Button &f1, const Button &f2) { return ((f1.flags.mask&0x1F) == (f2.flags.mask&0x1F)); } - friend bool operator!= (const Button &f1, const Button &f2) { return ((f1.flags.mask&0x1F) != (f2.flags.mask&0x1F)); } - - public: - Button(bool on, bool hold, uint8_t click, bool onetime, BA act = BA_NONE, const String& _param=String()) { - flags.direction = false; flags.mask = 0; flags.on = on; flags.hold = hold; flags.click = click; flags.onetime=onetime; action = act; param=_param; - } - Button(uint8_t mask, BA act = BA_NONE, const String& _param=String()) { - flags.direction = false; flags.mask = mask; action = act; param=_param; - } - bool activate(btnflags& flg, bool reverse); - String getName(); - const String& getParam() {return param;} - void setParam(const String&_param) {param=_param;} - - BA action; - btnflags flags; - private: - String param; -}; - -class Buttons { - private: - #pragma pack(push,4) - union { - struct { - bool buttonEnabled:1; // кнопка обрабатывается если true - bool holding:1; // кнопка удерживается - bool holded:1; // кнопка удерживалась (touch.isHolded() можно проверить только однократно!!!) - bool pinTransition:1; // ловим "нажатие" кнопки - bool onoffLampState:1; - }; - uint8_t btnflags = 0; // очистим флаги - }; - #pragma pack(pop) - uint8_t pin; // пин - uint8_t pullmode; // подтяжка - uint8_t state; // тип (нормально открытый/закрытый) - - byte clicks = 0; - Task *tButton = nullptr; // планировщик кнопки - Task *tClicksClear = nullptr; // очистка кол-ва нажатий, после таймаута - LList buttons; - - void resetStates() { clicks=0; holding=false; holded=false; touch.resetStates();} - - void isrPress(); - void isrEnable(); // enable "press" interrupt - void IRAM_ATTR isrRelease(); - - public: - bool getpinTransition() { return pinTransition; } - void setpinTransition(bool val) { pinTransition = val; } - int getPressTransitionType() {return pullmode==LOW_PULL ? RISING : FALLING;} - int getReleaseTransitionType() {return pullmode!=LOW_PULL ? RISING : FALLING;} - - // Enable/Disable button handling - void setButtonOn(bool flag); - bool isButtonOn() { return buttonEnabled; } - - inline Button* operator[](int i) { return buttons[i]; } - - int size(){ return buttons.size(); } - void add(Button *btn) { buttons.add(btn); } - void remove(int i) { buttons.remove(i); } - void clear(); - - - Buttons(uint8_t _pin=BTN_PIN, uint8_t _pullmode=PULL_MODE, uint8_t _state=NORM_OPEN); - - ~Buttons(){ setButtonOn(false); } - - int loadConfig(const char *cfg = nullptr); - void saveConfig(const char *cfg = nullptr); - - GButton touch; - void buttonTick(); // "дергатель" проверки гайвер-кнопки -}; diff --git a/src/char_const.h b/src/char_const.h index 46687e94..0399a7f5 100644 --- a/src/char_const.h +++ b/src/char_const.h @@ -2,13 +2,17 @@ /** набор служебных текстовых констант (не для локализации) */ -static constexpr const char* T_display_type = "dtype"; // LED Display engine type static constexpr const char* T_drawing = "drawing"; static constexpr const char* T_effect_dynCtrl = "eff_dynCtrl"; static constexpr const char* T_gpio = "gpio"; // gpio key for display configuration -static constexpr const char* T_ws2812 = "ws2812"; // ws2812 led stip type +static constexpr const char* T_logicL= "logicL"; // logic level for button + +// Display +static constexpr const char* TCONST_fcfg_display = "/display.json"; +static constexpr const char* T_display_type = "dtype"; // LED Display engine type // ws2812 config var names +static constexpr const char* T_ws2812 = "ws2812"; // ws2812 led stip type static constexpr const char* T_mx_gpio = "mx_gpio"; static constexpr const char* T_CLmt = "CLmt"; // лимит тока static constexpr const char* T_hcnt = "hcnt"; @@ -56,6 +60,26 @@ static constexpr const char* T_tm_lzero = "lzero"; static constexpr const char* T_tm_brt_on = "brtOn"; static constexpr const char* T_tm_brt_off = "brtOff"; +// Button events +static constexpr const char* T_benc_cfg = "/benc.json"; +static constexpr const char* T_btn_cfg = "btn_cfg"; +static constexpr const char* T_btn_event = "btn_event"; +static constexpr const char* T_btn_events = "btn_events"; +static constexpr const char* T_debounce = "debounce"; +static constexpr const char* T_lamp_event = "lamp_event"; +static constexpr const char* T_lamppwr = "lamppwr"; + +// Other +static constexpr const char* T_Active = "Active"; +static constexpr const char* T_arg = "arg"; +static constexpr const char* T_clicks = "clicks"; +static constexpr const char* T_edit = "edit"; +static constexpr const char* T_Enable = "Enable"; +static constexpr const char* T_enabled = "enabled"; +static constexpr const char* T_idx = "idx"; +static constexpr const char* T_pwr = "pwr"; + + static constexpr const char* TCONST_act = "act"; static constexpr const char* TCONST_afS = "afS"; static constexpr const char* TCONST_alarmPT = "alarmPT"; @@ -73,16 +97,8 @@ static constexpr const char* TCONST_bright = "bright"; static constexpr const char* TCONST_Btn = "Btn"; static constexpr const char* TCONST_buttList = "buttList"; static constexpr const char* TCONST_butt_conf = "butt_conf"; -static constexpr const char* TCONST_clicks = "clicks"; static constexpr const char* TCONST_control = "control"; static constexpr const char* TCONST_copy = "copy"; -static constexpr const char* TCONST_d1 = "d1"; -static constexpr const char* TCONST_d2 = "d2"; -static constexpr const char* TCONST_d3 = "d3"; -static constexpr const char* TCONST_d4 = "d4"; -static constexpr const char* TCONST_d5 = "d5"; -static constexpr const char* TCONST_d6 = "d6"; -static constexpr const char* TCONST_d7 = "d7"; static constexpr const char* TCONST_debug = "debug"; static constexpr const char* TCONST_delall = "delall"; static constexpr const char* TCONST_delCfg = "delCfg"; @@ -98,7 +114,6 @@ static constexpr const char* TCONST_ds18b20 = "ds18b20"; static constexpr const char* TCONST_DTimer = "DTimer"; //static constexpr const char* TCONST_edit_lamp_config = "edit_lamp_config"; static constexpr const char* TCONST_edit_text_config = "edit_text_config"; -static constexpr const char* TCONST_edit = "edit"; static constexpr const char* TCONST_eff_config = "eff_config"; static constexpr const char* TCONST_eff_fav = "eff_fav"; static constexpr const char* TCONST_eff_fulllist_json = "/eff_fulllist.json"; // a json serialized full list of effects and it's names for WebUI drop-down list on effects management page @@ -109,7 +124,6 @@ static constexpr const char* TCONST_eff_sel = "eff_sel"; static constexpr const char* TCONST_effHasMic = "effHasMic"; static constexpr const char* TCONST_effListConf = "effListConf"; static constexpr const char* TCONST_effname = "effname"; -static constexpr const char* TCONST_enabled = "enabled"; static constexpr const char* TCONST_encoder = "encoder"; static constexpr const char* TCONST_encTxtCol = "encTxtCol"; static constexpr const char* TCONST_encTxtDel = "encTxtDel"; @@ -123,7 +137,6 @@ static constexpr const char* TCONST_event_conf = "event_conf"; static constexpr const char* TCONST_evList = "evList"; static constexpr const char* TCONST_f_restore_state = "f_rstt"; // Lamp flag "restore state" static constexpr const char* TCONST_fcfg_gpio = "/gpio.json"; -static constexpr const char* TCONST_fcfg_display = "/display.json"; static constexpr const char* TCONST_fileName2 = "fileName2"; static constexpr const char* TCONST_fileName = "fileName"; static constexpr const char* TCONST_fill = "fill"; @@ -141,7 +154,6 @@ static constexpr const char* TCONST_lamptext = "lamptext"; static constexpr const char* TCONST_lamp_config = "lamp_config"; static constexpr const char* TCONST_limitAlarmVolume = "limitAlarmVolume"; static constexpr const char* TCONST_load = "load"; -static constexpr const char* TCONST_lV = "lV"; static constexpr const char* TCONST_Mac = "Mac"; //static constexpr const char* TCONST_main = "main"; static constexpr const char* TCONST_makeidx = "makeidx"; @@ -275,8 +287,9 @@ static constexpr const char* A_effect_dynCtrl = "eff_dynCtrl*"; static constexpr const char* A_display_hub75 = "display_hub75"; // HUB75 display configuration static constexpr const char* A_display_ws2812 = "display_ws2812"; // ws2812 display configuration static constexpr const char* A_display_tm1637 = "*et_display_tm1637"; // get/set tm1637 display configuration - - +static constexpr const char* A_button_gpio = "*et_button_gpio"; // get/set button gpio configuration +static constexpr const char* A_button_evt_edit = "button_evt_edit"; // edit button action form +static constexpr const char* A_button_evt_save = "button_evt_save"; // save/apply button action form static constexpr const char* A_ui_page_effects_config = "effects_config"; diff --git a/src/devices.cpp b/src/devices.cpp index f9127c25..3a462e9f 100644 --- a/src/devices.cpp +++ b/src/devices.cpp @@ -1,7 +1,5 @@ /* Copyright © 2023 Emil Muratov (vortigont) -Copyright © 2020 Dmytro Korniienko (kDn) -JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov This file is part of FireLamp_JeeUI. @@ -44,6 +42,9 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov // TM1637 disaplay class #include "tm1637display.hpp" +// ESPAsyncButton +#include "bencoder.hpp" + // Device object placesholders @@ -51,6 +52,9 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov // TM1637 display https://github.com/vortigont/TM1637/ TMDisplay *tm1637 = nullptr; +// GPIO button +GPIOButton *button = nullptr; +ButtonEventHandler *button_handler = nullptr; void tm1637_setup(){ DynamicJsonDocument doc(DISPLAY_CFG_JSIZE); @@ -61,7 +65,7 @@ void tm1637_setup(){ } void tm1637_configure(JsonVariantConst& tm){ - if (!tm[TCONST_enabled]){ + if (!tm[T_enabled]){ // TM module disabled or config is invalid if (tm1637){ delete tm1637; @@ -87,4 +91,79 @@ void tm1637_configure(JsonVariantConst& tm){ tm1637->clk_lzero = tm[T_tm_lzero]; tm1637->init(); -} \ No newline at end of file +} + +void button_cfg_load(){ + DynamicJsonDocument doc(BTN_EVENTS_CFG_JSIZE); + if (!embuifs::deserializeFile(doc, T_benc_cfg)) return; // config is missing, bad + + JsonVariantConst _cfg( doc[T_btn_cfg] ); + button_configure_gpio(_cfg); + + _cfg = doc[T_btn_events]; + button_configure_events(_cfg); +} + +void button_configure_gpio(JsonVariantConst btn_cfg){ + + if (!btn_cfg[T_enabled]){ + // button disabled or config is invalid + if (button){ + delete button; + button = nullptr; + } + if (button_handler){ + delete button_handler; + button_handler = nullptr; + } + return; + } + + int32_t gpio = btn_cfg[T_gpio] | -1; + if (gpio == -1) return; // pin disabled + bool debn = btn_cfg[T_debounce]; + + // create GPIOButton object + if (!button){ + button = new GPIOButton (static_cast(gpio), btn_cfg[T_logicL]); + if (!button) return; + button->setDebounce(debn); + } else { + if ( gpio != button->getGPIO() ){ + button->setGPIO(static_cast(gpio), btn_cfg[T_logicL]); + button->setDebounce(debn); + } + // button obj already exist, so no need to configure it further + return; + } + + button->enableEvent(ESPButton::event_t::press, false); + button->enableEvent(ESPButton::event_t::release, false); + + button->enableEvent(ESPButton::event_t::longPress); + button->enableEvent(ESPButton::event_t::longRelease); + button->enableEvent(ESPButton::event_t::autoRepeat); + button->enableEvent(ESPButton::event_t::multiClick); + + // set event loop to post events to + ESPButton::set_event_loop_hndlr(evt::get_hndlr()); + + button->enable(); + Serial.print("Button enabled\n"); +} + +void button_configure_events(JsonVariantConst btn_cfg){ + if (!button_handler){ + button_handler = new ButtonEventHandler(); + if (!button_handler) return; + // subscribe only once, when object is newly created + button_handler->subscribe(evt::get_hndlr()); + } + + button_handler->load(btn_cfg); // load config +} + + + + + diff --git a/src/devices.h b/src/devices.h index 78783fe4..a993978b 100644 --- a/src/devices.h +++ b/src/devices.h @@ -1,7 +1,5 @@ /* Copyright © 2023 Emil Muratov (vortigont) -Copyright © 2020 Dmytro Korniienko (kDn) -JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov This file is part of FireLamp_JeeUI. @@ -60,4 +58,10 @@ void tm1637_setup(); * * @param tm JsonObject with configuration params */ -void tm1637_configure(JsonVariantConst& tm); \ No newline at end of file +void tm1637_configure(JsonVariantConst& tm); + +void button_cfg_load(); + +void button_configure_gpio(JsonVariantConst btn_cfg); + +void button_configure_events(JsonVariantConst btn_cfg); diff --git a/src/evtloop.cpp b/src/evtloop.cpp index c3d1d14e..56fb5682 100644 --- a/src/evtloop.cpp +++ b/src/evtloop.cpp @@ -92,7 +92,7 @@ void debug(){ } void debug_hndlr(void* handler_args, esp_event_base_t base, int32_t id, void* event_data){ - Serial.printf("evt tracker: %s id:%d\n", base, id); + Serial.printf("evt tracker: %s:%d\n", base, id); } diff --git a/src/evtloop.h b/src/evtloop.h index b4cd0083..922b1574 100644 --- a/src/evtloop.h +++ b/src/evtloop.h @@ -67,10 +67,11 @@ enum class lamp_t:int32_t { pwrtoggle, // power toggle // brightness control, parameter value - int - brightness = 20, // set brightness according to current scale, param: unsigned n + brightness = 20, // set brightness according to current scale, param: int n brightness_nofade, // set brightness according to current scale and w/o fade effect brightness_lcurve, // set brightness luma curve brightness_scale, // set brightness scale + brightness_step, // step brightness incr/decr w/o fade, param: int n - step to shift // effects switching effSwitchTo = 30, // switch to specific effect num, param: unsigned n diff --git a/src/interface.cpp b/src/interface.cpp index 0492fcd7..88b76c97 100644 --- a/src/interface.cpp +++ b/src/interface.cpp @@ -176,16 +176,15 @@ void ui_page_selector(Interface *interf, const JsonObject *data, const char* act case page::mike : // страница настроек микрофона show_settings_mic(interf, nullptr, NULL); return; - #ifdef MP3PLAYER + case page::setup_dfplayer : // страница настроек dfplayer show_settings_mp3(interf, nullptr, NULL); return; - #endif // #ifdef MP3PLAYER -/* #ifdef ESP_USE_BUTTON + case page::setup_bttn : // страница настроек кнопки - show_settings_butt(interf, nullptr, NULL); - return; - #endif + return page_button_setup(interf, nullptr, NULL); + +/* #ifdef ENCODER case page::setup_encdr : // страница настроек кнопки show_settings_enc(interf, nullptr, NULL); @@ -306,11 +305,15 @@ void ui_page_setup_devices(Interface *interf, const JsonObject *data, const char // display setup interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_display), TINTF_display_setup); + + // Button configuration + interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_bttn), TINTF_013); + // tm1637 interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_tm1637), TINTF_setup_tm1637); -#ifdef MP3PLAYER + + // MP# player interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_dfplayer), TINTF_099); -#endif interf->json_frame_flush(); } @@ -329,6 +332,134 @@ void ui_page_tm1637_setup(Interface *interf, const JsonObject *data, const char* getset_tm1637(interf, nullptr, NULL); } +/** + * @brief build a page with Button configuration + * it contains a set of controls and options + */ +void page_button_setup(Interface *interf, const JsonObject *data, const char* action){ + interf->json_frame_interface(); + interf->json_section_uidata(); + interf->uidata_pick( "lampui.settings.button" ); + interf->json_frame_flush(); + + // call setter with no data, it will publish existing config values if any + getset_button_gpio(interf, nullptr, NULL); + + DynamicJsonDocument doc(4096); + if (!embuifs::deserializeFile(doc, T_benc_cfg)) return; // config is missing, bad + JsonArray bevents( doc[T_btn_events] ); + + interf->json_frame_interface(); + interf->json_section_begin("button_events_list"); + + int cnt = 0; + for (JsonVariant value : bevents) { + JsonObject obj = value.as(); + interf->json_section_begin(String("sec") + cnt, (const char*)0, false, false, true ); + interf->checkbox(P_EMPTY, obj[T_enabled], "Active"); + interf->checkbox(P_EMPTY, obj[T_pwr], "Pwr On/Off"); + + String s; + switch (obj[T_btn_event].as()){ + case 2: + s = "Click"; + break; + case 3: + s = "Long Press"; + break; + case 5: + s = "Hold repeat"; + break; + case 6: + s = "MultiClick:"; + s += obj[T_clicks].as(); + break; + default: + s = "Unknown"; + } + + interf->constant(s); + + switch (obj[T_lamp_event].as()){ + case 11: + s = "PwrOn"; + break; + case 12: + s = "PwrOff"; + break; + case 13: + s = "Pwr Toggle"; + break; + case 20: + s = "Brightness:"; + s += obj[T_arg].as(); + break; + case 30: + s = "Sw Effect to:"; + s += obj[T_arg].as(); + break; + case 31: + s = "Sw Effect next"; + break; + case 32: + s = "Sw Effect prev"; + break; + case 33: + s = "Sw Effect random"; + break; + default: + s = "Unknown"; + } + + interf->constant(s); + interf->button_value(button_t::generic, A_button_evt_edit, cnt , T_edit); + + interf->json_section_end(); + ++cnt; + } + + interf->json_frame_flush(); +} + +void page_button_evtedit(Interface *interf, const JsonObject *data, const char* action){ + DynamicJsonDocument doc(4096); + if (!embuifs::deserializeFile(doc, T_benc_cfg)) return; + JsonArray bevents( doc[T_btn_events] ); + int idx = (*data)[A_button_evt_edit]; + JsonObject obj = bevents[idx]; + + interf->json_frame_interface(); + interf->json_section_begin("button_events_edit", "Button Event Editor", true); + // side-load button configuration form + interf->json_section_uidata(); + interf->uidata_pick( "lampui.sections.button_event" ); + interf->json_section_end(); + interf->json_frame_flush(); + + // fill the form with values + interf->json_frame_value(obj, true); + interf->value(T_idx, idx); + interf->json_frame_flush(); +} + +void page_button_evt_save(Interface *interf, const JsonObject *data, const char* action){ + DynamicJsonDocument doc(4096); + if (!embuifs::deserializeFile(doc, T_benc_cfg)) doc.clear(); + JsonArray bevents( doc[T_btn_events] ); + int idx = (*data)[T_idx]; + JsonObject obj = idx < bevents.size() ? bevents[idx] : bevents.createNestedObject(); + + // copy keys from post'ed object + for (JsonPair kvp : *data) + obj[kvp.key()] = kvp.value(); + + embuifs::serialize2file(doc, T_benc_cfg); + + button_configure_events(doc[T_btn_events]); + + if (interf) page_button_setup(interf, nullptr, NULL); +} + /** * обработчик установок эффекта */ @@ -1267,9 +1398,7 @@ void user_settings_frame(Interface *interf, const JsonObject *data, const char* // other interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_devices), "Внешние устройства"); interf->button_value(button_t::generic, A_ui_page, e2int(page::mike), TINTF_020); -#ifdef ESP_USE_BUTTON - interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_bttn), TINTF_013); -#endif + #ifdef ENCODER interf->button_value(button_t::generic, A_ui_page, e2int(page::setup_encdr), TINTF_0DC); #endif @@ -1419,9 +1548,7 @@ void default_buttons(){ myButtons->add(new Button(true, true, 1, false, BA::BA_SPEED)); // удержание + 1 клик скорость myButtons->add(new Button(true, true, 2, false, BA::BA_SCALE)); // удержание + 2 клика масштаб } -#endif -#ifdef ESP_USE_BUTTON void load_button_config(const char* path){ if (path){ String filename(TCONST__backup_btn_); @@ -1637,6 +1764,11 @@ void embui_actions_register(){ embui.action.add(A_display_hub75, set_hub75); // Set options for HUB75 panel embui.action.add(A_display_tm1637, getset_tm1637); // get/set tm1637 display configuration + embui.action.add(A_button_gpio, getset_button_gpio); // button setup + embui.action.add(A_button_evt_edit, page_button_evtedit); // button event edit form + embui.action.add(A_button_evt_save, page_button_evt_save); // button save/apply event + + // to be refactored embui.action.add(TCONST_effListConf, set_effects_config_list); diff --git a/src/interface.h b/src/interface.h index c6d76007..fa02236a 100644 --- a/src/interface.h +++ b/src/interface.h @@ -106,17 +106,19 @@ void ui_page_setup_devices(Interface *interf, const JsonObject *data, const char */ void event_publisher(void* handler_args, esp_event_base_t base, int32_t id, void* event_data); - -// ========== -#ifdef ESP_USE_BUTTON -void default_buttons(); - /** - * @brief подгрузить конфигурацию кнопки из стороннего файла - * path should be relative to TCONST__backup_btn_ + * @brief webui page with button configuration + * + * @param interf + * @param data + * @param action */ -void load_button_config(const char* path = NULL); -#endif +void page_button_setup(Interface *interf, const JsonObject *data, const char* action); + +void getset_button_gpio(Interface *interf, const JsonObject *data, const char* action); + + +// ========== void section_effects_frame(Interface *interf, const JsonObject *data, const char* action); diff --git a/src/interface_actions.cpp b/src/interface_actions.cpp index cb9b188e..463bcbe9 100644 --- a/src/interface_actions.cpp +++ b/src/interface_actions.cpp @@ -53,6 +53,7 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov #include #include "traits.hpp" // embui traits #include "evtloop.h" +#include "bencoder.hpp" /** * @brief Set device display brightness @@ -320,6 +321,37 @@ void getset_tm1637(Interface *interf, const JsonObject *data, const char* action if (interf) ui_page_setup_devices(interf, nullptr, NULL); } +void getset_button_gpio(Interface *interf, const JsonObject *data, const char* action){ + { + DynamicJsonDocument doc(BTN_EVENTS_CFG_JSIZE); + if (!embuifs::deserializeFile(doc, T_benc_cfg)) doc.clear(); + + // if this is a request with no data, then just provide existing configuration and quit + if (!data || !(*data).size()){ + if (interf && doc.containsKey(T_btn_cfg)){ + interf->json_frame_value(doc[T_btn_cfg], true); + interf->json_frame_flush(); + } + return; + } + + JsonVariant dst = doc[T_btn_cfg].isNull() ? doc.createNestedObject(T_btn_cfg) : doc[T_btn_cfg]; + + // copy keys to a destination object + for (JsonPair kvp : *data) + dst[kvp.key()] = kvp.value(); + + embuifs::serialize2file(doc, T_benc_cfg); + + JsonVariantConst cfg(dst); + // reconfig button + button_configure_gpio(cfg); + } + + if (interf) ui_page_setup_devices(interf, nullptr, NULL); +} + + // a call-back handler that listens for status change events and publish it to EmbUI feeders void event_publisher(void* handler_args, esp_event_base_t base, int32_t id, void* event_data){ // quit if there are no feeders to notify diff --git a/src/lamp.cpp b/src/lamp.cpp index 5780bf94..699baf32 100644 --- a/src/lamp.cpp +++ b/src/lamp.cpp @@ -627,6 +627,9 @@ void Lamp::_event_picker_cmd(esp_event_base_t base, int32_t id, void* data){ case evt::lamp_t::brightness_lcurve : setLumaCurve(*( (luma::curve*)data) ); break; + case evt::lamp_t::brightness_step : + setBrightness(getBrightness() + *((int*) data), fade_t::off); + break; // Get State Commands diff --git a/src/main.cpp b/src/main.cpp index 674ba738..ea97fce5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -90,7 +90,7 @@ void setup() { // Start event loop task evt::start(); #ifdef LAMP_DEBUG - evt::debug(); + //evt::debug(); #endif #ifdef EMBUI_USE_UDP @@ -143,6 +143,8 @@ void setup() { display.start(); // start tm1637 tm1637_setup(); + // button setup + button_cfg_load(); embui.setPubInterval(30); // change periodic WebUI publish interval from EMBUI_PUB_PERIOD to 10 secs diff --git a/src/main.h b/src/main.h index f3c9396b..e702da58 100644 --- a/src/main.h +++ b/src/main.h @@ -43,9 +43,6 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov #include "config.h" -#ifdef ESP_USE_BUTTON -#include "buttons.h" -#endif #ifdef ENCODER #include "enc.h" #endif @@ -72,10 +69,6 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov // глобальные переменные для работы с ними в программе - -#ifdef ESP_USE_BUTTON -extern Buttons *myButtons; -#endif #ifdef MP3PLAYER #include "mp3player.h" extern MP3PlayerDevice *mp3; From 03443e735dc5bf1423c9347d41f6d5e23e46d27e Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Thu, 18 Jan 2024 00:04:12 +0900 Subject: [PATCH 2/5] tm display track brightness control from button events --- src/bencoder.cpp | 7 ++-- src/evtloop.h | 2 +- src/interface.cpp | 4 +-- src/interface_actions.cpp | 2 +- src/lamp.cpp | 51 ++++++++++++++++++++-------- src/lamp.h | 5 +-- src/tm1637display.cpp | 70 ++++++++++++++++++++++----------------- src/tm1637display.hpp | 6 ++-- 8 files changed, 91 insertions(+), 56 deletions(-) diff --git a/src/bencoder.cpp b/src/bencoder.cpp index 52ecc304..d1ba5b63 100644 --- a/src/bencoder.cpp +++ b/src/bencoder.cpp @@ -81,14 +81,14 @@ void ButtonEventHandler::_btnEventHandler(ESPButton::event_t e, const EventMsg* } for (auto &it : _event_map ){ - LOG(printf, "Lookup event: it_en:%u, it.e:%u, e:%u ilp:%u lp:%u\n", it.enabled, it.e, e, it.lamppwr, _lamp_pwr ); + //LOG(printf, "Lookup event: it_en:%u, it.e:%u, e:%u ilp:%u lp:%u\n", it.enabled, it.e, e, it.lamppwr, _lamp_pwr ); if ( it.enabled && (it.e == e) && (it.lamppwr == _lamp_pwr) ){ // check for multiclicks if (e == ESPButton::event_t::multiClick && msg->cntr != it.clicks) continue; // event matches - LOG(printf, "BTN Execute Event:%u\n", e2int( it.evt_lamp ) ); + LOG(printf, "BTN Execute LampEvent:%u\n", e2int( it.evt_lamp ) ); switch (it.evt_lamp){ case evt::lamp_t::effSwitchTo: @@ -128,9 +128,8 @@ void ButtonEventHandler::load(JsonVariantConst cfg){ _event_map.clear(); for(JsonVariantConst v : array) { - LOG(printf, "Add cfg Event:%u\n", v[T_btn_event].as() ); + //LOG(printf, "Add cfg Event:%u\n", v[T_btn_event].as() ); _event_map.emplace_back(ButtonAction(static_cast(v[T_btn_event].as()), static_cast(v[T_lamp_event].as()), v[T_clicks], v[T_arg], v[T_enabled], v[T_pwr] )); - //Serial.println(v.as()); } } diff --git a/src/evtloop.h b/src/evtloop.h index 922b1574..77d351c3 100644 --- a/src/evtloop.h +++ b/src/evtloop.h @@ -67,7 +67,7 @@ enum class lamp_t:int32_t { pwrtoggle, // power toggle // brightness control, parameter value - int - brightness = 20, // set brightness according to current scale, param: int n + brightness = 20, // set/get brightness according to current scale, param: int n brightness_nofade, // set brightness according to current scale and w/o fade effect brightness_lcurve, // set brightness luma curve brightness_scale, // set brightness scale diff --git a/src/interface.cpp b/src/interface.cpp index 88b76c97..59e9efd9 100644 --- a/src/interface.cpp +++ b/src/interface.cpp @@ -55,7 +55,7 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov #include "evtloop.h" // версия ресурсов в стороннем джейсон файле -#define UIDATA_VERSION 3 +#define UIDATA_VERSION 4 // placeholder for effect list rebuilder task Task *delayedOptionTask = nullptr; @@ -390,7 +390,7 @@ void page_button_setup(Interface *interf, const JsonObject *data, const char* ac case 13: s = "Pwr Toggle"; break; - case 20: + case 24: s = "Brightness:"; s += obj[T_arg].as(); break; diff --git a/src/interface_actions.cpp b/src/interface_actions.cpp index 463bcbe9..f9b6bf53 100644 --- a/src/interface_actions.cpp +++ b/src/interface_actions.cpp @@ -352,7 +352,7 @@ void getset_button_gpio(Interface *interf, const JsonObject *data, const char* a } -// a call-back handler that listens for status change events and publish it to EmbUI feeders +// a call-back handler that listens for status CHANGE events and publish it to EmbUI feeders void event_publisher(void* handler_args, esp_event_base_t base, int32_t id, void* event_data){ // quit if there are no feeders to notify if (!embui.feeders.available()) return; diff --git a/src/lamp.cpp b/src/lamp.cpp index 699baf32..9bda37eb 100644 --- a/src/lamp.cpp +++ b/src/lamp.cpp @@ -383,8 +383,9 @@ void Lamp::setBrightness(uint8_t tgtbrt, fade_t fade, bool bypass){ EVT_POST_DATA(LAMP_CHANGE_EVENTS, e2int(evt::lamp_t::brightness), &b, sizeof(unsigned)); } - globalBrightness = tgtbrt; // set configured brightness variable - embui.var(A_dev_brightness, tgtbrt); // save brightness variable + // set configured brightness variable + globalBrightness = tgtbrt > _brightnessScale ? _brightnessScale : tgtbrt; + embui.var(A_dev_brightness, globalBrightness); // save brightness variable } /* @@ -602,25 +603,32 @@ void Lamp::events_unsubsribe(){ esp_event_handler_instance_unregister_with(evt::get_hndlr(), ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, _events_lamp_cmd); } +// handle command events and react accordingly with lamp actions void Lamp::_event_picker_cmd(esp_event_base_t base, int32_t id, void* data){ - switch (static_cast(id)){ // Power control case evt::lamp_t::pwron : power(true); - break; + return; case evt::lamp_t::pwroff : power(false); - break; + return; case evt::lamp_t::pwrtoggle : power(); - break; + return; // Brightness control - case evt::lamp_t::brightness : - setBrightness(*((int*) data)); - break; + case evt::lamp_t::brightness : { + if (base == LAMP_SET_EVENTS){ + setBrightness(*((int*) data)); + } else { + // otherwise it's a GET event, publish current brightness + int32_t b = getBrightness(); + EVT_POST_DATA( LAMP_STATE_EVENTS, e2int(evt::lamp_t::brightness), &b, sizeof(int32_t) ); + } + return; + } case evt::lamp_t::brightness_nofade : setBrightness(*((int*) data), fade_t::off); break; @@ -631,6 +639,19 @@ void Lamp::_event_picker_cmd(esp_event_base_t base, int32_t id, void* data){ setBrightness(getBrightness() + *((int*) data), fade_t::off); break; + // Effect switching + case evt::lamp_t::effSwitchNext : + switcheffect(effswitch_t::next); + break; + case evt::lamp_t::effSwitchPrev : + switcheffect(effswitch_t::prev); + break; + case evt::lamp_t::effSwitchRnd : + switcheffect(effswitch_t::rnd); + break; + case evt::lamp_t::effSwitchTo : + switcheffect(effswitch_t::num, *((int*) data)); + break; // Get State Commands case evt::lamp_t::pwr : @@ -666,13 +687,10 @@ void Lamp::_event_picker_state(esp_event_base_t base, int32_t id, void* data){ // ********************************* /* LEDFader class implementation */ -void LEDFader::fadelight(const uint8_t _targetbrightness, const uint32_t _duration, std::function callback){ +void LEDFader::fadelight(int _targetbrightness, uint32_t _duration, std::function callback){ if (!lmp) return; LOG(printf, "Fader: tgt:%u, lamp:%u/%u, _br_scaled/_br_abs:%u/%u\n", _targetbrightness, lmp->getBrightness(), lmp->getBrightnessScale(), lmp->_get_brightness(), lmp->_get_brightness(true)); - int b = _targetbrightness; - EVT_POST_DATA(LAMP_CHANGE_EVENTS, e2int(evt::lamp_t::brightness), &b, sizeof(int)); - _brt = lmp->_get_brightness(true); // get current absolute Display brightness _tgtbrt = luma::curveMap(lmp->_curve, _targetbrightness, MAX_BRIGHTNESS, lmp->_brightnessScale); @@ -713,6 +731,13 @@ void LEDFader::fadelight(const uint8_t _targetbrightness, const uint32_t _durati lmp->_brightness(_tgtbrt, true); // set exact target brightness value LOG(printf, "Fading to %d done\n", _tgtbrt); EVT_POST(LAMP_CHANGE_EVENTS, e2int(evt::lamp_t::fadeEnd)); + + // send brightness change event only for positive brt values (0 is usually means that lamp has been switched off) + if (_targetbrightness){ + int b = _targetbrightness; + EVT_POST_DATA(LAMP_CHANGE_EVENTS, e2int(evt::lamp_t::brightness), &b, sizeof(int)); + } + // use new task for callback, 'cause effect switching will immidiatetly respawn new fader from callback, so I need to release a Task instance if(_cb) { new Task(FADE_STEPTIME, TASK_ONCE, [this](){ if (_cb) { _cb(); _cb = nullptr; } }, &ts, true, nullptr, nullptr, true ); } runner = nullptr; diff --git a/src/lamp.h b/src/lamp.h index adbf5866..39ad1d7a 100644 --- a/src/lamp.h +++ b/src/lamp.h @@ -333,6 +333,8 @@ class Lamp { bool getFaderFlag() {return flags.isFaderON; save_flags(); } void setClearingFlag(bool flag) {flags.isEffClearing = flag; save_flags(); } bool getClearingFlag() {return flags.isEffClearing; } + + void disableEffectsUntilText() {lampState.isEffectsDisabledUntilText = true; display.clear(); save_flags(); } void setOffAfterText() {lampState.isOffAfterText = true; save_flags(); } void setIsEventsHandled(bool flag) {flags.isEventsHandled = flag; save_flags(); } @@ -341,7 +343,6 @@ class Lamp { bool isDebugOn() {return flags.isDebug;} bool isDebug() {return lampState.isDebug;} void setDebug(bool flag) {flags.isDebug=flag; lampState.isDebug=flag; save_flags(); } - void setButton(bool flag) {flags.isBtn=flag; save_flags(); } // set/clear "restore on/off/demo" state on boot void setRestoreState(bool flag){ flags.restoreState = flag; save_flags(); } @@ -562,7 +563,7 @@ class LEDFader { * @param uint32_t _duration - fade effect duraion, ms * @param callback - callback-функция, которая будет выполнена после окончания затухания */ - void fadelight(const uint8_t _targetbrightness=0, const uint32_t _duration=FADE_TIME, std::function callback=nullptr); + void fadelight(int _targetbrightness=0, uint32_t _duration=FADE_TIME, std::function callback=nullptr); /** * @brief check if fade is in progress diff --git a/src/tm1637display.cpp b/src/tm1637display.cpp index 21b83e5f..7c0e9185 100644 --- a/src/tm1637display.cpp +++ b/src/tm1637display.cpp @@ -49,8 +49,8 @@ JeeUI2 lib used under MIT License Copyright (c) 2019 Marsel Akhkamov TMDisplay::~TMDisplay(){ - esp_event_handler_instance_unregister_with(evt::get_hndlr(), LAMP_CHANGE_EVENTS, ESP_EVENT_ANY_ID, _evt_ch_hndlr); - esp_event_handler_instance_unregister_with(evt::get_hndlr(), LAMP_SET_EVENTS, ESP_EVENT_ANY_ID, _evt_set_hndlr); + esp_event_handler_instance_unregister_with(evt::get_hndlr(), ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, _evt_state_hndlr); + //esp_event_handler_instance_unregister_with(evt::get_hndlr(), LAMP_SET_EVENTS, ESP_EVENT_ANY_ID, _evt_set_hndlr); WiFi.removeEvent(eid); }; @@ -65,8 +65,8 @@ void TMDisplay::init() { // Set WiFi event handlers eid = WiFi.onEvent( [this](WiFiEvent_t event, WiFiEventInfo_t info){ _onWiFiEvent(event, info); } ); - ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), LAMP_CHANGE_EVENTS, ESP_EVENT_ANY_ID, TMDisplay::event_hndlr, this, &_evt_ch_hndlr)); - ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), LAMP_SET_EVENTS, ESP_EVENT_ANY_ID, TMDisplay::event_hndlr, this, &_evt_set_hndlr)); + ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, TMDisplay::event_hndlr, this, &_evt_state_hndlr)); + //ESP_ERROR_CHECK(esp_event_handler_instance_register_with(evt::get_hndlr(), LAMP_SET_EVENTS, ESP_EVENT_ANY_ID, TMDisplay::event_hndlr, this, &_evt_set_hndlr)); LOG(println, "tm1637 initialized"); } @@ -128,22 +128,18 @@ void TMDisplay::event_hndlr(void* handler_args, esp_event_base_t base, int32_t i } void TMDisplay::_event_picker(esp_event_base_t base, int32_t id, void* data){ - switch (static_cast(id)){ - // Power control - case evt::lamp_t::pwron : - setBrightness(brtOn); - _addscroll(T_On); - //display(T_On, true, true); - //timer = 2; - break; - case evt::lamp_t::pwroff : - setBrightness(brtOff); - _addscroll(T_Off); - //display(T_Off, true, true); - //timer = 2; - break; - - default:; + if (base == LAMP_CHANGE_EVENTS){ + switch (static_cast(id)){ + // Power control + case evt::lamp_t::pwron : + setBrightness(brtOn); + _addscroll(T_On); + return; + case evt::lamp_t::pwroff : + setBrightness(brtOff); + _addscroll(T_Off); + return; + } } // pick only SET events @@ -151,20 +147,24 @@ void TMDisplay::_event_picker(esp_event_base_t base, int32_t id, void* data){ switch (static_cast(id)){ // Brightness control case evt::lamp_t::brightness_nofade : - case evt::lamp_t::brightness : { - String s("Br."); - unsigned b = *((unsigned*) data); - if (b<10) s.concat((char)0x20); // append space - s += b; - display(s); - timer = 2; - break; - } - - default:; + case evt::lamp_t::brightness : + _msg_brt(*((unsigned*) data)); + return; + case evt::lamp_t::brightness_step : + // disaply can't process this event, so let's request for real brt value + EVT_POST(LAMP_GET_EVENTS, static_cast(evt::lamp_t::brightness)); + return; } } + if (base == LAMP_STATE_EVENTS){ + switch (static_cast(id)){ + // Brightness state reply + case evt::lamp_t::brightness : + _msg_brt(*((unsigned*) data)); + return; + } + } } void TMDisplay::_onWiFiEvent(WiFiEvent_t event, WiFiEventInfo_t info){ @@ -199,3 +199,11 @@ void TMDisplay::brightness(uint8_t b, bool lampon){ lampon ? brtOn = b : brtOff = b; setBrightness(b); } + +void TMDisplay::_msg_brt(int32_t b){ + String s("Br."); + if (b<10) s.concat((char)0x20); // append space + s += b; + display(s); + timer = 2; +}; diff --git a/src/tm1637display.hpp b/src/tm1637display.hpp index 3e4f3571..532d6c90 100644 --- a/src/tm1637display.hpp +++ b/src/tm1637display.hpp @@ -115,8 +115,8 @@ class TMDisplay : private TM1637 { // *** Event bus members *** // instance that holds tm's command events handlers - esp_event_handler_instance_t _evt_ch_hndlr; - esp_event_handler_instance_t _evt_set_hndlr; + esp_event_handler_instance_t _evt_state_hndlr; + //esp_event_handler_instance_t _evt_set_hndlr; /** * @brief event picker method, processes incoming command events from a event_hndlr wrapper @@ -132,6 +132,8 @@ class TMDisplay : private TM1637 { * */ void _loop(); + + void _msg_brt(int32_t b); }; extern TMDisplay *tm1637; From f86cb70c4060119c974b0d87f3a31eefda68692f Mon Sep 17 00:00:00 2001 From: Emil Muratov Date: Thu, 18 Jan 2024 00:12:54 +0900 Subject: [PATCH 3/5] updare resources for new button lib support --- .gitignore | 1 + data/css/style.css.gz | Bin 3274 -> 3449 bytes data/index.html.gz | Bin 3224 -> 3231 bytes data/js/embui.js.gz | Bin 12002 -> 12011 bytes data/js/ui_lamp.json.gz | Bin 2125 -> 2692 bytes resources/html/buttons_config.json | 1 - resources/html/events_config.json | 1 - resources/html/index.html | 4 +- resources/html/js/ui_lamp.json | 203 ++++++++++++++++++++++++++++- 9 files changed, 205 insertions(+), 5 deletions(-) delete mode 100644 resources/html/buttons_config.json delete mode 100644 resources/html/events_config.json diff --git a/.gitignore b/.gitignore index 0bf36b5b..490d4954 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ user_*.ini .vscode/c_cpp_properties.json include/user_config.h include/user_cfg* +!/data/benc.json /data/*.json /data.* resources/embui.zip diff --git a/data/css/style.css.gz b/data/css/style.css.gz index fd3a5883df54dbff22cae85d76da1981fc035253..437daee4d5413c73cac080d2f4fd09a4fab1c6e5 100644 GIT binary patch literal 3449 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 I&AcuE0R8n;K>z>% diff --git a/data/index.html.gz b/data/index.html.gz index db9f3c3838add74c389d31cd65b59a48587eba10..134b7818da84a3e64a98ae3e4828df725cb577b4 100644 GIT binary patch delta 3209 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 delta 3202 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 J#%ElS004@QMj-$I literal 12002 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 Dt+gP_ diff --git a/resources/html/buttons_config.json b/resources/html/buttons_config.json deleted file mode 100644 index 87aca635..00000000 --- a/resources/html/buttons_config.json +++ /dev/null @@ -1 +0,0 @@ -[{"flg":36,"ac":4,"p":""},{"flg":40,"ac":6,"p":""},{"flg":34,"ac":14,"p":""},{"flg":38,"ac":13,"p":""},{"flg":2,"ac":1,"p":""},{"flg":6,"ac":1,"p":""},{"flg":37,"ac":5,"p":""},{"flg":41,"ac":9,"p":""},{"flg":45,"ac":10,"p":""},{"flg":49,"ac":8,"p":""},{"flg":53,"ac":12,"p":""},{"flg":57,"ac":11,"p":""},{"flg":3,"ac":1,"p":""},{"flg":7,"ac":2,"p":""},{"flg":11,"ac":3,"p":""},{"flg":61,"ac":16,"p":"253"}] \ No newline at end of file diff --git a/resources/html/events_config.json b/resources/html/events_config.json deleted file mode 100644 index a0beef9d..00000000 --- a/resources/html/events_config.json +++ /dev/null @@ -1 +0,0 @@ -[{"raw":63,"ut":1588828500,"ev":8,"rp":15,"sa":60,"msg":"Уже: %TM , просто для информации :)"},{"raw":62,"ut":1588828200,"ev":3,"rp":0,"sa":0,"msg":""},{"raw":127,"ut":1601545980,"ev":8,"rp":5,"sa":30,"msg":"Сейчас: %TM"},{"raw":1,"ut":1607197320,"ev":14,"rp":0,"sa":150,"msg":"3"},{"raw":1,"ut":1607515740,"ev":1,"rp":0,"sa":0,"msg":"13"},{"raw":1,"ut":1607977080,"ev":3,"rp":0,"sa":33,"msg":"Сейчас: %TM"},{"raw":1,"ut":1609451940,"ev":15,"rp":0,"sa":0,"msg":"[16711680,60000,500,3]"}] \ No newline at end of file diff --git a/resources/html/index.html b/resources/html/index.html index e07a4a8f..bddefe6e 100644 --- a/resources/html/index.html +++ b/resources/html/index.html @@ -102,7 +102,7 @@