Synchronised LED flying wings

Inspired by the night display at Weston park this year I am attempting to build several flying wings with synchronised LED lighting

QCAD screen shot

Some time ago I built a night flying wing based on the Flite Test Arrow. I modified this design making the centre fuselage section wider adding ribs and reworking the plans for 3mm depron. The depron was cut using a CNC cutter I built some years ago, each plane uses approximately 1 sheet of depron.

Depron cutter in action
Leds fitted to spa (x4)

Anyone familiar with the flight test models will recognise the same construction methods used here with the wing wrapped around a foam spa.
The ribs were added to help channel the LED light along the wing. there are 84 rib sections per wing these are glued in with CA. I thought this would take forever but managed to get into a decent rhythm.

Previously I had used WS2812 addressable 60LED/m strips, but this time I opted to use the SK9822 variety with the hope that I would run into less problems when it came to programming the microcontroller.

The micro controller used in the wing is a 5v 16MHz Arduino pro mini clone. the strips are in two groups ocupying 4 of the pro mini pins.

To deal with the wireless communication I opted for the HC12 breakout board which is a 433 Mhz serial transceiver.
It claims 1800m range although i took this with a grain of salt and opted for the lowest baud rate which gave me the highest receiver sensitivity. This is connected to a software serial port on the micro controller.

The only other connection on the microcontroller is a button to select the ID number of the model to provide flexibility depending on how many are flying together.

The pattern displayed by the model is selectable by sending a serial message containing the ID of the model (zero selects all), the number of the pattern to be displayed and up to 3 variables to change various parameters of the effect.

In addition to the ID, the maximum brightness can be set and standalone patterns can be selected using the button.

The code running on the wing is shown below.

#include 
#include softwareserial.h
#include acebutton.h
#include eeprom.h
using namespace ace_button;
//fast led defines
#define NUM_LEDS    84
#define WING_LEDS   21
#define FRAMES_PER_SECOND 120
#define COOLING  60  //fire settings
#define SPARKING 90
#define blinkTime 500 //blink time in ms
#define strobeTime 1500 //blink time in ms
// ID of the settings block
#define CONFIG_VERSION "ls2"
// Tell it where to store your config data in EEPROM
#define CONFIG_START 32
// settings structure
struct StoreStruct {
  // This is for mere detection if they are your settings
  char version[4];
  // The variables of your settings
  uint8_t settings[3];
}
storage = {
  CONFIG_VERSION,
  // The default values
  {1, 1, 8}
};
//serial
bool gReverseDirection = false;
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];        // temporary array for use when parsing
// variables to hold the parsed data
int c = 0;
byte r = 0;
byte g = 0;
byte b = 0;
byte idr = 0;
//Previous data for momentary modes (blink & Strobe)
int c_prev = 0;
byte r_prev = 0;
byte g_prev = 0;
byte b_prev = 0;
boolean newData = false;
long lastMillis = 0;
boolean buttonLatch = 0;
boolean blinkLatch = 0;
boolean strobeLatch = 0;
SoftwareSerial HC12(7, 8);//rx,tx
//fastled
CRGB leds[NUM_LEDS];
CRGB rgb (0, 0, 0);
CHSV hsv (0, 0, 0);
uint8_t hue = 0;
AceButton button(9);
uint8_t gHue = 0;
uint8_t Menu = 0;
bool isOn = false;
uint32_t lastStrobeMs = 0; // The previous light change time.

void loadConfig() {
  // To make sure there are settings, and they are YOURS!
  // If nothing is found it will use the default settings.
  if (EEPROM.read(CONFIG_START + 0) == CONFIG_VERSION[0] &&
      EEPROM.read(CONFIG_START + 1) == CONFIG_VERSION[1] &&
      EEPROM.read(CONFIG_START + 2) == CONFIG_VERSION[2])
    for (unsigned int t = 0; t < sizeof(storage); t++)
      *((char*)&storage + t) = EEPROM.read(CONFIG_START + t);
}

void saveConfig() {
  for (unsigned int t = 0; t < sizeof(storage); t++)
    EEPROM.update(CONFIG_START + t, *((char*)&storage + t));
}

