Skip to content

Instantly share code, notes, and snippets.

@l0c0luke
Created July 1, 2019 14:29

Revisions

  1. l0c0luke created this gist Jul 1, 2019.
    614 changes: 614 additions & 0 deletions LENSiE Saber
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,614 @@
    #include "Arduino.h"
    #include <FastLED.h>
    #include <WiFi.h>
    #include "heltec.h"
    #include "jsbutton.h"

    #define BAND 915E6

    #define STRING_LEN 128

    // -- Configuration specific key. The value should be modified if config structure was changed.
    #define CONFIG_VERSION "v6"

    // -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial
    // password to buld an AP. (E.g. in case of lost password)
    #define CONFIG_PIN 1

    #define PRG_PIN 0

    // Definition for the array of routines to display.
    #define ARRAY_SIZE(A) (sizeof(A) / sizeof((A)[0]))

    // -- Status indicator pin.
    // First it will light up (kept LOW), on Wifi connection it will blink,
    // when connected to the Wifi it will turn off (kept HIGH).
    #define STATUS_PIN LED_BUILTIN

    #define BRIGHTNESS 255
    #define TEMPERATURE ClearBlueSky

    #define LEDS_PIN_1 12
    #define LEDS_PIN_2 13

    #define NUM_LEDS 140

    #define CHIPSET WS2812
    #define COLOR_ORDER GRB

    int currentLedsState = 0;
    int proposedLedsState = 0;

    CRGB leds1[NUM_LEDS];
    CRGB leds2[NUM_LEDS];

    uint8_t gCurrentPatternNumber = 0; // Index number of which pattern is current
    uint8_t gHue = 0; // rotating "base color" used by many of the patterns

    typedef void (*SimplePatternList[])();

    // -- Callback method declarations.
    void oledUpdate();
    void processLoraPayload(String payload);
    void onLoraReceive(int packetSize);
    void sendLoraMessage(String outgoing);

    int pinState = HIGH;

    String incomingLoraMessage;
    String outgoingLoraMessage;
    String loraPayloadToSend;
    byte loraGroup = 0xBB;

    boolean inMenu = false;

    String lightModes[] = {
    "Rainbow",
    "Rainbow With Glitter",
    "Confetti",
    "Sine Lon",
    "Juggle",
    "BPM",
    "Bouncing Balls",
    "Lightning",
    "Moving Gradient",
    "Police Lights",
    "Fire",
    "Off"
    };

    // -- Moving Gradient
    // User variables
    CHSV gradStartColor(0,255,255); // Gradient start color.
    CHSV gradEndColor(161,255,255); // Gradient end color.
    uint8_t gradStartPos = 0; // Starting position of the gradient.
    #define gradLength 50 // How many pixels (in total) is the grad from start to end.
    int8_t gradDelta = 1; // 1 or -1. (Negative value reverses direction.)

    // If you wanted to move your gradient 32 pixels in 120 seconds, then:
    // 120sec / 32pixel = 3.75sec
    // 3.75sec x 1000miliseconds/sec = 3750milliseconds
    #define gradMoveDelay 10 // How fast to move the gradient (in Milliseconds)

    CRGB grad[gradLength]; // A place to save the gradient colors. (Don't edit this)

    // -- Lightning
    uint8_t frequency = 50; // controls the interval between strikes
    uint8_t flashes = 8; //the upper limit of flashes per strike
    unsigned int dimmer = 1;

    uint8_t ledstart; // Starting location of a flash
    uint8_t ledlen;
    // -- End Lightning

    // Fire
    CRGBPalette16 gPal;
    bool gReverseDirection = false;

    // --- Bouncing Balls ---
    #define GRAVITY -9.81 // Downward (negative) acceleration of gravity in m/s^2
    #define h0 1 // Starting height, in meters, of the ball (strip length)
    #define NUM_BALLS 4 // Number of bouncing balls you want (recommend < 7, but 20 is fun in its own way)

    float h[NUM_BALLS] ; // An array of heights
    float vImpact0 = sqrt( -2 * GRAVITY * h0 ); // Impact velocity of the ball when it hits the ground if "dropped" from the top of the strip
    float vImpact[NUM_BALLS] ; // As time goes on the impact velocity will change, so make an array to store those values
    float tCycle[NUM_BALLS] ; // The time since the last time the ball struck the ground
    int pos[NUM_BALLS] ; // The integer position of the dot on the strip (LED index)
    long tLast[NUM_BALLS] ; // The clock time of the last ground strike
    float COR[NUM_BALLS] ; // Coefficient of Restitution (bounce damping)
    // --- END BOUNCING BALLS ---

    void setup()
    {
    pinMode(PRG_PIN, INPUT);

    pinMode(16, OUTPUT);

    digitalWrite(16, LOW); // set GPIO16 low to reset OLED
    delay(50);
    digitalWrite(16, HIGH);

    Serial.begin(115200);
    Serial.println();
    Serial.println("Starting up...");

    Heltec.begin(true /*DisplayEnable Enable*/, true /*Heltec.LoRa Enable*/, true /*Serial Enable*/, true /*PABOOST Enable*/, BAND /*long BAND*/);

    // LEDs
    FastLED.addLeds<CHIPSET, LEDS_PIN_1, COLOR_ORDER>(leds1, NUM_LEDS).setCorrection(TypicalLEDStrip);
    FastLED.addLeds<CHIPSET, LEDS_PIN_2, COLOR_ORDER>(leds2, NUM_LEDS).setCorrection(TypicalLEDStrip);

    FastLED.setBrightness(BRIGHTNESS);
    FastLED.setTemperature(TEMPERATURE);

    fill_solid(leds1, NUM_LEDS, CRGB::Black);
    fill_solid(leds2, NUM_LEDS, CRGB::Black);

    FastLED.show();

    // setup variables for bouncing ball
    for (int i = 0 ; i < NUM_BALLS ; i++) { // Initialize variables
    tLast[i] = millis();
    h[i] = h0;
    pos[i] = 0; // Balls start on the ground
    vImpact[i] = vImpact0; // And "pop" up at vImpact0
    tCycle[i] = 0;
    COR[i] = 0.90 - float(i)/pow(NUM_BALLS,2);
    }

    // setup for moving gradient
    fill_gradient(grad, gradStartPos, gradStartColor, gradStartPos+gradLength-1, gradEndColor, SHORTEST_HUES);

    // fire
    //gPal = CRGBPalette16( CRGB::Black, CRGB::Red, CRGB::Yellow, CRGB::White);
    gPal = HeatColors_p;

    Serial.println("LENSiE Saber is ready.");
    }

    void onLoraReceive(int packetSize)
    {
    if (packetSize == 0)
    return; // if there's no packet, return

    // read packet header bytes:
    int recipient = LoRa.read(); // recipient address
    byte incomingLength = LoRa.read(); // incoming msg length

    String incomingLoraMessage = "";

    while (LoRa.available())
    {
    incomingLoraMessage += (char)LoRa.read();
    }

    if (incomingLength != incomingLoraMessage.length())
    { // check length for error
    Serial.println("error: message length does not match length");
    return;
    }

    // if the recipient isn't this device or broadcast,
    if (recipient != loraGroup)
    {
    Serial.println("This message is not for me.");
    return;
    }

    Serial.println("Processing LoRa: " + incomingLoraMessage);
    processLoraPayload(incomingLoraMessage);

    // if message is for this device, or broadcast, print details:
    //Serial.println("Message length: " + String(incomingLength));
    //Serial.println("Message: " + incomingLoraMessage);
    //Serial.println("RSSI: " + String(LoRa.packetRssi()));
    //Serial.println("Snr: " + String(LoRa.packetSnr()));
    }

    void processLoraPayload(String payload)
    {
    String noun; // who should listen
    String verb; // command they should do

    int firstSlash = payload.indexOf("/");

    if (firstSlash == -1)
    {
    Serial.println("Invalid LoRa command with: " + payload);
    }
    else
    {
    noun = payload.substring(0, firstSlash);
    int secondSlash = payload.indexOf("/", firstSlash + 1);
    verb = payload.substring(firstSlash + 1, payload.length());

    if (noun == "saber")
    {
    Serial.println("We got a saber command");
    currentLedsState = verb.toInt();
    proposedLedsState = verb.toInt();
    }
    else
    {
    Serial.println("Invalid LoRa command with: " + payload);
    }
    }
    }

    void sendLoraMessage(String outgoing)
    {
    Serial.print("LoRa Send: " + outgoing);
    LoRa.beginPacket(); // start packet
    LoRa.write(loraGroup); // add sender address
    LoRa.write(outgoing.length()); // add payload length
    LoRa.print(outgoing); // add payload
    LoRa.endPacket(); // finish packet and send it
    Serial.println(" done");
    }

    void oledUpdate()
    {
    Heltec.display->clear();
    Heltec.display->setColor(WHITE);
    Heltec.display->setFont(ArialMT_Plain_10);
    Heltec.display->setTextAlignment(TEXT_ALIGN_LEFT);
    Heltec.display->drawString(0, 0, "Name: " + WiFi.macAddress());

    if (WiFi.SSID() != 0)
    {
    Heltec.display->drawString(0, 10, "WiFi: " + WiFi.SSID() + " " + WiFi.RSSI());
    Heltec.display->drawString(0, 20, "IP: " + WiFi.localIP().toString());
    }
    else
    {
    Heltec.display->drawString(0, 10, "WiFi: Disconnected");
    }

    if (inMenu == false) {
    Heltec.display->drawString(0, 30, "* Long press for menu. *");
    } else {
    Heltec.display->drawString(0, 30, "Menu: " + lightModes[proposedLedsState] + "?");
    Heltec.display->drawString(0, 50, "(Long press to select.)");
    }
    Heltec.display->display();
    }

    // *************************
    // ** LEDEffect Functions **
    // *************************

    uint8_t V; //brightness for rainbow
    uint8_t S; //saturation for rainbow
    boolean toggleS;
    boolean toggleV;

    //Amount to tint (desaturate) rainbow. Can use either RGB or HSV format
    //CRGB tintAmt(128,128,128);
    CHSV tintAmt(0,0,90);

    void rainbow() {
    static uint16_t sPseudotime = 0;
    static uint16_t sLastMillis = 0;
    static uint16_t sHue16 = 0;

    uint8_t sat8 = beatsin88( 87, 220, 250);
    uint8_t brightdepth = beatsin88( 341, 96, 224);
    uint16_t brightnessthetainc16 = beatsin88( 203, (25 * 256), (40 * 256));
    uint8_t msmultiplier = beatsin88(147, 23, 60);

    uint16_t hue16 = sHue16;//gHue * 256;
    uint16_t hueinc16 = beatsin88(113, 1, 3000);

    uint16_t ms = millis();
    uint16_t deltams = ms - sLastMillis ;
    sLastMillis = ms;
    sPseudotime += deltams * msmultiplier;
    sHue16 += deltams * beatsin88( 400, 5,9);
    uint16_t brightnesstheta16 = sPseudotime;

    for( uint16_t i = 0 ; i < NUM_LEDS; i++) {
    hue16 += hueinc16;
    uint8_t hue8 = hue16 / 256;

    brightnesstheta16 += brightnessthetainc16;
    uint16_t b16 = sin16( brightnesstheta16 ) + 32768;

    uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536;
    uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536;
    bri8 += (255 - brightdepth);

    CRGB newcolor = CHSV( hue8, sat8, bri8);

    uint16_t pixelnumber = i;
    pixelnumber = (NUM_LEDS-1) - pixelnumber;

    nblend( leds1[pixelnumber], newcolor, 64);
    nblend( leds2[pixelnumber], newcolor, 64);
    }

    } // rainbow()



    void rainbowWithGlitter() {

    rainbow(); // Built-in FastLED rainbow, plus some random sparkly glitter.
    addGlitter(80);

    } // rainbowWithGlitter()



    void addGlitter(fract8 chanceOfGlitter) {

    if(random8() < chanceOfGlitter) {
    leds1[ random16(NUM_LEDS) ] += CRGB::White;
    leds2[ random16(NUM_LEDS) ] += CRGB::White;
    }

    } // addGlitter()



    void confetti() { // Random colored speckles that blink in and fade smoothly.

    fadeToBlackBy(leds1, NUM_LEDS, 10);
    fadeToBlackBy(leds2, NUM_LEDS, 10);
    int pos = random16(NUM_LEDS);
    leds1[pos] += CHSV(gHue + random8(64), 200, 255);
    leds2[pos] += CHSV(gHue + random8(64), 200, 255);

    } // confetti()



    void sinelon() { // A colored dot sweeping back and forth, with fading trails.

    fadeToBlackBy(leds1, NUM_LEDS, 20);
    fadeToBlackBy(leds2, NUM_LEDS, 20);
    int pos = beatsin16(13,0,NUM_LEDS-1);
    leds1[pos] += CHSV(gHue, 255, 192);
    leds2[pos] += CHSV(gHue, 255, 192);

    } // sinelon()



    void bpm() { // Colored stripes pulsing at a defined Beats-Per-Minute.

    uint8_t BeatsPerMinute = 62;
    CRGBPalette16 palette = PartyColors_p;
    uint8_t beat = beatsin8(BeatsPerMinute, 64, 255);

    for(int i = 0; i < NUM_LEDS; i++) { //9948
    leds1[i] = ColorFromPalette(palette, gHue+(i*2), beat-gHue+(i*10));
    leds2[i] = ColorFromPalette(palette, gHue+(i*2), beat-gHue+(i*10));
    }

    } // bpm()



    void juggle() { // Eight colored dots, weaving in and out of sync with each other.

    fadeToBlackBy(leds1, NUM_LEDS, 20);
    fadeToBlackBy(leds2, NUM_LEDS, 20);
    byte dothue = 0;

    for(int i = 0; i < 8; i++) {
    leds1[beatsin16(i+7,0,NUM_LEDS-1)] |= CHSV(dothue, 200, 255);
    leds2[beatsin16(i+7,0,NUM_LEDS-1)] |= CHSV(dothue, 200, 255);
    dothue += 32;
    }
    }

    void bouncingBalls() {
    for (int i = 0 ; i < NUM_BALLS ; i++) {
    tCycle[i] = millis() - tLast[i] ; // Calculate the time since the last time the ball was on the ground

    // A little kinematics equation calculates positon as a function of time, acceleration (gravity) and intial velocity
    h[i] = 0.5 * GRAVITY * pow( tCycle[i]/1000 , 2.0 ) + vImpact[i] * tCycle[i]/1000;

    if ( h[i] < 0 ) {
    h[i] = 0; // If the ball crossed the threshold of the "ground," put it back on the ground
    vImpact[i] = COR[i] * vImpact[i] ; // and recalculate its new upward velocity as it's old velocity * COR
    tLast[i] = millis();

    if ( vImpact[i] < 0.01 ) vImpact[i] = vImpact0; // If the ball is barely moving, "pop" it back up at vImpact0
    }
    pos[i] = round( h[i] * (NUM_LEDS - 1) / h0); // Map "h" to a "pos" integer index position on the LED strip
    }

    //Choose color of LEDs, then the "pos" LED on
    for (int i = 0 ; i < NUM_BALLS ; i++) {
    leds1[pos[i]] = CHSV( uint8_t (i * 40) , 255, 255);
    leds2[pos[i]] = CHSV( uint8_t (i * 40) , 255, 255);
    }
    FastLED.show();
    //Then off for the next loop around
    for (int i = 0 ; i < NUM_BALLS ; i++) {
    leds1[pos[i]] = CRGB::Black;
    leds2[pos[i]] = CRGB::Black;
    }
    FastLED.show();
    }

    void lightning() {
    ledstart = random8(NUM_LEDS); // Determine starting location of flash
    ledlen = random8(NUM_LEDS-ledstart); // Determine length of flash (not to go beyond NUM_LEDS-1)

    for (int flashCounter = 0; flashCounter < random8(3,flashes); flashCounter++) {
    if(flashCounter == 0) dimmer = 5; // the brightness of the leader is scaled down by a factor of 5
    else dimmer = random8(1,3); // return strokes are brighter than the leader

    fill_solid(leds1+ledstart,ledlen,CHSV(255, 0, 255/dimmer));
    fill_solid(leds2+ledstart,ledlen,CHSV(255, 0, 255/dimmer));
    FastLED.show(); // Show a section of LED's
    FastLED.delay(random8(4,10)); // each flash only lasts 4-10 milliseconds
    fill_solid(leds1+ledstart,ledlen,CHSV(255,0,0)); // Clear the section of LED's
    fill_solid(leds2+ledstart,ledlen,CHSV(255,0,0));
    FastLED.show();

    if (flashCounter == 0) delay (150); // longer delay until next flash after the leader

    FastLED.delay(50+random8(100)); // shorter delay between strokes
    } // for()

    FastLED.delay(random8(frequency)*100); // delay between strikes
    }

    void movingGradient() {
    EVERY_N_MILLISECONDS(gradMoveDelay) {
    uint8_t count = 0;
    for (uint8_t i = gradStartPos; i < gradStartPos+gradLength; i++) {
    leds1[i % NUM_LEDS] = grad[count];
    leds2[i % NUM_LEDS] = grad[count];
    count++;
    }
    FastLED.show(); // Display the pixels.
    FastLED.clear(); // Clear the strip to not leave behind lit pixels as grad moves.

    gradStartPos = gradStartPos + gradDelta; // Update start position.
    if ( (gradStartPos > NUM_LEDS-1) || (gradStartPos < 0) ) { // Check if outside NUM_LEDS range
    gradStartPos = gradStartPos % NUM_LEDS; // Loop around as needed.
    }
    }
    }

    int BOTTOM_INDEX = 0;
    int TOP_INDEX = int(NUM_LEDS/2);

    //-PERISTENT VARS
    int idex = 0; //-LED INDEX (0 to NUM_LEDS-1
    int idx_offset = 0; //-OFFSET INDEX (BOTTOM LED TO ZERO WHEN LOOP IS TURNED/DOESN'T REALLY WORK)
    int ihue = 0; //-HUE (0-360)
    int ibright = 0; //-BRIGHTNESS (0-255)
    int isat = 0; //-SATURATION (0-255)
    int bouncedirection = 0; //-SWITCH FOR COLOR BOUNCE (0-1)
    float tcount = 0.0; //-INC VAR FOR SIN LOOPS
    int lcount = 0; //-ANOTHER COUNTING VAR

    //-FIND INDEX OF ANTIPODAL OPPOSITE LED
    int antipodal_index(int i) {
    //int N2 = int(NUM_LEDS/2);
    int iN = i + TOP_INDEX;
    if (i >= TOP_INDEX) {iN = ( i + TOP_INDEX ) % NUM_LEDS; }
    return iN;
    }

    void policeLights() {
    idex++;
    if (idex >= NUM_LEDS) {idex = 0;}
    int idexR = idex;
    int idexB = antipodal_index(idexR);

    leds1[idexR] = CHSV( 255, 0, 0);
    leds1[idexB] = CHSV( 0, 0, 255);
    leds2[idexR] = CHSV( 255, 0, 0);
    leds2[idexB] = CHSV( 0, 0, 255);
    FastLED.delay(40);
    }

    #define COOLING 55

    // SPARKING: What chance (out of 255) is there that a new spark will be lit?
    // Higher chance = more roaring fire. Lower chance = more flickery fire.
    // Default 120, suggested range 50-200.
    #define SPARKING 120

    void Fire2012WithPalette()
    {
    // Array of temperature readings at each simulation cell
    static byte heat[NUM_LEDS];

    // Step 1. Cool down every cell a little
    for( int i = 0; i < NUM_LEDS; i++) {
    heat[i] = qsub8( heat[i], random8(0, ((COOLING * 10) / NUM_LEDS) + 2));
    }

    // Step 2. Heat from each cell drifts 'up' and diffuses a little
    for( int k= NUM_LEDS - 1; k >= 2; k--) {
    heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
    }

    // Step 3. Randomly ignite new 'sparks' of heat near the bottom
    if( random8() < SPARKING ) {
    int y = random8(7);
    heat[y] = qadd8( heat[y], random8(160,255) );
    }

    // Step 4. Map from heat cells to LED colors
    for( int j = 0; j < NUM_LEDS; j++) {
    // Scale the heat value from 0-255 down to 0-240
    // for best results with color palettes.
    byte colorindex = scale8( heat[j], 240);
    CRGB color = ColorFromPalette( gPal, colorindex);
    int pixelnumber;
    if( gReverseDirection ) {
    pixelnumber = (NUM_LEDS-1) - j;
    } else {
    pixelnumber = j;
    }
    leds1[pixelnumber] = color;
    leds2[pixelnumber] = color;
    }
    }

    void turnOff() { // Eight colored dots, weaving in and out of sync with each other.

    fadeToBlackBy(leds1, NUM_LEDS, 20);
    fadeToBlackBy(leds2, NUM_LEDS, 20);
    }

    SimplePatternList gPatterns = {rainbow, rainbowWithGlitter, confetti, sinelon, juggle, bpm, bouncingBalls, lightning, movingGradient, policeLights, Fire2012WithPalette, turnOff }; // Don't know why this has to be here. . .

    void readButton() { // Read the button and increase the mode

    uint8_t b = checkButton();

    if (b == 1) { // Just a click event to advance to next pattern
    if (inMenu == true) {
    if (proposedLedsState == ARRAY_SIZE(gPatterns) - 1) {
    proposedLedsState = 0;
    } else {
    proposedLedsState = proposedLedsState + 1;
    }
    }
    }

    if (b == 2) { // A double-click event to reset to 0 pattern
    Serial.println("double click");
    }

    if (b == 3) { // A hold event to write current pattern to EEPROM
    if (inMenu == false) {
    Serial.println("Going MENU");
    inMenu = true;
    } else {
    currentLedsState = proposedLedsState;
    sendLoraMessage("saber/" + String(currentLedsState));
    inMenu = false;
    Serial.println("Leaving MENU with " + String(currentLedsState));
    }
    }

    }

    void loop()
    {
    onLoraReceive(LoRa.parsePacket());
    oledUpdate();

    readButton();

    EVERY_N_MILLISECONDS(10) {
    gPatterns[currentLedsState](); // Call the current pattern function once, updating the 'leds' array
    }

    EVERY_N_MILLISECONDS(20) { // slowly cycle the "base color" through the rainbow
    gHue++;
    }

    FastLED.show();
    }