This commit is contained in:
Alex
2021-02-04 16:32:12 +03:00
parent 80c312a104
commit 889d25a7dc
24 changed files with 5 additions and 4 deletions

View File

@@ -0,0 +1,45 @@
#pragma once
#define BTN_DEB 100
#define BTN_HOLD 800
// (пин, инверт), инверт 1 - для pullup, 0 - для pulldown
class Button {
public:
Button (byte pin) : _pin(pin) {
pinMode(_pin, INPUT_PULLUP);
}
void setLevel(bool inv) {
_inv = inv;
}
void tick() {
uint32_t deb = millis() - _tmr;
if (state()) {
if (_flag && deb > BTN_HOLD) _hold = 1;
if (!_flag && deb > BTN_DEB) _flag = 1;
} else {
if (_flag) {
_flag = _hold = 0;
if (deb < BTN_HOLD) _click = 1;
}
_tmr = millis();
}
}
bool state() {
return (digitalRead(_pin) ^ _inv);
}
bool isHold() {
return _hold;
}
bool isClick() {
if (_click) {
_click = 0;
return 1;
} return 0;
}
private:
const byte _pin;
bool _inv = 1;
uint32_t _tmr = 0;
bool _flag = 0, _click = 0, _hold = 0;
};

View File

@@ -0,0 +1,72 @@
#pragma once
#define FFT_SIZE 64 // размер выборки (кратно степени 2)
static float sinF[] = {0.0, -1.0, -0.707107, -0.382683, -0.195090, -0.098017, -0.049068, -0.024541, -0.012272, -0.006136};
void FFT(int* AVal, int* FTvl) {
int i, j, m, Mmax, Istp, count = 0;
float Tmpr, Tmpi, Tmvl[FFT_SIZE * 2];
float Wpr, Wr, Wi;
for (i = 0; i < FFT_SIZE * 2; i += 2) {
Tmvl[i] = 0;
Tmvl[i + 1] = AVal[i / 2];
}
i = j = 1;
while (i < FFT_SIZE * 2) {
if (j > i) {
Tmpr = Tmvl[i];
Tmvl[i] = Tmvl[j];
Tmvl[j] = Tmpr;
Tmpr = Tmvl[i + 1];
Tmvl[i + 1] = Tmvl[j + 1];
Tmvl[j + 1] = Tmpr;
}
i = i + 2;
m = FFT_SIZE;
while ((m >= 2) && (j > m)) {
j = j - m;
m = m >> 1;
}
j = j + m;
}
Mmax = 2;
while (FFT_SIZE * 2 > Mmax) {
Wpr = sinF[count + 1] * sinF[count + 1] * 2;
Istp = Mmax * 2;
Wr = 1;
Wi = 0;
m = 1;
while (m < Mmax) {
i = m;
m = m + 2;
Tmpr = Wr;
Tmpi = Wi;
Wr += -Tmpr * Wpr - Tmpi * sinF[count];
Wi += Tmpr * sinF[count] - Tmpi * Wpr;
while (i < FFT_SIZE * 2) {
j = i + Mmax;
Tmpr = Wr * Tmvl[j] - Wi * Tmvl[j - 1];
Tmpi = Wi * Tmvl[j] + Wr * Tmvl[j - 1];
Tmvl[j] = Tmvl[i] - Tmpr;
Tmvl[j - 1] = Tmvl[i - 1] - Tmpi;
Tmvl[i] = Tmvl[i] + Tmpr;
Tmvl[i - 1] = Tmvl[i - 1] + Tmpi;
i = i + Istp;
}
}
count++;
Mmax = Istp;
}
for (i = 0; i < FFT_SIZE; i++) {
j = i * 2;
FTvl[i] = (int)(Tmvl[j] * Tmvl[j] + Tmvl[j + 1] * Tmvl[j + 1]) >> 18;
}
}
// по мотивам https://ru.wikibooks.org/wiki/%D0%A0%D0%B5%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D0%B8_%D0%B0%D0%BB%D0%B3%D0%BE%D1%80%D0%B8%D1%82%D0%BC%D0%BE%D0%B2/%D0%91%D1%8B%D1%81%D1%82%D1%80%D0%BE%D0%B5_%D0%BF%D1%80%D0%B5%D0%BE%D0%B1%D1%80%D0%B0%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5_%D0%A4%D1%83%D1%80%D1%8C%D0%B5

View File

@@ -0,0 +1,55 @@
#pragma once
#include <Arduino.h>
#define FF_SCALE 0
#define FF_PASS_MAX 1
#define FF_PASS_MIN 2
class FastFilter {
public:
void setK(byte k) {
_k1 = k;
_k2 = 32 - k;
}
void setDt(int dt) {
_dt = dt;
}
void setPass(byte pass) {
_pass = pass;
}
void setRaw(int raw) {
_raw = raw;
}
void setFil(int fil) {
_raw_f = fil;
}
bool checkPass(int val) {
if (_pass == FF_PASS_MAX && val > _raw_f) {
_raw_f = val;
return 1;
} else if (_pass == FF_PASS_MIN && val < _raw_f) {
_raw_f = val;
return 1;
}
return 0;
}
void compute() {
if (_dt == 0 || millis() - _tmr >= _dt) {
_tmr = millis();
_raw_f = (_k1 * _raw_f + _k2 * _raw) >> 5;
}
}
long getFil() {
return _raw_f;
}
long getRaw() {
return _raw;
}
private:
uint32_t _tmr = 0;
int _dt = 0;
byte _k1 = 20, _k2 = 12;
byte _pass = 0;
int _raw_f = 0, _raw = 0;
};

View File

@@ -0,0 +1,105 @@
// LOLIN(WEMOS) D1 R2 & mini
// ESP core 2.7.4+ http://arduino.esp8266.com/stable/package_esp8266com_index.json
// FastLED 3.4.0+ https://github.com/FastLED/FastLED/releases
// ---------- Настройки -----------
#define MAX_PRESETS 20 // макс количество режимов (не более 30)
#define GL_KEY "GL" // ключ сети
// ------------ Кнопка -------------
#define BTN_PIN 4 // пин кнопки GPIO4 (D2). Или 0 для схемы с ESP-01 !!
#define USE_BTN 1 // 1 использовать кнопку, 0 нет
// ------------ Лента -------------
#define STRIP_PIN 2 // пин ленты GPIO2 (D4)
#define MAX_LEDS 512 // макс. светодиодов
#define STRIP_CHIP WS2812 // чип ленты
#define STRIP_COLOR GRB // порядок цветов в ленте
#define STRIP_VOLT 5 // напряжение ленты, V
/*
WS2811, GBR, 12V
WS2812, GRB, 5V
WS2813, GRB, 5V
WS2815, GRB, 12V
WS2818, RGB, 12V
*/
// ------------ WiFi AP ------------
const char AP_NameChar[] = "GyverLamp2";
const char WiFiPassword[] = "12345678";
// ------------ Прочее -------------
#define MIC_VCC D6 // питание микрофона
#define PHOT_VCC D5 // питание фоторезистора
#define EE_TOUT 303000 // таймаут сохранения епром после изменения, мс
#define DEBUG_SERIAL // закомментируй чтобы выключить отладку (скорость 115200)
#define EE_KEY 40 // ключ сброса WiFi (измени для сброса всех настроек)
#define NTP_UPD_PRD 5 // период обновления времени с NTP сервера, минут
// ---------- БИБЛИОТЕКИ -----------
#include "data.h" // данные
#include "Time.h" // часы
#include "TimeRandom.h" // случайные числа по времени
#include "FastRandom.h" // быстрый рандом
#include "Button.h" // библа кнопки
#include "palettes.h" // палитры
#include "NTPClient-Gyver.h" // сервер времени (модиф)
#include "timerMillis.h" // таймер миллис
#include "VolAnalyzer.h" // анализатор громкости
#include "FFT_C.h" // фурье
#include <FastLED.h> // лента
#include <ESP8266WiFi.h> // базовая либа есп
#include <WiFiUdp.h> // общение по UDP
#include <EEPROM.h> // епром
#include "ESP8266httpUpdate.h" // OTA
// ------------------- ДАТА --------------------
Config cfg;
Preset preset[MAX_PRESETS];
Dawn dawn;
WiFiServer server(80);
WiFiUDP Udp;
WiFiUDP ntpUDP;
NTPClient ntp(ntpUDP);
CRGB leds[MAX_LEDS];
Time now;
Button btn(BTN_PIN);
timerMillis EEtmr(EE_TOUT), turnoffTmr;
TimeRandom trnd;
VolAnalyzer vol(A0), low, high;
FastFilter phot;
byte btnClicks = 0, brTicks = 0;
unsigned char matrixValue[11][16];
bool gotNTP = false;
// ------------------- SETUP --------------------
void setup() {
memset(matrixValue, 0, sizeof(matrixValue));
#ifdef DEBUG_SERIAL
Serial.begin(115200);
DEBUGLN();
#endif
EEPROM.begin(512); // старт епром
startStrip(); // показываем РГБ
btn.setLevel(digitalRead(BTN_PIN)); // смотрим что за кнопка
EE_startup(); // читаем епром
checkGroup(); // показываем или меняем адрес
checkButton(); // проверяем кнопку на удержание
startWiFi(); // старт вайфай
setupTime(); // выставляем время
setupADC(); // настраиваем анализ
}
void loop() {
timeTicker(); // обновляем время
yield();
parsing(); // ловим данные
yield();
checkEEupdate(); // сохраняем епром
presetRotation(); // смена режимов
effectsRoutine(); // мигаем
yield();
button(); // проверяем кнопку
checkAnalog(); // чтение звука и датчика
}

