Weihnachtsstern

Wir bauen einen modularen Weihnachtsstern. Er soll von einem kleinen Rechner gesteuert werden und mehrfarbig leuchten, alleine oder im Verbund mit anderen Sternen.

Was ist mit modular gemeint?

Liste der Anforderungen

  • (A) Die Teile des Sterns sollen gut gegeneinander abgegrenzt sein, mit einfachen mechanischen Verbindungen und leicht lösbaren elektrischen Schnittstellen. Die Farben sollen den einzelnen Armen des Sterns klar zugeordnet sein, an den Übergangsstellen jedoch etwas verwischen.
  • (B) Man soll den Rechner auch anderweitig verwenden können.
  • (C) Idealerweise sollen unterschiedliche Varianten des Rechners (ES32, ES8266, als Entwicklungsboard oder als barebone) einsetzbar sein.
  • (D) Die Montage soll einfach sein.
  • (E) Der fertige Stern soll als Modul innerhalb einer Gruppe von Sternen dienen können.
  • (F) Die Zahl der Arme eines Sterns soll relativ leicht änderbar sein.
  • (G) Es soll möglich sein, den Rechner innerhalb des Sterns zu haben, aber auch außerhalb.
  • (H) Bei großen Sternen soll auch der Akku (Powerbank oder Lithium-Akku) innerhalb des Sterns Platz finden können. Ein passendes Gehäuse für eine Powerbank wäre auch ganz nett.

Modularität

Aus (A) und (B) ergibt sich, dass wir ein eigenes kleines Gehäuse für das Rechnermodul schaffen müssen. Aus (A), (D) und (F) ergibt sich, dass wir einen separaten Träger für die LEDs benötigen, der steckbar mit dem Rechnergehäuse verbunden wird. Aus (B), (F) und (G) ergibt sich, dass der Stern eine Steckverbindung zu dem Rechnergehäuse aufweisen soll. Aus (G) folgt, dass ein leeres Rechnergehäuse im Inneren des Sterns als mechanisches Verbindungselement zum LED-Träger dient, falls der Rechner selbst außerhalb positioniert ist. Aus (D) leiten wir ab, dass der Stern aus zwei zusammensteckbaren Hälften bestehen soll.

Mechanische Bestandteile

  • Rechner, USB-Zwischenstecker, USB-Kabel
  • Rechnergehäuse mit Deckel
  • LED-Träger, LED-Streifen, Verbindungskabel
  • zwei sternförmige Halbschalen; eine davon besitzt eine Halterung zur Verbindung mit dem Rechnergehäuse

Der Steuercomputer

Der Steuerrechner soll möglichst klein sein und wenig Strom verbrauchen, muss aber über WLAN verfügen. Wir benutzen ein SoC (System on a Chip) auf Basis des ESP32. Das ist ein sehr preiswerter, aber dennoch ziemlich leistungsfähiger Rechner, der den Vorteil hat, dass man ihn über die weit verbreitete und kostenlose Arduino-Entwicklungsumgebung mit C bzw. C++ programmieren kann.

In der barebone-Ausführung haben solche SoCs keine USB Schnittstelle. Man muss dann eine zusätzliche Adapter-Platine benutzen, um die Software auf den Rechner zu übertragen. Das Bild zeigt den kleineren Bruder des ESP32, den ESP8266 (obere Platine) und einen USB-Adapter (untere Platine). Im fertigen Gerät sitzt nur der ESP8266, kleiner als eine Briefmarke…

Wesentlich einfacher ist es für unser Projekt, ein sog. Entwicklungsmodul zu benutzen. Am bequemsten ist es mit dem (sehr preiswerten) LILY TTGO. Er hat eine USB-Schnittstelle, einen kleinen TFT Screen, WLAN und Bluetooth, zwei Buttons, einen Magnetsensor, einen Reset-Taster und man kann über eine Stiftleiste auf zahlreiche GPIO Pins zugreifen. Das Board hat sogar noch einen Akku-Anschluss und eine passende Ladeschaltung dazu.

Ansicht der UNTERSEITE — Die USB-C-Buchse ist am linke Seitenrand.
Der weiße Adapter ist für den Anschluss eines 3.7 Volt-Lithium-Akkus vorgesehen.
Den schwarzen Winkeladapter haben wir so angelötet, dass die Stifte nach innen zeigen.

Wir benutzen die ersten 4 Pins neben der USB-Buchse: +5Volt, Ground, Pin 27 und Pin 26). Pin 26 dient uns als Datenleitung für den LED-Streifen, Pin 27 eignet sich für ein optionales Bedienelement. An diesen Pin kann man nämlich (anders als bei Pin 26) sogar einen touch-Sensor anschließen.

Zusammen mit dem Rechnermodul erhält man eine gerade Steckerleiste. Wir verwenden sie nicht, sondern löten eine gewinkelte Steckerleiste mit den Anschlüssen nach innen auf. So bleibt die Baueinheit schön kompakt.

Wollen wir noch mehr Sensoren anschließen, so sollten wir allerdings alle Pins des ESP32 mit (geraden oder gewinkelten) Lötstiften ausstatten.

Im Bild haben wir nur ein Kabel aufgesteckt:

Der LILY TTGO hat einen USB-C-Anschluss. Wir könnten dort direkt ein Kabel hineinstecken. Aus mechanischen Gründen benutzen wir jedoch einen Zwischenstecker, der auf der einen Seite in den TTGO passt und auf der anderen Seite eine Micro-USB-Buchse bietet. Um den Weihnachtsstern mit Strom zu versorgen, kann man auf ein billiges und meist ohnehin vorhandenes USB-Ladegerät mit Micro-USB-Kabel zurückgreifen.

Rechnergehäuse (TTGO Box)

Die TTGO Box nimmt den Rechner fest auf (Klemmsitz) und besitzt (siehe Anforderung (B)) Kabelauslässe an drei Seiten und im Boden. Sie erlaubt Zugang zu dem Reset-Taster, der seitlich auf der Platine angebracht ist und zu den beiden Tastern auf der Platine unterhalb des Displays. Das Gehäuse hat Außenmaße von 28.6 mm x 60 mm. Es ist 13 mm hoch (ohne Deckel). Der Außenbereich vom Boden bis zu 4mm Höhe dient der Klemm-Befestigung des Rechners. Der Bereich darüber dient zur Verbindung mit dem Deckel bzw. mit anderen Elementen, in unserem Fall mit dem LED-Träger.