void setup() {
  delay(3000); // sanity delay
  //serial
  loadConfig();
  pinMode(2, OUTPUT);                  // Output High for Transparent / Low for Command
  digitalWrite(2, HIGH);               // Enter Transparent mode
  delay(80);
  Serial.begin(9600);
  HC12.begin(2400);
  //fastled
  FastLED.addLeds(leds, 0, NUM_LEDS / 2); // left leds
  FastLED.addLeds(leds, NUM_LEDS / 2, NUM_LEDS / 2); // right leds
  FastLED.setBrightness( storage.settings[2] * 32 - 1 ); //set max brightness to value stored in eeprom
  FastLED.setMaxPowerInVoltsAndMilliamps(5, 2000);
  set_max_power_indicator_LED(13);
  // AceButton
  pinMode(9, INPUT_PULLUP);
  ButtonConfig* buttonConfig = button.getButtonConfig();
  buttonConfig->setEventHandler(handleEvent);
  buttonConfig->setFeature(ButtonConfig::kFeatureLongPress);
  buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterLongPress);
}

void loop() {
  gHue++; // slowly cycle the "base color" through the rainb
  recvWithStartEndMarkers();
  if (newData == true) {
    strcpy(tempChars, receivedChars);
    // this temporary copy is necessary to protect the original data
    //   because strtok() used in parseData() replaces the commas with \0
    parseData();
    //  showParsedData(); //debug
    newData = false;
  }
  if (buttonLatch == 1) {
    idDisplay();
  }
  else {
    switch (storage.settings[1]) {
      case 1:
        switch (c) {
          case 0:           //solid colour
            Dim(r, g, b);
            break;
          case 1:           //Fire
            Fire2012L(); // run simulation frame
            Fire2012R();
            break;
          case 2:           //Pride
            pride();
            break;
          case 3:         //blink
            blinko(r, g, b);
            break;
          case 4:
            rainbowWithGlitter_2(r, g);
            break;
          case 5:
            sinelon_2(r, g);
            break;
          case 6:
            bpm_2(r, g);
            break;
          case 7:
            confetti_2(r, g);
            break;
          case 8:
            juggle_2(r, g);
            break;
          case 9:
            strobe(r, g, b);
            break;
          default:
            // statements
            Dim(255, 255, 255);
            break;
        }
        break;
      case 2:
        EVERY_N_SECONDS(2)rainbowColour(33, 255);
        Dim(rgb.r, rgb.g, rgb.b);
        break;
      case 3:
        EVERY_N_SECONDS(2)randomColour(255);
        Dim(rgb.r, rgb.g, rgb.b);
        break;
      case 4:
        rainbowWithGlitter_2(4, 100);
        break;
      case 5:
        confetti_2(75, 29);
        break;
      case 6:
        sinelon_2(37, 4);
        break;
      case 7:
        bpm_2(120, 4);
        break;
      case 8:
        Fire2012L(); // run simulation frame
        Fire2012R();
        break;
    }
  }
  button.check();
  FastLED.show(); // display this frame
  FastLED.delay(1000 / FRAMES_PER_SECOND);
}

void strobe(uint8_t strobeMs, uint8_t HUE, uint8_t SAT) {
  if (strobeLatch == 0) {
    lastMillis = millis();
    strobeLatch = 1;
  }
  uint32_t ms = millis();
  if (ms - lastStrobeMs >= strobeMs) {
    fill_solid(leds, NUM_LEDS, isOn ? CHSV( 0, 0, 0) : CHSV( HUE, SAT, 255));
    isOn = !isOn;
    lastStrobeMs = ms;
  }
  if (strobeLatch == 1 && millis() - lastMillis >  strobeTime) {
    strobeLatch = 0;
    if (c == c_prev) {
      c = 0;
      r = 200;
      g = 0;
      b = 0;
    }
    else {
      c = c_prev;
      r = r_prev;
      g = g_prev;
      b = b_prev;
    }
  }
}

void blinko(byte valueR, byte valueG, byte valueB)
{
  if (blinkLatch == 0) {
    blinkLatch = 1;
    lastMillis = millis();
    fill_solid( &(leds[0]), NUM_LEDS, CRGB( valueR, valueG, valueB) );
  }
  else {
    if (millis() - lastMillis > blinkTime) {
      blinkLatch = 0;
      if (c == c_prev) {
        c = 0;
        r = 200;
        g = 0;
        b = 0;
      }
      else {
        c = c_prev;
        r = r_prev;
        g = g_prev;
        b = b_prev;
      }
    }
  }
}

