Bias lighting

Bias light - sunrise

As my aging eyesight can attest, the high contrast between my monitor and the surrounding darkness strains my eyes. Bias lighting, introduced behind a monitor, somewhat alleviates the problem. There are many LED strip-based products available commercially that can be used to cast some light behind the screen, but why not build my own. With my tried-and-true Arduino Uno and some nice Digital RGB NeoPixels from Adafruit (model number 1461) it is relatively easy to build a bias light. With the ability of NeoPixels to change the colour of each LED programatically, some pretty neat effects can be accomplished!

This short article does not dwell too much on the hardware implementation side since Adafruit has an excellent article about setting up the NeoPixels and even providing a software library for Arduino. You can read all about it here. Rather, the possibilities of using such setup are highlighted.

I have been dreaming to have a nice crackling fire in my view while working in front of a computer screen. Without the smoke, ashes and hauling the firewood. Thus, the bias light was programmed to simulate the flickering of a reddish-orange fire.  A still image of it is shown above. The flickering flames make a cozy environment. Similarly, by implementing a gradually changing colour ramp, say from dark blue to orange to yellowish-white, a nice sunrise effect can be implemented. The code listing below has such a function, where you can set the duration of your sunrise. However, sometimes only a solid colour is enough, so a simple RGB triad can be sent over the USB cable to set the light’s colours.

The RGB LED strip is mounted on a custom-made birch mount such that the actual LEDs are not visible from the front. Rather, the angled back face of the object positions the LEDs to cast their light on the wall behind my monitor in a nice, and sometimes eerie glow. Yes, Halloween is coming…

The code (or sketch, in Arduino parlance) below was written with total disregard to optimization; no effort was made of reduce memory consumption on the Arduino. However, hopefully, it is easy to read, understand, and of course modify to your liking!

Enjoy the fire.

// Programmable RGB LED strip light for bias lighting
//
// Written by: A.M. Zsaki 2014
//
// functionality implemented:
//
// setrgb rgb = ex. setrgb000111222 -> 000 for red 111 for green 222 for blue
// setramp rgb1 rgb2 seconds = ex. setramp2550000002552552550020 -> 255 000 000 to 255 255 255 over 0020 seconds
// sunrise seconds = ex. sunrise0060 -> sunrise over 60 seconds
// fire = without arguments -> flickering fire
//
//
//
#include <Adafruit_NeoPixel.h>

#define PIN 6

// Parameter 1 = number of pixels in strip (for this case, I have 30 of them)
// Parameter 2 = Arduino pin number (most are valid)
// Parameter 3 = pixel type flags, add together as needed:
// NEO_KHZ800 800 KHz bitstream (most NeoPixel products w/WS2812 LEDs)
// NEO_KHZ400 400 KHz (classic 'v1' (not v2) FLORA pixels, WS2811 drivers)
// NEO_GRB Pixels are wired for GRB bitstream (most NeoPixel products)
// NEO_RGB Pixels are wired for RGB bitstream (v1 FLORA pixels, not v2)
Adafruit_NeoPixel strip = Adafruit_NeoPixel(30, PIN, NEO_GRB + NEO_KHZ800);

// serial input handling
String inputString="";
boolean stringComplete=false;

// string for deciphering command from PC
String commandString="";
int commandRed=255;
int commandGreen=0;
int commandBlue=0;

// colors and variables for ramp and sunrise
int commandRed1=0;
int commandGreen1=0;
int commandBlue1=0;
int rampSeconds=0;
int rampCounter=0;
int rampRedDelta=0;
int rampGreenDelta=0;
int rampBlueDelta=0;
int sunriseSeconds=0;

// variables for fire
typedef struct {
  int center;
  int colorR[10];
  int colorG[10];
  int colorB[10];
  int movementPattern[10];
  int pixelLocation[10];
} flames;

flames flame[1];
int flameCycleCounter=0;
int otherFlameOffsets[5];

// general program counter
unsigned long time;
int displayroutine=0;

void setup() {
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'

  time=0;

  // initialize serial:
  Serial.begin(9600);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
}