View File

@@ -0,0 +1,192 @@
/**
* The MIT License (MIT)
* Copyright (c) 2015 by Fabrice Weinberg
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#include "NTPClient-Gyver.h"
NTPClient::NTPClient(UDP& udp) {
this->_udp = &udp;
}
NTPClient::NTPClient(UDP& udp, long timeOffset) {
this->_udp = &udp;
this->_timeOffset = timeOffset;
}
NTPClient::NTPClient(UDP& udp, const char* poolServerName) {
this->_udp = &udp;
this->_poolServerName = poolServerName;
}
NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset) {
this->_udp = &udp;
this->_timeOffset = timeOffset;
this->_poolServerName = poolServerName;
}
NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval) {
this->_udp = &udp;
this->_timeOffset = timeOffset;
this->_poolServerName = poolServerName;
this->_updateInterval = updateInterval;
}
void NTPClient::begin() {
this->begin(NTP_DEFAULT_LOCAL_PORT);
}
void NTPClient::begin(int port) {
this->_port = port;
this->_udp->begin(this->_port);
this->_udpSetup = true;
}
bool NTPClient::forceUpdate() {
#ifdef DEBUG_NTPClient
Serial.println("Update from NTP Server");
#endif
this->sendNTPPacket();
// Wait till data is there or timeout...
byte timeout = 0;
int cb = 0;
do {
delay ( 10 );
cb = this->_udp->parsePacket();
if (timeout > 100) return false; // timeout after 1000 ms
timeout++;
} while (cb == 0);
this->_lastUpdate = millis() - (10 * (timeout + 1)); // Account for delay in reading the time
this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE);
unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]);
unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]);
/// добавлено AlexGyver
uint32_t frac = (uint32_t) _packetBuffer[44] << 24
| (uint32_t) _packetBuffer[45] << 16
| (uint32_t) _packetBuffer[46] << 8
| (uint32_t) _packetBuffer[47] << 0;
uint16_t mssec = ((uint64_t) frac * 1000) >> 32;
//https://arduino.stackexchange.com/questions/49567/synching-local-clock-usign-ntp-to-milliseconds
_lastUpdate -= mssec;
/// добавлено AlexGyver
// combine the four bytes (two words) into a long integer
// this is NTP time (seconds since Jan 1 1900):
unsigned long secsSince1900 = highWord << 16 | lowWord;
this->_currentEpoc = secsSince1900 - SEVENZYYEARS;
return true;
}
bool NTPClient::update() {
if ((millis() - this->_lastUpdate >= this->_updateInterval) // Update after _updateInterval
|| this->_lastUpdate == 0) { // Update if there was no update yet.
if (!this->_udpSetup) this->begin(); // setup the UDP client if needed
return this->forceUpdate();
}
return true;
}
unsigned long NTPClient::getEpochTime() const {
return this->_timeOffset + // User offset
this->_currentEpoc + // Epoc returned by the NTP server
((millis() - this->_lastUpdate) / 1000); // Time since last update
}
int NTPClient::getDay() const {
return (((this->getEpochTime() / 86400L) + 4 ) % 7); //0 is Sunday
}
int NTPClient::getHours() const {
return ((this->getEpochTime() % 86400L) / 3600);
}
int NTPClient::getMinutes() const {
return ((this->getEpochTime() % 3600) / 60);
}
int NTPClient::getSeconds() const {
return (this->getEpochTime() % 60);
}
int NTPClient::getMillis() const {
return ((millis() - this->_lastUpdate) % 1000);
}
int NTPClient::getMillisLastUpd() const {
return (millis() - this->_lastUpdate);
}
String NTPClient::getFormattedTime() const {
unsigned long rawTime = this->getEpochTime();
unsigned long hours = (rawTime % 86400L) / 3600;
String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);
unsigned long minutes = (rawTime % 3600) / 60;
String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);
unsigned long seconds = rawTime % 60;
String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);
return hoursStr + ":" + minuteStr + ":" + secondStr;
}
void NTPClient::end() {
this->_udp->stop();
this->_udpSetup = false;
}
void NTPClient::setTimeOffset(int timeOffset) {
this->_timeOffset = timeOffset;
}
void NTPClient::setUpdateInterval(unsigned long updateInterval) {
this->_updateInterval = updateInterval;
}
void NTPClient::setPoolServerName(const char* poolServerName) {
this->_poolServerName = poolServerName;
}
void NTPClient::sendNTPPacket() {
// set all bytes in the buffer to 0
memset(this->_packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
this->_packetBuffer[0] = 0b11100011; // LI, Version, Mode
this->_packetBuffer[1] = 0; // Stratum, or type of clock
this->_packetBuffer[2] = 6; // Polling Interval
this->_packetBuffer[3] = 0xEC; // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
this->_packetBuffer[12] = 49;
this->_packetBuffer[13] = 0x4E;
this->_packetBuffer[14] = 49;
this->_packetBuffer[15] = 52;
// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
this->_udp->beginPacket(this->_poolServerName, 123); //NTP requests are to port 123
this->_udp->write(this->_packetBuffer, NTP_PACKET_SIZE);
this->_udp->endPacket();
}

View File

@@ -0,0 +1,102 @@
#pragma once
// добавлена синхронизация обнвления по миллисекундам
// добавлен вывод миллисекунд
#include "Arduino.h"
#include <Udp.h>
#define SEVENZYYEARS 2208988800UL
#define NTP_PACKET_SIZE 48
#define NTP_DEFAULT_LOCAL_PORT 1337
class NTPClient {
private:
UDP* _udp;
bool _udpSetup = false;
const char* _poolServerName = "pool.ntp.org"; // Default time server
int _port = NTP_DEFAULT_LOCAL_PORT;
long _timeOffset = 0;
unsigned long _updateInterval = 60000; // In ms
unsigned long _currentEpoc = 0; // In s
unsigned long _lastUpdate = 0; // In ms
byte _packetBuffer[NTP_PACKET_SIZE];
void sendNTPPacket();
public:
NTPClient(UDP& udp);
NTPClient(UDP& udp, long timeOffset);
NTPClient(UDP& udp, const char* poolServerName);
NTPClient(UDP& udp, const char* poolServerName, long timeOffset);
NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval);
/**
* Set time server name
*
* @param poolServerName
*/
void setPoolServerName(const char* poolServerName);
/**
* Starts the underlying UDP client with the default local port
*/
void begin();
/**
* Starts the underlying UDP client with the specified local port
*/
void begin(int port);
/**
* This should be called in the main loop of your application. By default an update from the NTP Server is only
* made every 60 seconds. This can be configured in the NTPClient constructor.
*
* @return true on success, false on failure
*/
bool update();
/**
* This will force the update from the NTP Server.
*
* @return true on success, false on failure
*/
bool forceUpdate();
int getDay() const;
int getHours() const;
int getMinutes() const;
int getSeconds() const;
int getMillis() const;
int getMillisLastUpd() const;
/**
* Changes the time offset. Useful for changing timezones dynamically
*/
void setTimeOffset(int timeOffset);
/**
* Set the update interval to another frequency. E.g. useful when the
* timeOffset should not be set in the constructor
*/
void setUpdateInterval(unsigned long updateInterval);
/**
* @return time formatted like `hh:mm:ss`
*/
String getFormattedTime() const;
/**
* @return time in seconds since Jan. 1, 1970
*/
unsigned long getEpochTime() const;
/**
* Stops the underlying UDP client
*/
void end();
};

View File

@@ -0,0 +1,58 @@
class Time {
public:
byte sec = 0;
byte min = 0;
byte hour = 0;
byte day = 0; // пн 0, вт 2.. вс 6
int ms = 0;
uint32_t weekMs = 0;
uint32_t weekS = 0;
int getMs() {
return (millis() - tmr);
}
void setMs(int ms) {
tmr = millis() + ms;
}
uint32_t getWeekS() {
return day * 86400ul + hour * 3600ul + min * 60 + sec;
}
bool newSec() {
if (prevSec != sec) {
prevSec = sec;
return true;
}
return false;
}
bool newMin() {
if (prevMin != min) {
prevMin = min;
return true;
}
return false;
}
void tick() {
ms = millis() - tmr;
if (ms >= 1000) {
tmr += 1000;
if (++sec >= 60) {
sec = 0;
if (++min >= 60) {
min = 0;
if (++hour >= 24) {
hour = 0;
if (++day >= 7) {
day = 0;
}
}
}
}
}
weekMs = getWeekS() * 1000ul + millis() - tmr;
}
private:
uint32_t tmr;
byte prevSec = 0;
byte prevMin = 0;
};

View File