Für jeden Rechner, den wir alternativ benutzen wollen (siehe Anforderung (C)), muss ein eigenes Gehäuse entworfen werden. Die Abmessungen der TTGO Box sind so gewählt, dass der LILY TTGO genau darin Platz hat. Andere ESP-32-Development Boards sind in der Regel kleiner und finden daher ebenfalls Platz. Wollte man allerdings einen Arduino als Steuerrechner verwenden, so müsste dieser außerhalb des Sterns platziert werden. Ein Arduino ist nämlich erheblich größer, zumal man auch noch ein Zusatzmodul verwenden müsste, um die WLAN-Verbindung herzustellen.

Ein geschlossener Deckel könnte nützlich sein, wenn der TFT-Screen nicht benötigt wird. Ein Deckel mit Display-Aussparung und mit kleinen Löchern für die beiden Taster wird uns zur Befestigung des LED-Trägers dienen. Wir können dann das TFT-Display als zusätzliches zentrales Licht in besonderen Fällen einsetzen. Außerdem kann es während der Entwicklungszeit für Kontroll-Ausgaben benutzt werden („Debugging“).

Der USB-Anschluss liegt an der Schmalseite (im Bild: vorn). Wir verwenden einen USB-C-zu Micro-USB-Adapter, um den Rechner mechanisch zu fixieren. Dies ist ganz praktisch und schafft sehr viel mechanische Stabilität; man könnte es aber auch anders machen.

Die TTGO Box wird mit ONSHAPE entworfen und mit weißem PLA gedruckt. Wir benutzen, wie bereits gesagt, die ersten vier Pins rechts vorn und führen das Kabel in einem leichten Bogen innerhalb des Gehäuses rechts im hinteren Bereich heraus. Das Gehäuse sitzt ziemlich knapp. Eventuell muss man in den Ecken mit einer kleinen Feile etwas Grat entfernen, damit die mechanische Beanspruchung der Platine beim vorsichtigen Hineindrücken nicht zu groß ist. Um die Platine wieder herauszubekommen, fährt man vorsichtig mit einem kleinen Schraubendreher in den Schlitz an der oberen hinteren Stirnseite und hebelt ein wenig.

Man drückt die Platine so tief hinein, dass der USB-C-Anschluss genau mittig hinter der Aussparung liegt. Dann wird der schwarze Adapter hineingedrückt und sitzt dann ziemlich fest in dem Gehäuse. Wenn man an dem weißen Stecker zieht, löst sich der Stecker vom Adapter; der Adapter bleibt jedoch fest im TTGO stecken. An der rechten Seite sieht man die Aussparung für den Reset Knopf.

Damit ist das erste Modul fertig. Seine Schnittstellen sind das Gehäuse, der USB-Adapter, der Reset-Taster, die beiden Buttons, der Hall-Sensor (zur Magnetfeld-Erkennung) und das Display.

Der LED-Träger

Wir konstruieren ein regelmäßiges Polygon (5 oder 7 Ecken), so dass auf jeder Außenfläche genau 2 LEDs eines LED-Streifens (60 LEDs/Meter) Platz haben. Das Polygon dient gleichzeitig als Deckel der TTGO Box. Der Umfang des Polygons ist ein Vielfaches von 33.3 mm, wobei die Ecken außen mit 1mm verrundet sind.

Man erkennt links vorn die Aussparung für den USB-Adapter. Sie sitzt an einer Spitze des Polygons, damit sie später mittig zwischen zwei Armen des Sterns herauskommt, denn die Arme sitzen wiederum mittig auf den Polygonflächen. Man könnte bei der Konstruktion das Polygon auch so gegenüber dem Deckel verdrehen, dass der USB-Auslass in der Mitte einer Polygon-Seite liegt. Dann würde allerdings das USB-Kabel im Inneren des Sterns bis zur Spitze eines Arms laufen, was optisch stören würde.

Rechts an der Seite ist ein Durchbruch für die drei Drähte, die an den LED-Streifen angelötet werden. An der Oberseite sieht man die Aussparungen für das Display und die beiden Buttons. An der Seite des Deckels, nahe bei den Buttons ist die Aussparung für den Reset-Taster.

Der Deckel sitzt durch Klemmpassung (0.2 mm Toleranz) ziemlich fest auf der TTGO-Box.

Den Deckel mit dem LED-Halter haben wir im Bild provisorisch auf die TTGO-Box gesteckt. Das Kabel wird zum Testen mit einem LED-Streifen verbunden. Die Buttons und der Reset-Taster können mit einem spitzen Gegenstand betätigt werden. Dies kann während der Software-Entwicklung nützlich sein – später werden sie nicht mehr gebraucht.

Im nächsten Schritt montieren wir den LED-Streifen. Beim Einbau muss man die aufgedruckte Pfeilrichtung beachten. Unsere drei Kabel werden an der Eingangsseite angelötet. Die Ausgangsseite wird nicht verbunden (bei größeren Streifen würde man die äußeren Kabel der Ausgangsseite zusätzlich mit GND und 5V verbinden). Wir schneiden 10 bzw. 14 LEDs von der Rolle ab ab und zwar nicht genau in der Mitte, sondern so, dass die Kontaktflächen an der START-Seite des Streifens etwas länger sind und am Ende etwas kürzer. Auf diese Weise ist die Lötfläche etwas größer. Wichtig ist, die Lackschicht bei den Kontakten mechanisch zu entfernen, bevor man lötet. Ein scharfes Messer oder eine Schere können zum Schaben eingesetzt werden. Es geht auch mit feinem Schleifpapier. Dann verzinnen wir die drei Kontaktflächen mit etwas Lot.

Wir kürzen die Adern 1,2,4 auf 5-6cm, isolieren sie ab und verzinnen sie dünn. Wir benutzen also +5V, GND und Pin #26. Die Ader von Pin #27 lassen wir etwas länger und isolieren sie nicht ab. Sie bleibt vorläufig ungenutzt. Wir können später einen Touch-Sensor daran anschließen, wenn wir wollen.

Dann führen wir die drei benötigten Kabel durch den Schlitz bis sie ganz heraushängen; das Kabel von Pin #27 bleibt im Inneren des Polygons.

