In December 2020 and 2021 Max and I made a 10 meter tall Yule Tree (Christmas tree) using scaffolding net, mounted on an old mast from a ship (flagpole in 2021) and lit up by 12 DMX controlled RGB LED spots.

The colors of the tree could be controlled by everybody from a web interface.

Main components of the system

User interface in browser:

  • Two way communication (Set color - Receive current color)
  • Built with html, css, javascript & JQuery

Server backend:

  • Serving html, css and js files
  • php script saving color state as json in a text file

Under the tree:

4G wifi access point for internet connection

  • Making a local wifi net for the Raspberry Pi to connect to

Raspberry Pi with Processing (java) sketch

  • Reading the color state from file on php server every second
  • Sending color data to Arduino board via USB serial port

Arduino Uno

  • Receiving color data from Processing
  • Fallback if no color data received
  • Using DMXSimple library and a MAX485 chip to send DMX data

RGB LED spots

  • 12 DMX Controlled and daisy chained

User interface

The aim was to make as simple as possible an interface - Intuitive enough to not need any explanation. I think I somewhat succeeded in that.

Demands:

  • The user should be able to make a “color composition” for the tree and then “transfer” this composition to the tree.
  • The user should be able to see that other people change colors, making it interesting to use the app even if the tree is not visible.
  • The page should be as light at possible, to load quickly
  • As much work as possible offloaded to the client, to have the server handle many clients without excessive server load.

Design of the client - html, css, js

I wanted to make a very lightweight client. Full page including all css, images and linked scripts are about 110 KB - That is light. First page load is ready in about 500 ms on a normal 3G network.
Every 2 seconds we get additional 470 B of data because we poll for changes of the color by other users.
Because I didn't know how popular this color picker would end up becoming, i wanted to make sure, that the webserver could serve many clients easily. Small load everywhere was the easy way.
Once cached by the users browser, the page is even faster and lighter.

To follow usage, I am using the privacy-centred plausible.io to avoid evil Google tracking of users. Less data to see. Less evil to be.

Client - Server interaction

When the user clicks the send button, and commits a color composition, a http post request with the color data is sent to a php page on the server.

The php-script on the server saves the color as an array to a public available json file.

   /uploads/getfixturecolorsontree.json

All 12 lamps green would look like this:

["0","255","0","0","255","0","0","255","0","0","255","0","0","255","0","0","255","0",

"0","255","0","0","255","0","0","255","0","0","255","0","0","255","0","0","255","0"]

The client polls the json file on the server every other second, to discover color changes. It would have been more smooth to use websockets for the continuous communication, but that was a bit too complicated to get working for this little project.

Under the tree

4G wifi access point.

A normal access point with a sim card is by far the easiest way to get internet access on the float of the tree.

Raspberry Pi

Mini computer. Running the standard Raspberry Pi desktop image.

The RPi has Processing and the Arduino Ide installed. Plus it runs VNC, to make it possible to connect to the desktop from anywhere. Using RealVNC service - That way it is not needed to have a known ip-address of the RPi on the float of the Yule tree.

Having the Arduino Ide installed makes it possible to make changes to the sketch running on the Arduino board from remote.

The Processing sketch

The Processing sketch makes a request to getfixturecolorsontree.json every second to get the colors for the lamps.

Over about a second the Processing sketch fades to the new color, while repeatedly sending updated color values via USBSerial to the attached Arduino board.

From Processing to Arduino

I am using a simple, fast color transfer protocol that Sonny Windstrup and I made up, years ago for another installation.

We simply limit color values to the range 0-254 and use 255 for termination.

This gives us a very fast and simple way to send many color values for the DMX lights.

The format is (Lamp n Red/Green/Blue):

L1R L1G L1B L2R L2G L2B  …. L12R L12G L12B  255

All white would be:

254 254 254 254 254 254 … 254 254 254   255

All red would be:

254 0 0 254 0 0 … 254 0 0  255

We are throwing a little bit of information out this way, but for plain color control like this, it is no problem at all to lose one bit.  