void idDisplay()
{
  fill_solid( &(leds[0]), NUM_LEDS, CRGB( 0, 0, 0) );
  for (int x = 1; x <= storage.settings[Menu]; x++) {
    leds[x * 2] = CHSV(Menu * 85, 255, 255);
    FastLED.setBrightness( storage.settings[2] * 32 - 1 );
  }
  if (millis() - lastMillis > 3000) {
    buttonLatch = 0;
    saveConfig();
  }
}

void Dim(byte valueR, byte valueG, byte valueB)
{
  fill_solid( &(leds[0]), NUM_LEDS, CRGB( valueR, valueG, valueB) );
}

void Fire2012L()
{
  // Array of temperature readings at each simulation cell
  static byte heat[WING_LEDS];
  // Step 1.  Cool down every cell a little
  for ( int i = 0; i < WING_LEDS; i++) {
    heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / WING_LEDS) + 2));
  }
  // Step 2.  Heat from each cell drifts 'up' and diffuses a little
  for ( int k = WING_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 < WING_LEDS; j++) {
    CRGB color = HeatColor( heat[j]);
    int pixelnumber;
    if ( gReverseDirection ) {
      pixelnumber = (WING_LEDS - 1) - j;
    } else {
      pixelnumber = j;
    }
    leds[pixelnumber] = color;
    leds[WING_LEDS * 2 - 1 - pixelnumber] = color;
  }
}

void Fire2012R()
{
  // Array of temperature readings at each simulation cell
  static byte heat[WING_LEDS];
  // Step 1.  Cool down every cell a little
  for ( int i = 0; i < WING_LEDS; i++) {
    heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / WING_LEDS) + 2));
  }
  // Step 2.  Heat from each cell drifts 'up' and diffuses a little
  for ( int k = WING_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 < WING_LEDS; j++) {
    CRGB color = HeatColor( heat[j]);
    int pixelnumber;
    if ( gReverseDirection ) {
      pixelnumber = (WING_LEDS - 1) - j;
    } else {
      pixelnumber = j;
    }
    leds[WING_LEDS * 2 + pixelnumber] = color;
    leds[WING_LEDS * 4 - 1 - pixelnumber] = color;
  }
}

void rainbowWithGlitter_2( uint8_t stripeDensity, uint8_t chanceOfGlitter)
{
  // built-in FastLED rainbow, plus some random sparkly glitter
  fill_rainbow( leds, NUM_LEDS, gHue, stripeDensity);
  addGlitter(chanceOfGlitter);
}

void addGlitter( fract8 chanceOfGlitter)
{
  if ( random8() < chanceOfGlitter) {
    leds[ random16(NUM_LEDS) ] += CRGB::White;
  }
}

void confetti_2( uint8_t colorVariation, uint8_t fadeAmount)
{
  // random colored speckles that blink in and fade smoothly
  fadeToBlackBy( leds, NUM_LEDS, fadeAmount);
  int pos = random16(NUM_LEDS);
  leds[pos] += CHSV( gHue + random8(colorVariation), 200, 255);
}

void sinelon_2( uint8_t bpmSpeed, uint8_t fadeAmount)
{
  // a colored dot sweeping back and forth, with fading trails
  fadeToBlackBy( leds, NUM_LEDS, fadeAmount);
  int pos = beatsin16(bpmSpeed, 0, NUM_LEDS);
  leds[pos] += CHSV( gHue, 255, 192);
}

void bpm_2( uint8_t bpmSpeed, uint8_t stripeWidth)
{
  // colored stripes pulsing at a defined Beats-Per-Minute (BPM)
  uint8_t BeatsPerMinute = bpmSpeed;
  CRGBPalette16 palette = PartyColors_p;
  uint8_t beat = beatsin8( BeatsPerMinute, 64, 255);
  for ( int i = 0; i < NUM_LEDS; i++) {
    leds[i] = ColorFromPalette(palette, gHue + (i * stripeWidth), beat);
  }
}

void juggle_2( uint8_t numDots, uint8_t baseBpmSpeed) {
  // eight colored dots, weaving in and out of sync with each other
  fadeToBlackBy( leds, NUM_LEDS, 100);
  byte dothue = 0;
  for ( int i = 0; i < numDots; i++) {
    leds[beatsin16(i + baseBpmSpeed, 0, NUM_LEDS)] |= CHSV(dothue, 255, 224);
    dothue += (256 / numDots);
  }
}