@@ -0,0 +1,122 @@
#pragma once
#include <Arduino.h>
#include "FastFilter.h"
class VolAnalyzer {
public:
VolAnalyzer (int pin = -1) {
volF.setDt(20);
volF.setPass(FF_PASS_MAX);
maxF.setPass(FF_PASS_MAX);
setVolK(25);
setAmpliK(31);
if (pin != -1) setPin(pin);
}
void setPin(int pin) {
_pin = pin;
pinMode(_pin, INPUT);
}
void setDt(int dt) {
_dt = dt;
}
void setPeriod(int period) {
_period = period;
}
void setVolDt(int volDt) {
volF.setDt(volDt);
}
void setAmpliDt(int ampliDt) {
_ampliDt = ampliDt;
}
void setWindow(int window) {
_window = window;
}
void setVolK(byte k) {
volF.setK(k);
}
void setAmpliK(byte k) {
maxF.setK(k);
minF.setK(k);
}
void setVolMin(int scale) {
_volMin = scale;
}
void setVolMax(int scale) {
_volMax = scale;
}
void setTrsh(int trsh) {
_trsh = trsh;
}
bool tick(int thisRead = -1) {
volF.compute();
if (millis() - tmr4 >= _ampliDt) { // период сглаживания амплитуды
tmr4 = millis();
maxF.setRaw(maxs);
minF.setRaw(mins);
maxF.compute();
minF.compute();
maxs = 0;
mins = 1023;
}
if (_period == 0 || millis() - tmr1 >= _period) { // период между захватом сэмплов
if (_dt == 0 || micros() - tmr2 >= _dt) { // период выборки
tmr2 = micros();
if (thisRead == -1) thisRead = analogRead(_pin);
if (thisRead > max) max = thisRead; // ищем максимум
if (!_first) {
_first = 1;
maxF.setFil(thisRead);
minF.setFil(thisRead);
}
if (++count >= _window) { // выборка завершена
tmr1 = millis();
raw = max;
if (max > maxs) maxs = max; // максимумы среди максимумов
if (max < mins) mins = max; // минимумы реди максимумов
maxF.checkPass(max); // проверка выше максимума
if (getMax() - getMin() < _trsh) max = 0; // если окно громкости меньше порого то 0
else max = constrain(map(max, getMin(), getMax(), _volMin, _volMax), _volMin, _volMax); // перевод в громкость
volF.setRaw(max); // фильтр столбика громкости
if (volF.checkPass(max)) _pulse = 1; // проверка выше максимума
max = count = 0;
return true; // выборка завершена
}
}
}
return false;
}
int getRaw() {
return raw;
}
int getVol() {
return volF.getFil();
}
int getMin() {
return minF.getFil();
}
int getMax() {
return maxF.getFil();
}
bool getPulse() {
if (_pulse) {
_pulse = false;
return true;
}
return false;
}
private:
int _pin;
int _dt = 600; // 600 мкс между сэмплами достаточно для музыки
int _period = 5; // 5 мс между выборами достаточно
int _ampliDt = 150;
int _window = 20; // при таком размере окна получаем длительность оцифровки 12 мс, вполне хватает
uint32_t tmr1 = 0, tmr2 = 0, tmr3 = 0, tmr4 = 0;
int raw = 0;
int max = 0, count = 0;
int maxs = 0, mins = 1023;
int _volMin = 0, _volMax = 100, _trsh = 30;
bool _pulse = 0, _first = 0;
FastFilter minF, maxF, volF;
};

View File

@@ -0,0 +1,106 @@
void setupADC() {
low.setDt(0);
low.setPeriod(0);
low.setWindow(0);
high.setDt(0);
high.setPeriod(0);
high.setWindow(0);
vol.setVolK(20);
low.setVolK(20);
high.setVolK(20);
vol.setTrsh(50);
low.setTrsh(50);
high.setTrsh(50);
vol.setVolMin(0);
low.setVolMin(0);
high.setVolMin(0);
vol.setVolMax(255);
low.setVolMax(255);
high.setVolMax(255);
phot.setDt(80);
phot.setK(31);
}
void checkAnalog() {
if (cfg.state) {
switch (cfg.adcMode) {
case GL_ADC_NONE: break;
case GL_ADC_BRI: checkPhot(); break;
case GL_ADC_MIC: checkMusic(); break;
case GL_ADC_BOTH:
{
static timerMillis tmr(1000, 1);
if (tmr.isReady()) {
switchToPhot();
phot.setRaw(analogRead(A0));
switchToMic();
} else {
checkMusic();
}
phot.compute();
}
break;
}
}
}
void checkMusic() {
if (CUR_PRES.soundMode > 1) {
if (CUR_PRES.soundMode == GL_MUS_VOL) { // громкость
vol.tick();
} else { // частоты
int raw[FFT_SIZE], spectr[FFT_SIZE];
for (int i = 0; i < FFT_SIZE; i++) raw[i] = analogRead(A0);
FFT(raw, spectr);
int low_raw = 0;
int high_raw = 0;
for (int i = 0; i < FFT_SIZE / 2; i++) {
spectr[i] = (spectr[i] * (i + 2)) >> 1;
if (i < 2) low_raw += spectr[i];
else high_raw += spectr[i];
}
low.tick(low_raw);
high.tick(high_raw);
}
}
}
void checkPhot() {
static timerMillis tmr(1000, true);
if (tmr.isReady()) phot.setRaw(analogRead(A0));
phot.compute();
}
byte getSoundVol() {
switch (CUR_PRES.soundMode) {
case GL_MUS_VOL: return vol.getVol();
case GL_MUS_LOW: return low.getVol();
case GL_MUS_HIGH: return high.getVol();
}
return 0;
}
void switchToMic() {
digitalWrite(PHOT_VCC, 0);
pinMode(PHOT_VCC, INPUT);
pinMode(MIC_VCC, OUTPUT);
digitalWrite(MIC_VCC, 1);
}
void switchToPhot() {
digitalWrite(MIC_VCC, 0);
pinMode(MIC_VCC, INPUT);
pinMode(PHOT_VCC, OUTPUT);
digitalWrite(PHOT_VCC, 1);
}
void disableADC() {
digitalWrite(PHOT_VCC, 0);
pinMode(PHOT_VCC, INPUT);
digitalWrite(MIC_VCC, 0);
pinMode(MIC_VCC, INPUT);
}

View File

@@ -0,0 +1,63 @@
#define CLICKS_TOUT 800
void button() {
#if (USE_BTN == 1)
static bool flag = 0, holdFlag = 0, brDir = 0;
static timerMillis stepTmr(80, true);
static uint32_t tmr = 0;
btn.tick();
if (btn.isClick()) {
btnClicks++;
tmr = millis();
}
if (btnClicks > 0 && millis() - tmr > CLICKS_TOUT) {
DEBUG("clicks: ");
DEBUGLN(btnClicks);
switch (btnClicks) {
case 1:
setPower(!cfg.state);
sendToSlaves(0, cfg.state);
break;
case 2:
changePreset(1);
sendToSlaves(1, cfg.curPreset);
break;
case 3:
changePreset(-1);
sendToSlaves(1, cfg.curPreset);
break;
case 5:
cfg.role = 0;
break;
case 6:
cfg.role = 1;
break;
}
EE_updateCfg();
btnClicks = 0;
}
if (cfg.state && btn.isHold()) {
if (stepTmr.isReady()) {
holdFlag = true;
int temp = cfg.bright;
temp += brDir ? 5 : -5;
temp = constrain(temp, 0, 255);
cfg.bright = temp;
brTicks = cfg.bright / 25;
}
} else {
if (holdFlag) {
holdFlag = false;
brDir = !brDir;
brTicks = 0;
DEBUG("Bright set to: ");
DEBUGLN(cfg.bright);
sendToSlaves(2, cfg.bright);
EE_updateCfg();
}
}
#endif
}

View File