Nun löten wir die drei Adern in der richtigen Zuordnung auf den Streifen. Achtung: Beim Löten darf der Streifen keinen Kontakt zu dem LED-Träger haben, denn das PLA würde sofort weich werden und sich verformen.

Abschließend wird das Kabel durch den Schlitz zurückgedrückt und der Streifen aufgeklebt. Wir sollten ihn an den Kanten sauber knicken, bevor wir den Klebestreifen abziehen, damit er sich gut anlegt. Eventuell fixieren wir das Ende mit einem Tesafilm, damit es nicht wieder aufsteht.

Als Luxus könnten wir danach noch die Farben der Drähte dokumentieren, z.B. auf einem kleinen Schild, das am Ende die Öffnung für die Buttons an der TTGO Box abdeckt. Hier kommt man allerdings schnell an die Auflösungsgrenzen des 3D-Drucks. Wir benutzen die übliche 0.4 mm Düse. Also empfiehlt es sich, Fettschrift einzusetzen und mit Zeichen zu sparen.

Der Stern

Der Stern besteht aus zwei Halbschalen, die durch Klemmpassung zusammengehalten werden. Wir bevorzugen aus ästhetischen Gründen eine ungerade Zahl von Armen und führen das Versorgungskabel zwischen zwei Armen des Sterns heraus. Man könnte auch noch einen Kometenschweif hinzufügen.

Wir konstruieren den Stern so, dass das Polygon mit den LEDs einen kleinen Abstand von den inneren Ecken hat. Auf diese Weise ergeben sich weiche Farbverläufe an den Übergangsstellen und dennoch sind die Arme des Sterns farblich deutlich getrennt.

Die Außenwände des Sterns sollen möglichst dünn sein (ca. 2 mm), damit das Licht gut durchscheint. Dann bleibt für das Zusammenstecken eine Wanddicke von 0.9 mm übrig (plus jeweils 0.1 mm Toleranz für die Klemmpassung). Für die Deckflächen auf der Vorder- und Rückseite genügen 0.6 mm Dicke.

Im Bild ist der Stern nur angedeutet durch eine Bodenplatte mit sieben Zacken (die Zacken sind zu klein und zu nah an dem Polygon). Man sieht die Schlaufe des Kabels, das zu dem LED-Streifen führt; das Kabel von Pin 27 ist in einen kleinen Schlauch rechts daneben versteckt. Der Rechner ist in Betrieb, das Display zeigt Testausgaben.

Hier die fertige Version mit 5 Zacken. Die kleine Powerbank liefert für einige Stunden Energie.

Wetterfeste Ausführung

Für den Außeneinsatz kann man spezielle Ausführungen des LED-Streifens verwenden. Man sollte dann allerdings den Rechner außerhalb des Sterns unterbringen, um ihn besser gegen Feuchtigkeit kapseln zu können. Das hat den zusätzlichen Vorteil, dass man mehrere Sterne mit einem einzigen Rechner steuern kann. Man benötigt dann dreiadrige Leitungen zur Verbindung zwischen dem Rechner und den Sternen.

Touch-Sensoren

Mechanische Tasten einzubauen ist mühsam und teuer. Man muss zu jeder Taste zwei Stromleitungen führen und wetterfest sind Taster auch nicht. Glücklicherweise können manche Eingänge des LILY TTGO kapazitive Ladungsänderungen erkennen, die entstehen, wenn man als Mensch den Eingang mit der Hand berührt. Es genügt daher, von diesen Eingängen aus jeweils einen Draht zu den Spitzen des Sterns zu führen.

Der Draht wird von innen durch einen „Hohlsaum“ in der Seitenwand gesteckt. Dann biegt man eine kleine Öse und zieht den Draht zurück, bis die Öse fest anliegt. Bei CAD-Design muss man darauf achten, dass in dem langen, dünnen Tunnel für den Draht kein Support gedruckt wird. Man schrägt dazu das Dach wie einen Giebel einseitig oder symmetrisch an. Für Winkel bis 45 Grad wird normalerweise kein Support beim Slicing erzeugt. Man kann diese Grenze in den Einstellungen von CURA bis auf 60 Grad hoch setzen.

Wenn man 7 weitere Leitungen an den TTGO anschließen will, muss man allerdings die entsprechenden Pins bereitstellen. In diesem Fall sollte man die Winkelstecker andersherum einlöten, so dass die Stifte nach außen zeigen, Das Gehäuse muss man entsprechend anpassen. Weil die Seitenwände jetzt weitgehend wegfallen, sorgen wir durch ein stärkere Bodenplatte, durch eine Fase in Richtung Rückwand und durch zwei Stege für mehr Stabilität.

Soviel zum mechanischen Aufbau. Jetzt fehlt uns noch die Software.

Es empfiehlt sich, jetzt den Beitrag über den LILY TTGO zu lesen.

Programm-Wahl und WLAN

Der Weihnachtsstern kann (autonom) lokal Farben wechseln. Dabei sollen unterschiedliche Farb-Programme möglich sein. Ein eleganter Weg, zwischen den Programmen zu wechseln, besteht darin, dass man einen Magneten kurz an eine bestimmte Stelle des Sterns hält.

Eines der Programme bewirkt, dass der Stern Kommandos von einem anderen Rechner erwartet. Damit er auf diese Weise ferngesteuert werden kann, muss er versuchen, sich mit einem WLAN zu verbinden. Misslingt dieser Versuch, soll er in den lokalen Modus zurückfallen.

Kommen längere Zeit keine Kommandos von dem Steuerrechner, soll der Stern ebenfalls in einen lokalen Modus zurückfallen.

Anforderungen an die Software

  1. autonomer Betrieb mit sanft wechselnden Farben
  2. Abschaltautomatik nach einigen Stunden??
  3. wechselbare Lichtmuster
  4. WLAN-Verbindung
  5. Erkennung des Schaltmagneten
  6. Empfang von Steuerkommandos über WLAN

Version 1

Das Programm erzeugt ein wanderndes Regenbogen-Muster, das ab und zu die Richtung wechselt. Das Display wird noch nicht angesteuert. Der Magnet wird nicht abgefragt. Es gibt keine LAN-Verbindung. Es gibt nur ein einziges, fest eingebautes Lichtprogramm, den RAINBOW.

