zavracky
Published © GPL3+

Holiday Lighting System

Want your holiday lights to wow their audience? Use the components in this system to create a light show.

AdvancedFull instructions provided264
Holiday Lighting System

Things used in this project

Hardware components

Kohree LED Fairy String Lights
These are four wire three color LED string lights. It's important that they have four wires. You can find similar lights with only three wires.
×1
Mini String Lights
These are what I call 'bipolar' string lights.
×1
Net Lights
×1
LED Strip Lights
These lights are designed to be cut into sections. They typically required a 12V supply. These use a four wire system where one is 12 Volt power and the others, when grounded will light either the red, green, or blue LEDs for the entire strip. Once again, no series resistor is required because each of the 5050 RGB LEDs has its own built in.
×1
Individually Addressable LED Strip
These LED strips are also designed to be cut into sections.
×1
FTDI FT232RL
If purchasing inexpensive Chinese devices, replace the surface mount chip with the genuine FTDI USB UART.
×1
DSD TECH USB to TTL Serial Adapter
Purchase this device to get a true FTDI USB UART.
×1
NXP Semiconductors PCA9955 16-channel Constant Current LED Driver
×1
5x7cm Double Sided PCB Board
These boards were used to mount power supplies and USB to Serial Interfaces on the top of a fairy stack.
×1
Arduino Nano R3
Arduino Nano R3
×1
Arduino Mega 2560
Arduino Mega 2560
×1

Story

Read more

Custom parts and enclosures

Fairy Board Extender

This simple part is used to support a fairy board (single element board) when placed in a stack. On one side are two small holes into which screws can be driven to attach to a fairy board. On the other side are two larger holes into which standoffs are secured.

Together with the two parts below and appropriately sized standoffs, a fairy board stack can be create.

Fairy Board Top Support (single element board)

This part is placed on top of a stack of fairy boards. It has corner holes for the standoffs and four small holes onto which protoboards can be attached. These protoboards will hold power supplies and/or USB to Serial interfaces.

Fairy Board Bottom Support (single element controller board)

Fairy Patch Board Support Side 1

This is one of two parts that together assemble to make a support structure for the fairy light patch panel.

Fairy Patch Board Support Side 2

This is a second component of the fairy light patch panel support structure.

Bipolar Patch Board Support

This part together with Fairy Patch Board Support Side 1 create a support structure for the Bipolar Patch Board.

Bipolar Driver Assembly Base Mount

This part creates a base for a stack of bipolar controllers. The part has four holes. The inner holes are used for standoffs while the outer holes are used to screw the assembled stack to a plywood base. The inner hole spacing matches the spacing on the short side of the bipolar controller boards.

Schematics

Bipolar Controller

This controller uses a PCA9955 LED driver to control 8 bipolar light strings or nets.

Bipolar Patch Board

Single Element Controller

Fairy Light Patch Board (single element controller patch board)

Code

Individually Addressable Controller (approach 1)

C/C++
This software is meant to run on an Arduino Mega. It can be used in a situation where there are multiple individually addressable strips to control separately. This is one of two approaches that can be employed. In this approach, the software creates strips that match the hardware configuration and then manages them. The strip definitions in this code must match the strip definitions used by Vixen.
// Individually Addressable Controller
//
// (c) Paul Zavracky, September, 2020; update Nov 2021

#define VERSION "2.0"
#include <Wire.h> // I2C library
#include <Adafruit_NeoPixel.h>

// start of strip definition
#define NUM_STRIPS 10
#define BRIGHTNESS 100

// strip definitions
#define UNDP 0 // element order in hardware - must be converted to match Vixen (see stripDataOrder)
#define UNDPpin 3 // Uppoer Nose Diagonal Port (1 O'clock on ten hour clock)
#define UNDPpixels 67 // Uppoer Nose Diagonal Port

#define DNTP 1
#define DNTPpin 4 // Diagonal Nose Top Port (2 O'clock)
#define DNTPpixels 78 // Diagonal Nose Top Port

#define HNP 2
#define HNPpin 5 // Horizontal Nose Port (3 O'Clock)
#define HNPpixels 78 // Horizontal Nose Port

#define DNBP 3
#define DNBPpin 6 // Diagonal Nose Bottom Port (4 O'Clock)
#define DNBPpixels 78 // Diagonal Nose Bottom Port

