diff --git a/firmware/GyverLamp_v1.4/GyverLamp_v1.4.ino b/firmware/GyverLamp_v1.4/GyverLamp_v1.4.ino index 7c9613f..956ab90 100644 --- a/firmware/GyverLamp_v1.4/GyverLamp_v1.4.ino +++ b/firmware/GyverLamp_v1.4/GyverLamp_v1.4.ino @@ -23,6 +23,11 @@ --- 11.07.2019 - Исправлена ошибка невыключения матрицы после срабатывания будильника, если до будильника матрица была выключенной - Дополнена таблица с тест кейсами + --- 14.07.2019 + - Исправлена ошибка выключения будильника, если перед его срабатыванием был активен эффект "матрица" (или другой эффект, где задействовано мало светодиодов) + - Добавлено управление по воздуху: + -- работает только в режиме WiFi клиента + -- работает при подключенной кнопке (потому что режим прошивки активируется кнопкой) */ // Ссылка для менеджера плат: @@ -63,6 +68,10 @@ #define ESP_CONF_TIMEOUT (300U) // время в секундах, которое ESP будет ждать ввода SSID и пароля WiFi сети роутера в конфигурационном режиме, после его истечения ESP перезагружается #define GENERAL_DEBUG // если строка не закомментирована, будут выводиться отладочные сообщения #define WIFIMAN_DEBUG (true) // вывод отладочных сообщений при подключении к WiFi сети: true - выводятся, false - не выводятся; настройка не зависит от GENERAL_DEBUG +#define OTA // если строка не закомментирована, модуль будет ждать два последдовательных запроса пользователя на прошивку по воздуху (см. документацию в "шапке") +#ifdef OTA +#define ESP_OTA_PORT (8266U) // номер порта, который будет "прослушиваться" в ожидании команды прошивки по воздуху +#endif // --- ESP (WiFi клиент) --------------- uint8_t STA_STATIC_IP[] ={}; // статический IP адрес: {} - IP адрес определяется роутером; {192, 168, 1, 66} - IP адрес задан явно (если DHCP на роутере не решит иначе); должен быть из того же диапазона адресов, что разадёт роутер @@ -101,6 +110,9 @@ uint8_t AP_STATIC_IP[] = {192, 168, 4, 1}; // статичес #ifdef USE_NTP #include #endif +#ifdef OTA +#include "OtaManager.h" +#endif // --- ИНИЦИАЛИЗАЦИЯ ОБЪЕКТОВ ---------- CRGB leds[NUM_LEDS]; @@ -115,6 +127,10 @@ timerMinim timeTimer(3000); #ifdef ESP_USE_BUTTON GButton touch(BTN_PIN, LOW_PULL, NORM_OPEN); #endif +#ifdef OTA +OtaManager otaManager; +OtaPhase OtaManager::OtaFlag = OtaPhase::None; +#endif // --- ИНИЦИАЛИЗАЦИЯ ПЕРЕМЕННЫХ ------- uint16_t localPort = ESP_UDP_PORT; @@ -303,6 +319,9 @@ void loop() #ifdef ESP_USE_BUTTON buttonTick(); #endif + #ifdef OTA + otaManager.HandleOtaUpdate(); // ожидание и обработка команды на обновление прошивки по воздуху + #endif ESP.wdtFeed(); // пнуть собаку yield(); // обработать все "служебные" задачи: WiFi подключение и т.д. } diff --git a/firmware/GyverLamp_v1.4/OtaManager.h b/firmware/GyverLamp_v1.4/OtaManager.h new file mode 100644 index 0000000..2398593 --- /dev/null +++ b/firmware/GyverLamp_v1.4/OtaManager.h @@ -0,0 +1,220 @@ +/* + * 11.07.2019 + * Класс, который отслеживает действия пользователя по запросу обновления прошивки по воздуху и выполняет эту прошивку. + * Запрос на обновление - это вызов метода RequestOtaUpdate(), его нужно поместить, например, в обработчик нажатия кнопки, приёма UDP пакета и т.д. + * Для обновления пользователь должен ДВАЖДЫ запросить обновление в течение заданного промежутка времени (CONFIRMATION_TIMEOUT) во избежание случайного перехода в режим обновления. + * Режим обновления - это прослушивание специального порта (ESP_OTA_PORT) в ожидании команды обновления прошивки по воздуху (по сети). + * Режим обновления работает параллельно с основным режимом функционирования, только при ESP_MODE == 1 (WiFi клиент), т.к. требует доступа к ESP по локальной сети и при подключенной кнопке (в данном сетапе, т.к. он вызывается кнопкой). + * Режим обновления активен в течение заданного промежутка времени (ESP_CONF_TIMEOUT). Потом ESP автоматически перезагружается. + * Обновление производится из Arduino IDE: меню Инструменты - Порт - <Выбрать обнаруженный СЕТЕВОЙ COM порт из списка> (если он не обнаружен, значит что-то настроено неправильно), затем обычная команда "Загрузка" для прошивки. + * Для включения опции обновления по воздуху в основном файле должен быть определён идентификатор OTA "#define OTA" и режим "#define ESP_MODE (1U)" (а также в данном проекте должна быть подключена кнопка). +*/ + +#ifdef OTA + +#include +#include + + +#define CONFIRMATION_TIMEOUT (30U) // время в сеундах, в течение которого нужно дважды подтвердить старт обновлениЯ по воздуху (иначе сброс в None) + +enum OtaPhase // определение стадий процесса обновления по воздуху: нет, получено первое подтверждение, получено второе подтверждение, получено второе подтверждение - в процессе, обновление окончено +{ + None = 0, + GotFirstConfirm, + GotSecondConfirm, + InProgress, + Done +}; + +class OtaManager +{ + public: + static OtaPhase OtaFlag; + + void RequestOtaUpdate() // пользователь однократно запросил обновление по воздуху + { + if (ESP_MODE != 1) + { + #ifdef GENERAL_DEBUG + Serial.printf("Запрос обновления по воздуху поддерживается только в режиме ESP_MODE = 1\n"); + #endif + + return; + } + + if (OtaFlag == OtaPhase::None) + { + OtaFlag = OtaPhase::GotFirstConfirm; + momentOfFirstConfirmation = millis(); + + #ifdef GENERAL_DEBUG + Serial.printf("Получено первое подтверждение обновления по воздуху\nОжидание второго подтверждения\n"); + #endif + + return; + } + + if (OtaFlag == OtaPhase::GotFirstConfirm) + { + OtaFlag = OtaPhase::GotSecondConfirm; + + #ifdef GENERAL_DEBUG + Serial.printf("Получено второе подтверждение обновления по воздуху\nСтарт режима обновления\n"); + #endif + + startOtaUpdate(); + return; + } + } + + void HandleOtaUpdate() + { + if (OtaFlag == OtaPhase::GotFirstConfirm && + millis() - momentOfFirstConfirmation >= CONFIRMATION_TIMEOUT * 1000) + { + OtaFlag = OtaPhase::None; + momentOfFirstConfirmation = 0; + + #ifdef GENERAL_DEBUG + Serial.printf("Таймаут ожидания второго подтверждения превышен\nСброс флага в исходное состояние\n"); + #endif + + return; + } + + if (OtaFlag == OtaPhase::GotSecondConfirm && + millis() - momentOfOtaStart >= ESP_CONF_TIMEOUT * 1000) + { + OtaFlag = OtaPhase::None; + momentOfOtaStart = 0; + + #ifdef GENERAL_DEBUG + Serial.printf("Таймаут ожидания прошивки по воздуху превышен\nСброс флага в исходное состояние\nПерезагрузка\n"); + delay(500); + #endif + + #if defined(ESP8266) + ESP.reset(); + #else + ESP.restart(); + #endif + + return; + } + + if (OtaFlag == OtaPhase::InProgress) + { + ArduinoOTA.handle(); + } + } + + private: + uint64_t momentOfFirstConfirmation = 0; // момент времени, когда получено первое подтверждение и с которого начинается отсчёт ожидания второго подтверждения + uint64_t momentOfOtaStart = 0; // момент времени, когда развёрнута WiFi точка доступа для обновления по воздуху + + void startOtaUpdate() + { + char espHostName[65]; + sprintf(espHostName, "%s-%u", AP_NAME, ESP.getChipId()); + ArduinoOTA.setPort(ESP_OTA_PORT); + ArduinoOTA.setHostname(espHostName); + ArduinoOTA.setPassword(AP_PASS); + + ArduinoOTA.onStart([this]() + { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + { + type = "sketch"; + } + else // U_SPIFFS + { + type = "filesystem"; + } + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + + #ifdef GENERAL_DEBUG + Serial.println("Start updating " + type); + #endif + }); + + ArduinoOTA.onEnd([this]() + { + OtaFlag = OtaPhase::Done; + + #ifdef GENERAL_DEBUG + Serial.printf("Обновление по воздуху выполнено\nПерезапуск"); + delay(500); + #endif + }); + + ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) + { + #ifdef GENERAL_DEBUG + Serial.printf("Ход выполнения: %u%%\r", (progress / (total / 100))); + #endif + }); + + ArduinoOTA.onError([this](ota_error_t error) + { + OtaFlag = OtaPhase::None; + + #ifdef GENERAL_DEBUG + Serial.printf("Обновление по воздуху завершилось ошибкой [%u]: ", error); + #endif + + if (error == OTA_AUTH_ERROR) + { + #ifdef GENERAL_DEBUG + Serial.println("Auth Failed"); + #endif + } + else if (error == OTA_BEGIN_ERROR) + { + #ifdef GENERAL_DEBUG + Serial.println("Begin Failed"); + #endif + } + else if (error == OTA_CONNECT_ERROR) + { + #ifdef GENERAL_DEBUG + Serial.println("Connect Failed"); + #endif + } + else if (error == OTA_RECEIVE_ERROR) + { + #ifdef GENERAL_DEBUG + Serial.println("Receive Failed"); + #endif + } + else if (error == OTA_END_ERROR) + { + #ifdef GENERAL_DEBUG + Serial.println("End Failed"); + #endif + } + + #ifdef GENERAL_DEBUG + Serial.printf("Сброс флага в исходное состояние\nПереход в режим ожидания запроса прошивки по воздуху\n"); + #endif + }); + + ArduinoOTA.setRebootOnSuccess(true); + ArduinoOTA.begin(); + OtaFlag = OtaPhase::InProgress; + momentOfFirstConfirmation = 0; + momentOfOtaStart = 0; + + #ifdef GENERAL_DEBUG + Serial.printf("Для обновления в Arduino IDE выберите пункт меню Инструменты - Порт - '%s at ", espHostName); + Serial.print(WiFi.localIP()); + Serial.println("'"); + Serial.printf("Затем нажмите кнопку 'Загрузка' в течение %u секунд и по запросу введите пароль '%ы'\n", ESP_CONF_TIMEOUT, AP_PASS); + Serial.println("Устройство с Arduino IDE должно быть в одной локальной сети с модулем ESP!"); + #endif + } +}; + +#endif diff --git a/firmware/GyverLamp_v1.4/button.ino b/firmware/GyverLamp_v1.4/button.ino index 4f1df84..740667c 100644 --- a/firmware/GyverLamp_v1.4/button.ino +++ b/firmware/GyverLamp_v1.4/button.ino @@ -43,6 +43,13 @@ void buttonTick() delay(1); } + if (ONflag && touch.isQuadruple()) + { + #ifdef OTA + otaManager.RequestOtaUpdate(); + #endif + } + if (ONflag && touch.isHolded()) { brightDirection = !brightDirection; diff --git a/firmware/GyverLamp_v1.4/time.ino b/firmware/GyverLamp_v1.4/time.ino index adf80db..1864ed1 100644 --- a/firmware/GyverLamp_v1.4/time.ino +++ b/firmware/GyverLamp_v1.4/time.ino @@ -37,6 +37,9 @@ void timeTick() { dawnFlag = false; manualOff = false; + FastLED.clear(); + delay(2); + FastLED.show(); changePower(); // выключение матрицы или установка яркости текущего эффекта в засисимости от того, была ли включена лампа до срабатывания будильника } } diff --git a/libraries/GyverButton/GyverButton.cpp b/libraries/GyverButton/GyverButton.cpp index 43816b9..ec3f6a6 100644 --- a/libraries/GyverButton/GyverButton.cpp +++ b/libraries/GyverButton/GyverButton.cpp @@ -106,6 +106,13 @@ boolean GButton::isTriple() { return true; } else return false; } +boolean GButton::isQuadruple() { + if (flags.tickMode) GButton::tick(); + if (flags.counter_flag && last_counter == 4) { + flags.counter_flag = false; + return true; + } else return false; +} boolean GButton::hasClicks() { if (flags.tickMode) GButton::tick(); if (flags.counter_flag) { diff --git a/libraries/GyverButton/GyverButton.h b/libraries/GyverButton/GyverButton.h index 2a39eba..efd5583 100644 --- a/libraries/GyverButton/GyverButton.h +++ b/libraries/GyverButton/GyverButton.h @@ -44,38 +44,39 @@ typedef struct class GButton { public: - GButton(uint8_t pin); // класс кнопки, принимает пин - + GButton(uint8_t pin); // класс кнопки, принимает пин + GButton(uint8_t pin, boolean type, boolean dir); // класс кнопки, принимает PIN пин, тип type (HIGH_PULL / LOW_PULL) и направление dir (NORM_OPEN / NORM_CLOSE) // HIGH_PULL - кнопка подключена к GND, пин подтянут к VCC, pinMode - INPUT_PULLUP (по умолчанию) // LOW_PULL - кнопка подключена к VCC, пин подтянут к GND, pinMode - INPUT // NORM_OPEN - кнопка по умолчанию разомкнута (по умолчанию) // NORM_CLOSE - кнопка по умолчанию замкнута - + void setDebounce(uint16_t debounce); // установка времени антидребезга (по умолчанию 80 мс) void setTimeout(uint16_t timeout); // установка таймаута удержания (по умолчанию 300 мс) void setClickTimeout(uint16_t timeout); // установка таймаута между кликами (по умолчанию 500 мс) void setStepTimeout(uint16_t step_timeout); // установка таймаута между инкрементами (по умолчанию 400 мс) void setType(boolean type); // установка типа кнопки (HIGH_PULL - подтянута к питанию, LOW_PULL - к gnd) void setDirection(boolean dir); // установка направления (разомкнута/замкнута по умолчанию - NORM_OPEN, NORM_CLOSE) - + void setTickMode(boolean tickMode); // (MANUAL / AUTO) ручной или автоматический опрос кнопки функцией tick() // MANUAL - нужно вызывать функцию tick() вручную // AUTO - tick() входит во все остальные функции и опрашивается сама - + void tick(); // опрос кнопки void tick(boolean state); // опрос внешнего значения (0 нажато, 1 не нажато) (для матричных, резистивных клавиатур и джойстиков) boolean isPress(); // возвращает true при нажатии на кнопку. Сбрасывается после вызова boolean isRelease(); // возвращает true при отпускании кнопки. Сбрасывается после вызова boolean isClick(); // возвращает true при клике. Сбрасывается после вызова - boolean isHolded(); // возвращает true при удержании дольше timeout. Сбрасывается после вызова + boolean isHolded(); // возвращает true при удержании дольше timeout. Сбрасывается после вызова boolean isHold(); // возвращает true при нажатой кнопке, не сбрасывается boolean state(); // возвращает состояние кнопки boolean isSingle(); // возвращает true при одиночном клике. Сбрасывается после вызова boolean isDouble(); // возвращает true при двойном клике. Сбрасывается после вызова boolean isTriple(); // возвращает true при тройном клике. Сбрасывается после вызова + boolean isQuadruple(); // возвращает true при четверном клике. Сбрасывается после вызова boolean hasClicks(); // проверка на наличие кликов. Сбрасывается после вызова uint8_t getClicks(); // вернуть количество кликов @@ -84,8 +85,8 @@ class GButton private: void init(); - GyverButtonFlags flags; - uint8_t _PIN = 0; + GyverButtonFlags flags; + uint8_t _PIN = 0; uint16_t _debounce = 0; uint16_t _timeout = 0; uint16_t _click_timeout = 0;