HINWEIS:
Wenn man eines der nachfolgenden Source-Code-Beispiele kopiert und in die Arduino-IDE oder in einen externen Editor einfügt, dann muss man die Zeichenkette „&“ durch ein einzelnes „&“ ersetzen.
Die Ursache dafür ist, dass das „&„-Zeichen in HTML eine Sonderbedeutung hat und deshalb als „&“ codiert wird (der Browser zeigt es jedoch als einfaches „&“ an).

// A star with softly changing rainbow colors

int NumLeds     = 14;   // 7 arms with two LEDs per arm

#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel strip = Adafruit_NeoPixel(        // define the LED strip (type WS2812)
    NumLeds,                                        // number of color triples
    26,                                             // pin connected to the strip
    NEO_GRB + NEO_KHZ800                            // 800 kHz bit stream in GRB sequence
);

unsigned long   speedDelay = 40;    // 40 msecs x 256 steps ~ 10 seconds per cycle
byte            colorPosition;      // the current color: 0..255 degrees in a color circle

void setup() {
  strip.begin();            // initialize LED strip
  colorPosition = 0;        // start at the top of the circle (red)
}

void loop() {
    int steps = 500-random(1000);    // positive = clockwise, negative = counter-clockwise
    rainbowCycle(speedDelay,steps);  // walk through the cycle for the given number of steps
}

void rainbowCycle(int speedDelay, int nrSteps) {
    // perform the given number of steps (256 steps would be a full cycle)
    // negative step numbers walk counter-clockwise
    
    byte *c;
    uint16_t l, j;

    for(j=0; j<abs(nrSteps); j++) {                     // for each step
        if (nrSteps>0) {
            if (++colorPosition>=255) colorPosition=0;  // increment position (clockwise)
        }
        else {
            if (--colorPosition<0) colorPosition=255;   // decrement position (counter clockwise)
        }
        // based on the color position divide the circle into NumLed parts
        // and assign the suitable color to each LED
        // note that this does not yet change the LEDs themselves
        // it only assigns values to the internal memory representation of the LEDs

        int factor = 1;     // you may want to try other values like 2, 5, 10
        // if we divided the circle into smaller portions (say, 2*NumLeds),
        // only half of the spectrum would be visible at any point in time
        // (which also might be nice)
        
        for(l=0; l< NumLeds; l++) {
            c= wheel(((l * 256 / NumLeds / factor) + colorPosition) % 256);
            strip.setPixelColor(l, *c, *(c+1), *(c+2));
        }

        // now transfer the memory as a bit stream to the LED strip
        strip.show();

        // wait before we advance the step counter
        // if we show smaller parts of the spectrum we extend the delay
        // so that we can enjoy the fine variations of color in peace ;-)
        delay(speedDelay*factor);
    }
}