#define VNB 4
#define VNBpin 7 // Vertical Nose Bottom (5 O'Clock)
#define VNBpixels 17 // Vertical Nose Bottom

#define DNBS 5
#define UNDSpin 8 // Uppoer Nose Diagonal Port (6 O'Clock)
#define UNDSpixels 78 // Uppoer Nose Diagonal Port

#define HNS 6
#define DNTSpin 9 // Diagonal Nose Top Port (7 O'Clock)
#define DNTSpixels 78 // Diagonal Nose Top Port

#define DNTS 7
#define HNSpin 10 // Horizontal Nose Port (8 O'Clock)
#define HNSpixels 78 // Horizontal Nose Port

#define UNDS 8
#define DNBSpin 11 // Diagonal Nose Bottom Port (9 O'Clock)
#define DNBSpixels 67 // Diagonal Nose Bottom Port (9)

#define VNT 9
#define VNTpin 12 // Vertical Nose Top (10 O'Clock)
#define VNTpixels 60 // Vertical Nose Top

#define MAX_CHANNELS 2046 // 3*(4*DNTPpixels+2*UNDPpixels+VNBpixels+VNTpixels) This is the total number of leds (counting each color) on the nose
#define BAUD_RATE0 115200 // this is the rate at which the serial monitor operates - wants to be higher than below 
#define BAUD_RATE3 115200 // this is the rate for data coming in from Vixen (on port 3)

// Display Driver Initialization from Adafruit
#include <Adafruit_GFX.h>

// Globals

int ch;
int ch1;
byte stripNum;
int state;
int chVal[MAX_CHANNELS] = {0};
byte STRIP_PINS[NUM_STRIPS] {UNDPpin, DNTPpin, HNPpin, DNBPpin, VNBpin, UNDSpin, DNTSpin, HNSpin, DNBSpin, VNTpin};
int NUM_PIXELS[NUM_STRIPS] {UNDPpixels, DNTPpixels, HNPpixels, DNBPpixels, VNBpixels, UNDSpixels, DNTSpixels, HNSpixels, DNBSpixels, VNTpixels};

byte stripDataOrder[] = {HNP,HNS,VNT,VNB,DNTS,DNBS,DNTP,DNBP,UNDP,UNDS}; // this translates to the order of elements in Vixen
       
bool serialErrorFlag = false;  // set error flags to true to force system to clear error
bool oldSerialErrorFlag = false; // this forces the display of the serial status
 
enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

