FastLED 3.9.15
Loading...
Searching...
No Matches
Chromancer.ino

This sketch is fully compatible with the FastLED web compiler.

This sketch is fully compatible with the FastLED web compiler. To use it do the following:

  1. Install Fastled: pip install fastled
  2. cd into this examples page.
  3. Run the FastLED web compiler at root: fastled
  4. When the compiler is done a web page will open.
// @filter: (memory is large)
/*
Original Source: https://github.com/ZackFreedman/Chromance
GaryWoo's Video: https://www.youtube.com/watch?v=-nSCtxa2Kp0
GaryWoo's LedMap: https://gist.github.com/Garywoo/b6cd1ea90cb5e17cc60b01ae68a2b770
GaryWoo's presets: https://gist.github.com/Garywoo/82fa67c6e1f9529dc16a01dd97d05d58
Chromance wall hexagon source (emotion controlled w/ EmotiBit)
Partially cribbed from the DotStar example
I smooshed in the ESP32 BasicOTA sketch, too
(C) Voidstar Lab 2021
*/
// FastLED.h must be included first to trigger precompiled headers for FastLED's build system
#include "FastLED.h"
#include "fl/log/log.h"
#include "fl/stl/stdio.h"
#include <FastLED.h>
#include "fl/math/math.h"
#include "fl/stl/json.h"
#include "fl/ui/ui.h"
#include "fl/stl/map.h"
#include "fl/stl/string.h"
#include "./mapping.h"
#include "./ripple.h"
#include "./detail.h"
enum {
RedStrip = 2,
BlueStrip = 3,
};
// Strips are different lengths because I am a dumb
constexpr int lengths[] = {
154, // Black strip
168, // Green strip
84, // Red strip
154 // Blue strip
};
// non emscripten uses separate arrays for each strip. Eventually emscripten
// should support this as well but right now we don't
CRGB leds2[lengths[RedStrip]] = {}; // Red
byte ledColors[40][14][3]; // LED buffer - each ripple writes to this, then we
// write this to the strips
//float decay = 0.97; // Multiply all LED's by this amount each tick to create
// fancy fading tails
fl::UISlider sliderDecay("decay", .97f, .8, 1.0, .01);
// These ripples are endlessly reused so we don't need to do any memory
// management
#define numberOfRipples 30
Ripple(0), Ripple(1), Ripple(2), Ripple(3), Ripple(4), Ripple(5),
Ripple(6), Ripple(7), Ripple(8), Ripple(9), Ripple(10), Ripple(11),
Ripple(12), Ripple(13), Ripple(14), Ripple(15), Ripple(16), Ripple(17),
Ripple(18), Ripple(19), Ripple(20), Ripple(21), Ripple(22), Ripple(23),
Ripple(24), Ripple(25), Ripple(26), Ripple(27), Ripple(28), Ripple(29),
};
// Biometric detection and interpretation
// IR (heartbeat) is used to fire outward ripples
float lastIrReading; // When our heart pumps, reflected IR drops sharply
float highestIrReading; // These vars let us detect this drop
unsigned long
lastHeartbeat; // Track last heartbeat so we can detect noise/disconnections
#define heartbeatLockout \
500 // Heartbeats that happen within this many milliseconds are ignored
#define heartbeatDelta 300 // Drop in reflected IR that constitutes a heartbeat
// Heartbeat color ripples are proportional to skin temperature
#define lowTemperature 33.0 // Resting temperature
#define highTemperature 37.0 // Really fired up
2.0; // Carries skin temperature from temperature callback to IR callback
// EDA code was too unreliable and was cut.
// TODO: Rebuild EDA code
// Gyroscope is used to reject data if you're moving too much
#define gyroAlpha 0.9 // Exponential smoothing constant
#define gyroThreshold \
300 // Minimum angular velocity total (X+Y+Z) that disqualifies readings
float gyroX, gyroY, gyroZ;
// If you don't have an EmotiBit or don't feel like wearing it, that's OK
// We'll fire automatic pulses
#define randomPulsesEnabled true // Fire random rainbow pulses from random nodes
#define cubePulsesEnabled true // Draw cubes at random nodes
fl::UICheckbox starburstPulsesEnabled("Starburst Pulses", true);
fl::UICheckbox simulatedBiometricsEnabled("Simulated Biometrics", true);
#define autoPulseTimeout \
5000 // If no heartbeat is received in this many ms, begin firing
// random/simulated pulses
#define randomPulseTime 2000 // Fire a random pulse every (this many) ms
unsigned long lastRandomPulse;
byte lastAutoPulseNode = 255;
#define autoPulseChangeTime 30000
unsigned long lastAutoPulseChange;
#define simulatedHeartbeatBaseTime \
600 // Fire a simulated heartbeat pulse after at least this many ms
#define simulatedHeartbeatVariance \
200 // Add random jitter to simulated heartbeat
#define simulatedEdaBaseTime 1000 // Same, but for inward EDA pulses
#define simulatedEdaVariance 10000
unsigned long nextSimulatedHeartbeat;
unsigned long nextSimulatedEda;
// Helper function to check if a node is on the border
bool isNodeOnBorder(byte node) {
for (int i = 0; i < numberOfBorderNodes; i++) {
if (node == borderNodes[i]) {
return true;
}
}
return false;
}
fl::UITitle title("Chromancer");
fl::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!");
fl::UICheckbox allWhite("All White", false);
fl::UIButton simulatedHeartbeat("Simulated Heartbeat");
fl::UIButton triggerStarburst("Trigger Starburst");
fl::UIButton triggerSpiral("Spiral Wave");
bool wasHeartbeatClicked = false;
bool wasStarburstClicked = false;
bool wasRainbowCubeClicked = false;
bool wasBorderWaveClicked = false;
bool wasSpiralClicked = false;
// Group related UI elements using fl::UIGroup template multi-argument constructor
void setup() {
Serial.begin(115200);
Serial.println("*** LET'S GOOOOO ***");
Serial.println("JSON SCREENMAP");
fl::printf("Parsed %d segment maps\n", int(segmentMaps.size()));
for (auto kv : segmentMaps) {
Serial.print(kv.first.c_str());
Serial.print(" ");
Serial.println(kv.second.getLength());
}
// fl::ScreenMap screenmaps[4];
fl::ScreenMap red, black, green, blue;
bool ok = true;
auto red_it = segmentMaps.find("red_segment");
ok = (red_it != segmentMaps.end()) && ok;
if (red_it != segmentMaps.end()) red = red_it->second;
auto black_it = segmentMaps.find("back_segment");
ok = (black_it != segmentMaps.end()) && ok;
if (black_it != segmentMaps.end()) black = black_it->second;
auto green_it = segmentMaps.find("green_segment");
ok = (green_it != segmentMaps.end()) && ok;
if (green_it != segmentMaps.end()) green = green_it->second;
auto blue_it = segmentMaps.find("blue_segment");
ok = (blue_it != segmentMaps.end()) && ok;
if (blue_it != segmentMaps.end()) blue = blue_it->second;
if (!ok) {
Serial.println("Failed to get all segment maps");
return;
}
CRGB* red_leds = leds[RedStrip];
CRGB* black_leds = leds[BlackStrip];
CRGB* green_leds = leds[GreenStrip];
CRGB* blue_leds = leds[BlueStrip];
FastLED.addLeds<WS2812, 2>(black_leds, lengths[BlackStrip]).setScreenMap(black);
FastLED.addLeds<WS2812, 3>(green_leds, lengths[GreenStrip]).setScreenMap(green);
FastLED.addLeds<WS2812, 1>(red_leds, lengths[RedStrip]).setScreenMap(red);
FastLED.addLeds<WS2812, 4>(blue_leds, lengths[BlueStrip]).setScreenMap(blue);
FastLED.show();
}
void loop() {
unsigned long benchmark = millis();
FL_UNUSED(benchmark);
// Fade all dots to create trails
for (int strip = 0; strip < 40; strip++) {
for (int led = 0; led < 14; led++) {
for (int i = 0; i < 3; i++) {
ledColors[strip][led][i] *= sliderDecay.value();
}
}
}
for (int i = 0; i < numberOfRipples; i++) {
ripples[i].advance(ledColors);
}
for (int segment = 0; segment < 40; segment++) {
for (int fromBottom = 0; fromBottom < 14; fromBottom++) {
int strip = ledAssignments[segment][0];
int led = fl::round(fmap(fromBottom, 0, 13, ledAssignments[segment][2],
ledAssignments[segment][1]));
leds[strip][led] = CRGB(ledColors[segment][fromBottom][0],
ledColors[segment][fromBottom][1],
ledColors[segment][fromBottom][2]);
}
}
if (allWhite) {
// for all strips
for (int i = 0; i < 4; i++) {
for (int j = 0; j < lengths[i]; j++) {
leds[i][j] = CRGB::White;
}
}
}
FastLED.show();
// Check if buttons were clicked
// Trigger spiral wave effect from center
unsigned int baseColor = random(0xFFFF);
byte centerNode = 15; // Center node
// Create 6 ripples in a spiral pattern
for (int i = 0; i < 6; i++) {
if (nodeConnections[centerNode][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
centerNode, i,
baseColor + (0xFFFF / 6) * i, 255, 255),
0.3 + (i * 0.1), // Varying speeds creates spiral effect
2000,
i % 2 ? alwaysTurnsLeft : alwaysTurnsRight); // Alternating turn directions
break;
}
}
}
}
}
// Trigger immediate border wave effect
unsigned int baseColor = random(0xFFFF);
// Start ripples from each border node in sequence
for (int i = 0; i < numberOfBorderNodes; i++) {
byte node = borderNodes[i];
// Find an inward direction
for (int dir = 0; dir < 6; dir++) {
if (nodeConnections[node][dir] >= 0 &&
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, dir,
baseColor + (0xFFFF / numberOfBorderNodes) * i,
255, 255),
.4, 2000, 0);
break;
}
}
break;
}
}
}
}
// Trigger immediate rainbow cube effect
int node = cubeNodes[random(numberOfCubeNodes)];
unsigned int baseColor = random(0xFFFF);
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
baseColor + (0xFFFF / 6) * i, 255, 255),
.5, 2000, behavior);
break;
}
}
}
}
}
// Trigger immediate starburst effect
unsigned int baseColor = random(0xFFFF);
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
}
// Trigger immediate heartbeat effect
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(15, i, 0xEE1111,
float(random(100)) / 100.0 * .1 + .4, 1000, 0);
break;
}
}
}
}
// When biometric data is unavailable, visualize at random
unsigned int baseColor = random(0xFFFF);
if (currentAutoPulseType == 255 ||
byte possiblePulse = 255;
while (true) {
possiblePulse = random(3);
if (possiblePulse == currentAutoPulseType)
continue;
switch (possiblePulse) {
case 0:
continue;
break;
case 1:
continue;
break;
case 2:
continue;
break;
default:
continue;
}
currentAutoPulseType = possiblePulse;
break;
}
}
case 0: {
int node = 0;
bool foundStartingNode = false;
while (!foundStartingNode) {
node = random(25);
foundStartingNode = true;
for (int i = 0; i < numberOfBorderNodes; i++) {
// Don't fire a pulse on one of the outer nodes - it
// looks boring
if (node == borderNodes[i])
foundStartingNode = false;
}
if (node == lastAutoPulseNode)
foundStartingNode = false;
}
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
// strip0.ColorHSV(baseColor
// + (0xFFFF / 6) * i,
// 255, 255),
Adafruit_DotStar_ColorHSV(baseColor, 255,
255),
float(random(100)) / 100.0 * .2 + .5, 3000,
1);
break;
}
}
}
}
break;
}
case 1: {
int node = cubeNodes[random(numberOfCubeNodes)];
while (node == lastAutoPulseNode)
node = cubeNodes[random(numberOfCubeNodes)];
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
if (nodeConnections[node][i] >= 0) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, i,
// strip0.ColorHSV(baseColor
// + (0xFFFF / 6) * i,
// 255, 255),
Adafruit_DotStar_ColorHSV(baseColor, 255,
255),
.5, 2000, behavior);
break;
}
}
}
}
break;
}
case 2: {
byte behavior = random(2) ? alwaysTurnsLeft : alwaysTurnsRight;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
break;
}
default:
break;
}
}
// Simulated heartbeat
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
15, i, 0xEE1111,
float(random(100)) / 100.0 * .1 + .4, 1000, 0);
break;
}
}
}
}
// Simulated EDA ripples
for (int i = 0; i < 10; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
byte targetNode =
byte direction = 255;
while (direction == 255) {
direction = random(6);
if (nodeConnections[targetNode][direction] < 0)
direction = 255;
}
ripples[j].start(
targetNode, direction, 0x1111EE,
float(random(100)) / 100.0 * .5 + 2, 300, 2);
break;
}
}
}
}
}
}
// Serial.print("Benchmark: ");
// Serial.println(millis() - benchmark);
}
void setup()
void loop()
fl::UIDescription description("Demo of the Animatrix effects. @author of fx is StefanPetrick")
fl::UITitle title("Animartrix")
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")
byte ledColors[40][14][3]
#define highTemperature
fl::UIGroup displayControls("Display", sliderDecay, allWhite)
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]]
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
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
fl::u32 millis()
Universal millisecond timer - returns milliseconds since system startup.
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.