byte *wheel(byte wheelPos) {
    // implements a color circle ("wheel") beginning with (1) red, changing to (2) green and (3) to blue
    // the wheel if divided into 256 steps, so 0=red, 256/3=green, 256*2/3=blue, etc.
    // the <wheelPos> parameter is a number between 0..255
    // the function returns an RGB array of three bytes
    
    static byte c[3];   // the resulting RGB triple

    // we have three segments (0..84, 85..169, 170..255)
    // in each segment one RGB color is completely missing (i.e. it has a value of 0)
    // another one is growing from 0..255 and the third color goes down from 255 to 0
    // this means that we have equal brightness over the whole spectrum

    if(wheelPos < 85) {             // first segment 0..84
        c[0]= 255 - wheelPos * 3;   // declining RED
        c[1]= wheelPos * 3;         // growing   GREEN
        c[2]= 0;                    // no        BLUE
    
    } else if(wheelPos < 170) {     // second segment 85..169
        wheelPos -= 85;             // make position relative to segment start
        c[0]= 0;                    // no        RED
        c[1]= 255 - wheelPos * 3;   // declining GREEN
        c[2]= wheelPos * 3;         // growing   BLUE
        
    } else {                        // third segment  170..255
        wheelPos -= 170;            // make position relative to segment start
        c[0]= wheelPos * 3;         // growing   RED
        c[1]= 0;                    // no        GREEN 
        c[2]= 255 - wheelPos * 3;   // declining BLUE
    }

    return c;                       // return the RGB triple
}
Zur Illustration hier ein beispielhafter Farbkreis; es geht oben mit „rot“ los; dann folgen im Uhrzeigersinn gelb (als Mischfarbe aus rot und grün, dann grün und türkis (Mischung von grün und blau), dann blau und violett (Mischung von blau und rot). Bei unserer Art der Berechnung ist grün allerdings bereits bei 120° erreicht, also nach einem Drittel-Kreis; man sieht, dass es nicht „den“ Farbkreis gibt, sondern viele Spielarten.

Version 2

Wir fügen ein zweites Lichtmuster hinzu, das einen farbigen Lichtfleck kreisen lässt.

void runningDots(int speedDelay, int nrSteps) {
    // move a colored dot around and change its color every full round

    int l=0;                // current LED
    byte *rgb;              // red, green ,blue
    unsigned long color;    // color of running pixel

    for(int n=0;n<nrSteps;n++) {
        
        // at the beginning of a new cycle: switch color
        if (l==0) {
            rgb=wheel(random(255));
            color= strip.Color(rgb[0],rgb[1],rgb[2]);
        }
        
        strip.setPixelColor(l,0);           // switch off current LED
        l=(l+1) % NumLeds;                  // advance to next LED
        strip.setPixelColor(l,color);       // set color for next LED
        strip.show();                       // transfer LED settings to the strip
        delay(speedDelay);                  // wait a moment
    }
}

Version 3

Wir schließen Drähte an diejenigen Pins des TTGO an, die eine kapazitive Ladungsänderung erkennen können („Touch-Pins“). Wir bauen das Programm so um, dass man duch das Berühren der Drähte die Farbeffekte beeinflussen kann.

// A star with rainbow colors rotating at adjustable speed
  
int NumLeds         = 14;                       // 7 arms with two LEDs per arm
int touchPins[]     = {32,27,12,13,15,2,33,20}; // arms 0 .. 7
 
#include <Adafruit_NeoPixel.h>
 
Adafruit_NeoPixel strip = Adafruit_NeoPixel(    // define the LED strip (type WS2812)
    NumLeds,                                    // number of color triples
    26,                                         // pin connected to the strip
    NEO_GRB + NEO_KHZ800                        // 800 kHz bit stream in GRB sequence
);
 
byte colorPosition = 0;         // the current position of the color wheel; 0 = red
  
void setup() {
    Serial.begin(115200);       // for monitoring
    delay(100);
     
    strip.begin();              // initialize LED strip
 
    rainbowCycle();             // start the cycle
}
  
void loop() {
}
  
void rainbowCycle() {
      
    byte *c;
    uint16_t l, j;
 
    unsigned long   speedDelay  = 50;   // delay between iterations
    int             gap         =  1;   // gap between color increments
    int             segment     = 10;   // segment size of color circle 
                                        // (10=full, 20=half, ..)
     
    for(;;) {   // forever
         
        // increment color position
        colorPosition= (colorPosition+gap) % 256;
 
        // based on the color position divide the circle into NumLed parts
        // and assign the suitable color to each LED
        // note that this does not yet change the LEDs themselves
        // it only assigns values to the internal memory representation 
        // of the LEDs
  
        // if we divide the circle into smaller portions (e.g. segment=20),
        // only half of the spectrum would be visible at any point in time
 
          
        for(l=0; l< NumLeds; l++) {
            c= wheel(((l * 256 / NumLeds * 10 / segment) 
                + colorPosition) % 256
            );
            strip.setPixelColor(l, *c, *(c+1), *(c+2));
        }
  
        // now transfer the memory as a bit stream to the LED strip
        strip.show();
  
        // check touch sensors
        if (touched(1) && speedDelay<1000) speedDelay++;     // slower
        if (touched(2) && speedDelay>   1) speedDelay--;
        if (touched(3) && gap>1           ) gap--;           // closer
        if (touched(4) && gap<100         ) gap++;
        if (touched(5) && segment>1       ) segment--;       // smaller
        if (touched(6) && segment<50  ) segment++;
        if (touched(0) ) { 
            speedDelay=random(20); 
            gap=random(10); 
            segment=random(10); 
        }
 
        Serial.println(
            "delay="+String(speedDelay)+
            "  gap="+String(gap)+
            "  segment="+String(segment)
        );
  
        // wait before we advance the step counter        
        delay(speedDelay);
    }
}
  
byte *wheel(byte wheelPos) {
    // implements a color circle ("wheel") beginning with 
    //     (1) red, changing to (2) green and (3) to blue
    // the wheel if divided into 256 steps, 
    //     so 0=red, 256/3=green, 256*2/3=blue, etc.
    // the <wheelPos> parameter is a number between 0..255
    // the function returns an RGB array of three bytes
      
    static byte c[3];   // the resulting RGB triple
  
    // we have three segments (0..84, 85..169, 170..255)
    // in each segment one RGB color is completely missing 
    // (i.e. it has a value of 0)
    // another one is growing from 0..255 and the third color 
    // goes down from 255 to 0
    // this means that we have equal brightness 
    // over the whole spectrum
  
    if(wheelPos < 85) {             // first segment 0..84
        c[0]= 255 - wheelPos * 3;   // declining RED
        c[1]= wheelPos * 3;         // growing   GREEN
        c[2]= 0;                    // no        BLUE
      
    } else if(wheelPos < 170) {     // second segment 85..169
        wheelPos -= 85;             // make position relative to segment
        c[0]= 0;                    // no        RED
        c[1]= 255 - wheelPos * 3;   // declining GREEN
        c[2]= wheelPos * 3;         // growing   BLUE
          
    } else {                        // third segment  170..255
        wheelPos -= 170;            // make position relative to segment
        c[0]= wheelPos * 3;         // growing   RED
        c[1]= 0;                    // no        GREEN 
        c[2]= 255 - wheelPos * 3;   // declining BLUE
    }
  
    return c;                       // return the RGB triple
}
 
boolean touched(int arm) {
    int t= touchRead(touchPins[arm]);
    if (t==0) return false;
    if (t< 30) {       // a reasonable threshold is 30
        delay(100);    // wait a moment so the user can release the touch
        return true;        
    }
    return false;
}

Version 4

Wir nehmen das TFT Display in Betrieb und zeigen dort ständig die aktuellen Einstellungen an.

// A star with rainbow colors rotating at adjustable speed
// settings are shown on the TFT display
// note that the library for the TFT must have been installed in the Arduino IDE
 
// ===================== DISPLAY ==============
 
#include <SPI.h>                                  // SPI interface for TFT display
#include <TFT_eSPI.h>                                 // Hardware-specific TFT library                                
TFT_eSPI tft = TFT_eSPI();                          // define a variable for the TFT display
 
#define NUM_ARMS  7                                 // the star has 7 arms ("legs")
#define NUM_LEDS (2*NUM_ARMS)                       // we have two LEDs per arm
 
// ===================== LED STRIP ============
 
#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel strip = Adafruit_NeoPixel(        // define the LED strip (type WS2812)
    NUM_LEDS,                                       // number of color triples
    26,                                             // the pin# connected to the strip
    NEO_GRB + NEO_KHZ800                            // 800 kHz bit stream in GRB sequence
);
byte colorPosition  = 0;                            // initial position of color wheel (0 = red)
 
// ===================== TOUCH PINS ===========
 
int touchPins[] = {32,27,12,13,15,2,33,20};         // pins (#0..#6) connectesd to touch contacts
 
// ===================== SETUP ================
  
void setup() {
    Serial.begin(115200);                           // for monitoring
    delay(100);
 
    tft.begin();                                    // initialize, 240 x 135 pixels (~ 20x4 chars)
    tft.setRotation(1);                             // 1=landscape orientation
    tft.setTextDatum(TL_DATUM);                     // top left reference point for texts
    tft.setTextColor(TFT_WHITE, TFT_BLACK);         // white on black
    tft.fillScreen(TFT_BLACK);                      // clear screen, all black
    tft.setFreeFont(&FreeSans12pt7b);               // fairly large font (allows for 4 lines)
     
    strip.begin();                                  // initialize LED strip
 
    rainbowCycle();                                 // start the cycle
}
  
void loop() {
    // we never get here because rainbowCycle contains and endless loop
}
 
// ===================== RAINBOW ================
  