This means, that on the Arduino, we can just start reading when we get 255

Arduino

A sketch on the Arduino board reads the color values incoming via serial data, and pass those colors on to the DMX lamps, using the DmxSimple library and a MAX485 chip to make the balanced signal for the DMX.

The Arduino sketch has a fallback mode, so if no data has been received for about a second, the Arduino sketch starts generating its own color data for the DMX lamps.

This to make sure that if the Processing sketch should fail, some sort of change in the light will still happen.

#include <DmxSimple.h>
#include <FastLED.h>

uint8_t gHue = 0;
uint8_t  gHueDelta = 3;
uint8_t randomhue = 0;
uint8_t noiserandom = 0;

CRGB leds[12];
CRGB ledring1[4];
CRGB ledring2[8];
uint8_t ledring1fixtures[4] = {   5,       17,         29,         41      };
uint8_t ledring2fixtures[8] = {1,    9, 13,    21, 25,     33, 37,      45 };


uint8_t ticks = 0;
unsigned long nextTick;
unsigned long delayTick = 50;

uint8_t scene = 0;
unsigned long nextScene;
unsigned long delayScene = 60000;

const int fixtureCount = 12; // Number of dmx-lamps
const int channelCount = 4 * fixtureCount;
const int maxDmxChannel = channelCount + 1;
const int dmxStartAddress = 1; // 1 is the first dmx address possible

int dmxDataArrayPos = 0; // Position of the current fixture data to set
int serialDataCounter = 0;
int colorChannelCounter = 0;

unsigned long lastSerialEventTime = 0;
unsigned long waitBeforeDefault = 1000;


void setup() {
  // put your setup code here, to run once:
  DmxSimple.usePin(3);

  /* DMX devices typically need to receive a complete set of channels
  ** even if you only need to adjust the first channel. You can
  ** easily change the number of channels sent here. If you don't
  ** do this, DmxSimple will set the maximum channel number to the
  ** highest channel you DmxSimple.write() to. */
  DmxSimple.maxChannel(maxDmxChannel);
  Serial.begin(115200);
  for (int i = 0; i < 12; i++) {
    leds[i] = CRGB::Red;
  }
}

void loop() {
  int c;
  //  long time = millis();
  while (Serial.available()) {
    c = Serial.read();
    // Check if we have received a terminator-char and then send the dmx-values
    // Use 255 for run - 59 for debug ';'
    if (c == 255 || dmxDataArrayPos > channelCount) {

      dmxDataArrayPos = 0; // Reset pointers
      colorChannelCounter = 0;
      Serial.println(" end255 ");
      lastSerialEventTime = millis(); // Update time each time we get a serial event
    }
    else if (c >= 0) {

      DmxSimple.write(dmxDataArrayPos + dmxStartAddress, c);
      Serial.print(" ");
      Serial.print(c);
      //dmxDataArray[dmxDataArrayPos] = c;
      dmxDataArrayPos++;
      colorChannelCounter++;
    }

  }

  // If no event for some time
  if (millis() > lastSerialEventTime + waitBeforeDefault) {
    // time to increase tick
    if (millis() > nextTick) {
      ticks += 1;
      nextTick += delayTick;
      noiserandom = random(0, 255);

    }

    if (millis() > nextScene) {
      scene += 1;
      if (scene > 5) scene = 0;
      nextScene += delayScene;
      randomhue = random(0, 255);
      Serial.print("no data - showing scene ");
      Serial.println(scene);
    }
    if (scene == 0) {
      onecolorhue(1, 200, 225, 255);
      onecolorhue(2, 60, 225, 255);

    }
    if (scene == 1) {
      rotateOneHue1(215);
      rotateOneHue2(60);
    }
    if (scene == 2) {
      rotateOneHue1(30);
      rotateOneHue2(63);

    }
    if (scene == 3) {
      rotateOneHue1(220);
      rotateOneHue2(60);

    }
    if (scene == 4) {
      rotateOneHue1(40);
      rotateOneHue2(57);

    }
    if (scene == 5) {
      rotateOneHue1(220);
      rotateOneHue2(60);

    }
    showring1();
    showring2();

  }



}

