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