void rainbowCycle() {
     
    // initial settings:
    unsigned long   speedDelay  = 50;   // delay between iterations in msecs
    int             gap         =  1;   // gap between color increments (wheel steps)
    int             segment     = 10;   // segment size of color circle (10=full, 20=half, ..)
     
    byte *c;                            // a triple of color values obtained from the wheel
 
    tftPrint(0,"Rotating Rainbow");
 
    for(;;) {   // forever
         
        colorPosition = (colorPosition+gap) % 256;      // increment color position
 
        // divide the circle into equi-distant colors for the LEDs
        // if segments==10 use full spectrum, else use less (e.g. 10%) or more (eg 200%)
        // assign a color to each LED starting from the current color position
          
        for(int l=0; l< NUM_LEDS; l++) {
            c= wheel(((l * 256 / NUM_LEDS * 10 / segment) + colorPosition) % 256);
            strip.setPixelColor(l, *c, *(c+1), *(c+2));
        }
  
        strip.show();               // now transfer the LED memory to the LED strip
  
        // check touch sensors and modify settings if desired
        if (touched(1) && speedDelay<1000) speedDelay++;     // slower
        if (touched(2) && speedDelay>   1) speedDelay--;
        if (touched(3) && gap>1           ) gap--;           // closer
        if (touched(4) && gap<100         ) gap++;
        if (touched(5) && segment>1       ) segment--;       // smaller
        if (touched(6) && segment<50  ) segment++;
        if (touched(0) ) { 
            speedDelay=random(20); 
            gap=random(10); 
            segment=random(10); 
        }
        
        // show current settings for debugging on serial line
        Serial.println("delay="+String(speedDelay)+"  gap="+String(gap)+"  segment="+String(segment));
 
        // show current settings for the user on the TFT display
        tftPrint(1,"speed ="+String(speedDelay));
        tftPrint(2,"gap  ="+String(gap));
        tftPrint(3,"segment ="+String(segment));
         
        // wait for the configured time (msecs)        
        delay(speedDelay);
    }
}
 
// ===================== COLOR WHEEL ================
  
byte *wheel(byte wheelPos) {
    // implements a color circle ("wheel") beginning with (1) red, changing to (2) green and (3) to blue
    // the wheel if divided into 256 steps, so 0=red, 256/3=green, 256*2/3=blue, etc.
    // the <wheelPos> parameter is a number between 0..255
    // the function returns an RGB array of three bytes
      
    static byte c[3];   // the resulting RGB triple
  
    // we have three segments (0..84, 85..169, 170..255)
    // in each segment one RGB color is completely missing (i.e. it has a value of 0)
    // another one is growing from 0..255 and the third color goes down from 255 to 0
    // this means that we have equal brightness over the whole spectrum
  
    if(wheelPos < 85) {             // first segment 0..84
        c[0]= 255 - wheelPos * 3;   // declining RED
        c[1]= wheelPos * 3;         // growing   GREEN
        c[2]= 0;                    // no        BLUE
      
    } else if(wheelPos < 170) {     // second segment 85..169
        wheelPos -= 85;             // make position relative to segment start
        c[0]= 0;                    // no        RED
        c[1]= 255 - wheelPos * 3;   // declining GREEN
        c[2]= wheelPos * 3;         // growing   BLUE
          
    } else {                        // third segment  170..255
        wheelPos -= 170;            // make position relative to segment start
        c[0]= wheelPos * 3;         // growing   RED
        c[1]= 0;                    // no        GREEN 
        c[2]= 255 - wheelPos * 3;   // declining BLUE
    }
  
    return c;                       // return the RGB triple
}
 
// ===================== TOUCH DETECTION ================
 
boolean touched(int arm) {
    // return true if the user touches the arm
    // if a touch is detected the function will delay the return for 100 msecs
    // this allows finer control for the user
     
    int t= touchRead(touchPins[arm]);   // high values:untouched, lower values: maybe touched
    if (t==0) return false;             // ignore a value of zero
    if (t< 30) {                     // a reasonable to detect a touch is a value of 30..60
        delay(100);                     // wait a moment so that the user can release the touch
        return true;
    }
    return false;                       // retun immediately if no touch was detected
}
 
// ===================== DISPLAY ================
 
String tftLines[4] = {"","","",""};     // the current four lines shown on the display
 
void tftPrint(int nr, String text) {
    // print text into the specified line (0..3)
 
    // we must fill the text to be shown with spaces; 
    // otherwise portions of previous contents would still be visible 
    // if the new text was shorter than the previous text
    String line = (text+"                    ").substring(0,20);
     
    // In case the text has not changed: do nothing (avoiding flickering of the display)
    if (tftLines[nr]==line) return;
 
    // store line
    tftLines[nr]=line;
     
    // print the line, calculating the vertical position from the line number
    tft.drawString(line,0,nr*33,1); 
}
 
// ===================== END ================

Version 5

Wir fügen ein kleines Spiel hinzu, zu dem man gelangt, wenn man den Arm #0 berührt. Der Stern gibt eine Farbe vor und zeigt sie auf drei Armen. Wir können durch das Berühren der Arme #1 … #6 eine zweite Farbe einstellen, die auf den restlichen vier Armen gezeigt wird. Dabei sind #6/#1 für den ROT-Anteil zuständig, #5/#2 für GRÜN und #4/#3 für BLAU. Das Ziel ist, unsere Farbe der vorgegebenen Farbe so ähnlich wie möglich zu machen.
Um zu gewinnen, muss der Spieler den Farbton nicht perfekt treffen, ihm aber doch ziemlich nahe kommen.

Gelangt der Spieler in die Nähe der richtigen Farbe, so wird er durch Bildschirmmeldungen und/oder akustische Signale unterstützt – für die akustische Ausgabe haben wir dazu einen kleinen Buzzer an Pin #25 angeschlossen). Wenn das Spiel beginnt, wird am Bildschirm gezeigt, welche Art der Unterstützung gerade (zufällig) aktiviert wurde. Ist keinerlei Unterstützung aktiv, so ist es nicht einfach, den Farbton genau genug zu treffen.