void onecolor(int ring, uint8_t r, uint8_t g, uint8_t b, uint8_t am) {
  int fixtures = 0;
  if (ring == 1) {
    for (int i = 0; i < 4; i++) {
      ledring1[i] = CRGB(r, g, b);
    }
  }
  if (ring == 2) {
    for (int i = 0; i < 8; i++) {
      ledring2[i] = CRGB(r, g, b);

    }
  }
}
void onecolorhue(uint8_t ring, uint8_t hue, uint8_t sat, uint8_t val) {
  uint8_t fixtures = 0;
  if (ring == 1) {
    for (uint8_t i = 0; i < 4; i++) {
      ledring1[i] = CHSV(hue, sat, val);
    }
  }
  if (ring == 2) {
    for (uint8_t i = 0; i < 8; i++) {
      ledring2[i] = CHSV(hue, sat, val);

    }
  }
}

void rotateOneHue1(uint8_t hueoffset) {
  for (uint8_t i = 0; i < 4; i++) {
    uint8_t hue = sin8(ticks) / 6 + hueoffset + i * 2;
    ledring1[i] = CHSV(hue, 245, 255);

  }
}
void rotateOneHue2(uint8_t hueoffset) {
  for (uint8_t i = 0; i < 8; i++) {
    uint8_t hue = sin8(ticks+i*3) / 6 + hueoffset;
    ledring2[i] = CHSV(hue, 245, 255);

  }
}

void randomcolor(int ring) {
  if (ring == 1) {
    for (int i = 0; i < 4; i++) {
      uint8_t randhue = noiserandom + random(0, 30);
      ledring1[i] = CHSV(randhue, 205, 255);
    }
  }
  if (ring == 2) {
    for (int i = 0; i < 8; i++) {
      uint8_t randhue = noiserandom + random(0, 30);
      ledring2[i] = CHSV(randhue, 205, 255);

    }
  }

}

void setspot(int fixture, uint8_t r, uint8_t g, uint8_t b, uint8_t am) {
  // 4 addresses pr fixture, first is numbered 1
  int address = 4 * fixture - 3; // 1: 4*1 -4 +1 = 1   2: 4*2 - 4 + 1 = 5
  DmxSimple.write(address, r);
  DmxSimple.write(address + 1, g);
  DmxSimple.write(address + 2, b);
  DmxSimple.write(address + 3, am);
}

void showring1() {
  for (uint8_t i = 0; i < 4; i++) {
    // do something with myValues[i]
    uint8_t r = ledring1[i].red;
    uint8_t g = ledring1[i].green;
    uint8_t b = ledring1[i].blue;
    uint8_t am = 0;

    int address = ledring1fixtures[i];
    //Serial.println(address);
    DmxSimple.write(address, r);
    DmxSimple.write(address + 1, g);
    DmxSimple.write(address + 2, b);
    DmxSimple.write(address + 3, am);
  }

}
void showring2() {
  for (uint8_t i = 0; i < 8; i++) {
    uint8_t r = ledring2[i].red;
    uint8_t g = ledring2[i].green;
    uint8_t b = ledring2[i].blue;
    uint8_t am = 0;

    int address = ledring2fixtures[i];
    //Serial.println(address);
    DmxSimple.write(address, r);
    DmxSimple.write(address + 1, g);
    DmxSimple.write(address + 2, b);
    DmxSimple.write(address + 3, am);
  }

}

DMX RGB lamps

These are just standard cheapish RGBW spots used for theater and music everywhere. They are daisy chained using XLR cables and individually addressed taking up 4 addresses each.

First lamp address is 1, second is 5, third is 9 etc.

The lamps are protected from rain and water using transparent plastic bags pulled down over the lamp.

Power

Power comes from land via a long good quality rubber cable along with the mooring line.