forked from PlummersSoftwareLLC/NightDriverStrip
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathledstripeffect.h
636 lines (519 loc) · 22.6 KB
/
ledstripeffect.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
//+--------------------------------------------------------------------------
//
// File: LEDStripEffect.h
//
// NightDriverStrip - (c) 2018 Plummer's Software LLC. All Rights Reserved.
//
// This file is part of the NightDriver software project.
//
// NightDriver 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.
//
// NightDriver 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 Nightdriver. It is normally found in copying.txt
// If not, see <https://www.gnu.org/licenses/>.
//
// Description:
//
// Defines the base class for effects that run locally on the chip
//
// History: Apr-13-2019 Davepl Created for NightDriverStrip
//
//---------------------------------------------------------------------------
#pragma once
#include "effects.h"
#include "jsonserializer.h"
#include "types.h"
#include "gfxbase.h"
#include "ledmatrixgfx.h"
#include <memory>
#include <list>
#include <stdlib.h>
// Declarations related to effect settings, and their SettingSpecs. The definitions revolving around
// SettingSpecs are mainly there because getting it right is a bit finnicky due to the container type
// used and the class static nature of the actual containers. Particularly adding an initializer for
// a SettingSpec container is easy to overlook.
// The type for effect SettingSpec containers
using EffectSettingSpecs = std::vector<SettingSpec, psram_allocator<SettingSpec>>;
// Declares a static class member variable that contains the SettingSpecs for an effect, if it has them.
// If an effect uses this macro, it also needs a matching INIT_EFFECT_SETTING_SPECS invocation in
// effects.cpp, or linker errors will ensue.
#define DECLARE_EFFECT_SETTING_SPECS(memberName) \
static EffectSettingSpecs memberName
// Initializes the effect setting specs member that's been added to an effect class. There must be one
// use of this in effects.cpp for every use DECLARE_EFFECT_SETTING_SPECS, or the linker will balk.
#define INIT_EFFECT_SETTING_SPECS(effectName, specsMember) \
EffectSettingSpecs effectName::specsMember = {}
// This macro returns from the invoking function (which would usually be SetSetting())
// if the settingName and propertyName passed to it match, and the "value" was thus
// assigned to the "property".
#define RETURN_IF_SET(settingName, propertyName, property, value) \
if (SetIfSelected(settingName, propertyName, property, value)) \
return true
// LEDStripEffect
//
// Base class for an LED strip effect. At a minimum they must draw themselves and provide a unique name.
class LEDStripEffect : public IJSONSerializable
{
private:
// This enum is a set of bit flags of known JSON data migrations that either have or have not (yet) been
// performed for a particular effect. "All" should always be a bitwise OR of all other flags in the enum.
enum class JSONMigrations : uint
{
MaximumEffectTime = 1, // next one = 2, one after that = 4, etc.
All = MaximumEffectTime // | next one | one after that | etc.
};
DECLARE_EFFECT_SETTING_SPECS(_baseSettingSpecs);
std::vector<std::reference_wrapper<SettingSpec>> _settingSpecReferences;
bool _coreEffect = false;
// This "lazy loads" the SettingSpec instances for LEDStripEffect. Note that it adds the actual
// instances to a static vector, meaning they are loaded once for all effects. The _settingSpecReferences
// instance variable vector only contains reference_wrappers to the actual SettingSpecs to save
// memory.
void FillBaseSettingSpecs()
{
// If the base SettingSpec instances already exist, bail out...
if (_baseSettingSpecs.size() != 0)
return;
// ...otherwise, create and add them
_baseSettingSpecs.emplace_back(
ACTUAL_NAME_OF(_friendlyName),
"Friendly name",
"The friendly name of the effect, as shown in the web UI and/or on the matrix.",
SettingSpec::SettingType::String
);
_baseSettingSpecs.emplace_back(
ACTUAL_NAME_OF(_maximumEffectTime),
"Maximum effect time",
"The maximum time in ms that the effect is shown per effect rotation. This duration is only applied if it's "
"shorter than the default effect interval. A value of 0 means no maximum effect time is set.",
SettingSpec::SettingType::PositiveBigInteger
);
_baseSettingSpecs.emplace_back(
"hasMaximumEffectTime",
"Has maximum effect time set",
"Indicates if the effect has a maximum effect time set.",
SettingSpec::SettingType::Boolean
).Access = SettingSpec::SettingAccess::ReadOnly;
_baseSettingSpecs.emplace_back(
"clearMaximumEffectTime",
"Clear maximum effect time",
"Clear maximum effect time. Set to true to reset the maximum effect time to the default value.",
SettingSpec::SettingType::Boolean
).Access = SettingSpec::SettingAccess::WriteOnly;
}
protected:
size_t _cLEDs = 0;
int _effectNumber;
String _friendlyName;
bool _enabled = true;
size_t _maximumEffectTime = 0;
// JSON document size used for serializations of this class. Should probably be made bigger for effects (i.e. subclasses)
// that serialize additional properties.
static constexpr int _jsonSize = 192;
std::vector<std::shared_ptr<GFXBase>> _GFX;
// Macro that assigns a value to a property if two names match
#define SET_IF_NAMES_MATCH(firstName, secondName, property, value) if (firstName == secondName) \
{ \
property = (value); \
return true; \
} \
return false
// Helper functions for known setting types, as defined in SettingSpec::SettingType
bool SetIfSelected(const String& settingName, const String& propertyName, int& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, value.toInt());
}
bool SetIfSelected(const String& settingName, const String& propertyName, size_t& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, strtoul(value.c_str(), NULL, 10));
}
bool SetIfSelected(const String& settingName, const String& propertyName, float& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, value.toFloat());
}
bool SetIfSelected(const String& settingName, const String& propertyName, bool& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, BoolFromText(value));
}
bool SetIfSelected(const String& settingName, const String& propertyName, String& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, value);
}
bool SetIfSelected(const String& settingName, const String& propertyName, CRGBPalette16& property, const String& value)
{
if (settingName != propertyName)
return false;
StaticJsonDocument<384> src;
deserializeJson(src, value);
CRGB colors[16];
int colorIndex = 0;
const auto & componentsArray = src.as<JsonArrayConst>();
for (const auto &v: componentsArray)
{
colors[colorIndex++] = v.as<CRGB>();
}
property = CRGBPalette16(colors);
return true;
}
bool SetIfSelected(const String& settingName, const String& propertyName, CRGB& property, const String& value)
{
SET_IF_NAMES_MATCH(settingName, propertyName, property, CRGB(strtoul(value.c_str(), NULL, 10)));
}
// Overrides of this method should fill the respective effect's SettingSpec vector and return a pointer to it.
// Returning nullptr indicates the effect has no SettingSpec instances to add to the base set.
virtual EffectSettingSpecs* FillSettingSpecs()
{
return nullptr;
}
static float fmap(const float x, const float in_min, const float in_max, const float out_min, const float out_max)
{
return (out_max - out_min) * (x - in_min) / (in_max - in_min) + out_min;
}
public:
LEDStripEffect(int effectNumber, const String & strName) :
_effectNumber(effectNumber)
{
if (!strName.isEmpty())
_friendlyName = strName;
}
LEDStripEffect(const JsonObjectConst& jsonObject)
: _effectNumber(jsonObject[PTY_EFFECTNR]),
_friendlyName(jsonObject["fn"].as<String>())
{
if (jsonObject.containsKey("es"))
_enabled = jsonObject["es"].as<int>() == 1;
if (jsonObject.containsKey("mt"))
_maximumEffectTime = jsonObject["mt"];
// Pull the migrations bitmap from the JSON object if it has one, otherwise default to "nothing set"
uint performedMigrations = 0;
if (jsonObject.containsKey("mi"))
performedMigrations = jsonObject["mi"];
// If we haven't migrated the "has no maximum effect time" yet, do so now
if (!(performedMigrations & to_value(JSONMigrations::MaximumEffectTime)) && _maximumEffectTime == UINT_MAX)
_maximumEffectTime = 0;
}
virtual ~LEDStripEffect()
{
}
virtual bool Init(std::vector<std::shared_ptr<GFXBase>>& gfx)
{
debugV("Init %s", _friendlyName.c_str());
_GFX = gfx; // There are up to 8 channel in play per effect and when we
// start up, we are given copies to their graphics interfaces
// so that we can call them directly later from other calls
_cLEDs = _GFX[0]->GetLEDCount();
debugV("Init Effect %s with %zu LEDs\n", _friendlyName.c_str(), _cLEDs);
return true;
}
virtual void Start() {} // Optional method called when time to clean/init the effect
virtual void Draw() = 0; // Your effect must implement these
std::shared_ptr<GFXBase> g(size_t channel = 0) const
{
return _GFX[channel];
}
// mg is a shortcut for MATRIX projects to retrieve a pointer to the specialized LEDMatrixGFX type
#if USE_HUB75
std::shared_ptr<LEDMatrixGFX> mg(size_t channel = 0)
{
return std::static_pointer_cast<LEDMatrixGFX>(_GFX[channel]);
}
#endif
#if HEXAGON
std::shared_ptr<HexagonGFX> hg(size_t channel = 0)
{
return std::static_pointer_cast<HexagonGFX>(_GFX[channel]);
}
#endif
virtual bool CanDisplayVUMeter() const
{
return true;
}
virtual const String & FriendlyName() const // User-visible effect name
{
return _friendlyName;
}
int EffectNumber() const
{
return _effectNumber;
}
virtual size_t DesiredFramesPerSecond() const // Desired framerate of the LED drawing
{
return 30;
}
virtual size_t MaximumEffectTime() const // For splash screens and similar, a max display time for the effect
{
return _maximumEffectTime;
}
virtual bool HasMaximumEffectTime() const
{
return MaximumEffectTime() != 0;
}
virtual bool ShouldShowTitle() const // True if the effect should show the title overlay
{
return true;
}
// RequiresDoubleBuffering
//
// If a matrix effect requires the state of the last buffer be preserved, then it requires double buffering.
// If, on the other hand, it renders from scratch every time, starting witha black fill, etc, then it does not,
// and it can override this method and return false;
virtual bool RequiresDoubleBuffering() const
{
return true;
}
// RandomRainbowColor
//
// Returns a random color of the rainbow
// BUGBUG Should likely be in GFXBase, not in LEDStripEffect
static CRGB RandomRainbowColor()
{
static const CRGB colors[] =
{
CRGB::Green,
CRGB::Red,
CRGB::Blue,
CRGB::Orange,
CRGB::Indigo,
CRGB::Violet
};
int randomColorIndex = random_range(0U, std::size(colors));
return colors[randomColorIndex];
}
// RandomSaturatedColor
//
// A random, but fully saturated, color
static CRGB RandomSaturatedColor()
{
CRGB c;
c.setHSV(random_range(0,255), 255, 255);
return c;
}
// GetBlackBodyHeatColor
//
// Given a temp in the 0-1 range, returns a fire-appropriate black body radiator color for it
virtual CRGB GetBlackBodyHeatColor(float temp) const
{
return ColorFromPalette(HeatColors_p, 255 * temp);
}
// The variant allows you to specify a base flame color other than red, and the result
// is interpolated from black to your color and on through yellow and white
virtual CRGB GetBlackBodyHeatColor(float temp, CRGB baseColor) const
{
if (baseColor == CRGB::Red)
return GetBlackBodyHeatColor(temp);
temp = std::clamp(temp, 0.0f, 1.0f);
if (temp < 0.33f)
return ColorFraction(baseColor, temp * 3.0f); // Interpolate from black to baseColor
if (temp < 0.66f)
return baseColor + ColorFraction(CRGB::Yellow - baseColor, (temp - 0.33f) * 3.0f); // Interoplate from baseColor to Yellow
return CRGB::Yellow + ColorFraction(CRGB::Blue, (temp - 0.66f) * 3.0f); // Interpolate from Yellow to White
}
// fillSolidOnAllChannels
//
// Fill all of the LEDs specified with the color indicated. Can have arbitrary start, length, and step
void fillSolidOnAllChannels(CRGB color, int iStart = 0, int numToFill = 0, uint everyN = 1)
{
if (!_GFX[0])
throw std::runtime_error("Graphics not set up properly");
if (numToFill == 0)
numToFill = _cLEDs-iStart;
if (iStart + numToFill > _cLEDs)
{
printf("Boundary Exceeded in FillRainbow");
return;
}
for (auto& device : _GFX)
{
for (int i = iStart; i < iStart + numToFill; i+= everyN)
device->setPixel(i, color);
}
}
// SetPixelsFOnAllChannels
//
// Smooth drawing on fractional pixels on all channels in the given color; if merge is specified,
void setPixelsFOnAllChannels(float fPos, float count, CRGB c, bool bMerge = false)
{
for (auto& device : _GFX)
device->setPixelsF(fPos, count, c, bMerge);
}
// ClearFrameOnAllChannels
//
// Clears ALL the channels
void ClearFrameOnAllChannels()
{
for (auto& device : _GFX)
device->Clear();
}
// ColorFraction
//
// Returns a fraction of a color; abstracts the fadeToBlack away so that we can later
// do better color correction as needed
static CRGB ColorFraction(const CRGB colorIn, float fraction)
{
fraction = std::clamp(fraction, 0.0f, 1.0f);
return CRGB(colorIn).fadeToBlackBy(255 * (1.0f - fraction));
}
// fillRainbowAllChannels
//
// Fill all channels with a progressive rainbow, using arbitrary start, length, step, and initial color and hue change rate
void fillRainbowAllChannels(int iStart, int numToFill, uint8_t initialhue, uint8_t deltahue, uint8_t everyNth = 1)
{
if (iStart + numToFill > _cLEDs)
{
printf("Boundary Exceeded in FillRainbow");
return;
}
CHSV hsv;
hsv.hue = initialhue;
hsv.val = 255;
hsv.sat = 240;
for (int i = 0; i < numToFill; i+=everyNth)
{
CRGB rgb;
hsv2rgb_rainbow(hsv, rgb);
setPixelOnAllChannels(iStart + i, rgb);
hsv.hue += deltahue;
for (int q = 1; q < everyNth; q++)
_GFX[q]->setPixel(iStart + i + q, CRGB::Black);
}
}
// fadePixelToBlackOnAllChannelsBy
//
// Given a 0-255 fade value, fades all channels by that amount
void fadePixelToBlackOnAllChannelsBy(int pixel, uint8_t fadeValue) const
{
for (auto& device : _GFX)
{
CRGB crgb = device->getPixel(pixel);
crgb.fadeToBlackBy(fadeValue);
device->setPixel(pixel, crgb);
}
}
void fadeAllChannelsToBlackBy(uint8_t fadeValue) const
{
for (int i = 0; i < _cLEDs; i++)
fadePixelToBlackOnAllChannelsBy(i, fadeValue);
}
void setAllOnAllChannels(uint8_t r, uint8_t g, uint8_t b) const
{
for (auto& device : _GFX)
for (int i = 0; i < _cLEDs; i++)
device->setPixel(i, r, g, b);
}
// setPixelOnAllChannels
//
// Sets the indexed pixel to a given color on all channels
void setPixelOnAllChannels(int i, CRGB c)
{
for (auto& device : _GFX)
device->setPixel(i, c);
}
void setPixelOnAllChannels(int x, int y, CRGB c)
{
for (auto& device : _GFX)
device->setPixel(x, y, c);
}
// setPixelsOnAllChannels
//
// Smooth drawing on fractional pixels on all channels in the given color; if merge is specified,
// color drawing is additive, otherwise replacement
void setPixelsOnAllChannels(float fPos, float count, CRGB c, bool bMerge = false) const
{
for (auto& device : _GFX)
device->setPixelsF(fPos, count, c, bMerge);
}
// SerializeToJSON
//
// Serialize this effects paramters to a JSON document
bool SerializeToJSON(JsonObject& jsonObject) override
{
StaticJsonDocument<_jsonSize> jsonDoc;
jsonDoc[PTY_EFFECTNR] = _effectNumber;
jsonDoc["fn"] = _friendlyName;
jsonDoc["es"] = _enabled ? 1 : 0;
// Migrations are done when the effect is constructed from JSON, so by definition all known
// migrations have been performed by the time we get here.
jsonDoc["mi"] = to_value(JSONMigrations::All);
// Only add the max effect time and core effect flag if they're not the default, to save space
if (HasMaximumEffectTime())
jsonDoc["mt"] = _maximumEffectTime;
if (_coreEffect)
jsonDoc[PTY_COREEFFECT] = 1;
assert(!jsonDoc.overflowed());
return jsonObject.set(jsonDoc.as<JsonObjectConst>());
}
virtual bool IsEnabled() const
{
return _enabled;
}
virtual void SetEnabled(bool enabled)
{
_enabled = enabled;
}
void MarkAsCoreEffect()
{
_coreEffect = true;
}
bool IsCoreEffect() const
{
return _coreEffect;
}
// Lazily loads the SettingsSpecs for this effect if they haven't been loaded yet, and
// returns a vector with reference_wrappers to them.
virtual const std::vector<std::reference_wrapper<SettingSpec>>& GetSettingSpecs()
{
// If the SettingSpecs reference_wrapper vector is already filled, return that
if (_settingSpecReferences.size() > 0)
return _settingSpecReferences;
// Create the SettingSpec instances that are available for all effects
FillBaseSettingSpecs();
// Add reference_wrappers for the base SettingSpecs instances to the vector
_settingSpecReferences.insert(_settingSpecReferences.end(), _baseSettingSpecs.begin(), _baseSettingSpecs.end());
// Get any SettingSpec instances that our effect subclass has defined
auto pEffectSettingSpecs = FillSettingSpecs();
if (pEffectSettingSpecs)
{
// Add reference_wrappers for the effect SettingSpecs instances to the vector
_settingSpecReferences.insert(_settingSpecReferences.end(), pEffectSettingSpecs->begin(), pEffectSettingSpecs->end());
}
return _settingSpecReferences;
}
// Serialize the "known effect settings" for this effect to JSON. In principle, there
// should be a SettingSpec instance returned by GetSettingSpecs() for every setting value
// that's serialized by this function.
virtual bool SerializeSettingsToJSON(JsonObject& jsonObject)
{
StaticJsonDocument<_jsonSize> jsonDoc;
jsonDoc[ACTUAL_NAME_OF(_friendlyName)] = _friendlyName;
jsonDoc[ACTUAL_NAME_OF(_maximumEffectTime)] = _maximumEffectTime;
jsonDoc["hasMaximumEffectTime"] = HasMaximumEffectTime();
if (jsonDoc.overflowed())
debugE("JSON buffer overflow while serializing settings for LEDStripEffect - object incomplete!");
return jsonObject.set(jsonDoc.as<JsonObjectConst>());
}
// Changes the value for one "known" effect setting. All setting values are passed to this
// function are Strings; the conversion to the target type of a member variable that
// corresponds with a setting can be taken care of by using one of the SetIfSelected()
// overloads (either via RETURN_IF_SET or directly).
virtual bool SetSetting(const String& name, const String& value)
{
RETURN_IF_SET(name, ACTUAL_NAME_OF(_friendlyName), _friendlyName, value);
RETURN_IF_SET(name, ACTUAL_NAME_OF(_maximumEffectTime), _maximumEffectTime, value);
bool clearMaximumEffectTime = false;
if (SetIfSelected(name, "clearMaximumEffectTime", clearMaximumEffectTime, value))
{
if (clearMaximumEffectTime)
_maximumEffectTime = 0;
return true;
}
return false;
}
};