Aug. 25, 2020

Zauberkugel

als interaktives Streaming Gadget

Da meine Freundin gelegentlich auf Twitch streamt, kam mir die Idee, ein interaktives Gadget zu bauen, das man als Viewer im Stream beeinflussen kann und welches wiederum auch zu ihrem „Magie“ Thema passt. Eine Zauberkugel hat sich da perfekt an geboten! Je nach Interaktion, sollte diese in der Lage sein ihre Farbe realtime im Stream ändern zu können. Also machte ich mich auf die Suche nach einer Kugelförmigen Lampe, die im Fuß noch genügend Stauraum für einen ESP 32 bot. Wir entschieden uns für eine Tischleuchte von Paul Neuhaus in der Farbe Messing. Im Baumark meines Vertrauens habe ich ein schönes Kabel und einen Schalter ausfindig machen können, den ESP 32 und die WS2801a LEDs hatte ich bereits zuhause.

Zauberkugel Vorher ESP 32

Hardware

Nachdem ich die Lampe auseinander geschraubt hatte, musste ich mir eine Befestigung für die LEDs überlegen. Ich brauchte also eine Art Rohr, welches ich über die alte Lampenfassung stülpen konnte. Eine zerschnittene Papprolle kann man gut zusammendrehen und bei gewünschtem Durchmesser zusammen kleben. Anschließend habe ich die LEDs rund um das Rohr befestigt. Die Verbindungen zwischen den einzelnen Strips verlaufen zudem im inneren des Rohrs, so sorgen die Kabel automatisch für besseren halt an der Lampenfassung. Nun mussten nur noch VCC, GND und Data mit dem ESP32 verbunden werden und es konnte los programmiert werden.

Zauberkugel Technik Zauberkugel Technik 2

Software

Der ESP musste nun zwei Aufgaben übernehmen: die LEDs steuern und Kommandos über das Netzwerk annehmen. Die LEDs sollten dabei möglichst organisch ineinander überblenden. Über die API sollte es möglich sein eine von 9 vorgegebenen Farben zu wählen. Die API habe ich als klassischen Webserver realisiert, welcher auf GET-requests wartet. Erhält er eine Anfrage an esp32.ip/farbe, ändert er eine globale Variable auf die requestete Farbe. Für das Steuern der LEDs habe ich FastLED verwendet. Zu Beginn werden allen LEDs eine aus der Farbpalette randomisierte Farbe zugeteilt. Um die Übergänge der einzelnen LEDs zu ermöglichen habe ich einen doppelten Puffer erzeugt, welcher sowohl die momentane Farbe der LEDs speichert als auch die Soll Farbe. Über die blend() Funktion von FastLED konnte man leicht einen sehr weichen Übergang zwischen zwei Farben erhalten. Hier blende ich zwischen der momentanen Farbe und der Soll Farbe. Alle 600-700ms ändert sich dann die Soll-Farbe. Sollte es in der Zwischenzeit zu einer GET-Request an den Webserver gekommen sein, wird automatisch die neue Farbpalette gewählt.

Mehr anzeigen
#include <FastLED.h>
#include <WiFi.h>

#define DATA_PIN        13
#define NUM_LEDS        16
#define LEDS_PER_STRIP  3
#define SECRTIONS       3
#define DELAY           20
const char* ssid = "YOUR_SSID;
const char* password = "YOUR_PASSWORD";
WiFiServer server(80);
String header;
int old_color = 8;
int current_color;
CRGB leds[NUM_LEDS];
CRGB old_leds[NUM_LEDS];
CRGB new_leds[NUM_LEDS];
TaskHandle_t Lights;
enum color {
  FIRE,
  PURPLE,
  GREEN,
  TEAL,
  BLUE,
  RED,
  PINK,
  WHITE,
  OFF
};
CRGB colorsFire[] = {CRGB(255,0,255), CRGB(255, 200, 255), CRGB(70,0,70)};
CRGB colorsPurple[] = {CRGB(220, 120, 255), CRGB(170, 0, 255), CRGB(225, 0, 255)};
CRGB colorsGreen[] = {CRGB(0, 255, 5), CRGB(55, 255, 0), CRGB(250, 255, 0)};
CRGB colorsTeal[] = {CRGB(50, 255, 60), CRGB(0, 255, 90), CRGB(10, 255, 120)};
CRGB colorsBlue[] = {CRGB(50, 140, 140), CRGB(30, 200, 30), CRGB(70, 0, 255)};
CRGB colorsRed[] = {CRGB(120, 20, 0), CRGB(150, 0, 0), CRGB(130, 130, 0)};
CRGB colorsPink[] = {CRGB(255, 110, 120), CRGB(255, 0, 150), CRGB(255, 0, 70)};
CRGB colorsWhite[] = {CRGB(255, 255, 100), CRGB(200, 200, 60), CRGB(150, 150, 40)};
CRGB colorsOff[] = {CRGB(0, 0, 0), CRGB(0, 0, 0), CRGB(0, 0, 0)};
CRGB *colors[] = {colorsFire, colorsPurple, colorsGreen, colorsTeal, colorsBlue, colorsRed, colorsPink, colorsWhite, colorsOff};