@@ -0,0 +1,104 @@
// -------------- ВНУТР. КОНСТАНТЫ ---------------
#define GL_ADC_NONE 1
#define GL_ADC_BRI 2
#define GL_ADC_MIC 3
#define GL_ADC_BOTH 4
#define GL_TYPE_STRIP 1
#define GL_TYPE_ZIG 2
#define GL_TYPE_PARAL 3
#define GL_MUS_NONE 1
#define GL_MUS_VOL 2
#define GL_MUS_LOW 3
#define GL_MUS_HIGH 4
#define GL_REACT_BRI 1
#define GL_REACT_SCL 2
#define GL_REACT_LEN 3
#define GL_SLAVE 0
#define GL_MASTER 1
// ------------------- МАКРО --------------------
#ifdef DEBUG_SERIAL
#define DEBUGLN(x) Serial.println(x)
#define DEBUG(x) Serial.print(x)
#else
#define DEBUGLN(x)
#define DEBUG(x)
#endif
#define FOR_i(x,y) for (int i = (x); i < (y); i++)
#define FOR_j(x,y) for (int j = (x); j < (y); j++)
#define CUR_PRES preset[cfg.curPreset]
byte scaleFF(byte x, byte b) {
return ((uint16_t)x * (b + 1)) >> 8;
}
int mapFF(byte x, byte min, byte max) {
return (((max - min) * x + (min << 8) + 1) >> 8);
}
const char OTAhost[] = "http://ota.alexgyver.ru/GL2_latest.bin";
const char *NTPservers[] = {
"pool.ntp.org",
"europe.pool.ntp.org",
"ntp1.stratum2.ru",
"ntp2.stratum2.ru",
"ntp.msk-ix.ru",
};
#define CFG_SIZE 13
struct Config {
byte GMT = 3; // часовой пояс +13
byte NTP = 1; // 1..5 ВЫЧЕСТЬ 1
byte bright = 100; // яркость
byte adcMode = 1; // режим ацп (1 выкл, 2 ярк, 3 муз)
byte minBright = 0; // мин яркость
byte maxBright = 255; // макс яркость
byte rotation = 0; // смена режимов: 0 ручная, 1 авто
byte rotRnd = 0; // тип автосмены: 0 в порядке, 1 рандом
byte rotPeriod = 1; // период смены (1,5..)
byte deviceType = 1; // 1 лента, 2 зигзаг, 3 параллел
byte maxCur = 5; // макс ток (мА/100)
byte workFrom = 0; // часы работы (0,1.. 23)
byte workTo = 0; // часы работы (0,1.. 23)
int length = 100; // длина ленты
int width = 1; // ширина матрицы
byte state = 1; // состояние 0 выкл, 1 вкл
byte group = 1; // группа девайса (1-10)
byte role = 0; // 0 slave, 1 master
byte WiFimode = 0; // 0 AP, 1 local
byte presetAmount = 1; // количество режимов
byte manualOff = 0; // выключали вручную?
int8_t curPreset = 0; // текущий режим
int minLight = 0; // мин освещённость
int maxLight = 1023; // макс освещённость
char ssid[16]; // логин wifi
char pass[16]; // пароль wifi
};
#define PRES_SIZE 13
struct Preset {
byte effect = 1; // тип эффекта (1,2...) ВЫЧЕСТЬ 1
byte fadeBright = 0; // флаг на свою яркость (0/1)
byte bright = 100; // своя яркость (0.. 255)
byte soundMode = 1; // тип звука (1,2...) ВЫЧЕСТЬ 1
byte soundReact = 1; // реакция на звук (1,2...) ВЫЧЕСТЬ 1
byte min = 0; // мин сигнал светомузыки (0.. 255)
byte max = 0; // макс сигнал светомузыки (0.. 255)
byte speed = 10; // скорость (0.. 255)
byte palette = 1; // палитра (1,2...) ВЫЧЕСТЬ 1
byte scale = 1; // масштаб (0.. 255)
byte fromCenter = 0; // эффект из центра (0/1)
byte color = 0; // цвет (0.. 255)
byte rnd = 0; // случайный (0/1)
};
struct Dawn {
byte state[7] = {0, 0, 0, 0, 0, 0, 0}; // (1/0)
byte hour[7] = {0, 0, 0, 0, 0, 0, 0}; // (0.. 59)
byte minute[7] = {0, 0, 0, 0, 0, 0, 0}; // (0.. 59)
byte bright = 100; // (0.. 255)
byte time = 1; // (5,10,15,20..)
};

View File

@@ -0,0 +1,68 @@
bool EEcfgFlag = false;
bool EEdawnFlag = false;
bool EEpresetFlag = false;
void EE_startup() {
// старт епром
if (EEPROM.read(511) != EE_KEY) {
EEPROM.write(511, EE_KEY);
EEPROM.put(0, cfg);
EEPROM.put(sizeof(cfg), dawn);
EEPROM.put(sizeof(cfg) + sizeof(dawn), preset);
EEPROM.commit();
DEBUGLN("First start");
}
EEPROM.get(0, cfg);
EEPROM.get(sizeof(cfg), dawn);
EEPROM.get(sizeof(cfg) + sizeof(dawn), preset);
// запускаем всё
trnd.setChannel(cfg.group);
FastLED.setMaxPowerInVoltsAndMilliamps(STRIP_VOLT, cfg.maxCur * 100);
}
void EE_updateCfg() {
EEcfgFlag = true;
EEtmr.restart();
}
void EE_updateDawn() {
EEdawnFlag = true;
EEtmr.restart();
}
void EE_updatePreset() {
EEpresetFlag = true;
EEtmr.restart();
}
void checkEEupdate() {
if (EEtmr.isReady()) {
if (EEcfgFlag || EEdawnFlag || EEpresetFlag) {
if (EEcfgFlag) {
EEcfgFlag = false;
EEPROM.put(0, cfg);
DEBUGLN("save cfg");
}
if (EEdawnFlag) {
EEdawnFlag = false;
EEPROM.put(sizeof(cfg), dawn);
DEBUGLN("save dawn");
}
if (EEpresetFlag) {
EEpresetFlag = false;
EEPROM.put(sizeof(cfg) + sizeof(dawn), preset);
DEBUGLN("save preset");
}
EEPROM.commit();
}
EEtmr.stop();
}
}
void EE_updCfgRst() {
EE_updCfg();
delay(100);
ESP.restart();
}
void EE_updCfg() {
EEPROM.put(0, cfg);
EEPROM.commit();
}

View File