Adafruit_NeoPixel strip[] = {
  Adafruit_NeoPixel(NUM_PIXELS[0], STRIP_PINS[0], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[1], STRIP_PINS[1], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[2], STRIP_PINS[2], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[3], STRIP_PINS[3], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[4], STRIP_PINS[4], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[5], STRIP_PINS[5], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[6], STRIP_PINS[6], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[7], STRIP_PINS[7], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[8], STRIP_PINS[8], NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(NUM_PIXELS[9], STRIP_PINS[9], NEO_GRB + NEO_KHZ800)
};
    
unsigned long int pixelSum; // used to keep track of the assignment of Vixen data with pixel count and strip
unsigned long int color; // must convert chVal()s RGB into long integer

void setup() {
  
  Serial2.begin(BAUD_RATE0);
  Serial.begin(BAUD_RATE3);
  Serial2.println();
  Serial2.print("Individually Addressable Controller, VERSION "); Serial2.println(VERSION);
  
  for (int i = 0; i<NUM_STRIPS; i++) {
    strip[i].setBrightness(BRIGHTNESS);
    strip[i].begin();
    clearStrip(i); // Initialize all pixels to 'off'
    Serial2.print("strip "); Serial2.println(i);
  }
  //delay(1000);
  
  Serial2.println("Sys Chk complete" );
  delay(3000);
  
  state = IDLE;
  ch = 0;
}

void loop() { 
  
  if (Serial.available())
  {
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial.read() == '+')
        {
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial.read() == '>')
        {
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial.read();
        if (ch > MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        pixelSum = 0;
        for(int j = 0; j < NUM_STRIPS; j++) { // for all the strips
          for(long int i = 0; i < NUM_PIXELS[stripDataOrder[j]]; i++) { // for all pixels within a strip
            strip[stripDataOrder[j]].setPixelColor(i, chVal[3*i+pixelSum], chVal[3*i+1+pixelSum], chVal[3*i+2+pixelSum]); // set pixel to Vixen color            
            /*
            Serial2.print(3*i+pixelSum);Serial2.print("    ");Serial2.print(chVal[3*i+pixelSum]);Serial2.print("    ");
            Serial2.print(chVal[3*i+1+pixelSum]);Serial2.print("    ");
            Serial2.println(chVal[3*i+2+pixelSum]);
            Serial2.println();
            */
          }
          strip[stripDataOrder[j]].show();
          pixelSum = pixelSum + 3 * NUM_PIXELS[stripDataOrder[j]];
        }
        //Serial2.println("end of line");Serial2.println();
      break;
    } 
  }
}

void updateErrorDisplay(){

  if (serialErrorFlag) {
    Serial2.println("No Serial Data"); 
  }
  else {
    Serial2.println("serial bus OK");
  }
}

void clearStrip(byte j) {
  for( long int i = 0; i<NUM_PIXELS[j]; i++){
    strip[j].setPixelColor(i, 0x000000); strip[j].show();
  }
}

unsigned long int pzRGB(byte r, byte g, byte b) {
  return (r*65536 + g*256 + b);
}

Individually Addressable Controller (approach 2)

C/C++
This is the second approach to individually addressable strips. It assumes that the strip data lines are tied end to end and therefore, only one Arduino pin is required to drive several elements in the series. In this approach it is imperative that the count of LEDs in each element exactly matches that defined in Vixen.
// Individually Addressable Controller
//
// (c) Paul Zavracky, September, 2020; update Nov 2021

#define VERSION "2.0"
#include <Wire.h> // I2C library
#include <Adafruit_NeoPixel.h>

// start of strip definition
#define BRIGHTNESS 64

// strip definitions

#define BNRpin 4
#define BNRpixels 252

#define MAX_CHANNELS 756 // This is the total number of leds (counting each color) on the nose
#define BAUD_RATE0 2000000 // this is the rate at which the serial monitor operates - wants to be higher than below 
#define BAUD_RATE3 115200 // this is the rate for data coming in from Vixen (on port 3)

// Display Driver Initialization from Adafruit
#include <Adafruit_GFX.h>

// Globals

int ch;
int state;
int chVal[MAX_CHANNELS] = {0};
char serialInput;
       
bool serialErrorFlag = false;  // set error flags to true to force system to clear error
bool oldSerialErrorFlag = false; // this forces the display of the serial status
bool firstData;
 
enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

Adafruit_NeoPixel strip(BNRpixels, BNRpin, NEO_GRB + NEO_KHZ800);
    
unsigned long int pixelSum; // used to keep track of the assignment of Vixen data with pixel count and strip
unsigned long int color; // must convert chVal()s RGB into long integer

void setup() {  
  Serial.begin(BAUD_RATE0);
  Serial3.begin(BAUD_RATE3);
  Serial.println();
  Serial.print("Individually Addressable Controller, VERSION "); Serial.println(VERSION);
  
  strip.setBrightness(BRIGHTNESS);
  strip.begin();
  showStrip(); // Turn all lights on white
  Serial.println("made it to show strip");
  delay(1000);
  clearStrip(); // Initialize all pixels to 'off'
  Serial.println("made it to clear strip");
  delay(1000);
  
  Serial.println("Sys Chk complete" );
  delay(3000);
  
  state = IDLE;
  ch = 0;
  firstData = true; // true for test of first serial data response
}

void loop() { 
  
  if (Serial3.available())
  {
    if (firstData) { Serial.println("got serial data"); firstData = false;}
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial3.read() == '+')
        {
          Serial.println("+");
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial3.read() == '>')
        {
          Serial.println(">");
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial3.read();
        if (ch > MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        pixelSum = 0;
        for(long int i = 0; i < BNRpixels; i++) { // for all pixels within a strip
          strip.setPixelColor(i, chVal[3*i], chVal[3*i+1], chVal[3*i+2]); // set pixel to Vixen color        
        }
        strip.show();
      break;
    } 
  }
}

void updateErrorDisplay(){

  if (serialErrorFlag) {
    Serial.println("No Serial Data"); 
  }
  else {
    Serial.println("serial bus OK");
  }
}

void showStrip() {
  for( long int i = 0; i < BNRpixels; i++){
    strip.setPixelColor(i, 0x111111); 
  }
  strip.show();
}

void clearStrip() {
  for( long int i = 0; i < BNRpixels; i++){
    strip.setPixelColor(i, 0x000000); 
  }
  strip.show();
}

unsigned long int pzRGB(byte r, byte g, byte b) {
  return (r*65536 + g*256 + b);
}

16 Channel Fairy Light Driver

C/C++
This software was loaded on an Arduino Nano. It is meant to control four wire fairy light strings.
// 16 Channel Fairy Light Driver
//
// (c) Paul Zavracky, January, 2018, updated August 2020

#define VERSION "5.4"
#include <Wire.h> // I2C library

#define OE 2 // pca9955 location of Output Enable pin, active low
#define RESET 3 // pca9955 location of reset pin, active low
#define NUM_BOARDS 3
#define MAX_CHANNELS NUM_BOARDS*15 // Three Boards 15 channels each, three RGB LEDs
#define BAUD_RATE 57600

// Display Driver Initialization from Adafruit
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Globals

int ch;
int state;
int chVal[MAX_CHANNELS] = {0};
byte error;
       //   0:success
       //   1:data too long to fit in transmit buffer
       //   2:received NACK on transmit of address
       //   3:received NACK on transmit of data
       //   4:other i2c error
       //   5:no serial data recieved

const char error_0[] PROGMEM = "i2c Success"; // "String 0" etc are strings to store - change to suit.
const char error_1[] PROGMEM = "i2c too long";
const char error_2[] PROGMEM = "i2c addr NACK";
const char error_3[] PROGMEM = "i2c data NACK";
const char error_4[] PROGMEM = "other i2c error";
const char error_5[] PROGMEM = "no serial data";

const char *const error_table[] PROGMEM = {error_0, error_1, error_2, error_3, error_4, error_5}; // create a table of strings in PROGMEM

const char messages_0[] PROGMEM = "Welcome to the Looneypoons 16 channel fairy light driver, V"; // this string is 29 character long
const char messages_1[] PROGMEM = "SSD1306 allocation failed";
const char messages_2[] PROGMEM = "Looneypoon";
const char messages_3[] PROGMEM = "i2c check";
const char messages_4[] PROGMEM = "Scanning...";
const char messages_5[] PROGMEM = "No Serial Data";
const char messages_6[] PROGMEM = "Sys Chk complete";
const char messages_7[] PROGMEM = "serial bus OK";

const char *const messages_table[] PROGMEM = {messages_0, messages_1, messages_2, messages_3, messages_4, messages_5, messages_6, messages_7};

char buffer[30]; // length of maximum message at minimum
       
bool i2cErrorFlag = false;                    
byte i2cError[MAX_CHANNELS] = {0}; // initial to zero so that change detection will work

bool serialErrorFlag = false;  // set error flags to true to force system to clear error from display
bool oldSerialErrorFlag = true; // this forces the display of the i2c status and the serial status
              
byte i2cAddress[NUM_BOARDS] = {0x5C, 0x5D, 0x5E};
unsigned int loopCounter = 0; // keeps track of how many times through the main loop collecting no serial data
 
enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

void setup() {
  int nDevices;
  byte address;

  Serial.begin(BAUD_RATE);
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[0])));  // Necessary casts and dereferencing, just copy.
  Serial.print(buffer); Serial.println(VERSION);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[1]))); // SSD1306 allocation failed
    Serial.println(buffer);
    for(;;); // Don't proceed, loop forever
  }
  
  // Set up PCA9955 Chip Reset and Output Enable
  pinMode(RESET, INPUT);           // set pin to input
  digitalWrite(RESET, HIGH);       // turn on pullup resistors
  pinMode(RESET, OUTPUT);
  digitalWrite(RESET, HIGH); // reset for the PCA9955 is active low
  delay(1000); // wait 1 second
  digitalWrite(RESET, LOW); // reset for the PCA9955 is active low
  delay(100); // send 100 ms reset pulse
  digitalWrite(RESET, HIGH); // reset for the PCA9955 is active low
  delay(100); // provide time for 9955 to reset

  pinMode(OE, INPUT);           // set pin to input
  digitalWrite(OE, HIGH);       // turn on pullup resistors
  pinMode(OE, OUTPUT);
  digitalWrite(OE, HIGH); // output enable for the PCA9955 is active low
  delay(1000); // wait 1 second
  digitalWrite(OE, LOW); // output enable for the PCA9955 is active low
  delay(100); // provide time for 9955 to settle

  // Set Initial Display
  display.clearDisplay();
  display.setTextSize(2);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[2]))); // SSD1306 allocation failed
  display.println(buffer); //"Looneypoon"
  display.display();
  delay(1000);
  
  display.setTextSize(1);
  display.print(F("V"));display.println(VERSION);
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[3]))); // SSD1306 allocation failed
  display.println(buffer); //"i2c check"
  display.println();
  display.display();
  delay(1000);

  // Start by scanning i2c slave addresses
  Wire.begin();  // I2C library
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[4]))); // SSD1306 allocation failed
  display.println(buffer); //"Scanning..."
  display.display();
  
  nDevices = 0;
  for(address = 1; address < 127; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    // Serial.print("error = "); Serial.println(error);
    if (error == 0)
    {
      display.print(F("0x"));
      if (address<16)
        display.print(F("0"));
        display.print(address,HEX);
        display.print(", ");
        nDevices++;
      }
    else if (error==4)
    {
      display.print(F("Unknown error at "));
      if (address<16)
        display.print(F("0"));
      display.println(address,HEX);
    }    
  }
  display.display();
  delay(1000);
  if (nDevices == 0)
    display.println(F("No I2C devices found\n"));
  else
    display.println("done\n");
  display.display();
  delay(3000); // delay so one can read screen
    
  //Serial.println("The above lists the device/board addresses found.\n\r  Address 70h and 76h are common to all PCA9955 devices.");

  // Set the output current level
  display.clearDisplay();
  display.setCursor(0,0);             // Start at top-left corner
  display.setTextSize(2);   
  display.println(F("Looneypoon"));
  display.display();
  display.setTextSize(1);
  for (int board = 0; board < NUM_BOARDS; board++) {
    Wire.beginTransmission(byte(i2cAddress[board])); // transmit to PCA9955 device specific address
    Wire.write(byte(0x45));            // Selects the IREFALL register (pg 32 of PCA9955B data sheet)  
    Wire.write(255);             // sends current value byte  
    error = Wire.endTransmission();     // stop transmitting  
    display.print("board ");display.print(board);display.print(" error = "); display.println(error);
  }
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[6])));
  display.println(buffer); // "Sys Chk complete" 
  display.println(""); // space
  display.display();  
  
  Serial.println(buffer); // "Sys Chk complete" 
  delay(3000);
  
  state = IDLE;
  ch = 0;


}