/*
    This program shows rotation rainbow colors and offers a game
    where you try to match a given random color
 
    The program is made of the following components:
        ------------------------------------------------------------
        * Touch pin recognition
        * TFT display control
        * LED strip control and color wheel
        ------------------------------------------------------------
        * Rainbow patterns: arms #1..6 control the pattern, #0=EXIT  
        * ColorMatcher:     arms #1..6 change colors, #0=EXIT
        ------------------------------------------------------------
        * SETUP and LOOP
         ------------------------------------------------------------
 
    VERSION: 5
*/
 
// ===================== TOUCH DETECTION ================
 
int touchPins[]={32,27,12,13,15,2,33};  // pins (#0..#6) connected to touch contacts
 
#define TOUCH_THRESHOLD 20              // values below mean "touched" 
 
boolean touched(int arm, int wait=0) {
    // return true if the user touches the given arm
    // if a touch is detected the function will delay the return for 100 msecs
    // this allows finer control for the user
       
    int t= touchRead(touchPins[arm]);   // high values:untouched, lower values: maybe touched
    if (t==0) return false;             // ignore a value of zero
    if (t<= TOUCH_THRESHOLD) {          // a reasonable to detect a touch is a value of 30..60
        delay(5);                       // try a second time after a short pause
        t =touchRead(touchPins[arm]);
        if (t==0 || t > TOUCH_THRESHOLD) return false;
        delay(wait);                    // wait a moment so that the user can release the touch
        return true;
    }
    return false;                       // return immediately if no touch was detected
}
 
 
// ===================== DISPLAY ================
    
#include <SPI.h>                            // SPI interface for TFT display
#include <TFT_eSPI.h>                       // Hardware-specific TFT library                                
TFT_eSPI tft = TFT_eSPI();                  // define a variable for the TFT display
 
String tftLines[4] = {"","","",""};         // the current four lines shown on the display
 
void tftClear() {
    tft.fillScreen(TFT_BLACK);     
}
void tftPrint(int nr, String text) {
    // print text into the specified line (0..3)
   
    // we must fill the text to be shown with spaces; 
    // otherwise portions of previous content might still be visible 
    // if the new text was shorter than the previous text
    String line = (text+"                    ").substring(0,20);
       
    // In case the text has not changed: do nothing (avoiding flickering of the display)
    if (tftLines[nr]==line) return;
   
    // store line
    tftLines[nr]=line;
       
    // print the line, calculating the vertical position from the line number
    tft.drawString(line,0,nr*33,1); 
}
 
 
// ===================== SOUND BUZZER ================
 
#define BUZZER_PIN      25
#define BUZZER_CHANNEL  0
 
void buzzerSetup() {
    int vol = 64; // 127 = 50% duty cycle = max volume
    int freq = 443;
    int resolution = 8;
    ledcSetup(BUZZER_CHANNEL, freq, resolution);
    ledcAttachPin(BUZZER_PIN, BUZZER_CHANNEL);
    ledcWrite(BUZZER_CHANNEL,vol);              // set duty cycle (aka volume)
    ledcWriteTone(BUZZER_CHANNEL,0);            // mute
}
 
void buzzerPlay(int freq, int duration, int vol) {
    Serial.println("buzzer "+String(freq)+" "+String(duration)+" "+String(vol));
    ledcWrite(BUZZER_CHANNEL,vol); // set duty cycle 0..127 (=50%)
    ledcWriteTone(BUZZER_CHANNEL, freq);
    delay(duration);
    ledcWriteTone(BUZZER_CHANNEL,0);   // mute
}
 
void buzzerPlay(int freq, int duration) {
    buzzerPlay(freq,duration,127);              // use max. volume
}
 
void buzzerBeep() {
    buzzerPlay(443,100,127);    // short note "a"
}
 
 
// ===================== LED STRIP and COLOR WHEEL ============
 
#define NUM_ARMS  7                                 // the star has 7 arms ("legs")
#define NUM_LEDS (2*NUM_ARMS)                       // we have two LEDs per arm
 
#include <Adafruit_NeoPixel.h>
Adafruit_NeoPixel strip = Adafruit_NeoPixel(        // define the LED strip (type WS2812)
    NUM_LEDS,                                       // number of color triples
    26,                                             // the pin# connected to the strip
    NEO_GRB + NEO_KHZ800                            // 800 kHz bit stream in GRB sequence
);
byte colorPosition  = 0;                            // initial position of color wheel (0 = red)
    
byte *wheel(byte wheelPos) {
    // implements a color circle ("wheel") beginning with (1) red, changing to (2) green and (3) to blue
    // the wheel if divided into 256 steps, so 0=red, 256/3=green, 256*2/3=blue, etc.
    // the <wheelPos> parameter is a number between 0..255
    // the function returns an RGB array of three bytes
        
    static byte c[3];   // the resulting RGB triple
    
    // we have three segments (0..84, 85..169, 170..255)
    // in each segment one RGB color is completely missing (i.e. it has a value of 0)
    // another one is growing from 0..255 and the third color goes down from 255 to 0
    // this means that we have equal brightness over the whole spectrum
    
    if(wheelPos < 85) {             // first segment 0..84
        c[0]= 255 - wheelPos * 3;   // declining RED
        c[1]= wheelPos * 3;         // growing   GREEN
        c[2]= 0;                    // no        BLUE
        
    } else if(wheelPos < 170) {     // second segment 85..169
        wheelPos -= 85;             // make position relative to segment start
        c[0]= 0;                    // no        RED
        c[1]= 255 - wheelPos * 3;   // declining GREEN
        c[2]= wheelPos * 3;         // growing   BLUE
            
    } else {                        // third segment  170..255
        wheelPos -= 170;            // make position relative to segment start
        c[0]= wheelPos * 3;         // growing   RED
        c[1]= 0;                    // no        GREEN 
        c[2]= 255 - wheelPos * 3;   // declining BLUE
    }
    
    return c;                       // return the RGB triple
}
 
   
   
// ===================== RAINBOW ================
    