@@ -0,0 +1,219 @@
void effectsRoutine() {
static timerMillis effTmr(30, true);
if (cfg.state && effTmr.isReady()) {
FastLED.setBrightness(getBright());
int thisLength = getLength();
byte thisScale = getScale();
int thisWidth = (cfg.deviceType > 1) ? cfg.width : 1;
switch (CUR_PRES.effect) {
case 1: // =================================== ПЕРЛИН ===================================
FastLED.clear();
if (cfg.deviceType > 1) {
FOR_j(0, cfg.length) {
FOR_i(0, cfg.width) {
leds[getPix(i, j)] = ColorFromPalette(paletteArr[CUR_PRES.palette - 1],
inoise8(
i * (thisScale / 5) - cfg.width * (thisScale / 5) / 2,
j * (thisScale / 5) - cfg.length * (thisScale / 5) / 2,
(now.weekMs >> 1) * CUR_PRES.speed / 255),
255, LINEARBLEND);
}
}
} else {
FOR_i(0, cfg.length) {
leds[i] = ColorFromPalette(paletteArr[CUR_PRES.palette - 1],
inoise8(i * (thisScale / 5) - cfg.length * (thisScale / 5) / 2,
(now.weekMs >> 1) * CUR_PRES.speed / 255),
255, LINEARBLEND);
}
}
break;
case 2: // ==================================== ЦВЕТ ====================================
FastLED.clear();
{
fill_solid(leds, cfg.length * thisWidth, CHSV(CUR_PRES.color, thisScale, CUR_PRES.min));
CRGB thisColor = CHSV(CUR_PRES.color, thisScale, CUR_PRES.max);
if (CUR_PRES.fromCenter) {
fillStrip(cfg.length / 2, cfg.length / 2 + thisLength / 2, thisColor);
fillStrip(cfg.length / 2 - thisLength / 2, cfg.length / 2, thisColor);
} else {
fillStrip(0, thisLength, thisColor);
}
}
break;
case 3: // ================================= СМЕНА ЦВЕТА =================================
FastLED.clear();
{
CRGB thisColor = ColorFromPalette(paletteArr[CUR_PRES.palette - 1], (now.weekMs >> 5) * CUR_PRES.speed / 255, CUR_PRES.min, LINEARBLEND);
fill_solid(leds, cfg.length * thisWidth, thisColor);
thisColor = ColorFromPalette(paletteArr[CUR_PRES.palette - 1], (now.weekMs >> 5) * CUR_PRES.speed / 255, CUR_PRES.max, LINEARBLEND);
if (CUR_PRES.fromCenter) {
fillStrip(cfg.length / 2, cfg.length / 2 + thisLength / 2, thisColor);
fillStrip(cfg.length / 2 - thisLength / 2, cfg.length / 2, thisColor);
} else {
fillStrip(0, thisLength, thisColor);
}
}
break;
case 4: // ================================== ГРАДИЕНТ ==================================
FastLED.clear();
if (CUR_PRES.fromCenter) {
FOR_i(cfg.length / 2, cfg.length) {
byte bright = 255;
if (CUR_PRES.soundReact == GL_REACT_LEN) bright = (i < cfg.length / 2 + thisLength / 2) ? (CUR_PRES.max) : (CUR_PRES.min);
CRGB thisColor = ColorFromPalette(
paletteArr[CUR_PRES.palette - 1], // (x*1.9 + 25) / 255 - быстрый мап 0..255 в 0.1..2
(i * (thisScale * 1.9 + 25) / cfg.length) + ((now.weekMs >> 3) * (CUR_PRES.speed - 128) / 128),
bright, LINEARBLEND);
if (cfg.deviceType > 1) fillRow(i, thisColor);
else leds[i] = thisColor;
}
if (cfg.deviceType > 1) FOR_i(0, cfg.length / 2) fillRow(i, leds[(cfg.length - i)*cfg.width - 1]);
else FOR_i(0, cfg.length / 2) leds[i] = leds[cfg.length - i - 1];
} else {
FOR_i(0, cfg.length) {
byte bright = 255;
if (CUR_PRES.soundReact == GL_REACT_LEN) bright = (i < thisLength) ? (CUR_PRES.max) : (CUR_PRES.min);
CRGB thisColor = ColorFromPalette(
paletteArr[CUR_PRES.palette - 1], // (x*1.9 + 25) / 255 - быстрый мап 0..255 в 0.1..2
(i * (thisScale * 1.9 + 25) / cfg.length) + ((now.weekMs >> 3) * (CUR_PRES.speed - 128) / 128),
bright, LINEARBLEND);
if (cfg.deviceType > 1) fillRow(i, thisColor);
else leds[i] = thisColor;
}
}
break;
case 5: // =================================== ЧАСТИЦЫ ===================================
//FastLED.clear();
FOR_i(0, cfg.length * cfg.width) leds[i].fadeToBlackBy(70);
if (cfg.deviceType > 1) {
uint16_t rndVal = 0;
FOR_i(0, thisScale / 8) {
int thisY = inoise16(i * 100000000ul + (now.weekMs << 6) * CUR_PRES.speed / 255);
thisY = map(thisY, 20000, 45000, 0, cfg.length);
int thisX = inoise16(i * 100000000ul + 2000000000ul + (now.weekMs << 6) * CUR_PRES.speed / 255);
thisX = map(thisX, 20000, 45000, 0, cfg.width);
rndVal = rndVal * 2053 + 13849; // random2053 алгоритм
if (thisY >= 0 && thisY < cfg.length && thisX >= 0 && thisX < cfg.width)
leds[getPix(thisX, thisY)] = CHSV(CUR_PRES.rnd ? rndVal : CUR_PRES.color, 255, 255);
}
} else {
uint16_t rndVal = 0;
FOR_i(0, thisScale / 8) {
int thisPos = inoise16(i * 100000000ul + (now.weekMs << 6) * CUR_PRES.speed / 255);
thisPos = map(thisPos, 20000, 45000, 0, cfg.length);
rndVal = rndVal * 2053 + 13849; // random2053 алгоритм
if (thisPos >= 0 && thisPos < cfg.length) leds[thisPos] = CHSV(CUR_PRES.rnd ? rndVal : CUR_PRES.color, 255, 255);
}
}
break;
case 6: // ==================================== ОГОНЬ ====================================
FastLED.clear();
{
if (cfg.deviceType > 1) { // 2D огонь
fireRoutine();
} else { // 1D огонь
static byte heat[MAX_LEDS];
CRGBPalette16 gPal;
if (CUR_PRES.color < 5) gPal = HeatColors_p;
else gPal = CRGBPalette16(CRGB::Black, CHSV(CUR_PRES.color, 255, 255), CRGB::White);
if (CUR_PRES.fromCenter) thisLength /= 2;
for (int i = 0; i < thisLength; i++) heat[i] = qsub8(heat[i], random8(0, ((((255 - thisScale) / 2 + 20) * 10) / thisLength) + 2));
for (int k = thisLength - 1; k >= 2; k--) heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
if (random8() < 120 ) {
int y = random8(7);
heat[y] = qadd8(heat[y], random8(160, 255));
}
if (CUR_PRES.fromCenter) {
for (int j = 0; j < thisLength; j++) leds[cfg.length / 2 + j] = ColorFromPalette(gPal, scale8(heat[j], 240));
FOR_i(0, cfg.length / 2) leds[i] = leds[cfg.length - i - 1];
} else {
for (int j = 0; j < thisLength; j++) leds[j] = ColorFromPalette(gPal, scale8(heat[j], 240));
}
}
}
break;
case 7: // ================================== КОНФЕТТИ ==================================
FOR_i(0, thisScale >> 3) {
byte x = random(0, cfg.length * cfg.width);
if (leds[x] == CRGB(0, 0, 0)) leds[x] = CHSV(CUR_PRES.rnd ? random(0, 255) : CUR_PRES.color, 255, 255);
}
FOR_i(0, cfg.length * cfg.width) {
if (leds[i].r >= 10 || leds[i].g >= 10 || leds[i].b >= 10) leds[i].fadeToBlackBy(CUR_PRES.speed / 2);
else leds[i] = 0;
}
break;
}
// выводим нажатия кнопки
if (btnClicks > 0) fill_solid(leds, btnClicks, CRGB::White);
if (brTicks > 0) fill_solid(leds, brTicks, CRGB::Cyan);
FastLED.show();
}
}
byte getBright() {
int maxBr = cfg.bright;
byte fadeBr = 255;
if (CUR_PRES.fadeBright) fadeBr = CUR_PRES.bright; // ограничен вручную
if (cfg.adcMode == GL_ADC_BRI || cfg.adcMode == GL_ADC_BOTH) { // ----> датчик света
maxBr = constrain(phot.getFil(), cfg.minLight, cfg.maxLight);
maxBr = map(maxBr, cfg.minLight, cfg.maxLight, cfg.minBright, cfg.maxBright);
} else if (cfg.adcMode > 2 && // ----> ацп мик
CUR_PRES.soundMode > 1 && // светомузыка вкл
CUR_PRES.soundReact == GL_REACT_BRI) { // режим яркости
fadeBr = mapFF(getSoundVol(), CUR_PRES.min, CUR_PRES.max);
}
return scaleFF(maxBr, fadeBr);
}
int getLength() {
if (cfg.adcMode > 2 // ацп мик
&& CUR_PRES.soundMode > 1 // светомузыка вкл
&& CUR_PRES.soundReact == GL_REACT_LEN // режим длины
) return mapFF(getSoundVol(), 0, cfg.length);
else return cfg.length;
}
byte getScale() {
if (cfg.adcMode > 2 // ацп мик
&& CUR_PRES.soundMode > 1 // светомузыка вкл
&& CUR_PRES.soundReact == GL_REACT_SCL // режим масштаба
) return mapFF(getSoundVol(), CUR_PRES.min, CUR_PRES.max);
else return CUR_PRES.scale;
}
void fillStrip(int from, int to, CRGB color) {
if (cfg.deviceType > 1) {
FOR_i(from, to) {
FOR_j(0, cfg.width) leds[getPix(j, i)] = color;
}
} else {
FOR_i(from, to) leds[i] = color;
}
}
void fillRow(int row, CRGB color) {
FOR_i(cfg.width * row, cfg.width * (row + 1)) leds[i] = color;
}
// получить номер пикселя в ленте по координатам
uint16_t getPix(int x, int y) {
if ( !(y & 1) || (cfg.deviceType - 2) ) return (y * cfg.width + x); // если чётная строка
else return (y * cfg.width + cfg.width - x - 1); // если нечётная строка
}
/*
целочисленный мап
y = ( (y1 - y2) * x + (x1y2 - x2y1) ) / (x1-x2)
y = ( (y2 - y1) * x + 255 * y1 ) / 255
(x + 128) / 255 -> 0.5-2
(x*5 + 51) / 255 -> 0.2-5
(x*1.9 + 25) / 255 -> 0.1-1
*/

View File

@@ -0,0 +1,25 @@
#ifndef FastRandom_h
#define FastRandom_h
#include <Arduino.h>
class FastRandom {
public:
// установить сид
void setSeed(uint16_t seed) {
_seed = seed;
}
uint16_t get() {
_seed = (_seed * 2053ul) + 13849;
return _seed;
}
uint16_t get(uint16_t max) {
return ((uint32_t)max * get()) >> 16;
}
uint16_t get(uint16_t min, uint16_t max) {
return (get(max - min) + min);
}
private:
uint16_t _seed;
};
#endif

View File

