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