void setup() {
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);
  FastLED.setBrightness(30);
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  Serial.print("Connecting...");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print("...");
  }
  Serial.print("Connected! IP address:");
  Serial.print(WiFi.localIP());
  server.begin();
  xTaskCreatePinnedToCore(
    lights_control,
    "Lights",
    1000,
    NULL,
    1,
    &Lights,
    0);
}

void loop() {
  WiFiClient client = server.available();
  if (client) {
    String currentLine = "";
    while (client.connected()) {
        if (client.available()) {
          char c = client.read();
          Serial.write(c);
          header += c;
          if (c == '\n') {                    
            if (currentLine.length() == 0) {
              client.println("HTTP/1.1 204 No Content");
              client.println("Content-type:text/html");
              client.println("Connection: close");
              client.println();
              if (header.indexOf("GET /fire") >= 0) {
                current_color = FIRE;
              } else if (header.indexOf("GET /purple") >= 0) {
                current_color = PURPLE;
              } else if (header.indexOf("GET /green") >= 0) {
                  current_color = GREEN;
              } else if (header.indexOf("GET /teal") >= 0) {
                  current_color = TEAL;
              } else if (header.indexOf("GET /blue") >= 0) {
                  current_color = BLUE;
              } else if (header.indexOf("GET /red") >= 0) {
                  current_color = RED;
              } else if (header.indexOf("GET /pink") >= 0) {
                  current_color = PINK;
              } else if (header.indexOf("GET /white") >= 0) {
                  current_color = WHITE;
              } else if (header.indexOf("GET /off") >= 0) {
                  current_color = OFF;
              }
            }
        }
      }
    }
  }
  header = "";
  client.stop();
}


void lights_control(void * parameter) {
  unsigned long currentMillis, startMillis;
  unsigned long random_millis;
  int color_palette = 0;
  int pixel_colors[19];
  int new_pixels[19];
  int rotation_time = 499;
  for(;;) {
      if (old_color != current_color) {
        startMillis = millis();
        old_color = current_color;
        color_palette = current_color;
        for (int i = 0; i < 19; i++) {
          pixel_colors[i] = random(3);
        }
        old_leds[0] = colors[color_palette][pixel_colors[0]];
        old_leds[1] = colors[color_palette][pixel_colors[1]];
        old_leds[2] = colors[color_palette][pixel_colors[2]];
        old_leds[3] = colors[color_palette][pixel_colors[6]];
        old_leds[4] = colors[color_palette][pixel_colors[7]];
        old_leds[5] = colors[color_palette][pixel_colors[8]];
        old_leds[6] = colors[color_palette][pixel_colors[12]];
        old_leds[7] = colors[color_palette][pixel_colors[13]];
        old_leds[8] = colors[color_palette][pixel_colors[14]];
        old_leds[9] = colors[color_palette][pixel_colors[3]];
        old_leds[10] = colors[color_palette][pixel_colors[4]];
        old_leds[11] = colors[color_palette][pixel_colors[9]];
        old_leds[12] = colors[color_palette][pixel_colors[10]];
        old_leds[13] = colors[color_palette][pixel_colors[14]];
        old_leds[14] = colors[color_palette][pixel_colors[16]];
        old_leds[15] = colors[color_palette][pixel_colors[18]];
        for (int i = 0; i<18; i++){
          new_pixels[(i+3)%18] = pixel_colors[i];
          }
        new_leds[0] = colors[color_palette][new_pixels[0]];
        new_leds[1] = colors[color_palette][new_pixels[1]];
        new_leds[2] = colors[color_palette][new_pixels[2]];
        new_leds[3] = colors[color_palette][new_pixels[6]];
        new_leds[4] = colors[color_palette][new_pixels[7]];
        new_leds[5] = colors[color_palette][new_pixels[8]];
        new_leds[6] = colors[color_palette][new_pixels[12]];
        new_leds[7] = colors[color_palette][new_pixels[13]];
        new_leds[8] = colors[color_palette][new_pixels[14]];
        new_leds[9] = colors[color_palette][new_pixels[3]];
        new_leds[10] = colors[color_palette][new_pixels[4]];
        new_leds[11] = colors[color_palette][new_pixels[10]];
        new_leds[12] = colors[color_palette][new_pixels[11]];
        new_leds[13] = colors[color_palette][new_pixels[15]];
        new_leds[14] = colors[color_palette][new_pixels[16]];
        new_leds[15] = colors[color_palette][new_pixels[18]];
    }
  currentMillis = millis();
  if (currentMillis - random_millis >= rotation_time) {
    for (int i = 0; i<18; i++){
      new_pixels[(i+3)%18] = random(3);
    }
    new_pixels[18] = random(3);
    memcpy(&pixel_colors, &new_pixels, sizeof(int)*19);
    new_leds[0] = colors[color_palette][new_pixels[0]];
    new_leds[1] = colors[color_palette][new_pixels[1]];
    new_leds[2] = colors[color_palette][new_pixels[2]];
    new_leds[3] = colors[color_palette][new_pixels[6]];
    new_leds[4] = colors[color_palette][new_pixels[7]];
    new_leds[5] = colors[color_palette][new_pixels[8]];
    new_leds[6] = colors[color_palette][new_pixels[12]];
    new_leds[7] = colors[color_palette][new_pixels[13]];
    new_leds[8] = colors[color_palette][new_pixels[14]];
    new_leds[9] = colors[color_palette][new_pixels[3]];
    new_leds[10] = colors[color_palette][new_pixels[4]];
    new_leds[11] = colors[color_palette][new_pixels[10]];
    new_leds[12] = colors[color_palette][new_pixels[11]];
    new_leds[13] = colors[color_palette][new_pixels[15]];
    new_leds[14] = colors[color_palette][new_pixels[16]];
    new_leds[15] = colors[color_palette][new_pixels[18]];
    rotation_time = random(600,700);
    random_millis = currentMillis;
  }
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = blend(old_leds[i], new_leds[i],2);
    memcpy(&old_leds, &leds, sizeof(CRGB)*NUM_LEDS);
  }
  FastLED.show();
  FastLED.delay(5);
  }
}

