Files
fahnen_esp32/.pio/libdeps/esp01_1m/FastLED/examples/Chromancer/Chromancer.ino

608 lines
22 KiB
C++

/// @file Chromancer.ino
/// @brief Hexagonal LED display visualization
/// @example Chromancer.ino
///
/// 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.
/*
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
*/
#include "fl/sketch_macros.h"
#include "fl/warn.h"
#if !SKETCH_HAS_LOTS_OF_MEMORY
// Platform does not have enough memory
// Other platforms have weird issues. Will revisit this later.
#include <Arduino.h>
void setup() {
// Use Serial.println instead of FL_WARN to prevent optimization away
Serial.begin(115200);
Serial.println("Chromancer.ino: setup() - Platform has insufficient memory for full demo");
}
void loop() {
// Use Serial.println instead of FL_WARN to prevent optimization away
Serial.println("Chromancer.ino: loop() - Platform has insufficient memory for full demo");
delay(1000); // Prevent rapid printing
}
#else
#include <FastLED.h>
#include "fl/screenmap.h"
#include "fl/math_macros.h"
#include "fl/json.h"
#include "fl/ui.h"
#include "fl/map.h"
#include "fl/str.h"
#include "./screenmap.json.h"
#include "./mapping.h"
#include "./ripple.h"
#include "./detail.h"
using namespace fl;
enum {
BlackStrip = 0,
GreenStrip = 1,
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 leds0[lengths[BlackStrip]] = {};
CRGB leds1[lengths[GreenStrip]] = {};
CRGB leds2[lengths[RedStrip]] = {}; // Red
CRGB leds3[lengths[BlueStrip]] = {};
CRGB *leds[] = {leds0, leds1, leds2, leds3};
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
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 ripples[numberOfRipples] = {
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
float lastKnownTemperature =
(lowTemperature + highTemperature) /
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
UICheckbox starburstPulsesEnabled("Starburst Pulses", true);
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;
byte numberOfAutoPulseTypes =
randomPulsesEnabled + cubePulsesEnabled + int(starburstPulsesEnabled);
byte currentAutoPulseType = 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;
}
UITitle title("Chromancer");
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!");
UICheckbox allWhite("All White", false);
UIButton simulatedHeartbeat("Simulated Heartbeat");
UIButton triggerStarburst("Trigger Starburst");
UIButton triggerRainbowCube("Rainbow Cube");
UIButton triggerBorderWave("Border Wave");
UIButton triggerSpiral("Spiral Wave");
bool wasHeartbeatClicked = false;
bool wasStarburstClicked = false;
bool wasRainbowCubeClicked = false;
bool wasBorderWaveClicked = false;
bool wasSpiralClicked = false;
// Group related UI elements using UIGroup template multi-argument constructor
UIGroup effectTriggers("Effect Triggers", simulatedHeartbeat, triggerStarburst, triggerRainbowCube, triggerBorderWave, triggerSpiral);
UIGroup automationControls("Automation", starburstPulsesEnabled, simulatedBiometricsEnabled);
UIGroup displayControls("Display", sliderDecay, allWhite);
void setup() {
Serial.begin(115200);
Serial.println("*** LET'S GOOOOO ***");
Serial.println("JSON SCREENMAP");
Serial.println(JSON_SCREEN_MAP);
fl::fl_map<fl::string, ScreenMap> segmentMaps;
ScreenMap::ParseJson(JSON_SCREEN_MAP, &segmentMaps);
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());
}
// ScreenMap screenmaps[4];
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 = 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
wasHeartbeatClicked = bool(simulatedHeartbeat);
wasStarburstClicked = bool(triggerStarburst);
wasRainbowCubeClicked = bool(triggerRainbowCube);
wasBorderWaveClicked = bool(triggerBorderWave);
wasSpiralClicked = bool(triggerSpiral);
if (wasSpiralClicked) {
// 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,
Adafruit_DotStar_ColorHSV(
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;
}
}
}
}
lastHeartbeat = millis();
}
if (wasBorderWaveClicked) {
// 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 &&
!isNodeOnBorder(nodeConnections[node][dir])) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
node, dir,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / numberOfBorderNodes) * i,
255, 255),
.4, 2000, 0);
break;
}
}
break;
}
}
}
lastHeartbeat = millis();
}
if (wasRainbowCubeClicked) {
// 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,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.5, 2000, behavior);
break;
}
}
}
}
lastHeartbeat = millis();
}
if (wasStarburstClicked) {
// 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(
starburstNode, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
lastHeartbeat = millis();
}
if (wasHeartbeatClicked) {
// 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;
}
}
}
lastHeartbeat = millis();
}
if (millis() - lastHeartbeat >= autoPulseTimeout) {
// When biometric data is unavailable, visualize at random
if (numberOfAutoPulseTypes &&
millis() - lastRandomPulse >= randomPulseTime) {
unsigned int baseColor = random(0xFFFF);
if (currentAutoPulseType == 255 ||
(numberOfAutoPulseTypes > 1 &&
millis() - lastAutoPulseChange >= autoPulseChangeTime)) {
byte possiblePulse = 255;
while (true) {
possiblePulse = random(3);
if (possiblePulse == currentAutoPulseType)
continue;
switch (possiblePulse) {
case 0:
if (!randomPulsesEnabled)
continue;
break;
case 1:
if (!cubePulsesEnabled)
continue;
break;
case 2:
if (!starburstPulsesEnabled)
continue;
break;
default:
continue;
}
currentAutoPulseType = possiblePulse;
lastAutoPulseChange = millis();
break;
}
}
switch (currentAutoPulseType) {
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;
}
lastAutoPulseNode = node;
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)];
lastAutoPulseNode = node;
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;
lastAutoPulseNode = starburstNode;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
ripples[j].start(
starburstNode, i,
Adafruit_DotStar_ColorHSV(
baseColor + (0xFFFF / 6) * i, 255, 255),
.65, 1500, behavior);
break;
}
}
}
break;
}
default:
break;
}
lastRandomPulse = millis();
}
if (simulatedBiometricsEnabled) {
// Simulated heartbeat
if (millis() >= nextSimulatedHeartbeat) {
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;
}
}
}
nextSimulatedHeartbeat = millis() + simulatedHeartbeatBaseTime +
random(simulatedHeartbeatVariance);
}
// Simulated EDA ripples
if (millis() >= nextSimulatedEda) {
for (int i = 0; i < 10; i++) {
for (int j = 0; j < numberOfRipples; j++) {
if (ripples[j].state == dead) {
byte targetNode =
borderNodes[random(numberOfBorderNodes)];
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;
}
}
}
nextSimulatedEda = millis() + simulatedEdaBaseTime +
random(simulatedEdaVariance);
}
}
}
// Serial.print("Benchmark: ");
// Serial.println(millis() - benchmark);
}
#endif // __AVR__