void pride() {
  // This function draws rainbows with an ever-changing,
  // widely-varying set of parameters.
  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( leds[pixelnumber], newcolor, 64);
  }
}

void recvWithStartEndMarkers() {
  // HC12 receive data
  static boolean recvInProgress = false;
  static byte ndx = 0;
  char startMarker = '<';
  char endMarker = '>';
  char rc;
  while (HC12.available() > 0 && newData == false) {
    rc = HC12.read();
    if (recvInProgress == true) {
      if (rc != endMarker) {
        receivedChars[ndx] = rc;
        ndx++;
        if (ndx >= numChars) {
          ndx = numChars - 1;
        }
      }
      else {
        receivedChars[ndx] = '\0'; // terminate the string
        recvInProgress = false;
        ndx = 0;
        newData = true;
      }
    }
    else if (rc == startMarker) {
      recvInProgress = true;
    }
  }
}

void parseData() {      // split the data into its parts
  char * strtokIndx; 	// this is used by strtok() as an index
  strtokIndx = strtok(tempChars, ",");     // get the first part -
  idr = atoi(strtokIndx);
  if (idr == storage.settings[0] || idr == 0 || idr == (storage.settings[0] % 2 + 10)) { //only use if id is the same or 0 (all) or odd/even
    strtokIndx = strtok(NULL, ",");
    c_prev = c;
    c = atoi(strtokIndx);
    strtokIndx = strtok(NULL, ","); // this continues where the previous call left off
    r_prev = r;
    r = atoi(strtokIndx);     		// convert this part to an integer
    strtokIndx = strtok(NULL, ",");
    g_prev = g;
    g = atoi(strtokIndx);
    strtokIndx = strtok(NULL, ",");
    b_prev = b;
    b = atoi(strtokIndx);
  }
}

void handleEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) {
  switch (eventType) {
    case AceButton::kEventReleased:
      if (millis() - lastMillis < 1500) {
        storage.settings[Menu]++;
        if (storage.settings[Menu] > 8)storage.settings[Menu] = 1;
      }
      buttonLatch = 1;
      lastMillis = millis();
      break;
    case AceButton::kEventLongPressed:
      buttonLatch = 1;
      Menu++;
      if (Menu > sizeof(storage.settings) - 1)Menu = 0;
      lastMillis = millis();
      break;
  }
}

void rainbowColour(byte hueStep, byte v) {
  hue = hue + hueStep;
  hsv = CHSV(hue, 255, v);
  hsv2rgb_rainbow( hsv, rgb);  //convert HSV to RGB
}

void randomColour(byte v) {
  hue = random8();
  hsv = CHSV(hue, 255, v);
  hsv2rgb_rainbow( hsv, rgb);  //convert HSV to RGB
}

Next I built the control box, this is just an Arduino Nano with 12 buttons and a HC12 module to send the signal.

Manual Contoller

I had originally thought that I would just use this controller to control the LEDs on the wing but I quickly realised it was quite difficult to press the buttons fast enough to keep up with any music playing.

Following a lot of searching I found xlights.org, which is free and open source show sequencing software normally used for fancy Christmas lighting effects. It looked like is would fit the bill and I got to working out how to get it all talking to each other.
The amount of data that XLights pumps out is quite a lot and baud rate used was incompatible with the 2400 baud I had selected for the HC12 module. So an interface was required to take the 9600 baud from XLights trim it down and re transmit at 2400 baud. What I thought was going to be quite simple ended up requiring me to get a more powerful microcontroller to handle the data. The Arduino Nano I started with only has one hardware serial port, and creating a second software serial resulted in all sorts of glitches with the transmission due to interrupts.

Xlights serial interface

After trying to work through these problems for a few days, I cut my losses and ordered a TeensyLC which has 3 hardware serial ports and works flawlessly.
Another benefit of using XLight was that you can also get software for a Raspberry Pi, coupled with an Just boom AMP Hat and some old speakers I had lying around I was able to create a stand alone unit powered by a 4s battery.

All that was left to do now was to select and sequence the music.
At the time of writing this article test flights have been done, all the wings seem to be flying well and the control system seems to be holding up.

If anyone else is interested in building one, all the files, code and connection details are here.

See you on bonfire night!