@@ -0,0 +1,83 @@
const unsigned char valueMask[11][16] PROGMEM = {
{8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 , 8 , 0 , 0 , 0 , 0 , 0 , 0 , 8 },
{16 , 0 , 0 , 0 , 0 , 0 , 0 , 16 , 16 , 0 , 0 , 0 , 0 , 0 , 0 , 16 },
{32 , 0 , 0 , 0 , 0 , 0 , 0 , 32 , 32 , 0 , 0 , 0 , 0 , 0 , 0 , 32 },
{64 , 0 , 0 , 0 , 0 , 0 , 0 , 64 , 64 , 0 , 0 , 0 , 0 , 0 , 0 , 64 },
{96 , 32 , 0 , 0 , 0 , 0 , 32 , 96 , 96 , 32 , 0 , 0 , 0 , 0 , 32 , 96 },
{128, 64 , 32 , 0 , 0 , 32 , 64 , 128, 128, 64 , 32 , 0 , 0 , 32 , 64 , 128},
{160, 96 , 64 , 32 , 32 , 64 , 96 , 160, 160, 96 , 64 , 32 , 32 , 64 , 96 , 160},
{192, 128, 96 , 64 , 64 , 96 , 128, 192, 192, 128, 96 , 64 , 64 , 96 , 128, 192},
{255, 160, 128, 96 , 96 , 128, 160, 255, 255, 160, 128, 96 , 96 , 128, 160, 255},
{255, 192, 160, 128, 128, 160, 192, 255, 255, 192, 160, 128, 128, 160, 192, 255},
{255, 220, 185, 150, 150, 185, 220, 255, 255, 220, 185, 150, 150, 185, 220, 255},
};
const unsigned char hueMask[11][16] PROGMEM = {
{8 , 16, 32, 36, 36, 32, 16, 8 , 8 , 16, 32, 36, 36, 32, 16, 8 },
{5 , 14, 29, 31, 31, 29, 14, 5 , 5 , 14, 29, 31, 31, 29, 14, 5 },
{1 , 11, 19, 25, 25, 22, 11, 1 , 1 , 11, 19, 25, 25, 22, 11, 1 },
{1 , 8 , 13, 19, 25, 19, 8 , 1 , 1 , 8 , 13, 19, 25, 19, 8 , 1 },
{1 , 8 , 13, 16, 19, 16, 8 , 1 , 1 , 8 , 13, 16, 19, 16, 8 , 1 },
{1 , 5 , 11, 13, 13, 13, 5 , 1 , 1 , 5 , 11, 13, 13, 13, 5 , 1 },
{1 , 5 , 11, 11, 11, 11, 5 , 1 , 1 , 5 , 11, 11, 11, 11, 5 , 1 },
{0 , 1 , 5 , 8 , 8 , 5 , 1 , 0 , 0 , 1 , 5 , 8 , 8 , 5 , 1 , 0 },
{0 , 0 , 1 , 5 , 5 , 1 , 0 , 0 , 0 , 0 , 1 , 5 , 5 , 1 , 0 , 0 },
{0 , 0 , 0 , 1 , 1 , 0 , 0 , 0 , 0 , 0 , 0 , 1 , 1 , 0 , 0 , 0 },
{0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 },
};
byte fireLine[100];
void fireRoutine() {
shiftUp();
FOR_i(0, cfg.width) fireLine[i] = random(64, 255);
drawFrame(30);
}
void shiftUp() {
for (int y = cfg.length - 1; y > 0; y--) {
for (int x = 0; x < cfg.width; x++) {
int newX = x;
if (x > 15) newX = x - 15;
if (y > 10) continue;
matrixValue[y][newX] = matrixValue[y - 1][newX];
}
}
for (int x = 0; x < cfg.width; x++) {
int newX = x;
if (x > 15) newX = x - 15;
matrixValue[0][newX] = fireLine[newX];
}
}
void drawFrame(int pcnt) {
int nextv;
for (int y = cfg.length - 1; y > 0; y--) {
for (byte x = 0; x < cfg.width; x++) {
int newX = x;
if (x > 15) newX = x - 15;
if (y < 11) {
nextv =
(((100.0 - pcnt) * matrixValue[y][newX]
+ pcnt * matrixValue[y - 1][newX]) / 100.0)
- pgm_read_byte(&(valueMask[y][newX]));
leds[getPix(x, y)] = CHSV(
CUR_PRES.color * 2.5 + pgm_read_byte(&(hueMask[y][newX])), // H
255, // S
(uint8_t)max(0, nextv) // V
);
}
}
}
for (int x = 0; x < cfg.width; x++) {
int newX = x;
if (x > 15) newX = x - 15;
leds[getPix(newX, 0)] = CHSV(
CUR_PRES.color * 2.5 + pgm_read_byte(&(hueMask[0][newX])), // H
255, // S
(uint8_t)(((100.0 - pcnt) * matrixValue[0][newX] + pcnt * fireLine[newX]) / 100.0) // V
);
}
}

View File

@@ -0,0 +1,249 @@
#include <FastLED.h> // лента
// http://soliton.vm.bytemark.co.uk/pub/cpt-city/
DEFINE_GRADIENT_PALETTE( Fire_gp ) {
0, 0, 0, 0,
128, 255, 0, 0,
224, 255, 255, 0,
255, 255, 255, 255
};
DEFINE_GRADIENT_PALETTE( Sunset_Real_gp ) {
0, 120, 0, 0,
22, 179, 22, 0,
51, 255, 104, 0,
85, 167, 22, 18,
135, 100, 0, 103,
198, 16, 0, 130,
255, 0, 0, 160
};
DEFINE_GRADIENT_PALETTE( dkbluered_gp ) {
0, 1, 0, 4,
8, 1, 0, 13,
17, 1, 0, 29,
25, 1, 0, 52,
33, 1, 0, 83,
42, 1, 0, 123,
51, 1, 0, 174,
59, 1, 0, 235,
68, 1, 2, 255,
76, 4, 17, 255,
84, 16, 45, 255,
93, 37, 82, 255,
102, 69, 127, 255,
110, 120, 168, 255,
119, 182, 217, 255,
127, 255, 255, 255,
135, 255, 217, 184,
144, 255, 168, 123,
153, 255, 127, 73,
161, 255, 82, 40,
170, 255, 45, 18,
178, 255, 17, 5,
186, 255, 2, 1,
195, 234, 0, 1,
204, 171, 0, 1,
212, 120, 0, 1,
221, 79, 0, 1,
229, 48, 0, 1,
237, 26, 0, 1,
246, 12, 0, 1,
255, 4, 0, 1
};
DEFINE_GRADIENT_PALETTE( Optimus_Prime_gp ) {
0, 5, 16, 18,
25, 5, 16, 18,
51, 7, 25, 39,
76, 8, 38, 71,
102, 64, 99, 106,
127, 194, 189, 151,
153, 182, 63, 42,
178, 167, 6, 2,
204, 100, 3, 1,
229, 53, 1, 1,
255, 53, 1, 1
};
DEFINE_GRADIENT_PALETTE( warmGrad_gp ) {
0, 252, 252, 172,
25, 239, 255, 61,
53, 247, 45, 17,
76, 197, 82, 19,
96, 239, 255, 61,
124, 83, 4, 1,
153, 247, 45, 17,
214, 23, 15, 17,
255, 1, 1, 1
};
DEFINE_GRADIENT_PALETTE( coldGrad_gp ) {
0, 66, 186, 192,
43, 1, 22, 71,
79, 2, 104, 142,
117, 66, 186, 192,
147, 2, 104, 142,
186, 1, 22, 71,
224, 2, 104, 142,
255, 4, 27, 28
};
DEFINE_GRADIENT_PALETTE( hotGrad_gp ) {
0, 157, 21, 2,
35, 229, 244, 16,
73, 255, 44, 7,
107, 142, 7, 1,
153, 229, 244, 16,
206, 142, 7, 1,
255, 135, 36, 0
};
DEFINE_GRADIENT_PALETTE( pinkGrad_gp ) {
0, 249, 32, 145,
28, 208, 1, 7,
43, 249, 1, 19,
56, 126, 152, 10,
73, 234, 23, 84,
89, 224, 45, 119,
107, 232, 127, 158,
127, 244, 13, 89,
150, 188, 6, 52,
175, 177, 70, 14,
221, 194, 1, 8,
255, 112, 0, 1
};
DEFINE_GRADIENT_PALETTE( comfy_gp ) {
0, 255, 255, 45,
43, 208, 93, 1,
137, 224, 1, 242,
181, 159, 1, 29,
255, 63, 4, 68
};
DEFINE_GRADIENT_PALETTE( cyperpunk_gp ) {
0, 3, 6, 72,
38, 12, 50, 188,
109, 217, 35, 1,
135, 242, 175, 12,
178, 161, 32, 87,
255, 24, 6, 108
};
DEFINE_GRADIENT_PALETTE( girl_gp ) {
0, 103, 1, 10,
33, 109, 1, 12,
76, 159, 5, 48,
119, 175, 55, 103,
127, 175, 55, 103,
178, 159, 5, 48,
221, 109, 1, 12,
255, 103, 1, 10
};
DEFINE_GRADIENT_PALETTE( xmas_gp ) {
0, 0, 12, 0,
40, 0, 55, 0,
66, 1, 117, 2,
77, 1, 84, 1,
81, 0, 55, 0,
119, 0, 12, 0,
153, 42, 0, 0,
181, 121, 0, 0,
204, 255, 12, 8,
224, 121, 0, 0,
244, 42, 0, 0,
255, 42, 0, 0
};
DEFINE_GRADIENT_PALETTE( acid_gp ) {
0, 0, 12, 0,
61, 153, 239, 112,
127, 0, 12, 0,
165, 106, 239, 2,
196, 167, 229, 71,
229, 106, 239, 2,
255, 0, 12, 0
};
DEFINE_GRADIENT_PALETTE( blueSmoke_gp ) {
0, 0, 0, 0,
12, 1, 1, 3,
53, 8, 1, 22,
80, 4, 6, 89,
119, 2, 25, 216,
145, 7, 10, 99,
186, 15, 2, 31,
233, 2, 1, 5,
255, 0, 0, 0
};
DEFINE_GRADIENT_PALETTE( gummy_gp ) {
0, 8, 47, 5,
31, 77, 122, 6,
63, 249, 237, 7,
95, 232, 51, 1,
127, 215, 0, 1,
159, 47, 1, 3,
191, 1, 7, 16,
223, 52, 22, 6,
255, 239, 45, 1,
};
DEFINE_GRADIENT_PALETTE( leo_gp ) {
0, 0, 0, 0,
16, 0, 0, 0,
32, 0, 0, 0,
18, 0, 0, 0,
64, 16, 8, 0,
80, 80, 40, 0,
96, 16, 8, 0,
112, 0, 0, 0,
128, 0, 0, 0,
144, 0, 0, 0,
160, 0, 0, 0,
176, 0, 0, 0,
192, 0, 0, 0,
208, 0, 0, 0,
224, 0, 0, 0,
240, 0, 0, 0,
255, 0, 0, 0,
};
DEFINE_GRADIENT_PALETTE ( aurora_gp ) {
0, 17, 177, 13, //Greenish
64, 121, 242, 5, //Greenish
128, 25, 173, 121, //Turquoise
192, 250, 77, 127, //Pink
255, 171, 101, 221 //Purple
};
CRGBPalette16 paletteArr[] = {
HeatColors_p,
Fire_gp,
LavaColors_p,
PartyColors_p,
RainbowColors_p,
RainbowStripeColors_p,
CloudColors_p,
OceanColors_p,
ForestColors_p,
Sunset_Real_gp,
dkbluered_gp,
Optimus_Prime_gp,
warmGrad_gp,
coldGrad_gp,
hotGrad_gp,
pinkGrad_gp,
comfy_gp,
cyperpunk_gp,
girl_gp,
xmas_gp,
acid_gp,
blueSmoke_gp,
gummy_gp,
leo_gp,
aurora_gp,
};