void loop() {

  time=time+1;

  // do one of the display routines for one step
  if (displayroutine==0) {
    displaySolidColor();
  }
  else if (displayroutine==1) {
    displayRampOneStep();
  }
  else if (displayroutine==2) {
    displaySunriseOneStep();
  }
  else if (displayroutine==3) {
    displayFireOneStep();
  }
  else {

  }

  if (displayroutine<3) {
    delay(1000);
  }
  else {
    delay(150);
  }

  // decipher the command string
  if (stringComplete) {
    Serial.println(inputString);

    commandString=inputString;

    if (commandString.startsWith("setrgb",0)) {
      Serial.println("setrgb command found");

      String redToken=commandString.substring(6,9);
      Serial.print("red:");
      commandRed=redToken.toInt();
      Serial.println(redToken);
      Serial.println(commandRed);
      String greenToken=commandString.substring(9,12);
      Serial.print("green:");
      commandGreen=greenToken.toInt();
      Serial.println(greenToken);
      Serial.println(commandGreen);
      String blueToken=commandString.substring(12,15);
      Serial.print("blue:");
      commandBlue=blueToken.toInt();
      Serial.println(blueToken);
      Serial.println(commandBlue);

      displayroutine=0;
    }

    if (commandString.startsWith("setramp",0)) {
      Serial.println("setramp command found");

      String redToken=commandString.substring(7,10);
      Serial.print("red:");
      commandRed=redToken.toInt();
      Serial.println(redToken);
      Serial.println(commandRed);
      String greenToken=commandString.substring(10,13);
      Serial.print("green:");
      commandGreen=greenToken.toInt();
      Serial.println(greenToken);
      Serial.println(commandGreen);
      String blueToken=commandString.substring(13,16);
      Serial.print("blue:");
      commandBlue=blueToken.toInt();
      Serial.println(blueToken);
      Serial.println(commandBlue);

      String redToken1=commandString.substring(16,19);
      Serial.print("red1:");
      commandRed1=redToken1.toInt();
      Serial.println(redToken1);
      Serial.println(commandRed1);
      String greenToken1=commandString.substring(19,22);
      Serial.print("green1:");
      commandGreen1=greenToken1.toInt();
      Serial.println(greenToken1);
      Serial.println(commandGreen1);
      String blueToken1=commandString.substring(22,25);
      Serial.print("blue1:");
      commandBlue1=blueToken1.toInt();
      Serial.println(blueToken1);
      Serial.println(commandBlue1);
      String secondsToken=commandString.substring(25,29);
      Serial.print("seconds:");
      rampSeconds=secondsToken.toInt();
      Serial.println(secondsToken);
      Serial.println(rampSeconds);

      rampCounter=0;
      rampRedDelta=commandRed1-commandRed;
      rampGreenDelta=commandGreen1-commandGreen;
      rampBlueDelta=commandBlue1-commandBlue;

      Serial.println("rampDeltas");
      Serial.println(rampRedDelta);
      Serial.println(rampGreenDelta);
      Serial.println(rampBlueDelta);

      displayroutine=1;
    }

    if (commandString.startsWith("sunrise",0)) {
      Serial.println("sunrise command found");

      String secondsToken=commandString.substring(8,13);
      Serial.print("seconds:");
      sunriseSeconds=secondsToken.toInt();
      Serial.println(secondsToken);
      Serial.println(sunriseSeconds);

      rampCounter=0;

      displayroutine=2;
    }

    if (commandString.startsWith("fire",0)) {
      Serial.println("fire command found");

      // initialize flames
      flame[0].center=15;

      flame[0].colorR[0]=68;
      flame[0].colorG[0]=34;
      flame[0].colorB[0]=0;

      flame[0].colorR[1]=225;
      flame[0].colorG[1]=86;
      flame[0].colorB[1]=10;

      flame[0].colorR[2]=241;
      flame[0].colorG[2]=30;
      flame[0].colorB[2]=8;

      flame[0].colorR[3]=232;
      flame[0].colorG[3]=129;
      flame[0].colorB[3]=11;

      flame[0].colorR[4]=154;
      flame[0].colorG[4]=34;
      flame[0].colorB[4]=10;

      flame[0].colorR[5]=164;
      flame[0].colorG[5]=35;
      flame[0].colorB[5]=0;

      flame[0].colorR[6]=255;
      flame[0].colorG[6]=127;
      flame[0].colorB[6]=2;

      flame[0].colorR[7]=212;
      flame[0].colorG[7]=90;
      flame[0].colorB[7]=0;

      flame[0].colorR[8]=120;
      flame[0].colorG[8]=0;
      flame[0].colorB[8]=5;

      flame[0].colorR[9]=76;
      flame[0].colorG[9]=19;
      flame[0].colorB[9]=0;

      flame[0].movementPattern[0]=0;
      flame[0].movementPattern[1]=-1;
      flame[0].movementPattern[2]=-2;
      flame[0].movementPattern[3]=-1;
      flame[0].movementPattern[4]=0;
      flame[0].movementPattern[5]=1;
      flame[0].movementPattern[6]=2;
      flame[0].movementPattern[7]=1;
      flame[0].movementPattern[8]=0;
      flame[0].movementPattern[9]=-1;

      flame[0].pixelLocation[0]=-4;
      flame[0].pixelLocation[1]=-3;
      flame[0].pixelLocation[2]=-2;
      flame[0].pixelLocation[3]=-1;
      flame[0].pixelLocation[4]=0;
      flame[0].pixelLocation[5]=1;
      flame[0].pixelLocation[6]=2;
      flame[0].pixelLocation[7]=3;
      flame[0].pixelLocation[8]=4;
      flame[0].pixelLocation[9]=5;

      flameCycleCounter=0;

      otherFlameOffsets[0]=0;
      otherFlameOffsets[1]=-11;
      otherFlameOffsets[2]=12;
      otherFlameOffsets[3]=-11;
      otherFlameOffsets[4]=12;

      displayroutine=3;
    }

    inputString="";
    stringComplete=false;
  }
}