void rainbowCycle() {
 
    tftClear();
    tftPrint(0,"Rotating Rainbow");
 
    unsigned long   speedDelay; // delay between iterations in msecs
    int             gap;        // gap between color increments (wheel steps)
    int             segment;    // segment size of color circle (10=full, 20=half, ..)
 
    // start with random settings:
    speedDelay  = random(20); 
    gap         = random(10); 
    segment     = random(10); 
 
    byte *c;                            // a triple of color values obtained from the wheel
    boolean changed=true;
    for(;;) {   // forever
           
        colorPosition = (colorPosition+gap) % 256;      // increment color position
   
        // divide the circle into equi-distant colors for the LEDs
        // if segments==10 use full spectrum, else use less (e.g. 10%) or more (eg 200%)
        // assign a color to each LED starting from the current color position
            
        for(int l=0; l< NUM_LEDS; l++) {
            c= wheel(((l * 256 / NUM_LEDS * 10 / segment) + colorPosition) % 256);
            strip.setPixelColor(l, *c, *(c+1), *(c+2));
        }
    
        strip.show();               // now transfer the LED memory to the LED strip
    
        // check touch sensors and modify settings if desired
        if (touched(1,100) && speedDelay < 1000 ) { changed=true; speedDelay++; } // slower
        if (touched(2,100) && speedDelay >    1 ) { changed=true; speedDelay--; }
        if (touched(3,100) && gap>1             ) { changed=true; gap--;        } // closer
        if (touched(4,100) && gap<100           ) { changed=true; gap++;        }
        if (touched(5,100) && segment>1         ) { changed=true; segment--;    } // smaller
        if (touched(6,100) && segment<50        ) { changed=true; segment++;    }
          
        // show current settings for debugging on serial line
        if (changed) {
            Serial.println("delay="+String(speedDelay)+"  gap="+String(gap)+"  segment="+String(segment));
            changed=false;
        }
   
        // show current settings for the user on the TFT display
        tftPrint(1,"delay   ="+String(speedDelay));
        tftPrint(2,"gap     ="+String(gap));
        tftPrint(3,"segment ="+String(segment));
           
        // wait for the configured time (msecs)        
        delay(speedDelay);
 
        if (touched(0,400)) break;     // EXIT rainbow if user touches arm #0
    }
}
 
 
// ===================== COLOR MATCH GAME ================
    
void colorMatch(boolean withSound, boolean withHints) {
 
    tftClear();
    tftPrint(0,"Color Matcher");
    tftPrint(1,withSound? "SOUND" : "SILENT");
    tftPrint(2,withHints? "HINTS" : "NO HINTS");
 
    uint8_t r,rr,g,gg,b,bb;
 
    boolean success=true;   // triggers a new color to be chosen
     
    for(;;) {   // forever
 
        if (success) {

            // choose a new color
            rr=random(256), gg=random(256), bb=random(256);    // the color to be matched
            r=10, g=10, b=10;                                  // the current color
            for(int l=0; l< NUM_LEDS; l++) {
                if(l>=3 && l<=10)   strip.setPixelColor(l, r, g, b );
                else                strip.setPixelColor(l, rr,gg,bb);
            }
            strip.show();               // now transfer the LED memory to the LED strip
            success=false;
            tftPrint(3,"");                
        }
 
        // check touch sensors and modify settings if desired
        if (touched(1,5)) if (r<255) r++;
        if (touched(2,5)) if (g<255) g++;
        if (touched(3,5)) if (b<255) b++;
        if (touched(4,5)) if (b>0) b--;
        if (touched(5,5)) if (g>0) g--;
        if (touched(6,5)) if (r>0) r--;
 
        if (touched(1) || touched(2) || touched(3) || touched(4) || touched(5) || touched(6)) {
             
            for(int l=3; l<=10; l++) strip.setPixelColor(l, r ,g ,b );
            strip.show();
             
            int distance = (r-rr)*(r-rr)+(g-gg)*(g-gg)+(b-bb)*(b-bb);   // sum of squares
             
            if (abs(r-rr)<=5 && abs(g-gg)<=5 && abs(b-bb) <=5) {   // success
                tftPrint(3,"well done!");
				// play tone scale
				for (int f=264;f<=264;f++) {
					float freq=f;
					for(int n=0;n<=12;n++) {
						if (n==0||n==2||n==4||n==5||n==7||n==9||n==11|| n==12) buzzerPlay(round(freq),200);
						freq=freq*1.059463;
						delay(100);     
					}
					delay(500);
				}
                success=true;
                for(int n=0;n<5;n++) {
                    delay(200);
                    for(int l=3; l<=10; l++) strip.setPixelColor(l,0,0,0);
                    strip.show();
                    delay(200);
                    for(int l=3; l<=10; l++) strip.setPixelColor(l, r ,g ,b );
                    strip.show();
                }
            }
            else if (abs(r-rr)<=10 && abs(g-gg)<=10 && abs(b-bb) <=10) {   // close
                if (withHints) tftPrint(3,"looks good");
            }
            else if (abs(r-rr)<=20 && abs(g-gg)<=20 && abs(b-bb) <=20) {   // closer
                if (withHints) tftPrint(3,"not bad");
            }
            else {
                tftPrint(3,"");                
            }
            if (withSound && distance<3000) buzzerPlay(800-distance/6,100);   // buzzer feed back
             
            Serial.printf("red %d %d    green %d %d      blue %d %d \n",
                rr,r-rr,gg,g-gg,bb,b-bb);
        }
        if (touched(0,400)) break;     // EXIT rainbow if user touches arm #0
    }
}
 
   
// ===================== SETUP ================
    
void setup() {
    Serial.begin(115200);                           // for monitoring
    delay(100);
   
    tft.begin();                                    // initialize, 240 x 135 pixels (~ 20x4 chars)
    tft.setRotation(1);                             // 1=landscape orientation
    tft.setTextDatum(TL_DATUM);                     // top left reference point for texts
    tft.setTextColor(TFT_WHITE, TFT_BLACK);         // white on black
    tft.fillScreen(TFT_BLACK);                      // clear screen, all black
    tft.setFreeFont(&FreeSans12pt7b);               // fairly large font (allows for 4 lines)
       
    strip.begin();                                  // initialize LED strip
     
    buzzerSetup();                                  // a piezo electric buzzer
    buzzerBeep();
}
    
void loop() {
    // toggle between rainbow and color matching game
    rainbowCycle();
    colorMatch(random(2) ? true:false,random(2) ? true:false);               
}
 
// ===================== END ================

Mathe-Stern

Wenn man einen Stern mit 7 Zacken und 7 Sensoren gebaut hat, kann man viele weitere Ideen mit Software umsetzen.
Wir haben dazu eine eigene Seite über den Mathe-Stern verfasst.

Veröffentlicht am
Kategorisiert in Allgemein