void loop() {   
  if (Serial.available())
  {
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial.read() == '+')
        {
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial.read() == '>')
        {
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial.read();
        if (ch >= MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        i2cErrorFlag = false; // initialize
        for (int board = 0; board < NUM_BOARDS; board++) {
          for (ch=15*board; ch<15*(1+board); ch++) // each board has 15 channels
          {
            //Serial.print(ch);Serial.print("   ");Serial.println(chVal[ch]);
            Wire.beginTransmission(byte(i2cAddress[board])); // transmit to PCA9955 device specific address
            Wire.write(byte(0x08 + ch%15));            // Selects the PWM6 register (pg 14 of PCA9955B data sheet) 
            //Serial.println(byte(0x08+ch%15)); 
            Wire.write(chVal[ch]);                            // sends PWM value byte  
            error = Wire.endTransmission();           // stop transmitting 
            if (i2cError[ch] != error) {i2cErrorFlag = true;} // set error flag only on a change of error condition
            i2cError[ch] = error;
          }
        }
        loopCounter = 0; // we got serial data, so can reset the loop counter
        serialErrorFlag = false;  // also reset flag
      break;
    }
  }
  loopCounter++;
  if (loopCounter == 0) {serialErrorFlag = true; } // we've gone x times without recieving data
  if((serialErrorFlag != oldSerialErrorFlag) || i2cErrorFlag) {updateErrorDisplay();}  // check for errors
  oldSerialErrorFlag = serialErrorFlag;
}