View File

@@ -0,0 +1,143 @@
void parsing() {
if (Udp.parsePacket()) {
static uint32_t tmr = 0;
static char buf[UDP_TX_PACKET_MAX_SIZE + 1];
int n = Udp.read(buf, UDP_TX_PACKET_MAX_SIZE);
if (millis() - tmr < 500) return; // принимаем посылки не чаще 2 раз в секунду
tmr = millis();
buf[n] = NULL;
DEBUGLN(buf); // пакет вида <ключ>,<канал>,<тип>,<дата1>,<дата2>...
byte keyLen = strlen(GL_KEY);
if (!strncmp(buf, GL_KEY, keyLen)) { // парсим если это наша "сеть"
byte data[MAX_PRESETS * PRES_SIZE + keyLen];
memset(data, 0, 30);
int count = 0;
char *str, *p = buf + keyLen; // сдвиг до даты
char *ssid, *pass;
while ((str = strtok_r(p, ",", &p)) != NULL) {
data[count++] = atoi(str);
if (count == 4) ssid = str;
if (count == 5) pass = str;
}
// широковещательный запрос времени для local устройств в сети AP лампы
if (data[0] == 0 && cfg.WiFimode && !gotNTP) {
now.hour = data[1];
now.min = data[2];
now.setMs(0);
}
if (data[0] != cfg.group) return; // не наш адрес, выходим
switch (data[1]) { // тип 0 - control, 1 - config, 2 - effects, 3 - dawn
case 0: DEBUGLN("Control");
switch (data[2]) {
case 0: setPower(0); break; // выкл
case 1: setPower(1); break; // вкл
case 2: cfg.minLight = phot.getRaw(); break; // мин яркость
case 3: cfg.maxLight = phot.getRaw(); break; // макс яркость
case 4: changePreset(-1); break; // пред пресет
case 5: changePreset(1); break; // след пресет
case 6: setPreset(data[3] - 1); break; // конкретный пресет data[3]
case 7: cfg.WiFimode = data[3]; EE_updCfgRst(); break; // смена режима WiFi
case 8: cfg.role = data[3]; break; // смена роли
case 9: cfg.group = data[3]; break; // смена группы
case 10: // установка настроек WiFi
strcpy(cfg.ssid, ssid);
strcpy(cfg.pass, pass);
break;
case 11: EE_updCfgRst(); break; // рестарт
case 12: if (gotNTP) { // OTA обновление, если есть интернет
delay(100);
FastLED.clear();
FastLED.show();
ESPhttpUpdate.update(OTAhost);
} break;
case 13: // выключить через
if (data[3] == 0) turnoffTmr.stop();
else {
turnoffTmr.setInterval((uint32_t)data[3] * 60000ul);
turnoffTmr.restart();
}
break;
}
EE_updCfg();
break;
case 1: DEBUGLN("Config");
FOR_i(0, CFG_SIZE) {
*((byte*)&cfg + i) = data[i + 2]; // загоняем в структуру
}
cfg.length = data[17] | (data[16] << 8); // склеиваем
cfg.width = data[20] | (data[19] << 8); // склеиваем
if (cfg.deviceType == GL_TYPE_STRIP) cfg.width = 1;
ntp.setTimeOffset((cfg.GMT - 13) * 3600);
ntp.setPoolServerName(NTPservers[cfg.NTP - 1]);
FastLED.setMaxPowerInVoltsAndMilliamps(STRIP_VOLT, cfg.maxCur * 100);
if (cfg.adcMode == GL_ADC_BRI) switchToPhot();
else if (cfg.adcMode == GL_ADC_MIC) switchToMic();
else disableADC();
EE_updCfg();
break;
case 2: DEBUGLN("Preset");
cfg.presetAmount = data[2]; // кол-во режимов
FOR_j(0, cfg.presetAmount) {
FOR_i(0, PRES_SIZE) {
*((byte*)&preset + j * PRES_SIZE + i) = data[j * PRES_SIZE + i + 3]; // загоняем в структуру
}
}
EE_updatePreset();
break;
case 3: DEBUGLN("Dawn");
FOR_i(0, (2 + 3 * 7)) {
*((byte*)&dawn + i) = data[i + 2]; // загоняем в структуру
}
EE_updateDawn();
break;
case 4: DEBUGLN("From master");
if (cfg.role == GL_SLAVE) {
switch (data[2]) {
case 0: setPower(data[3]); break; // вкл выкл
case 1: setPreset(data[3]); break; // пресет
case 2: cfg.bright = data[3]; break; // яркость
}
EE_updateCfg();
}
break;
}
}
}
}
void sendToSlaves(byte data1, byte data2) {
if (cfg.role == GL_MASTER) {
IPAddress ip = WiFi.localIP();
ip[3] = 255;
char reply[20] = GL_KEY;
byte keylen = strlen(GL_KEY);
reply[keylen++] = ',';
reply[keylen++] = cfg.group + '0';
reply[keylen++] = ',';
reply[keylen++] = '4';
reply[keylen++] = ',';
reply[keylen++] = data1 + '0';
reply[keylen++] = ',';
itoa(data2, reply + (keylen++), DEC);
DEBUG("Sending: ");
DEBUGLN(reply);
FOR_i(0, 3) {
Udp.beginPacket(ip, 8888);
Udp.write(reply);
Udp.endPacket();
delay(10);
}
}
}

View File

@@ -0,0 +1,43 @@
void presetRotation() {
if (cfg.rotation && now.newMin()) { // если автосмена и новая минута
if (cfg.rotRnd) { // случайная
cfg.curPreset = trnd.fromMin(cfg.rotPeriod, cfg.presetAmount);
DEBUG("Rnd changed to ");
DEBUGLN(cfg.curPreset);
} else { // по порядку
cfg.curPreset = ((trnd.getMin() / cfg.rotPeriod) % cfg.presetAmount);
DEBUG("In order changed to ");
DEBUGLN(cfg.curPreset);
}
}
}
void changePreset(int dir) {
if (!cfg.rotation) { // ручная смена
cfg.curPreset += dir;
if (cfg.curPreset >= cfg.presetAmount) cfg.curPreset = 0;
if (cfg.curPreset < 0) cfg.curPreset = cfg.presetAmount - 1;
DEBUG("Preset changed to ");
DEBUGLN(cfg.curPreset);
}
}
void setPreset(byte pres) {
if (!cfg.rotation) { // ручная смена
cfg.curPreset = constrain(pres, 0, cfg.presetAmount - 1);
DEBUG("Preset set to ");
DEBUGLN(cfg.curPreset);
}
}
void setPower(bool state) {
if (state) cfg.manualOff = 0;
if (cfg.state && !state) cfg.manualOff = 1;
cfg.state = state;
if (!state) {
delay(100); // чтобы пролететь мин. частоту обновления
FastLED.clear();
FastLED.show();
}
DEBUGLN(state ? "Power on" : "Power off");
}

View File

