muwerk mupplet Core Library
muwerk applets; mupplets: functional units that support specific hardware or reusable applications
Loading...
Searching...
No Matches
mup_neopixel.h
1#pragma once
2
3#include "ustd_array.h"
4#include "scheduler.h"
5#include "mupplet_core.h"
6#include "helper/mup_astro.h"
7#include "Adafruit_NeoPixel.h"
8
9namespace ustd {
10
11uint32_t RGB32(uint8_t r, uint8_t g, uint8_t b) {
12 return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
13}
14void RGB32Parse(uint32_t rgb, uint8_t *r = nullptr, uint8_t *g = nullptr,
15 uint8_t *b = nullptr) {
16 if (r)
17 *r = (uint8_t)((rgb >> 16) & 0xff);
18 if (g)
19 *g = (uint8_t)((rgb >> 8) & 0xff);
20 if (b)
21 *b = (uint8_t)(rgb & 0xff);
22}
23
24class SpecialEffects {
25 public:
26 static const int effectCount = 7;
27 enum EffectType : uint16_t { Default = 0,
28 ButterLamp = 1,
29 Fire = 2,
30 Waves = 3,
31 Forest = 4,
32 Evening = 5,
33 Concentration = 6
34 };
35 static constexpr const char *effectName[effectCount] = {"Static", "Butterlamp", "Fire", "Waves", "Forest", "Evening", "Concentration"};
36 uint16_t rows, cols;
37 SpecialEffects(uint16_t rows, uint16_t cols)
38 : rows(rows), cols(cols) {
39 }
40 ~SpecialEffects() {
41 }
42
43 bool setFrame(EffectType type, ustd::array<uint32_t> *pf) {
44 switch (type) {
45 default:
46 return false;
47 case EffectType::ButterLamp:
48 return butterLampFrame(pf, rows, cols);
49 case EffectType::Forest:
50 return forestFrame(pf, rows, cols);
51 case EffectType::Waves:
52 return wavesFrame(pf, rows, cols);
53 }
54 return false;
55 }
56
57 bool bUseModulator = false;
58 time_t manualSet = 0;
59 bool useAutoTimer = false;
60 uint8_t start_hour = 18, start_minute = 0, end_hour = 0, end_minute = 0;
61 bool bUnitBrightness = true;
62 double unitBrightness = 1.0;
63 double amp = 20.0;
64 double oldMx = -1.0;
65
66 void configButterLampModulator(bool _bUseModulator, bool _bUseAutoTimer, uint8_t _start_hour, uint8_t _start_minute, uint8_t _end_hour, uint8_t _end_minute) {
67 bUseModulator = _bUseModulator;
68 useAutoTimer = _bUseAutoTimer;
69 start_hour = _start_hour;
70 start_minute = _start_minute;
71 end_hour = _end_hour;
72 end_minute = _end_minute;
73 }
74
75 void configButterLampState(bool _bUnitBrightness = true, double _unitBrightness = 1.0) {
76 bUnitBrightness = _bUnitBrightness;
77 unitBrightness = _unitBrightness;
78 manualSet = time(nullptr);
79 }
80
81 double butterLampModulator() {
82 double m1 = 1.0;
83 double m2 = 0.0;
84 time_t now = time(nullptr);
85
86 if (!bUseModulator)
87 return m1;
88 long dt = now - manualSet;
89 if (dt < 3600) {
90 m2 = (3600.0 - (double)dt) / 3600.0;
91 }
92
93 if (useAutoTimer) {
94 struct tm *pTm = localtime(&now);
95
96 if (Astro::inHourMinuteInterval(pTm->tm_hour, pTm->tm_min, start_hour, start_minute, end_hour,
97 end_minute)) {
98 int deltaAll = Astro::deltaHourMinuteTime(start_hour, start_minute, end_hour, end_minute);
99 int deltaCur =
100 Astro::deltaHourMinuteTime(start_hour, start_minute, pTm->tm_hour, pTm->tm_min);
101 if (deltaAll > 0.0)
102 m1 = (deltaAll - deltaCur) / (double)deltaAll;
103 else
104 m1 = 0.0;
105 } else {
106 m1 = 0.0;
107 }
108 } else {
109 m1 = 0.0;
110 }
111
112 if (bUnitBrightness) {
113 if (m1 > 0.0 || m2 > 0.0) {
114 m1 = m1 * unitBrightness;
115 m2 = m2 * unitBrightness;
116 }
117 }
118 if (m2 != 0.0) {
119 if (m2 > 0.75)
120 m1 = m2;
121 else
122 m1 = (m1 + m2) / 2.0;
123 }
124 return m1;
125 }
126
127 int f1 = 0, f2 = 0, max_b = 20;
128 double wind = 50;
129 bool butterLampFrame(ustd::array<uint32_t> *pf, uint16_t rows, uint16_t cols) {
134 int ce, cr, cg, cb, mf;
135 int flic[] = {4, 7, 8, 9, 10, 12, 16, 20, 32, 30, 32, 20, 24, 16, 8, 6};
136 uint16_t x, y, index, cx, cy;
137 if (pf->length() != rows * cols) {
138 for (uint16_t i = 0; i < pf->length(); i++)
139 (*pf)[i] = RGB32(255, 0, 0);
140 return false;
141 }
142
143 for (y = 0; y < rows; y++) {
144 for (x = 0; x < cols; x++) {
145 index = y * cols + x;
146 cx = x % 4;
147 cy = y % 4;
148 if ((cx == 1 || cx == 2 || cols < 4) && (cy == 1 || cy == 2 || rows < 4)) {
149 ce = 1; // centre
150 } else {
151 ce = 0;
152 }
153 if (ce == 1) { // center of one lamp
154 cr = 40;
155 cg = 15;
156 cb = 0;
157 mf = flic[f1];
158 f1 += rand() % 3 - 1;
159 if (f1 < 0)
160 f1 = 15;
161 if (f1 > 15)
162 f1 = 0;
163 mf = 32 - ((32 - mf) * wind) / 100;
164 } else { // border
165 cr = 20;
166 cg = 4;
167 cb = 0;
168 mf = flic[f2];
169 f2 += rand() % 3 - 1;
170 if (f2 < 0)
171 f2 = 15;
172 if (f2 > 15)
173 f2 = 0;
174 mf = 32 - ((32 - mf) * wind) / 100;
175 }
176
177 cr = cr + rand() % 2;
178 cg = cg + rand() % 2;
179 cb = cb + rand() % 1;
180
181 if (cr > max_b)
182 max_b = cr;
183 if (cg > max_b)
184 max_b = cg;
185 if (cb > max_b)
186 max_b = cb;
187
188 cr = (cr * amp * 4 * mf) / (max_b * 50);
189 cg = (cg * amp * 4 * mf) / (max_b * 50);
190 cb = (cb * amp * 4 * mf) / (max_b * 50);
191
192 if (cr > 255)
193 cr = 255;
194 if (cr < 0)
195 cr = 0;
196 if (cg > 255)
197 cg = 255;
198 if (cg < 0)
199 cg = 0;
200 if (cb > 255)
201 cb = 255;
202 if (cb < 0)
203 cb = 0;
204
205 if (bUseModulator) {
206 double mx = butterLampModulator();
207 double dx = fabs(oldMx - mx);
208 if (dx > 0.05) {
209 oldMx = mx;
210 }
211 cr = ((double)cr * mx);
212 cg = ((double)cg * mx);
213 cb = ((double)cb * mx);
214 }
215 (*pf)[index] = RGB32(cr, cg, cb);
216 }
217 }
218 return true;
219 }
220
221 uint8_t varyByte(uint8_t b, uint8_t var, uint8_t min = 0, uint8_t max = 255) {
222 int d = random(2 * var + 1) - var;
223 if ((int)b + d < (int)min) return min;
224 if ((int)b + d > (int)max) return max;
225 return (uint8_t)((int)b + d);
226 }
227
228 bool wavesFrame(ustd::array<uint32_t> *pf, uint16_t rows, uint16_t cols) {
229 uint16_t num, ind;
230 uint8_t r, g, b;
231 uint32_t color;
232 num = rows * cols;
233 for (int i = 0; i < 20; i++) {
234 ind = random(num);
235 color = (*pf)[ind];
236 RGB32Parse(color, &r, &g, &b);
237 b = varyByte(b, 20, 20, 170);
238 g = varyByte(g, 10, 0, 50);
239 r = varyByte(r, 10, 0, 20);
240 color = RGB32(r, g, b);
241 (*pf)[ind] = color;
242 }
243 return true;
244 }
245 bool forestFrame(ustd::array<uint32_t> *pf, uint16_t rows, uint16_t cols) {
246 uint16_t num, ind;
247 uint8_t r, g, b;
248 uint32_t color;
249 num = rows * cols;
250 for (int i = 0; i < 20; i++) {
251 ind = random(num);
252 color = (*pf)[ind];
253 RGB32Parse(color, &r, &g, &b);
254 b = varyByte(b, 10, 0, 70);
255 g = varyByte(g, 20, 20, 200);
256 r = varyByte(r, 10, 0, 30);
257 color = RGB32(r, g, b);
258 (*pf)[ind] = color;
259 }
260 return true;
261 }
262}; // SpecialEffects
263
264class NeoPixel {
265 public:
266 String NEOPIXEL_VERSION = "0.1.0";
267 Scheduler *pSched;
268 int tID;
269 String name;
270 bool bStarted = false;
271 uint8_t pin;
272 uint16_t numRows, numCols, numPixels;
273 uint8_t options;
274 uint8_t gr, gg, gb;
275 double gbr;
276 double unitBrightness;
277 Adafruit_NeoPixel *pPixels;
278 ustd::array<uint32_t> *phwBuf;
279 ustd::array<uint32_t> *phwFrameBuf;
280 bool state;
281 unsigned long ticker = 0;
282 unsigned long lastTicker = 0;
283 double zeroBrightnessUpperBound = 0.02;
284 SpecialEffects::EffectType effectType;
285 SpecialEffects *pEffects = nullptr;
286 bool isFirstLoop = true;
287 bool scheduled = false;
288 int startHour, endHour, startMin, endMin;
289
290 NeoPixel(String name, uint8_t pin, uint16_t numRows = 1, uint16_t numCols = 1,
291 uint16_t options = NEO_RGB + NEO_KHZ800)
292 : name(name), pin(pin), numRows(numRows), numCols(numCols), options(options) {
293 if (numRows < 1) numRows = 1;
294 if (numCols < 1) numCols = 1;
295 numPixels = numRows * numCols;
296 }
297
298 ~NeoPixel() {
299 // XXX Framebuffers cleanup
300 }
301
302 void begin(Scheduler *_pSched) {
303 pSched = _pSched;
304
305 pPixels = new Adafruit_NeoPixel(numPixels, pin, options);
306 phwBuf = new ustd::array<uint32_t>(numPixels, numPixels);
307 phwFrameBuf = new ustd::array<uint32_t>(numPixels, numPixels);
308 pEffects = new SpecialEffects(numRows, numCols);
309 for (uint16_t i = 0; i < numPixels; i++) {
310 pixel(i, 0, 0, 0, false);
311 }
312 pPixels->begin();
313 auto ft = [=]() { this->loop(); };
314 tID = pSched->add(ft, name, 50000);
315 auto fnall = [=](String topic, String msg, String originator) {
316 this->subsMsg(topic, msg, originator);
317 };
318 pSched->subscribe(tID, name + "/light/#", fnall);
319 pSched->subscribe(tID, "mqtt/state", fnall);
320 setEffect(SpecialEffects::EffectType::Default, true);
321 publishState();
322 publishColor();
323 bStarted = true;
324 }
325
326 void pixel(uint16_t i, uint8_t r, uint8_t g, uint8_t b, bool update = true) {
327 if (i >= numPixels)
328 return;
329 (*phwFrameBuf)[i] = RGB32(r, g, b);
330 // setEffect(SpecialEffects::EffectType::Default);
331 if (update)
332 pixelsUpdate();
333 }
334
335 bool setFrame(ustd::array<uint32_t> *pFr) {
336 if (!pFr || pFr->length() != (*phwFrameBuf).length()) return false;
337 for (uint16_t i = 0; i < numPixels; i++) {
338 (*phwBuf)[i] = (*phwFrameBuf)[i];
339 }
340 pixelsUpdate();
341 }
342
343 void setEffect(SpecialEffects::EffectType _type, bool force = false) {
344 if (_type != effectType || force) {
345 effectType = _type;
346 isFirstLoop = true;
347 publishEffect();
348 }
349 }
350
351 String getEffectList() {
353 String eff;
354 eff = "";
355 bool first = true;
356 for (int i = 0; i < SpecialEffects::effectCount; i++) {
357 if (!first) eff += ", ";
358 first = false;
359 eff += SpecialEffects::effectName[i];
360 }
361 return eff;
362 }
363
364 bool setSchedule(String startTime, String endTime) {
365 int sh, sm, eh, em;
366 if (pEffects == nullptr) return false;
367 if (!ustd::Astro::parseHourMinuteString(startTime, &sh, &sm))
368 return false;
369 if (!ustd::Astro::parseHourMinuteString(endTime, &eh, &em))
370 return false;
371 startHour = sh;
372 endHour = eh;
373 startMin = sm;
374 endMin = em;
375 if (effectType == SpecialEffects::EffectType::ButterLamp) {
376 pEffects->configButterLampModulator(true, true, sh, sm, eh, em);
377 }
378 return true;
379 }
380 void pixelsUpdate(bool notify = true) {
381 pPixels->show();
382 uint32_t st = 0;
383 double br = 0.0;
384 uint32_t dr = 0, dg = 0, db = 0;
385 uint8_t r, g, b, rs, gs, bs;
386 for (uint16_t i = 0; i < numPixels; i++) {
387 (*phwBuf)[i] = (*phwFrameBuf)[i];
388 st |= (*phwBuf)[i];
389 RGB32Parse((*phwBuf)[i], &r, &g, &b);
390 br += ((double)r + (double)g + (double)b) / 3.0;
391 dr += r;
392 dg += g;
393 db += b;
394 if (unitBrightness != 1.0) {
395 rs = (uint8_t)(r * unitBrightness);
396 gs = (uint8_t)(g * unitBrightness);
397 bs = (uint8_t)(b * unitBrightness);
398 } else {
399 rs = r;
400 gs = g;
401 bs = b;
402 }
403 pPixels->setPixelColor(i, pPixels->Color(rs, gs, bs));
404 }
405 gbr = (br / (double)numPixels) / 255.0;
406 gr = dr / numPixels;
407 gg = dg / numPixels;
408 gb = db / numPixels;
409 pPixels->show();
410 if (st && unitBrightness > zeroBrightnessUpperBound)
411 state = true;
412 else
413 state = false;
414 if (notify) {
415 publishState();
416 publishColor();
417 }
418 }
419
420 void brightness(double _unitBrightness, bool update = true, bool resetEffect = true) {
421 if (_unitBrightness < 0.0) _unitBrightness = 0.0;
422 if (_unitBrightness > 1.0) _unitBrightness = 1.0;
423 // if (_unitBrightness == 1.0 && gbr < zeroBrightnessUpperBound) color(0xff, 0xff, 0xff, false);
424 if (_unitBrightness < zeroBrightnessUpperBound) _unitBrightness = 0.0;
425 unitBrightness = _unitBrightness;
426 if (resetEffect)
427 setEffect(SpecialEffects::EffectType::Default);
428 if (update) pixelsUpdate();
429 }
430
431 void color(uint8_t r, uint8_t g, uint8_t b, bool update = true, bool resetEffect = true) {
432 for (uint16_t i = 0; i < numPixels; i++) {
433 pixel(i, r, g, b, false);
434 }
435 if (resetEffect)
436 setEffect(SpecialEffects::EffectType::Default);
437 if (update) {
438 pixelsUpdate();
439 }
440 }
441
442 void publishBrightness() {
443 char buf[32];
444 sprintf(buf, "%5.3f", unitBrightness);
445 pSched->publish(name + "/light/unitbrightness", buf);
446 }
447
448 void publishColor(int16_t index = -1) {
449 char buf[64];
450 if (index == -1) {
451 sprintf(buf, "%d,%d,%d", gr, gg, gb);
452 pSched->publish(name + "/light/color", buf);
453 } else {
454 uint8_t r, g, b;
455 RGB32Parse((*phwBuf)[index], &r, &g, &b);
456 sprintf(buf, "%d,%d,%d", r, g, b);
457 pSched->publish(name + "/light/" + String(index) + "/color", buf);
458 }
459 }
460
461 void publishEffect() {
462 String nameE = SpecialEffects::effectName[(int)effectType];
463 pSched->publish(name + "/light/effect", nameE);
464 }
465
466 void publishState() {
467 if (state) {
468 pSched->publish(name + "/light/state", "on");
469 this->state = true;
470 } else {
471 pSched->publish(name + "/light/state", "off");
472 this->state = false;
473 }
474 publishBrightness();
475 publishEffect();
476 }
477
478 void loop() {
479 if (bStarted) {
480 ++ticker;
481 switch (effectType) {
482 case SpecialEffects::EffectType::Default:
483 if (isFirstLoop) {
484 color(128, 128, 128, false, false);
485 brightness(0.2, true, false);
486 isFirstLoop = false;
487 }
488 break;
489 case SpecialEffects::EffectType::ButterLamp:
490 if (ticker % 3 != 0) return;
491 if (isFirstLoop) {
492 brightness(1.0, false, false);
493 pEffects->setFrame(effectType, phwFrameBuf);
494 pixelsUpdate(true);
495 isFirstLoop = false;
496 } else {
497 pEffects->setFrame(effectType, phwFrameBuf);
498 pixelsUpdate(false);
499 }
500 break;
501 case SpecialEffects::EffectType::Fire:
502 if (isFirstLoop) {
503 brightness(1.0, false, false);
504 pEffects->setFrame(SpecialEffects::EffectType::ButterLamp, phwFrameBuf); // Fire is 'fast' butterlamp
505 pixelsUpdate(true);
506 isFirstLoop = false;
507 } else {
508 pEffects->setFrame(SpecialEffects::EffectType::ButterLamp, phwFrameBuf); // Fire is 'fast' butterlamp
509 pixelsUpdate(false);
510 }
511 break;
512 case SpecialEffects::EffectType::Waves:
513 if (ticker % 5 != 0) return;
514 if (isFirstLoop) {
515 color(20, 50, 192, false, false);
516 brightness(0.1, false, false);
517 pEffects->setFrame(SpecialEffects::EffectType::Waves, phwFrameBuf); // Fire is 'fast' butterlamp
518 pixelsUpdate(true);
519 isFirstLoop = false;
520 } else {
521 pEffects->setFrame(SpecialEffects::EffectType::Waves, phwFrameBuf); // Fire is 'fast' butterlamp
522 pixelsUpdate(false);
523 }
524 break;
525 case SpecialEffects::EffectType::Forest:
526 if (ticker % 10 != 0) return;
527 if (isFirstLoop) {
528 color(0, 128, 0, false, false);
529 brightness(0.2, false, false);
530 pEffects->setFrame(SpecialEffects::EffectType::Forest, phwFrameBuf); // Fire is 'fast' butterlamp
531 pixelsUpdate(true);
532 isFirstLoop = false;
533 } else {
534 pEffects->setFrame(SpecialEffects::EffectType::Forest, phwFrameBuf); // Fire is 'fast' butterlamp
535 pixelsUpdate(false);
536 }
537 break;
538 case SpecialEffects::EffectType::Evening:
539 if (isFirstLoop) {
540 isFirstLoop = false;
541 color(255, 128, 0, false, false);
542 brightness(0.1, true, false);
543 }
544 break;
545 case SpecialEffects::EffectType::Concentration:
546 if (isFirstLoop) {
547 isFirstLoop = false;
548 color(128, 128, 255, false, false);
549 brightness(0.8, true, false);
550 }
551 break;
552 default:
553 break;
554 }
555 }
556 }
557
558 void subsMsg(String topic, String msg, String originator) {
559 uint8_t r, g, b;
560 String leader = name + "/light/";
561 if (topic == name + "/light/state/get") {
562 publishState();
563 } else if (topic == name + "/light/unitbrightness/get") {
564 publishBrightness();
565 } else if (topic == name + "/light/color/get") {
566 publishColor(-1);
567 } else if (topic == name + "/light/set" || topic == name + "/light/state/set" || topic == name + "/light/unitbrightness/set") {
568 // if (ticker - lastTicker < 6) return; // ignore anything that follows too "fast" after color-sets.
569 bool ab;
570 msg.toLowerCase();
571 if (msg == "on" || msg == "true")
572 ab = true;
573 else
574 ab = false;
575 double br = parseUnitLevel(msg);
576 if (ab && unitBrightness > zeroBrightnessUpperBound) br = unitBrightness;
577 brightness(br);
578 lastTicker = ticker;
579 } else if (topic == name + "/light/color/set") {
580 parseColor(msg, &r, &g, &b);
581 color(r, g, b);
582 lastTicker = ticker;
583 } else if (topic == name + "/light/effect/set") {
584 for (int eff = 0; eff < SpecialEffects::effectCount; eff++) {
585 String thisName = SpecialEffects::effectName[eff];
586 if (thisName == msg) {
587 setEffect((SpecialEffects::EffectType)eff);
588 }
589 }
590 } else if (topic.startsWith(leader)) {
591 String sub = topic.substring(leader.length());
592 int pi = sub.indexOf('/');
593 if (pi != -1) {
594 int index = (int)strtol(sub.substring(0, pi).c_str(), 0, 10);
595 if (index >= 0 && index < numPixels) {
596 String cmd = sub.substring(pi + 1);
597 if (cmd == "set") {
598 if (msg.startsWith("#") || msg.startsWith("0x") || msg.indexOf(',') != -1) {
599 if (parseColor(msg, &r, &g, &b)) {
600 pixel(index, r, g, b);
601 }
602 } else {
603 bool newState = parseBoolean(msg);
604 if (newState) {
605 pixel(index, 0xff, 0xff, 0xff);
606 } else {
607 pixel(index, 0xff, 0xff, 0xff);
608 }
609 }
610 }
611 if (cmd == "color/set") {
612 if (parseColor(msg, &r, &g, &b)) {
613 pixel(index, r, g, b);
614 }
615 }
616 if (cmd == "color/get") {
617 publishColor(index);
618 }
619 }
620 }
621 } else if (topic == "mqtt/state" && msg == "connected") {
622 publishState();
623 publishColor();
624 }
625 }
626}; // NeoPixel
627
628} // namespace ustd
static bool inHourMinuteInterval(uint8_t test_hour, uint8_t test_minute, uint8_t start_hour, uint8_t start_minute, uint8_t end_hour, uint8_t end_minute)
Definition: mup_astro.h:256
static int deltaHourMinuteTime(uint8_t h1, uint8_t m1, uint8_t h2, uint8_t m2)
Definition: mup_astro.h:236
The muwerk namespace.
Definition: home_assistant.h:10
int8_t parseBoolean(String arg)
Definition: mupplet_core.h:51
double parseUnitLevel(String arg)
Definition: mupplet_core.h:154
bool parseColor(String arg, uint8_t *pr, uint8_t *pg, uint8_t *pb, uint8_t *pw=nullptr, uint8_t *pww=nullptr)
Definition: mupplet_core.h:228