void updateErrorDisplay(){
  byte j = 1;
  
  clearDisplayText();
  display.setTextSize(1);
  // check i2c errors
  if (i2cErrorFlag) {
    for (int i = 0; i < MAX_CHANNELS; i++) {
      if(i2cError[i] != 0) {
        display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
        display.print("channel ");display.print(ch);display.print(" error = "); display.println(i2cError[i]);
        j++;
      }
    }
  }
  else {  // if the above did not detect an i2c error then...
    display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
    display.print("no I2C errors "); 
    j++;
    }
  display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
  if (serialErrorFlag) {
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[5])));
    display.print(buffer); // "No Serial Data"
    //Serial.println(buffer);  
  }
  else {
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[7])));
    display.print(buffer); // "serial bus OK"
    //Serial.println(buffer);
  }
  display.display();
}

void clearDisplayText() {
  display.fillRect(0, 16, 127, 47, BLACK);
  display.display();
}

Single_Element_Controller

C/C++
This code is meant to run on an Arduino Nano. It will access all 16 pulse width modulated outputs on the fairy board and is capable of using 14 of the 16 available digital I/O of the Arduino. When interfacing to Vixen, the first 16 channels correspond to the PWM outputs. The next 16 correspond to the digital I/O.
// 16 Channel Single Element Driver with 16 relay outputs
//
// (c) Paul Zavracky, January, 2018, updated August 2020