@@ -0,0 +1,170 @@
void checkButton() {
#if (USE_BTN == 1)
DEBUGLN(cfg.WiFimode ? "local mode" : "AP mode");
if (btn.isHold()) { // кнопка зажата
FastLED.clear();
byte count = 0;
bool state = 0;
while (btn.state()) { // пока зажата кнопка
fill_solid(leds, constrain(count, 0, 8), CRGB::Red);
count++;
if (count == 9) { // на счёт 9 поднимаем яркость и флаг
FastLED.setBrightness(120);
state = 1;
} else if (count == 16) { // на счёт 16 опускаем флаг выходим
state = 0;
break;
}
FastLED.show();
delay(300);
}
if (state) {
DEBUGLN("change mode");
cfg.WiFimode = !cfg.WiFimode;
EEPROM.put(0, cfg);
EEPROM.commit();
delay(100);
ESP.restart();
}
}
FastLED.setBrightness(50);
FastLED.clear();
FastLED.show();
#endif
}
void checkGroup() {
fill_solid(leds, cfg.group, (cfg.WiFimode) ? (CRGB::Blue) : (CRGB::Green));
FastLED.show();
uint32_t tmr = millis();
bool flag = 0;
while (millis() - tmr < 3000) {
#if (USE_BTN == 1)
btn.tick();
if (btn.isClick()) {
if (++cfg.group > 10) cfg.group = 1;
FastLED.clear();
fill_solid(leds, cfg.group, (cfg.WiFimode) ? (CRGB::Blue) : (CRGB::Green));
FastLED.show();
flag = 1;
tmr = millis();
}
if (btn.isHold()) {
return;
}
#endif
yield();
}
if (flag) {
EEPROM.put(0, cfg);
EEPROM.commit();
delay(100);
ESP.reset();
}
DEBUG("group: ");
DEBUGLN(cfg.group);
DEBUG("role: ");
DEBUGLN(cfg.role);
}
void startStrip() {
delay(500);
FastLED.addLeds<STRIP_CHIP, STRIP_PIN, STRIP_COLOR>(leds, MAX_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setMaxPowerInVoltsAndMilliamps(STRIP_VOLT, 500);
FastLED.setBrightness(50);
leds[0] = CRGB::Red;
leds[1] = CRGB::Green;
leds[2] = CRGB::Blue;
FastLED.show();
FastLED.clear();
delay(1500);
}
void startWiFi() {
if (!cfg.WiFimode) setupAP(); // режим точки доступа
else setupLocal(); // подключаемся к точке
DEBUG("UDP port: ");
DEBUGLN(8888);
Udp.begin(8888);
FastLED.clear();
FastLED.show();
}
void setupAP() {
fill_solid(leds, 8, CRGB::Yellow);
FastLED.show();
delay(500);
WiFi.disconnect();
WiFi.mode(WIFI_AP);
delay(100);
WiFi.softAP(AP_NameChar, WiFiPassword);
server.begin();
DEBUGLN("Setting AP Mode");
DEBUG("AP IP: ");
DEBUGLN(WiFi.softAPIP());
delay(500);
}
void setupLocal() {
if (cfg.ssid[0] == NULL && cfg.pass[0] == NULL) {
DEBUGLN("WiFi not configured");
setupAP();
} else {
DEBUGLN("Connecting to AP...");
WiFi.softAPdisconnect();
WiFi.disconnect();
WiFi.mode(WIFI_STA);
delay(100);
uint32_t tmr = millis();
bool connect = false;
int8_t count = 0, dir = 1;
byte failCount = 0;
while (1) {
WiFi.begin(cfg.ssid, cfg.pass);
while (millis() - tmr < 10000) {
if (WiFi.status() == WL_CONNECTED) {
connect = true;
break;
}
FastLED.clear();
leds[count] = CRGB::Yellow;
FastLED.show();
count += dir;
if (count >= 7 || count <= 0) dir *= -1;
delay(50);
}
if (connect) {
fill_solid(leds, 8, CRGB::Green);
FastLED.show();
server.begin();
DEBUG("Connected! Local IP: ");
DEBUGLN(WiFi.localIP());
delay(500);
return;
} else {
DEBUGLN("Failed!");
FOR_i(0, 3) {
fill_solid(leds, 8, CRGB::Red);
FastLED.show();
delay(300);
FastLED.clear();
FastLED.show();
delay(300);
}
failCount++;
tmr = millis();
if (failCount >= 3) {
setupAP();
return;
/*DEBUGLN("Reboot to AP!");
cfg.WiFimode = 0;
EE_updCfg();
delay(100);
ESP.restart();*/
}
}
}
}
}

View File

@@ -0,0 +1,108 @@
void setupTime() {
ntp.setUpdateInterval(NTP_UPD_PRD / 2 * 60000ul); // меньше в два раза, ибо апдейт вручную
ntp.setTimeOffset((cfg.GMT - 13) * 3600);
ntp.setPoolServerName(NTPservers[cfg.NTP - 1]);
if (cfg.WiFimode) {
// если подключены - запрашиваем время с сервера
ntp.begin();
if (ntp.update() && !gotNTP) gotNTP = true;
}
}
// сохраняет счёт времени после обрыва связи
void timeTicker() {
static timerMillis tmr(10, true);
if (tmr.isReady()) {
updateTime(); // обновляем время
sendTimeToSlaves(); // отправляем время слейвам
trnd.update(now.hour, now.min, now.sec); // обновляем рандомайзер
if (gotNTP) checkWorkTime(); // проверяем расписание, если подключены к Интернет
checkTurnoff(); // проверяем таймер отключения
}
}
void updateTime() {
if (cfg.WiFimode && WiFi.status() == WL_CONNECTED) { // если вайфай подключен
now.sec = ntp.getSeconds();
now.min = ntp.getMinutes();
now.hour = ntp.getHours();
now.day = ntp.getDay();
now.day = (now.day == 0) ? 6 : (now.day - 1); // перевод из вс0 в пн0
now.weekMs = now.getWeekS() * 1000ul + ntp.getMillis();
now.setMs(ntp.getMillis());
if (now.min % NTP_UPD_PRD == 0 && now.sec == 0) {
// берём время с интернета каждую NTP_UPD_PRD минуту, ставим флаг что данные с NTP получены, значит мы онлайн
if (ntp.update() && !gotNTP) gotNTP = true;
}
} else { // если нет
now.tick(); // тикаем своим счётчиком
}
}
void sendTimeToSlaves() {
if (!cfg.WiFimode) { // если мы AP
static byte prevSec = 0;
if (prevSec != now.sec) { // новая секунда
prevSec = now.sec;
if (now.min % 1 == 0 && now.sec == 0) sendTime(); // ровно каждые 5 мин отправляем время
}
}
}
void checkTurnoff() {
if (turnoffTmr.isReady()) {
turnoffTmr.stop();
setPower(0);
}
}
void checkWorkTime() {
if (!isWorkTime(now.hour, cfg.workFrom, cfg.workTo)) {
if (cfg.state) {
cfg.state = false;
FastLED.clear();
FastLED.show();
}
} else {
if (!cfg.state && !cfg.manualOff) {
cfg.state = true;
}
}
}
void sendTime() {
IPAddress ip = WiFi.localIP();
ip[3] = 255;
char reply[20] = GL_KEY;
byte keylen = strlen(GL_KEY);
reply[keylen++] = ',';
reply[keylen++] = 0 + '0';
reply[keylen++] = ',';
char hours[4];
itoa(now.hour, hours, DEC);
strncpy(reply + keylen, hours, 3);
keylen += strlen(hours);
reply[keylen++] = ',';
char mins[4];
itoa(now.min, mins, DEC);
strncpy(reply + keylen, mins, 3);
keylen += strlen(mins);
reply[keylen++] = NULL;
DEBUG("Sending time: ");
DEBUGLN(reply);
Udp.beginPacket(ip, 8888);
Udp.write(reply);
Udp.endPacket();
}
bool isWorkTime(byte t, byte from, byte to) {
if (from == to) return 1;
else if (from < to) {
if (t >= from && t < to) return 1;
else return 0;
} else {
if (t >= from || t < to) return 1;
else return 0;
}
}

View File

@@ -0,0 +1,68 @@
#ifndef TimeRandom_h
#define TimeRandom_h
#include <Arduino.h>
class TimeRandom {
public:
// установить канал (по умолч 0)
void setChannel(byte channel) {
_c = channel;
}
// обновить ЧМС
void update(byte h, byte m, byte s) {
_h = h;
_m = m;
_s = s;
}
// количество секунд с начала суток
uint32_t getSec() {
return (_h * 3600ul + _m * 60 + _s);
}
// количество минут с начала суток
uint32_t getMin() {
return (_h * 60 + _m);
}
// случайное число, обновляется каждые every секунд
uint16_t fromSec(int every) {
uint16_t s = getSec() / every;
uint16_t val = (uint16_t)(_c + 1) * (_h + 1) * (_m + 1) * (s + 1);
for (uint16_t i = 0; i < s & 0b1111; i++) val = (val * 2053ul) + 13849;
return val;
}
// случайное число от 0 до max, обновляется каждые every секунд
uint16_t fromSec(byte every, uint16_t max) {
return ((uint32_t)max * fromSec(every)) >> 16;
}
// случайное число от min до max, обновляется каждые every секунд
uint16_t fromSec(byte every, uint16_t min, uint16_t max) {
return (fromSec(every, max - min) + min);
}
// случайное число, обновляется каждые every минут
uint16_t fromMin(int every) {
uint16_t m = getMin() / every;
uint16_t val = (uint16_t)(_c + 1) * (_h + 1) * (m + 1);
for (uint16_t i = 0; i < m & 0b1111; i++) val = (val * 2053ul) + 13849;
return val;
}
// случайное число от 0 до max, обновляется каждые every минут
uint16_t fromMin(byte every, uint16_t max) {
return ((uint32_t)max * fromMin(every)) >> 16;
}
// случайное число от min до max, обновляется каждые every минут
uint16_t fromMin(byte every, uint16_t min, uint16_t max) {
return (fromMin(every, max - min) + min);
}
private:
byte _h = 0, _m = 0, _s = 0, _c = 0;
};
#endif

View File

@@ -0,0 +1,36 @@
class timerMillis {
public:
timerMillis() {}
timerMillis(uint32_t interval, bool active = false) {
_interval = interval;
reset();
if (active) restart();
else stop();
}
void setInterval(uint32_t interval) {
_interval = (interval == 0) ? 1 : interval;
}
boolean isReady() {
if (_active && millis() - _tmr >= _interval) {
//_tmr += _interval;
reset();
return true;
}
return false;
}
void reset() {
_tmr = millis();
}
void restart() {
reset();
_active = true;
}
void stop() {
_active = false;
}
private:
uint32_t _tmr = 0;
uint32_t _interval = 0;
boolean _active = false;
};