FastLED 3.9.15
Loading...
Searching...
No Matches
Chromancer.ino
Go to the documentation of this file.
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 {
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
64CRGB leds2[lengths[RedStrip]] = {}; // Red
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
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
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
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;
126
130#define autoPulseChangeTime 30000
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
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");
160UIButton triggerSpiral("Spiral Wave");
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
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
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,
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
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,
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
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,
330 baseColor + (0xFFFF / 6) * i, 255, 255),
331 .5, 2000, behavior);
332 break;
333 }
334 }
335 }
336 }
337 lastHeartbeat = millis();
338 }
339
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,
351 baseColor + (0xFFFF / 6) * i, 255, 255),
352 .65, 1500, behavior);
353 break;
354 }
355 }
356 }
357 lastHeartbeat = millis();
358 }
359
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
377 millis() - lastRandomPulse >= randomPulseTime) {
378 unsigned int baseColor = random(0xFFFF);
379
380 if (currentAutoPulseType == 255 ||
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:
393 continue;
394 break;
395
396 case 1:
398 continue;
399 break;
400
401 case 2:
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
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,
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
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
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 =
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
560 random(simulatedEdaVariance);
561 }
562 }
563 }
564
565 // Serial.print("Benchmark: ");
566 // Serial.println(millis() - benchmark);
567}
568
569#endif // __AVR__
CRGB leds[NUM_LEDS]
Definition Apa102.ino:11
byte currentAutoPulseType
#define simulatedHeartbeatBaseTime
CRGB leds2[lengths[RedStrip]]
bool isNodeOnBorder(byte node)
UICheckbox allWhite("All White", false)
bool wasStarburstClicked
UISlider sliderDecay("decay",.97f,.8, 1.0,.01)
bool wasRainbowCubeClicked
byte lastAutoPulseNode
unsigned long nextSimulatedHeartbeat
@ GreenStrip
@ RedStrip
@ BlackStrip
@ BlueStrip
byte ledColors[40][14][3]
#define highTemperature
void setup()
CRGB leds3[lengths[BlueStrip]]
unsigned long lastAutoPulseChange
#define lowTemperature
UICheckbox simulatedBiometricsEnabled("Simulated Biometrics", true)
unsigned long lastRandomPulse
#define simulatedEdaBaseTime
UITitle title("Chromancer")
float lastKnownTemperature
float lastIrReading
UIButton triggerBorderWave("Border Wave")
#define simulatedEdaVariance
bool wasSpiralClicked
float gyroX
#define randomPulsesEnabled
CRGB leds1[lengths[GreenStrip]]
UIButton triggerSpiral("Spiral Wave")
constexpr int lengths[]
unsigned long lastHeartbeat
#define autoPulseTimeout
UICheckbox starburstPulsesEnabled("Starburst Pulses", true)
bool wasHeartbeatClicked
UIButton simulatedHeartbeat("Simulated Heartbeat")
UIButton triggerStarburst("Trigger Starburst")
float highestIrReading
#define simulatedHeartbeatVariance
#define randomPulseTime
float gyroZ
bool wasBorderWaveClicked
UIButton triggerRainbowCube("Rainbow Cube")
#define numberOfRipples
#define autoPulseChangeTime
unsigned long nextSimulatedEda
byte numberOfAutoPulseTypes
float gyroY
Ripple ripples[numberOfRipples]
#define cubePulsesEnabled
CRGB leds0[lengths[BlackStrip]]
void loop()
CFastLED FastLED
Global LED strip management instance.
Definition FastLED.cpp:58
central include file for FastLED, defines the CFastLED class/object
UIDescription description("This Fire demo wraps around the cylinder. It uses Perlin noise to create a fire effect.")
WS2812 controller class.
Definition FastLED.h:193
constexpr size_t size() const
Definition map.h:226
bool get(const Key &key, Value *value) const
Definition map.h:118
static bool ParseJson(const char *jsonStrScreenMap, FixedMap< Str, ScreenMap, 16 > *segmentMaps, Str *err=nullptr)
Definition screenmap.cpp:37
uint32_t Adafruit_DotStar_ColorHSV(uint16_t hue, uint8_t sat, uint8_t val)
Definition detail.h:6
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
Implements a simple red square effect for 2D LED grids.
Definition crgb.h:16
void net_loop()
Definition net.h:159
void net_init()
Definition net.h:37
@ 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:635
Representation of an RGB pixel (Red, Green, Blue)
Definition crgb.h:54
#define round(x)
Definition util.h:10