void serialEvent() 
{
  while (Serial.available()) {
    char inChar=(char)Serial.read();

    inputString+=inChar;
    if (inChar=='\n') {
      stringComplete=true;
    }
  }
}

void displaySolidColor()
{
  Serial.println("entering displaySolidColor()");
  for (int i=0;i<strip.numPixels();i++) {
    strip.setPixelColor(i,strip.Color(commandRed,commandGreen,commandBlue));
  }
  strip.show();
  Serial.println("exiting displaySolidColor()");
}

void displayRampOneStep()
{
  Serial.println("entering displayRampOneStep()");

  float floatRampCounter=rampCounter*1.0;

  float fractionRed=floatRampCounter/rampSeconds;
  float fractionGreen=floatRampCounter/rampSeconds;
  float fractionBlue=floatRampCounter/rampSeconds;

  Serial.println("timer fractions");
  Serial.println(fractionRed);
  Serial.println(fractionGreen);
  Serial.println(fractionBlue);

  int currentRed=commandRed+fractionRed*rampRedDelta;
  int currentGreen=commandGreen+fractionGreen*rampGreenDelta;
  int currentBlue=commandBlue+fractionBlue*rampBlueDelta;

  Serial.println("current colors");
  Serial.println(currentRed);
  Serial.println(currentGreen);
  Serial.println(currentBlue);

  rampCounter=rampCounter+1;

  if (rampCounter==rampSeconds) {
    rampCounter=0;
  }

  Serial.println("rampCounter");
  Serial.println(rampCounter);

  for (int i=0;i<strip.numPixels();i++) {
    strip.setPixelColor(i,strip.Color(currentRed,currentGreen,currentBlue));
  }
  strip.show();

  Serial.println("exiting displayRampOneStep()");
}

void displaySunriseOneStep()
{
  Serial.println("entering displaySunriseOneStep()");

  float floatRampCounter=rampCounter*1.0;

  float fraction=floatRampCounter/sunriseSeconds;
  Serial.println("timer fraction");
  Serial.println(fraction);

  int currentRed;
  int currentGreen;
  int currentBlue;

  if (rampCounter<sunriseSeconds/2) {
    fraction=fraction*2.0;

    Serial.println("ADJUSTED timer fraction");
    Serial.println(fraction);

    // ramp from 0,0,10 (dark blue) to 255,127,0 (orange)
    currentRed=0+fraction*255;
    currentGreen=0+fraction*127;
    currentBlue=10+fraction*-10;

    Serial.println("current colors - first half");
    Serial.println(currentRed);
    Serial.println(currentGreen);
    Serial.println(currentBlue);
  }
  else {
    fraction=fraction*2.0-1.0;

    Serial.println("ADJUSTED timer fraction");
    Serial.println(fraction);

    // ramp from 255,127,0 (orange) to 255,255,255 (white)
    currentRed=255+fraction*0;
    currentGreen=127+fraction*127;
    currentBlue=0+fraction*255;

    Serial.println("current colors - second half");
    Serial.println(currentRed);
    Serial.println(currentGreen);
    Serial.println(currentBlue);
  }

  for (int i=0;i<strip.numPixels();i++) {
    strip.setPixelColor(i,strip.Color(currentRed,currentGreen,currentBlue));
  }
  strip.show();

  rampCounter=rampCounter+1;

  if (rampCounter==sunriseSeconds) {
    rampCounter=0;
  }

  Serial.println("rampCounter");
  Serial.println(rampCounter);

  Serial.println("exiting displaySunriseOneStep()");
}

void displayFireOneStep()
{
  Serial.println("entering displayFireOneStep()");

  int stripColorR[30];
  int stripColorG[30];
  int stripColorB[30];

  // set everything black
  for (int i=0;i<strip.numPixels();i++) {
    stripColorR[i]=0;
    stripColorG[i]=0;
    stripColorB[i]=0;
  }

  for (int j=0;j<3;j++) {
    for (int i=0;i<10;i++) {

      int pixelLocation=flame[0].center+flame[0].pixelLocation[i]+flame[0].movementPattern[flameCycleCounter]+otherFlameOffsets[j];

      if (pixelLocation>0 && pixelLocation<30) {
        stripColorR[pixelLocation]=flame[0].colorR[i];
        stripColorG[pixelLocation]=flame[0].colorG[i];
        stripColorB[pixelLocation]=flame[0].colorB[i];
      }
    }
  }

  for (int i=0;i<strip.numPixels();i++) {
    strip.setPixelColor(i,strip.Color(stripColorR[i],stripColorG[i],stripColorB[i]));
  }
  strip.show();

  flameCycleCounter=flameCycleCounter+1;

  if (flameCycleCounter==10) {
    flameCycleCounter=0;
  }

}