#define VERSION "1.1"
#include <Wire.h> // I2C library

#define OE 2 // pca9955 location of Output Enable pin, active low
#define RESET 3 // pca9955 location of reset pin, active low
#define MAX_CHANNELS 32 // 16 pca9955 Channels and 16 digital channels (relays)
#define BAUD_RATE 115200

// Define relay output pins
#define DIGITAL_OUT_0 5
#define DIGITAL_OUT_1 6
#define DIGITAL_OUT_2 7
#define DIGITAL_OUT_3 8

#define DIGITAL_OUT_4 9
#define DIGITAL_OUT_5 10
#define DIGITAL_OUT_6 11
#define DIGITAL_OUT_7 12

#define DIGITAL_OUT_8 A0
#define DIGITAL_OUT_9 A1
#define DIGITAL_OUT_10 A2
#define DIGITAL_OUT_11 A3
// A4 and A5 are used for i2c interface
#define DIGITAL_OUT_12 A6 // not currently accessable on board
#define DIGITAL_OUT_13 A7 // not currently accessable on board
#define DIGITAL_OUT_14 4
#define DIGITAL_OUT_15 13

// Display Driver Initialization from Adafruit
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Globals

int ch;
int state;
int chVal[MAX_CHANNELS] = {0};
byte error;
       //   0:success
       //   1:data too long to fit in transmit buffer
       //   2:received NACK on transmit of address
       //   3:received NACK on transmit of data
       //   4:other i2c error
       //   5:no serial data recieved

const char error_0[] PROGMEM = "i2c Success"; // "String 0" etc are strings to store - change to suit.
const char error_1[] PROGMEM = "i2c too long";
const char error_2[] PROGMEM = "i2c addr NACK";
const char error_3[] PROGMEM = "i2c data NACK";
const char error_4[] PROGMEM = "other i2c error";
const char error_5[] PROGMEM = "no serial data";

const char *const error_table[] PROGMEM = {error_0, error_1, error_2, error_3, error_4, error_5}; // create a table of strings in PROGMEM

const char messages_0[] PROGMEM = "Welcome to the Looneypoons 16 channel Single Element driver, V"; // this string is 29 character long
const char messages_1[] PROGMEM = "SSD1306 allocation failed";
const char messages_2[] PROGMEM = "Looneypoon";
const char messages_3[] PROGMEM = "i2c check";
const char messages_4[] PROGMEM = "Scanning...";
const char messages_5[] PROGMEM = "No Serial Data";
const char messages_6[] PROGMEM = "Sys Chk complete";
const char messages_7[] PROGMEM = "serial bus OK";

const char *const messages_table[] PROGMEM = {messages_0, messages_1, messages_2, messages_3, messages_4, messages_5, messages_6, messages_7};

char buffer[30]; // length of maximum message at minimum
       