Jetzt benötigte ich eine Verbindung zwischen dem Stream und der Lampe. Da Twitch leider keine leicht verwendbare Schnittstelle besitzt, benutzen wir stattdessen Tipeeestream. Über diese Plattform ist es möglich direkt live Events im Stream anzeigen zu lassen. Dafür bietet Tipeeestream einen Online Baukasten für diese Notifications an. Zudem gibt es auch eine API welche sehr einfach per Websocket abgefragt werden kann. Über die API erhalte ich also alle Events in Echtzeit. Doch da ein Event im Stream aber 7.5 Sekunden dauert, können diese nicht einfach nur direkt in GET-Requests an die Kugel weitergeleitet werden. Die Events werden folglich dessen in einem Queue gespeichert und nach und nach abgearbeitet.

Mehr anzeigen
const API_KEY = 'YOUR_API_KEY'
const USERNAME = 'YOUR_USERNAME';
const LAMP_IP = 'LAMP_LOCAL_IP';
const TIME_BETWEEN_ALERTS = 7500;
const oReq = new XMLHttpRequest();
var timeOfLastAlert = Date.now() - TIME_BETWEEN_ALERTS;
var queue = [];

let socket = io('https://sso-cf.tipeeestream.com:443', {
  query: {
      access_token: API_KEY
  }
});

socket.on('connect', () => {
    socket.emit('join', {
        room: API_KEY,
        username: USERNAME
    })
});

socket.on('new-event', data => {
  queue.push(data.event.type);
  console.log(data);
});

function changeLampColor(color) {
  oReq.open("get", "http://"+LAMP_IP+"/"+color);
  oReq.send();
}

function changeColorAndSwitchBack(color) {
  changeLampColor(color);
  setTimeout(() => {changeLampColor("fire");}, 5800);
}

function checkQueueForElements() {
    if (queue.length) {
      if (Date.now() - timeOfLastAlert >= TIME_BETWEEN_ALERTS) {
          if (queue[0] == "donation") {
              changeColorAndSwitchBack("blue");
          } else if (queue[0] == "follow") {
              changeColorAndSwitchBack("green");
          } else if (queue[0] == "hosting") {
              changeColorAndSwitchBack("red");
          } else if (queue[0] == "subscription") {
              changeColorAndSwitchBack("white");
          }
          timeOfLastAlert = Date.now();
          queue.shift();
      }
    }
  }

  setInterval(checkQueueForElements, 50);
Zauberkugel Kabel ESP

Finishing Touches

Alles funktionierte einwandfrei, sodass die Lampe nur noch zusammengebaut werden musste. Der ESP32 wurde im Sockel versteckt und mit seiner originalen Abdichtung abgedeckt. Das alte Lampenkabel habe ich durch ein beiges Textilkabel ersetzt, einen einfachen Lampenschalter hinzugefügt und am unteren Ende einen USB Stecker gelötet. So kann die Lampe in jedes beliebige USB Ladegerät gesteckt werden.

Zauberkugel Zauberkugel Kabel
Zurück