FastLED 3.9.15
Loading...
Searching...
No Matches
Chromancer.ino
Go to the documentation of this file.
1// @filter: (memory is large)
2
12
13/*
14 Original Source: https://github.com/ZackFreedman/Chromance
15 GaryWoo's Video: https://www.youtube.com/watch?v=-nSCtxa2Kp0
16 GaryWoo's LedMap: https://gist.github.com/Garywoo/b6cd1ea90cb5e17cc60b01ae68a2b770
17 GaryWoo's presets: https://gist.github.com/Garywoo/82fa67c6e1f9529dc16a01dd97d05d58
18 Chromance wall hexagon source (emotion controlled w/ EmotiBit)
19 Partially cribbed from the DotStar example
20 I smooshed in the ESP32 BasicOTA sketch, too
21
22 (C) Voidstar Lab 2021
23*/
24
25// FastLED.h must be included first to trigger precompiled headers for FastLED's build system
26#include "FastLED.h"
27
29#include "fl/log/log.h"
30#include "fl/stl/stdio.h"
31
32#include <FastLED.h>
33
34#include "fl/math/screenmap.h"
35#include "fl/math/math.h"
36#include "fl/stl/json.h"
37#include "fl/ui/ui.h"
38#include "fl/stl/map.h"
39
40#include "fl/stl/string.h"
41
42#include "./screenmap.json.h"
43#include "./mapping.h"
44#include "./ripple.h"
45#include "./detail.h"
46
47enum {
52};
53
54
55// Strips are different lengths because I am a dumb
56
57constexpr int lengths[] = {
58 154, // Black strip
59 168, // Green strip
60 84, // Red strip
61 154 // Blue strip
62};
63
64
65
66
67
68
69
70// non emscripten uses separate arrays for each strip. Eventually emscripten
71// should support this as well but right now we don't
74CRGB leds2[lengths[RedStrip]] = {}; // Red
77
78
79byte ledColors[40][14][3]; // LED buffer - each ripple writes to this, then we
80 // write this to the strips
81//float decay = 0.97; // Multiply all LED's by this amount each tick to create
82 // fancy fading tails
83
84fl::UISlider sliderDecay("decay", .97f, .8, 1.0, .01);
85
86// These ripples are endlessly reused so we don't need to do any memory
87// management
88#define numberOfRipples 30
90 Ripple(0), Ripple(1), Ripple(2), Ripple(3), Ripple(4), Ripple(5),
91 Ripple(6), Ripple(7), Ripple(8), Ripple(9), Ripple(10), Ripple(11),
92 Ripple(12), Ripple(13), Ripple(14), Ripple(15), Ripple(16), Ripple(17),
93 Ripple(18), Ripple(19), Ripple(20), Ripple(21), Ripple(22), Ripple(23),
94 Ripple(24), Ripple(25), Ripple(26), Ripple(27), Ripple(28), Ripple(29),
95};
96
97// Biometric detection and interpretation
98// IR (heartbeat) is used to fire outward ripples
99float lastIrReading; // When our heart pumps, reflected IR drops sharply
100float highestIrReading; // These vars let us detect this drop
101unsigned long
102 lastHeartbeat; // Track last heartbeat so we can detect noise/disconnections
103#define heartbeatLockout \
104 500 // Heartbeats that happen within this many milliseconds are ignored
105#define heartbeatDelta 300 // Drop in reflected IR that constitutes a heartbeat
106
107// Heartbeat color ripples are proportional to skin temperature
108#define lowTemperature 33.0 // Resting temperature
109#define highTemperature 37.0 // Really fired up
112 2.0; // Carries skin temperature from temperature callback to IR callback
113
114// EDA code was too unreliable and was cut.
115// TODO: Rebuild EDA code
116
117// Gyroscope is used to reject data if you're moving too much
118#define gyroAlpha 0.9 // Exponential smoothing constant
119#define gyroThreshold \
120 300 // Minimum angular velocity total (X+Y+Z) that disqualifies readings
122
123// If you don't have an EmotiBit or don't feel like wearing it, that's OK
124// We'll fire automatic pulses
125#define randomPulsesEnabled true // Fire random rainbow pulses from random nodes
126#define cubePulsesEnabled true // Draw cubes at random nodes
127fl::UICheckbox starburstPulsesEnabled("Starburst Pulses", true);
128fl::UICheckbox simulatedBiometricsEnabled("Simulated Biometrics", true);
129
130#define autoPulseTimeout \
131 5000 // If no heartbeat is received in this many ms, begin firing
132 // random/simulated pulses
133#define randomPulseTime 2000 // Fire a random pulse every (this many) ms
134unsigned long lastRandomPulse;
136
140#define autoPulseChangeTime 30000
142
143#define simulatedHeartbeatBaseTime \
144 600 // Fire a simulated heartbeat pulse after at least this many ms
145#define simulatedHeartbeatVariance \
146 200 // Add random jitter to simulated heartbeat
147#define simulatedEdaBaseTime 1000 // Same, but for inward EDA pulses
148#define simulatedEdaVariance 10000
150unsigned long nextSimulatedEda;
151
152// Helper function to check if a node is on the border
153bool isNodeOnBorder(byte node) {
154 for (int i = 0; i < numberOfBorderNodes; i++) {
155 if (node == borderNodes[i]) {
156 return true;
157 }
158 }
159 return false;
160}
161
162fl::UITitle title("Chromancer");
163fl::UIDescription description("Take 6 seconds to boot up. Chromancer is a wall-mounted hexagonal LED display that originally reacted to biometric data from an EmotiBit sensor. It visualizes your heartbeat, skin temperature, and movement in real-time. Chromancer also has a few built-in effects that can be triggered with the push of a button. Enjoy!");
164fl::UICheckbox allWhite("All White", false);
165
166fl::UIButton simulatedHeartbeat("Simulated Heartbeat");
167fl::UIButton triggerStarburst("Trigger Starburst");
175bool wasSpiralClicked = false;
176
177// Group related UI elements using fl::UIGroup template multi-argument constructor
181
182void setup() {
183 Serial.begin(115200);
184
185 Serial.println("*** LET'S GOOOOO ***");
186
187 Serial.println("JSON SCREENMAP");
188 Serial.println(JSON_SCREEN_MAP);
189
192
193 fl::printf("Parsed %d segment maps\n", int(segmentMaps.size()));
194 for (auto kv : segmentMaps) {
195 Serial.print(kv.first.c_str());
196 Serial.print(" ");
197 Serial.println(kv.second.getLength());
198 }
199
200
201 // fl::ScreenMap screenmaps[4];
202 fl::ScreenMap red, black, green, blue;
203 bool ok = true;
204
205 auto red_it = segmentMaps.find("red_segment");
206 ok = (red_it != segmentMaps.end()) && ok;
207 if (red_it != segmentMaps.end()) red = red_it->second;
208
209 auto black_it = segmentMaps.find("back_segment");
210 ok = (black_it != segmentMaps.end()) && ok;
211 if (black_it != segmentMaps.end()) black = black_it->second;
212
213 auto green_it = segmentMaps.find("green_segment");
214 ok = (green_it != segmentMaps.end()) && ok;
215 if (green_it != segmentMaps.end()) green = green_it->second;
216
217 auto blue_it = segmentMaps.find("blue_segment");
218 ok = (blue_it != segmentMaps.end()) && ok;
219 if (blue_it != segmentMaps.end()) blue = blue_it->second;
220 if (!ok) {
221 Serial.println("Failed to get all segment maps");
222 return;
223 }
224
225
226 CRGB* red_leds = leds[RedStrip];
227 CRGB* black_leds = leds[BlackStrip];
228 CRGB* green_leds = leds[GreenStrip];
229 CRGB* blue_leds = leds[BlueStrip];
230
231 FastLED.addLeds<WS2812, 2>(black_leds, lengths[BlackStrip]).setScreenMap(black);
232 FastLED.addLeds<WS2812, 3>(green_leds, lengths[GreenStrip]).setScreenMap(green);
233 FastLED.addLeds<WS2812, 1>(red_leds, lengths[RedStrip]).setScreenMap(red);
234 FastLED.addLeds<WS2812, 4>(blue_leds, lengths[BlueStrip]).setScreenMap(blue);
235
236 FastLED.show();
237}
238
239
240void loop() {
241 unsigned long benchmark = millis();
242 FL_UNUSED(benchmark);
243
244 // Fade all dots to create trails
245 for (int strip = 0; strip < 40; strip++) {
246 for (int led = 0; led < 14; led++) {
247 for (int i = 0; i < 3; i++) {
248 ledColors[strip][led][i] *= sliderDecay.value();
249 }
250 }
251 }
252
253 for (int i = 0; i < numberOfRipples; i++) {
254 ripples[i].advance(ledColors);
255 }
256
257 for (int segment = 0; segment < 40; segment++) {
258 for (int fromBottom = 0; fromBottom < 14; fromBottom++) {
259 int strip = ledAssignments[segment][0];
260 int led = fl::round(fmap(fromBottom, 0, 13, ledAssignments[segment][2],
261 ledAssignments[segment][1]));
262 leds[strip][led] = CRGB(ledColors[segment][fromBottom][0],
263 ledColors[segment][fromBottom][1],
264 ledColors[segment][fromBottom][2]);
265 }
266 }
267
268 if (allWhite) {
269 // for all strips
270 for (int i = 0; i < 4; i++) {
271 for (int j = 0; j < lengths[i]; j++) {
272 leds[i][j] = CRGB::White;
273 }
274 }
275 }
276
277 FastLED.show();
278
279
280 // Check if buttons were clicked
286
287 if (wasSpiralClicked) {
288 // Trigger spiral wave effect from center
289 unsigned int baseColor = random(0xFFFF);
290 byte centerNode = 15; // Center node
291
292 // Create 6 ripples in a spiral pattern
293 for (int i = 0; i < 6; i++) {
294 if (nodeConnections[centerNode][i] >= 0) {
295 for (int j = 0; j < numberOfRipples; j++) {
296 if (ripples[j].state == dead) {
297 ripples[j].start(
298 centerNode, i,
300 baseColor + (0xFFFF / 6) * i, 255, 255),
301 0.3 + (i * 0.1), // Varying speeds creates spiral effect
302 2000,
303 i % 2 ? alwaysTurnsLeft : alwaysTurnsRight); // Alternating turn directions
304 break;
305 }
306 }
307 }
308 }
309 lastHeartbeat = millis();
310 }
311
313 // Trigger immediate border wave effect
314 unsigned int baseColor = random(0xFFFF);
315
316 // Start ripples from each border node in sequence
317 for (int i = 0; i < numberOfBorderNodes; i++) {
318 byte node = borderNodes[i];
319 // Find an inward direction
320 for (int dir = 0; dir < 6; dir++) {
321 if (nodeConnections[node][dir] >= 0 &&
322 !isNodeOnBorder(nodeConnections[node][dir])) {
323 for (int j = 0; j < numberOfRipples; j++) {
324 if (ripples[j].state == dead) {
325 ripples[j].start(
326 node, dir,
328 baseColor + (0xFFFF / numberOfBorderNodes) * i,
329 255, 255),
330 .4, 2000, 0);
331 break;
332 }
333 }
334 break;
335 }
336 }
337 }
338 lastHeartbeat = millis();
339 }
340
342 // Trigger immediate rainbow cube effect
343 int node = cubeNodes[random(numberOfCubeNodes)];
344 unsigned int baseColor = random(0xFFFF);
345 byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
346
347 for (int i = 0; i < 6; i++) {
348 if (nodeConnections[node][i] >= 0) {
349 for (int j = 0; j < numberOfRipples; j++) {
350 if (ripples[j].state == dead) {
351 ripples[j].start(
352 node, i,
354 baseColor + (0xFFFF / 6) * i, 255, 255),
355 .5, 2000, behavior);
356 break;
357 }
358 }
359 }
360 }
361 lastHeartbeat = millis();
362 }
363
365 // Trigger immediate starburst effect
366 unsigned int baseColor = random(0xFFFF);
367 byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
368
369 for (int i = 0; i < 6; i++) {
370 for (int j = 0; j < numberOfRipples; j++) {
371 if (ripples[j].state == dead) {
372 ripples[j].start(
373 starburstNode, i,
375 baseColor + (0xFFFF / 6) * i, 255, 255),
376 .65, 1500, behavior);
377 break;
378 }
379 }
380 }
381 lastHeartbeat = millis();
382 }
383
385 // Trigger immediate heartbeat effect
386 for (int i = 0; i < 6; i++) {
387 for (int j = 0; j < numberOfRipples; j++) {
388 if (ripples[j].state == dead) {
389 ripples[j].start(15, i, 0xEE1111,
390 float(random(100)) / 100.0 * .1 + .4, 1000, 0);
391 break;
392 }
393 }
394 }
395 lastHeartbeat = millis();
396 }
397
398 if (millis() - lastHeartbeat >= autoPulseTimeout) {
399 // When biometric data is unavailable, visualize at random
401 millis() - lastRandomPulse >= randomPulseTime) {
402 unsigned int baseColor = random(0xFFFF);
403
404 if (currentAutoPulseType == 255 ||
407 byte possiblePulse = 255;
408 while (true) {
409 possiblePulse = random(3);
410
411 if (possiblePulse == currentAutoPulseType)
412 continue;
413
414 switch (possiblePulse) {
415 case 0:
417 continue;
418 break;
419
420 case 1:
422 continue;
423 break;
424
425 case 2:
427 continue;
428 break;
429
430 default:
431 continue;
432 }
433
434 currentAutoPulseType = possiblePulse;
435 lastAutoPulseChange = millis();
436 break;
437 }
438 }
439
440 switch (currentAutoPulseType) {
441 case 0: {
442 int node = 0;
443 bool foundStartingNode = false;
444 while (!foundStartingNode) {
445 node = random(25);
446 foundStartingNode = true;
447 for (int i = 0; i < numberOfBorderNodes; i++) {
448 // Don't fire a pulse on one of the outer nodes - it
449 // looks boring
450 if (node == borderNodes[i])
451 foundStartingNode = false;
452 }
453
454 if (node == lastAutoPulseNode)
455 foundStartingNode = false;
456 }
457
458 lastAutoPulseNode = node;
459
460 for (int i = 0; i < 6; i++) {
461 if (nodeConnections[node][i] >= 0) {
462 for (int j = 0; j < numberOfRipples; j++) {
463 if (ripples[j].state == dead) {
464 ripples[j].start(
465 node, i,
466 // strip0.ColorHSV(baseColor
467 // + (0xFFFF / 6) * i,
468 // 255, 255),
469 Adafruit_DotStar_ColorHSV(baseColor, 255,
470 255),
471 float(random(100)) / 100.0 * .2 + .5, 3000,
472 1);
473
474 break;
475 }
476 }
477 }
478 }
479 break;
480 }
481
482 case 1: {
483 int node = cubeNodes[random(numberOfCubeNodes)];
484
485 while (node == lastAutoPulseNode)
486 node = cubeNodes[random(numberOfCubeNodes)];
487
488 lastAutoPulseNode = node;
489
490 byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
491
492 for (int i = 0; i < 6; i++) {
493 if (nodeConnections[node][i] >= 0) {
494 for (int j = 0; j < numberOfRipples; j++) {
495 if (ripples[j].state == dead) {
496 ripples[j].start(
497 node, i,
498 // strip0.ColorHSV(baseColor
499 // + (0xFFFF / 6) * i,
500 // 255, 255),
501 Adafruit_DotStar_ColorHSV(baseColor, 255,
502 255),
503 .5, 2000, behavior);
504
505 break;
506 }
507 }
508 }
509 }
510 break;
511 }
512
513 case 2: {
514 byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
515
517
518 for (int i = 0; i < 6; i++) {
519 for (int j = 0; j < numberOfRipples; j++) {
520 if (ripples[j].state == dead) {
521 ripples[j].start(
522 starburstNode, i,
524 baseColor + (0xFFFF / 6) * i, 255, 255),
525 .65, 1500, behavior);
526
527 break;
528 }
529 }
530 }
531 break;
532 }
533
534 default:
535 break;
536 }
537 lastRandomPulse = millis();
538 }
539
541 // Simulated heartbeat
542 if (millis() >= nextSimulatedHeartbeat) {
543 for (int i = 0; i < 6; i++) {
544 for (int j = 0; j < numberOfRipples; j++) {
545 if (ripples[j].state == dead) {
546 ripples[j].start(
547 15, i, 0xEE1111,
548 float(random(100)) / 100.0 * .1 + .4, 1000, 0);
549
550 break;
551 }
552 }
553 }
554
557 }
558
559 // Simulated EDA ripples
560 if (millis() >= nextSimulatedEda) {
561 for (int i = 0; i < 10; i++) {
562 for (int j = 0; j < numberOfRipples; j++) {
563 if (ripples[j].state == dead) {
564 byte targetNode =
566 byte direction = 255;
567
568 while (direction == 255) {
569 direction = random(6);
570 if (nodeConnections[targetNode][direction] < 0)
571 direction = 255;
572 }
573
574 ripples[j].start(
575 targetNode, direction, 0x1111EE,
576 float(random(100)) / 100.0 * .5 + 2, 300, 2);
577
578 break;
579 }
580 }
581 }
582
584 random(simulatedEdaVariance);
585 }
586 }
587 }
588
589 // Serial.print("Benchmark: ");
590 // Serial.println(millis() - benchmark);
591}
fl::UIDescription description("Demo of the Animatrix effects. @author of fx is StefanPetrick")
fl::CRGB leds[NUM_LEDS]
byte currentAutoPulseType
#define simulatedHeartbeatBaseTime
fl::UIGroup automationControls("Automation", starburstPulsesEnabled, simulatedBiometricsEnabled)
CRGB leds2[lengths[RedStrip]]
bool isNodeOnBorder(byte node)
bool wasStarburstClicked
bool wasRainbowCubeClicked
byte lastAutoPulseNode
unsigned long nextSimulatedHeartbeat
@ GreenStrip
@ RedStrip
@ BlackStrip
@ BlueStrip
fl::UIButton simulatedHeartbeat("Simulated Heartbeat")
fl::UITitle title("Chromancer")
byte ledColors[40][14][3]
#define highTemperature
fl::UIGroup displayControls("Display", sliderDecay, allWhite)
void setup()
CRGB leds3[lengths[BlueStrip]]
unsigned long lastAutoPulseChange
#define lowTemperature
unsigned long lastRandomPulse
#define simulatedEdaBaseTime
fl::UIGroup effectTriggers("Effect Triggers", simulatedHeartbeat, triggerStarburst, triggerRainbowCube, triggerBorderWave, triggerSpiral)
float lastKnownTemperature
fl::UIButton triggerBorderWave("Border Wave")
float lastIrReading
fl::UICheckbox starburstPulsesEnabled("Starburst Pulses", true)
#define simulatedEdaVariance
fl::UICheckbox allWhite("All White", false)
bool wasSpiralClicked
float gyroX
#define randomPulsesEnabled
CRGB leds1[lengths[GreenStrip]]
constexpr int lengths[]
unsigned long lastHeartbeat
fl::UIButton triggerSpiral("Spiral Wave")
#define autoPulseTimeout
bool wasHeartbeatClicked
fl::UICheckbox simulatedBiometricsEnabled("Simulated Biometrics", true)
float highestIrReading
#define simulatedHeartbeatVariance
#define randomPulseTime
float gyroZ
bool wasBorderWaveClicked
#define numberOfRipples
fl::UISlider sliderDecay("decay",.97f,.8, 1.0,.01)
#define autoPulseChangeTime
unsigned long nextSimulatedEda
byte numberOfAutoPulseTypes
fl::UIButton triggerStarburst("Trigger Starburst")
float gyroY
Ripple ripples[numberOfRipples]
#define cubePulsesEnabled
fl::UIButton triggerRainbowCube("Rainbow Cube")
CRGB leds0[lengths[BlackStrip]]
void loop()
TestState state
FL_DISABLE_WARNING_PUSH FL_DISABLE_WARNING_GLOBAL_CONSTRUCTORS CFastLED FastLED
Global LED strip management instance.
static bool ParseJson(const char *jsonStrScreenMap, fl::flat_map< string, ScreenMap > *segmentMaps, string *err=nullptr) FL_NOEXCEPT
size_type size() const FL_NOEXCEPT
Definition flat_map.h:96
iterator end() FL_NOEXCEPT
Definition flat_map.h:84
iterator find(const Key &key) FL_NOEXCEPT
Definition flat_map.h:136
uint32_t Adafruit_DotStar_ColorHSV(uint16_t hue, uint8_t sat, uint8_t val)
Definition detail.h:6
fl::CRGB CRGB
Definition crgb.h:25
FastLED's Elegant JSON Library: fl::json
Centralized logging categories for FastLED hardware interfaces and subsystems.
int starburstNode
Definition mapping.h:156
int numberOfCubeNodes
Definition mapping.h:152
int nodeConnections[25][6]
Definition mapping.h:15
int ledAssignments[40][3]
Definition mapping.h:95
int numberOfBorderNodes
Definition mapping.h:147
int borderNodes[]
Definition mapping.h:148
int cubeNodes[]
Definition mapping.h:153
void printf(const char *format, const Args &... args) FL_NOEXCEPT
Printf-like formatting function that prints directly to the platform output.
Definition stdio.h:635
enable_if<!is_integral< T >::value, T >::type round(T value) FL_NOEXCEPT
Definition math.h:319
@ dead
Definition ripple.h:17
@ alwaysTurnsRight
Definition ripple.h:27
@ alwaysTurnsLeft
Definition ripple.h:28
float fmap(float x, float in_min, float in_max, float out_min, float out_max)
Definition ripple.h:31
#define FL_UNUSED(x)
const char JSON_SCREEN_MAP[]
@ White
<div style='background:#FFFFFF;width:4em;height:4em;'></div>
Definition crgb.h:646
#define Serial
Definition serial.h:304
Aggregator header for the fl/ui/ family of per-element UI types.