bool i2cErrorFlag = false;                    
byte i2cError[MAX_CHANNELS] = {0}; // initial to zero so that change detection will work

bool serialErrorFlag = false;  // set error flags to true to force system to clear error from display
bool oldSerialErrorFlag = true; // this forces the display of the i2c status and the serial status
              
byte i2cAddress = 0x5A;
unsigned int loopCounter = 0; // keeps track of how many times through the main loop collecting no serial data

byte digitalOut[] = {DIGITAL_OUT_0, DIGITAL_OUT_1, DIGITAL_OUT_2, DIGITAL_OUT_3, DIGITAL_OUT_4, DIGITAL_OUT_5, DIGITAL_OUT_6, DIGITAL_OUT_7, DIGITAL_OUT_8, 
                      DIGITAL_OUT_9, DIGITAL_OUT_10, DIGITAL_OUT_11, DIGITAL_OUT_12, DIGITAL_OUT_13, DIGITAL_OUT_14, DIGITAL_OUT_15};

enum states
{
  IDLE,
  DELIM,
  READ,
  DISP
};

void setup() {
  int nDevices;
  byte address;

  Serial.begin(BAUD_RATE);
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[0])));  // Necessary casts and dereferencing, just copy.
  Serial.print(buffer); Serial.println(VERSION);

  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3C for 128x64
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[1]))); // SSD1306 allocation failed
    Serial.println(buffer);
    for(;;); // Don't proceed, loop forever
  }
  
  // Set up PCA9955 Chip Reset and Output Enable
  pinMode(RESET, INPUT);           // set pin to input
  digitalWrite(RESET, HIGH);       // turn on pullup resistors
  pinMode(RESET, OUTPUT);
  digitalWrite(RESET, HIGH); // reset for the PCA9955 is active low
  delay(1000); // wait 1 second
  digitalWrite(RESET, LOW); // reset for the PCA9955 is active low
  delay(100); // send 100 ms reset pulse
  digitalWrite(RESET, HIGH); // reset for the PCA9955 is active low
  delay(100); // provide time for 9955 to reset

  pinMode(OE, INPUT);           // set pin to input
  digitalWrite(OE, HIGH);       // turn on pullup resistors
  pinMode(OE, OUTPUT);
  digitalWrite(OE, HIGH); // output enable for the PCA9955 is active low
  delay(1000); // wait 1 second
  digitalWrite(OE, LOW); // output enable for the PCA9955 is active low
  delay(100); // provide time for 9955 to settle

  // Set Initial Display
  display.clearDisplay();
  display.setTextSize(2);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[2]))); // SSD1306 allocation failed
  display.println(buffer); //"Looneypoon"
  display.display();
  delay(1000);
  
  display.setTextSize(1);
  display.print(F("V"));display.println(VERSION);
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[3]))); // SSD1306 allocation failed
  display.println(buffer); //"i2c check"
  display.println();
  display.display();
  delay(1000);

  // Start by scanning i2c slave addresses
  Wire.begin();  // I2C library
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[4]))); // SSD1306 allocation failed
  display.println(buffer); //"Scanning..."
  display.display();
  
  nDevices = 0;
  for(address = 1; address < 127; address++ )
  {
    // The i2c_scanner uses the return value of
    // the Write.endTransmisstion to see if
    // a device did acknowledge to the address.
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
    // Serial.print("error = "); Serial.println(error);
    if (error == 0)
    {
      display.print(F("0x"));
      if (address<16)
        display.print(F("0"));
        display.print(address,HEX);
        display.print(", ");
        nDevices++;
      }
    else if (error==4)
    {
      display.print(F("Unknown error at "));
      if (address<16)
        display.print(F("0"));
      display.println(address,HEX);
    }    
  }
  display.display();
  delay(1000);
  if (nDevices == 0)
    display.println(F("No I2C devices found\n"));
  else
    display.println("done\n");
  display.display();
  delay(3000); // delay so one can read screen
    
  //Serial.println("The above lists the device/board addresses found.\n\r  Address 70h and 76h are common to all PCA9955 devices.");

  // Set the output current level
  display.clearDisplay();
  display.setCursor(0,0);             // Start at top-left corner
  display.setTextSize(2);   
  display.println(F("Looneypoon"));
  display.display();
  display.setTextSize(1);

  Wire.beginTransmission(byte(i2cAddress)); // transmit to PCA9955 device specific address
  Wire.write(byte(0x45));            // Selects the IREFALL register (pg 32 of PCA9955B data sheet)  
  Wire.write(255);             // sends current value byte  
  error = Wire.endTransmission();     // stop transmitting  
  display.print(" error = "); display.println(error);
    
  strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[6])));
  display.println(buffer); // "Sys Chk complete" 
  display.println(""); // space
  display.display();  
  
  Serial.println(buffer); // "Sys Chk complete" 
  delay(3000);

  for (int i = 0; i<12; i++) { // set up output ports
    pinMode(digitalOut[i], OUTPUT);
    digitalWrite(digitalOut[i], HIGH); // logic low activates relay
  }
  state = IDLE;
  ch = 0;
}

void loop() {   
  if (Serial.available())
  {
    switch (state)
    {
      case IDLE:  
        ch = 0;
        if (Serial.read() == '+')
        {
          state = DELIM;          
        }
        else
        {
          state = IDLE;
        }
      break;
        
      case DELIM:
        ch = 0;
        if (Serial.read() == '>')
        {
          state = READ;
        }
        else
        {
          state = IDLE;
        }
      break;
      
      case READ:
        chVal[ch++] = Serial.read();
        if (ch >= MAX_CHANNELS)
        {
          ch = 0;
          state = DISP;
        }
      break; 
      
      case DISP:
        state = IDLE;
        //Serial.println("made it to disp");
        i2cErrorFlag = false; // initialize
        for (ch=0; ch<16; ch++) // board has 16 PWM channels
        {
          Serial.print(ch);Serial.print("   ");Serial.println(chVal[ch]);
          Wire.beginTransmission(byte(i2cAddress)); // transmit to PCA9955 device specific address
          Wire.write(byte(0x08 + ch));            // Selects the PWM6 register (pg 14 of PCA9955B data sheet) 
          Wire.write(chVal[ch]);                            // sends PWM value byte  
          error = Wire.endTransmission();           // stop transmitting 
          if (i2cError[ch] != error) {i2cErrorFlag = true;} // set error flag only on a change of error condition
          i2cError[ch] = error;
        }
        for (ch=16; ch<MAX_CHANNELS; ch++) // board has 8 digital channels
        {
          Serial.print(ch);Serial.print("   ");Serial.println(chVal[ch]);
          digitalWrite(digitalOut[ch - 16],(chVal[ch]>0) ? LOW : HIGH ); // sends value as boolean (relays are active low!!)
        }
        loopCounter = 0; // we got serial data, so can reset the loop counter
        serialErrorFlag = false;  // also reset flag
      break;
    }
  }
  loopCounter++;
  if (loopCounter == 0) {serialErrorFlag = true; } // we've gone x times without recieving data
  if((serialErrorFlag != oldSerialErrorFlag) || i2cErrorFlag) {updateErrorDisplay();}  // check for errors
  oldSerialErrorFlag = serialErrorFlag;
}

void updateErrorDisplay(){
  byte j = 1;
  
  clearDisplayText();
  display.setTextSize(1);
  // check i2c errors
  if (i2cErrorFlag) {
    for (int i = 0; i < MAX_CHANNELS; i++) {
      if(i2cError[i] != 0) {
        display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
        display.print("channel ");display.print(ch);display.print(" error = "); display.println(i2cError[i]);
        j++;
      }
    }
  }
  else {  // if the above did not detect an i2c error then...
    display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
    display.print("no I2C errors "); 
    j++;
    }
  display.setCursor(0,16+9*j); // Start at top-left corner of blue area only ( there are 8 pages 8 pixels high, the first two are yellow)
  if (serialErrorFlag) {
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[5])));
    display.print(buffer); // "No Serial Data"
    //Serial.println(buffer);  
  }
  else {
    strcpy_P(buffer, (char *)pgm_read_word(&(messages_table[7])));
    display.print(buffer); // "serial bus OK"
    //Serial.println(buffer);
  }
  display.display();
}

void clearDisplayText() {
  display.fillRect(0, 16, 127, 47, BLACK);
  display.display();
}

Credits

zavracky

zavracky

1 project • 2 followers

Comments