The Planted Tank Forum banner

Guide: Arduino based LED controller for Current Satellite LED+

174K views 715 replies 69 participants last post by  Dahammer 
#1 · (Edited)
So I made an off-hand comment in one of Current USA's threads last night about an Arduino controller for their LED+ freshwater fixture. In the 12 or so hours since posting that, I have probably received a dozen PM's requesting code, IR protocol, sketches, or other information. Instead of responding to each request for information separately, it became obvious that I just needed to put together a thread for my project. This is that thread.

First, a disclaimer. While I'm not your average DIY tinkerer, I'm also not an electronics expert. I'm a mechanical engineer by day, and like to think of myself as a modern mad scientist by night. If you decide to build your own controller based on this thread, be aware that you're on your own. That's not to say I won't help... I think open source is the way of the future and love to pass knowledge on. I just mean that if you burn up your Arduino, yourself, or your house, that it's on you.

I'm putting together a guide to follow along, but none of the steps should be considered the last step. The nice thing about Arduino is how easy it is to constantly modify your code and add features. Throughout this thread, I will periodically post my newest code so that you can upload it and get the newer features. If you're experienced with Arduino, feel free to hack my code apart and have your way with it.

This process should work for any light fixture with some tweaking!

I'm writing this step-by-step so hopefully anyone can follow along. The more Arduino users we have in the world, the better life can be for everyone! If you're new to Arduino, don't be overwhelmed... it's not as difficult as it seems at first glance. And if you're an old hand please don't bash my primitive coding too much!



Everything in this post should be considered open source and public domain. Feel free to use my code, distribute it, hack it up, whatever.
 
See less See more
#2 · (Edited)
Check this post for the most up to date code and features.

Most Current Version : 3.6
Other versions can be found throughout this thread.

Functionality:
  • Allows user to set up to 24 triggers to activate pre-defined functions at any time.
  • Random thunderstorms.
  • RTC support maintains time even if power is lost.
  • Range of around 10 feet for IR emitter.
  • No modification to the fixture or remote required.
  • Can be powered by USB or via a wall wart.
  • Factory remote still functions. Will return to programmed functions at next trigger.
  • All factory functions can be programmed to activate at any time.
  • Supports a 20 x 4 LCD screen.

Default scheduling:
  • 07.00 am - Dawn/Dusk
  • 09.00 am - Cloud2
  • 11.00 am - FullSpec
  • 03.00 pm - Cloud2
  • 07.00 pm - Dawn/Dusk
  • 09.00 pm - Night2

Required libraries:
  • Time
  • TimeAlarms
  • RTClib
  • IRremote
  • LiquidCrystal

Code:
///////////////////////////////////////////////////////////////////
// Current Satellite LED+ Controller  V3.6                       //
//   Indychus...Dahammer...mistergreen @ plantedtank.net         //
//   This code is public domain.  Pass it on.                    //
//   Confirmed on Arduino UNO 1.0.5                              //
//   Req. Time, TimeAlarms, RTClib, IRremote                     //
///////////////////////////////////////////////////////////////////
//
// This version uses Ken Shirriff's IRremote library to Rx/Tx the IR codes
// http://www.righto.com/2009/08/multi-protocol-infrared-remote-library.html
// 
// This code does NOT use PIN 13 on the Uno, as do previous versions
// Instead PIN 3, which is a PWM pin, is used. So you'll need to connect
// your LED to PIN 3 instead of PIN 13 for it to work.

// Install LCD per instructions at http://learn.adafruit.com/character-lcds/overview

#include <Wire.h>
#include <RTClib.h>
#include <Time.h>
#include <TimeAlarms.h>
#include <IRremote.h>
#include <LiquidCrystal.h>

RTC_DS1307 RTC;
IRsend irsend;
LiquidCrystal lcd(7, 8, 9, 10, 11, 12);

/*********** BEGIN USER DEFINED VARIABLES ***********/
// DEBUG_IR adds the option to test the IR commands via the Arduino software's serial monitor
// You can send any value from 1 to 32 and it send the corresponding IR code
// The codes follow the remote controller, left to right, top to bottom
// e.g 1 = Orange, 2 = Blue, 21 = Moon1, etc
#define DEBUG_IR

int postDelay = 100;         // Delay after codes are sent
int randAnalogPin = 0;       // This needs to be set to an unused Analog pin, Used by ThunderStorm()

// Current Satellite+ IR Codes (NEC Protocol)
unsigned long codeHeader = 0x20DF; // Always the same

// Remote buttons listed left to right, top to bottom
unsigned int codeOrange = 0x3AC5;
unsigned int codeBlue = 0xBA45;
unsigned int codeRose = 0x827D;
unsigned int codePowerOnOff = 0x02FD;
unsigned int codeWhite = 0x1AE5;
unsigned int codeFullSpec = 0x9A65;
unsigned int codePurple = 0xA25D;
unsigned int codePlay = 0x22DD;
unsigned int codeRedUp = 0x2AD5;
unsigned int codeGreenUp = 0xAA55;
unsigned int codeBlueUp = 0x926D;
unsigned int codeWhiteUp = 0x12ED;
unsigned int codeRedDown = 0x0AF5;
unsigned int codeGreenDown = 0x8A75;
unsigned int codeBlueDown = 0xB24D;
unsigned int codeWhiteDown = 0x32CD;
unsigned int codeM1Custom = 0x38C7;
unsigned int codeM2Custom = 0xB847;
unsigned int codeM3Custom = 0x7887;
unsigned int codeM4Custom = 0xF807;
unsigned int codeMoon1 = 0x18E7;
unsigned int codeMoon2 = 0x9867;
unsigned int codeMoon3 = 0x58A7;
unsigned int codeDawnDusk = 0xD827;
unsigned int codeCloud1 = 0x28D7;
unsigned int codeCloud2 = 0xA857;
unsigned int codeCloud3 = 0x6897;
unsigned int codeCloud4 = 0xE817;
unsigned int codeStorm1 = 0x08F7;
unsigned int codeStorm2 = 0x8877;
unsigned int codeStorm3 = 0x48B7;
unsigned int codeStorm4 = 0xC837;

void SetAlarms()
{
  // Set up your desired alarms here
  // The default value of dtNBR_ALARMS is 6 in Alarms.h.
  // This code sets 12 alarms by default, so you'll need to change dtNBR_ALARMS to 12 or more
  Alarm.alarmRepeat(7,00,0, DawnDusk);
  Alarm.alarmRepeat(9,00,0, Cloud2);     // (HR,MIN,SEC,FUNCTION)
  Alarm.alarmRepeat(11,00,0, FullSpec);
  Alarm.alarmRepeat(15,00,0, Cloud2);
  Alarm.alarmRepeat(19,00,0, DawnDusk);
  Alarm.alarmRepeat(21,00,0, Moon2);
  
  // Comment these out if you don't want the chance of a random storm each day
  Alarm.alarmRepeat(12,00,00, ThunderStorm);
  ThunderStorm();  // Sets up intial storm so we don't have wait until alarm time
}
/************* END USER DEFINED VARIABLES *************/

void setup()
{
  Wire.begin();
  RTC.begin();
  lcd.begin(20, 4);
  Serial.begin(9600);
      //Serial.println(freeRam());
  
  if (! RTC.isrunning()) { 
    Serial.println("RTC Error");
    RTC.adjust(DateTime(__DATE__, __TIME__));}  //Adjust to compile time
    
  
  setSyncProvider(syncProvider);     //reference our syncProvider function instead of RTC_DS1307::get()
  
  Alarm.timerRepeat(900, digitalClockDisplay);  // Display the time every 15 minutes
  digitalClockDisplay();
  SetAlarms();
 Serial.print("SRAM : ");          //un-comment these line to check available SRAM
 Serial.println(freeRam());}   

void loop()
{
#ifdef DEBUG_IR 
  if (Serial.available() > 0) {
    delay(5); //Wait for transmission to finish
    CurrentCMDs(SerialReadInt());
  }
#endif
  Alarm.delay(100); 
  // Service alarms & wait (msec)
  lcdClockDisplay();
}

time_t syncProvider()
{
  //this does the same thing as RTC_DS1307::get()
  return RTC.now().unixtime();
}

void ThunderStorm ()
{ 
  // Schedules a storm between 1 & 9 in the evening
  // It sets Storm2, followed by Cloud2 or DawnDusk or Moon2, depending on when the storm is over
  randomSeed(analogRead(randAnalogPin));  // Generate random seed on unused pin
  byte RH = random(23);                   // Randomizer for thunderstorm
  byte RM = random(59);
  byte RS = random(59);
  byte TSDurationH = random(2);
  byte TSDurationM = random(59);
  
  if (RH <= 12)
    {
      Serial.println("No storm today");
      lcd.setCursor(0,1);
      lcd.print("No storm today");
      return;
    }
      
  if (RH > 12)                             // If random value is after 1 pm, allow storm
    {
      Alarm.alarmOnce(RH,RM,RS,Storm2);
      Serial.print("Next Storm: ");
      Serial.print(RH);
      printDigits(RM);
      printDigits(RS);
      Serial.print("   ");
      Serial.print("Duration = ");
      Serial.print(TSDurationH);
      printDigits(TSDurationM);
      Serial.println();
     lcd.setCursor(0,1);
     lcd.print("Next Storm: ");
     lcdHRdigits(RH);
     lcdDigits(RM);}
      
      if ((RH + TSDurationH) < 19)   // Return to Cloud2 if storm ends between 1-7pm
        {Alarm.alarmOnce((RH + TSDurationH),(RM + TSDurationM),RS,Cloud2);}
      else if ((RH + TSDurationH) < 21)  // Return to DawnDusk if storm ends between 7-9pm
        {Alarm.alarmOnce((RH + TSDurationH),(RM + TSDurationM),RS,DawnDusk);}
      else                                       // Return to Night2 if storm ends after 9pm
        {Alarm.alarmOnce((RH + TSDurationH),(RM + TSDurationM),RS,Moon2);}
    }


void digitalClockDisplay()          // Digital clock
{ 
  Serial.print("Time = ");
  Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.println(); }

  
void lcdClockDisplay()  
  {lcd.setCursor(0,0);
    lcdHRdigits(hour());
  lcdDigits(minute());}

void printDigits(int digits)        // Add :
{
  Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

void lcdDigits(int digits)        // Add :
{
  lcd.print(":");
  if(digits < 10)
    lcd.print('0');  
  lcd.print(digits);
}
void lcdHRdigits(int HRdigits)        // Preface hour with 0
{
  if(HRdigits < 10)
    lcd.print('0');  
  lcd.print(HRdigits);
}

#ifdef DEBUG_IR
int SerialReadInt()
{
  int i, numAva;
  char inBytes[3];                  // Array to hold the bytes
  char * inBytesPtr = &inBytes[0];  // Pointer to the first element of the array
      
    numAva = Serial.available();    // Read number of input bytes
    if (numAva > 2)
      numAva = 2;                   // Only allow 2 characters to prevent overflow
      
    for (i=0; i<numAva; i++)        // Load input bytes into array
      inBytes[i] = Serial.read();
    inBytes[i] =  '\0';             // Put NULL character at the end
    return atoi(inBytesPtr);        // Call atoi function and return result
}

void CurrentCMDs (int cmd)
{switch (cmd)
  {
    case 1:
      Orange();
      break;
    case 2:
      Blue();
      break;
    case 3:
      Rose();
      break;
    case 4:
      PowerOnOff();
      break;
    case 5:
      White();
      break;
    case 6:
      FullSpec();
      break;
    case 7:
      Purple();
      break;
    case 8:
      Play();
      break;
    case 9:
      RedUp();
      break;
    case 10:
      GreenUp();
      break;
    case 11:
      BlueUp();
      break;
    case 12:
      WhiteUp();
      break;
    case 13:
      RedDown();
      break;
    case 14:
      GreenDown();
      break;
    case 15:
      BlueDown();
      break;
    case 16:
      WhiteDown();
      break;
    case 17:
      M1Custom();
      break;
    case 18:
      M2Custom();
      break;
    case 19:
      M3Custom();
      break;
    case 20:
      M4Custom();
      break;
    case 21:
      Moon1();
      break;
    case 22:
      Moon2();
      break;
    case 23:
      Moon3();
      break;
    case 24:
      DawnDusk();
      break;
    case 25:
      Cloud1();
      break;
    case 26:
      Cloud2();
      break;
    case 27:
      Cloud3();
      break;
    case 28:
      Cloud4();
      break;
    case 29:
      Storm1();
      break;
    case 30:
      Storm2();
      break;
    case 31:
      Storm3();
      break;
    case 32:
      Storm4();
      break;
    default:
      Serial.println("Invalid Choice");}}
#endif

void SendCode (unsigned int code, byte numTimes, const char *sMessage)
{unsigned long irCode = (codeHeader << 16) + code; // Header is 2 bytes, shift all the way to left & add code to it
  
  for( int i = 0; i < numTimes; i++)
  {irsend.sendNEC(irCode,32); // Send code
    Alarm.delay(postDelay);}
    
  Serial.println(sMessage);  // Print message
  lcd.setCursor(6,0);
  lcd.print(sMessage);
  for(byte i = sizeof(sMessage); i <= 14; i++)
    lcd.print(" ");
  digitalClockDisplay();}

void Orange()
{SendCode(codeOrange, 2, "Orange");}

void Blue()
{SendCode(codeBlue, 2, "Blue");}

void Rose()
{SendCode(codeRose, 2, "Rose");}

void PowerOnOff()
{SendCode(codePowerOnOff, 1, "Power On/Off");}

void White()
{SendCode(codeWhite, 2, "White");}

void FullSpec()
{SendCode(codeFullSpec, 2, "Full Spectrum");}

void Purple()
{SendCode(codePurple, 2, "Purple");}

void Play()
{SendCode(codePlay, 1, "Play/Pause:");}

void RedUp()
{SendCode(codeRedUp, 1, "Red Up");}

void GreenUp()
{SendCode(codeGreenUp, 1, "Green Up");}

void BlueUp()
{SendCode(codeBlueUp, 1, "Blue");}

void WhiteUp()
{SendCode(codeWhiteUp, 1, "White Up");}

void RedDown()
{SendCode(codeRedDown, 1, "Red Down");}

void GreenDown()
{SendCode(codeGreenDown, 1, "Green Down");}

void BlueDown()
{SendCode(codeBlueDown, 1, "Blue Down");}

void WhiteDown()
{SendCode(codeWhite, 1, "White Down");}

void M1Custom()
{SendCode(codeM1Custom, 2, "Custom Mix 1");}

void M2Custom()
{SendCode(codeM2Custom, 2, "Custom Mix 2");}

void M3Custom()
{SendCode(codeM3Custom, 2, "Custom Mix 3");}

void M4Custom()
{SendCode(codeM4Custom, 2, "Custom Mix 4");}

void Moon1()
{SendCode(codeMoon1, 2, "Moonlight 1");}

void Moon2()
{SendCode(codeMoon2, 2, "Moonlight 2");}

void Moon3()
{SendCode(codeMoon3, 2, "Moonlight 3");}

void DawnDusk()
{SendCode(codeDawnDusk, 2, "Dawn/Dusk");}

void Cloud1()
{SendCode(codeCloud1, 2, "Cloud Cover 1");}

void Cloud2()
{SendCode(codeCloud2, 2, "Cloud Cover 2");}
  
void Cloud3()
{SendCode(codeCloud3, 2, "Cloud Cover 3");}

void Cloud4()
{SendCode(codeCloud4, 2, "Cloud Cover 4");}

void Storm1()
{SendCode(codeStorm1, 2, "Thunderstorm 1");}

void Storm2()
{SendCode(codeStorm2, 2, "Thunderstorm 2");}

void Storm3()
{SendCode(codeStorm3, 2, "Thunderstorm 3");}

void Storm4()
{SendCode(codeStorm4, 2, "Thunderstorm 4");}

int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}
 
#3 · (Edited)
Here's what you need to get started. These parts can be had at radioshack or other electonics supplies, although e b a y and Amazon are great sources.

  • Arduino: I used an Uno for this project, which is $29 straight from Arduino, or around $9 for a knock-off board. I prefer the genuine Arduino, but many of the knock-offs are very nice. Boards other than Uno can be used, but some of the pins may be different. I am using a Revision 3 board running Arduino version 1.0.5. If you're new to this, I'd suggest a starter kit that includes a breadboard, jumpers, and an assortment of goodies.

  • IR Receiver: The Current system uses a 38 kHz frequency just like most A/V equipment. At least, my 48" Satellite LED+ does. You can use an oscilloscope and photodiode to determine the frequency if you are trying to control a light other than Current. Any 38 khz IR receiver should work. They are around $2-3 usually, but can be had for as cheap as $.50 if you buy more than one. I used a TSOP4838.

  • IR Emitter LED: Again, any emitter should work, and they are dirt cheap. I used Lite-On LTE-5208A which are around $0.12 each. Buy a pack of 20 or so, you'll find uses for them.

  • 150 ohm Resistor: If you use the same parts as I have listed above, a 150 ohm resistor is needed. One resistor is around $0.02. Again, I recommend buying an assortment since they're so cheap. If you're not using identical components as those listed above, you will need to figure out what resistor is needed for your specific needs. I can help with this if needed.

  • Real Time Clock: An RTC is needed so that your Arduino knows what time it is and your program can resume operation properly after a power outage. I recommend any RTC based on the DS1307. You can buy kits that must be assembled, assembled boards that need the headers soldered on, or complete plug and play units. It's up to you. Cost is between $5-$25.

  • LCD Screen: The code supports a 20 x 4 LCD screen, but it is completely optional. If you want the extra ease of use and eye-candy, I suggest the 20 x 4 backlit LCD from Adafruit.
 
#5 ·
I'm not sure what is going on on the Current side of things. One of my goals was to maintain the remote functionality, so I chose to go with the IR and haven't looked into the fixture itself much. I'm fairly sure that it will be 5V PWM if you investigate it though.

I have the IR protocol in hex, I'll be posting it shortly! You should be able to skip the IR stuff and wire the IR lead from the fixture directly to the Arduino if wanted.
 
#7 ·
I've seen a few studies implying that fish in a system with dynamic lighting are more sexual active, and therefor healthier... But I don't think anything absolutely conclusive has been done. My main goal is the cool factor. With the Current product announcement and their own controllers out now, there isn't much reason to build your own economically speaking (I was a little shocked at how low their MSRP is), but this does allow you more control and the ability to tailor it exactly to your needs.

When I first started this, Current had no controller available. So I had a nice new light with dozens of functions that I had to control manually. It was a bit of a bummer.
 
#8 ·
I've never had occasion to post pics of my tanks (mainly because they look pretty shabby compared to many on here), so here's a pair of them. I have 6 total, but these are the only ones worthy of being posted.

Here's my 20H. This is my favorite tank right now. There's a pair of male dwarf gourami, a bunch of chili rasbora, some otos, and some cories in here.


And this is my 55. It's not looking too good right now, as it's recovering from the destruction caused by a yellow-bellied slider. This is the tank that the LED+ and automation is going on.
 
#12 · (Edited)
Now for the hardware!

This is the basic setup. The IR range is limited to around 3 feet like this due to the current limit on the Arduino's output channel. Later, I will build an amplifier to boost the range to hopefully around 10 feet. Since the controller will sit right next to the IR receiver on the fixture, this isn't really an issue.

Here's what your IR receiver should look like:


Now connect it to your Arduino, like this:


The IR lead is connected to the Arduino's 2 pin. If you're not using an UNO, any PWM or digital channel will work. Connect the 5V and ground leads to the appropriate pins on the Arduino.
 
#14 · (Edited)
Now for the emitter. LED's need to be installed with the proper polarity, in most cases the negative lead is denoted with a flat spot on the side of the bulb.



Next, you need a resistor to limit the current through the LED and protect the channel on the Arduino. In this case, I used a 150 ohm resistor which gives me a current of 33 mA... safely below the Arduino's rating of 40 mA for this channel. The emitter is designed to operate at 100 mA, so it has a short range right now, but a transistor amplifier will fix that in the near future.

Wire it like this:


The perspective in that picture is off, so don't let it fool you. The yellow wire is connected to the GND pin, and the green to pin 13. Again, you can use any non-digital pin for this purpose. Connect the ground to the - side of the emitter, then the + side to the resistor, then the resistor to channel 13 on the Arduino.

Note that the most current code uses PWM pin 3 instead of pin 13


That's all there is for the hardware, until I add an amplifier in the future and an RTC for timing. This is enough for basic functions and pretty much operates as an overly-complicated timer. More to come though...
 
#20 ·
Software

Ok, so now that you have the basic device built, it's time to make it do something. Again, I'm writing this in case anyone completely new to Arduino wants to follow along. If you're familiar with this stuff, feel free to skip ahead.

Here's the basic premise of the device:

  • Decode IR signal from remote
  • Store IR signal in an Arduino sketch
  • Keep time (now software based, adding an RTC soon)
  • Recall IR signal at set times to activate functions on the fixture

It's really that simple. The Arduino is basically doing the same thing you would do with the remote, but doing it automatically at set times. No modification to the remote or fixture is necessary.

Note that this procedure can be used to control any IR device that operates at 38 khz. That's pretty much everything... your TV, BluRay player, audio equipment, etc. And with a little tweaking and an appropriate receiver, it can work on devices which are not at a frequency of 38 kHz as well. In short, you can automate anything with an IR receiver on it.
 
#21 ·
So first off, go to the Arduino start up page to download the appropriate software and load a blink sketch to test your board. It can be found here:

http://arduino.cc/en/Guide/HomePage

Once that's done, you'll need to download and install a few libraries for this to work. You just download the .zip file, unzip into a folder on your desktop, then move the folder to the libraries folder inside the Arduino folder. Be sure to maintain any folder/subfolder structure in the libraries.

For this to work, you'll need these libraries:

  • Time
  • TimeAlarms
  • DateTime
  • DateTimeStrings

These can be found here (Time) and here (TimeAlarms). I'm pretty sure the other two are included in one of those libraries. If not, they can be found on Arduino Playground or via a quick google search.

As this project progresses, other libraries may be needed. If I write them, I will provide them, but most likely they'll be by someone else and I'll point you to a link to download them.

A lot of errors during compiling are due to libraries being in the wrong spot. Don't get frustrated if you get errors, they are usually easily resolved.
 
#22 · (Edited)
Once that's done, you'll need a sketch for decoding the IR signals from your remote. I've already done quite a few of them, but you can use this to operate any IR device, so it's a handy bit of code to know. I can't take credit for this code, as I got it from adafruit.com. That's the beauty of Arduino, for nearly every project you can find something someone else has done and tweak it to suit your needs.

Here's the sketch. Just copy it and paste it into a new sketch in the Arduino software. Save as IR Decoder or something similar. Once saved, change the value "#define IRpin x" to match the channel that your receiver is plugged in to. If you're following step by step, it's already ready to go. Upload to the Arduino, open the Serial Monitor (top right corner), wait for the prompt (Ready to decode IR), then point your remote at your receiver (1-2 inches away) and press a button.

Code:
/* Raw IR decoder sketch!
This sketch/program uses the Arduno and a PNA4602 to
decode IR received. This can be used to make a IR receiver
(by looking for a particular code)
or transmitter (by pulsing an IR LED at ~38KHz for the
durations detected
Code is public domain, check out www.ladyada.net and adafruit.com
for more tutorials!
*/
 
// We need to use the 'raw' pin reading methods
// because timing is very important here and the digitalRead()
// procedure is slower!
//uint8_t IRpin = 2;
// Digital pin #2 is the same as Pin D2 see
// http://arduino.cc/en/Hacking/PinMapping168 for the 'raw' pin mapping
#define IRpin_PIN PIND
#define IRpin 2
// for MEGA use these!
//#define IRpin_PIN PINE
//#define IRpin 4
 
// the maximum pulse we'll listen for - 65 milliseconds is a long time
#define MAXPULSE 65000
 
// what our timing resolution should be, larger is better
// as its more 'precise' - but too large and you wont get
// accurate timing
#define RESOLUTION 20
 
// we will store up to 100 pulse pairs (this is -a lot-)
uint16_t pulses[100][2]; // pair is high and low pulse
uint8_t currentpulse = 0; // index for pulses we're storing
 
void setup(void) {
  Serial.begin(9600);
  Serial.println("Ready to decode IR!");
}
 
void loop(void) {
  uint16_t highpulse, lowpulse; // temporary storage timing
  highpulse = lowpulse = 0; // start out with no pulse length
  
  
// while (digitalRead(IRpin)) { // this is too slow!
    while (IRpin_PIN & (1 << IRpin)) {
     // pin is still HIGH
 
     // count off another few microseconds
     highpulse++;
     delayMicroseconds(RESOLUTION);
 
     // If the pulse is too long, we 'timed out' - either nothing
     // was received or the code is finished, so print what
     // we've grabbed so far, and then reset
     if ((highpulse >= MAXPULSE) && (currentpulse != 0)) {
       printpulses();
       currentpulse=0;
       return;
     }
  }
  // we didn't time out so lets stash the reading
  pulses[currentpulse][0] = highpulse;
  
  // same as above
  while (! (IRpin_PIN & _BV(IRpin))) {
     // pin is still LOW
     lowpulse++;
     delayMicroseconds(RESOLUTION);
     if ((lowpulse >= MAXPULSE) && (currentpulse != 0)) {
       printpulses();
       currentpulse=0;
       return;
     }
  }
  pulses[currentpulse][1] = lowpulse;
 
  // we read one high-low pulse successfully, continue!
  currentpulse++;
}
 
void printpulses(void) {
  Serial.println("\n\r\n\rReceived: \n\rOFF \tON");
  for (uint8_t i = 0; i < currentpulse; i++) {
    Serial.print(pulses[i][0] * RESOLUTION, DEC);
    Serial.print(" usec, ");
    Serial.print(pulses[i][1] * RESOLUTION, DEC);
    Serial.println(" usec");
  }
  
  // print it in a 'array' format
  Serial.println("int IRsignal[] = {");
  Serial.println("// ON, OFF (in 10's of microseconds)");
  for (uint8_t i = 0; i < currentpulse-1; i++) {
    Serial.print("\t"); // tab
    Serial.print(pulses[i][1] * RESOLUTION / 10, DEC);
    Serial.print(", ");
    Serial.print(pulses[i+1][0] * RESOLUTION / 10, DEC);
    Serial.println(",");
  }
  Serial.print("\t"); // tab
  Serial.print(pulses[currentpulse-1][1] * RESOLUTION / 10, DEC);
  Serial.print(", 0};");
}
 
#23 ·
Once that's done, you'll get an output that looks like this in your Serial Monitor window:



These numbers are delays, in microseconds, for each flash of the IR emitter in a signal data transfer. I won't get into IR theory (it isn't that complicated and you should definitely check it out) but these are the codes we need to get things going. The first group is raw data, and the second group is the same data, in an easier to look at format. The very first value can be ignored, as it's the delay between starting the sketch and pressing the button.

I suggest creating a spreadsheet to keep the data organized, like the one I am working on here:



I will post the entire spreadsheet once I finish decoding and testing all of the functions. Here, you can see that each transfer is composed of a header, which is a device ID, the actual code, then a footer, which just tells the fixture that the transfer is done and to be ready to potentially expect another packet.

You device ID may be different than mine, though I doubt it. If it is, you just change the values in the control sketch (which I am about to post) to reflect your ID. Since the device ID and footer are identical on every transmission, you can just copy and paste the data and change appropriate values.
 
#24 ·
At this point, more advanced users may be asking where the hex codes are. Well, here you go.

If you're new, you can ignore this post.

Looking at the remote, these are ordered left to right, top to bottom. In every case, the footer is FFFFFFFF. The device ID (20DF) may or may not be the same on all Current products.

Orange 20DF3AC5
Blue 20DFBA45
Rose Pink 20DF827D
Power On/Off 20DF02FD
White 20DF1AE5
Full Spectrum 20DF9A65
Purple 20DFA25D
Play/Pause 20DF22DD
Red Up 20DF2AD5
Green Up 20DFAA55
Blue Up 20DF926D
White Up 20DF12ED
Red Down 20DF0AF5
Green Down 20DF8A75
Blue Down 20DFB24D
White Down 20DF32CD
M1 20DF38C7
M2 20DFB847
M3 20DF7887
M4 20DFF807
Moon 1 20DF18E7
Moon 2 20DF9867
Moon 3 20DF58A7
Dawn/Dusk 20DFD827
Cloud 1 20DF28D7
Cloud 2 20DFA857
Cloud 3 20DF6897
Fading Sun 20DFE817
T-Storm 1 20DF08F7
T-Storm 2 20DF8877
T-Storm 3 20DF48B7
Fade All 20DFC837
 
#25 · (Edited)
Now we're ready to automate a light!

Here's my code for the Current fixture. I will update it frequently as the device matures and more features are added. For right now, this is a good place to test your system. Run this code for a few days and make sure everything is working fine. I have had this code running for around 2 weeks now without issue.

How to adapt this code to your system:

There are several things you need to do to ensure this works for you. First, you'll see a value for "int IRLedPin" that's currently set to 13. Change this to whatever channel your emitter is connected to. Next, change the 9600 in "Serial.begin(9600)" to the baud rate you are using to communicate with your Arduino. If you don't know this, you can probably leave it alone and have no issues. Next, change the values in setTime(HR,MIN,SEC,MO,DAY,YR) to match the time right before you upload the sketch. This sets the Arduino's time. If power is lost or you edit the sketch, you will need to update the time again. This will be fixed soon when we add an RTC to the device. Finally, check the IR commands at the end of the code. Make sure the device ID (first 17 number pairs) is the same as the values you got from your remote and change them if necessary. It's ok if they're off by as much as 100. You're looking for values that are off by 500 or more.

In addition to those tweaks to the code, you'll also need to edit the header file for the TimeAlarm library. This is because the header that comes in the library sets the maximum number of alarms to 6. If you try to add more, they won't work. To fix this, open the file TimeAlarms.h in the Arduino > Libraries > TimeAlarms directory with a text editor (NotePad works fine). It will look like this:



In the highlighted area, you'll see mine says 24 and yours should say 6. Change the value to 24 and save the document. This allows you to have 24 alarm triggers. In the future, you can add more, but it's wise to not overdo it as each alarm reserves space in the Arduino's memory.

How it works:
Basically, this is a fancy alarm clock right now. The values under "ALARM FUNCTIONS" are triggers. Each trigger has parameters (HR,MIN,SEC,FUNCTION). The FUNCTION refers to the IR codes at the bottom of my sketch. Whenever the time here is reached, that function is fired. It fires the emitter once, then repeats it 1 second later to be sure the command was received. Change the time to whatever values you desire, using the same syntax as I have used. Note that the clock is 24hr format. The FUNCTION names to use are labeled in the "void XXXXXXX()" lines of the code under "FIXTURE FUNCTIONS."

Right now, these functions are available:
  • PowerButton - toggles power on/off
  • DawnDusk - puts fixture into dawn and dusk mode
  • Cloud2 - heavy cloud cover with full spectrum cycle
  • FullSpec - solid full spectrum light, all-on.
  • Night2 - soft, fading moonlight.



Code:
///////////////////////////////////////////////////////////////////
// Current Satellite LED+ Controller                             //
//   Ken Bunton (Indychus)                                       //
//   This code is public domain.  Pass it on.                    //
//   Confirmed on Arduino UNO 1.0.5                              //
//   Req. Time, TimeAlarms, DateTime, DateTimeStrings libraries  //
///////////////////////////////////////////////////////////////////

////////////SETUP//////////////////////////////////////////////////
#include <Time.h>
#include <TimeAlarms.h>
#include <DateTime.h>
#include <DateTimeStrings.h>

int IRledPin =  13;                  // Pin location for IR output

void setup()               
{ pinMode(IRledPin, OUTPUT);         // Designate IRledPin as Output
  Serial.begin(9600);                // Connect @ (Baud)
  setTime(13,04,00,7,23,13);          // set time (HR,MIN,SEC,MO,DAY,YR)
  
  
////////////ALARM FUNCTIONS/////////////////////////////////////////
  Alarm.alarmRepeat(7,00,0, DawnDusk);
  Alarm.alarmRepeat(7,00,1, DawnDusk);
  Alarm.alarmRepeat(9,00,0, Cloud2);     // (HR,MIN,SEC,FUNCTION)
  Alarm.alarmRepeat(9,00,1, Cloud2);
  Alarm.alarmRepeat(13,00,0, FullSpec);
  Alarm.alarmRepeat(13,00,1, FullSpec);
  Alarm.alarmRepeat(15,00,0, Cloud2);
  Alarm.alarmRepeat(15,00,1, Cloud2);
  Alarm.alarmRepeat(19,00,0, DawnDusk);
  Alarm.alarmRepeat(19,00,1, DawnDusk);
  Alarm.alarmRepeat(21,00,0, Night2);
  Alarm.alarmRepeat(21,00,1, Night2);    }


////////////CLOCK///////////////////////////////////////////////////
void  loop(){                       
  digitalClockDisplay();
  Alarm.delay(1000); }              // Clock display update frequency (msec)

void digitalClockDisplay()          // Digital clock
{ Serial.print(hour());
  printDigits(minute());
  printDigits(second());
  Serial.println(); }

void printDigits(int digits)        // Add :
{Serial.print(":");
  if(digits < 10)
    Serial.print('0');
  Serial.print(digits);}

  
////////////SIGNAL///////////////////////////////////////////////////
// Create Frequency (38khz/26msec)
void pulseIR(long microsecs) 
{  cli();                           // kill interupts
  while (microsecs > 0)
{  digitalWrite(IRledPin, HIGH);    // ~3 msec
   delayMicroseconds(7);            // ~delay
   digitalWrite(IRledPin, LOW);     // ~3 msec
   delayMicroseconds(7);            // ~delay
    microsecs -= 26;  }
sei();  }                           // zombie interupts


////////////FIXTURE FUNCTIONS////////////////////////////////////////
void PowerButton()                             //Fixture power on/off toggle

{ Serial.println("Power Toggle");
     pulseIR(8840);
  delayMicroseconds(4320);
     pulseIR(620);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(620);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(620);
  delayMicroseconds(1580);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(600);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(1600);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(38980);
    pulseIR(8860);
  delayMicroseconds(2120);
    pulseIR(620);  }

 
void Night2()                                   //Fixture Initialize Night2 Mode
 { Serial.println("Night 2 Mode Initialized");
    pulseIR(8840);
  delayMicroseconds(4320);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(620);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(620);
  delayMicroseconds(1580);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1640);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(1620);
    pulseIR(600);
  delayMicroseconds(1640);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(540);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(580);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(500);
    pulseIR(580);
  delayMicroseconds(1600);
    pulseIR(580);
  delayMicroseconds(1560);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(38980);
    pulseIR(8860);
  delayMicroseconds(2120);
    pulseIR(620);  }
  
    
  void Cloud2()                                         //Fixture Initialize Cloud2 Mode
 { Serial.println("Cloud Cover 2 Mode Initialized");
    pulseIR(8840);
  delayMicroseconds(4320);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(620);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(620);
  delayMicroseconds(1580);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(1620);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(600);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(600);
  delayMicroseconds(560);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1600);
    pulseIR(580);
  delayMicroseconds(1640);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(38980);
    pulseIR(8860);
  delayMicroseconds(2120);
    pulseIR(620);  }
  
  
    void FullSpec()                                  //Fixture Initialize Full Spectrum Mode
{ Serial.println("Full Spectrum Mode Initialized");
    pulseIR(8840);
  delayMicroseconds(4320);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(620);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(620);
  delayMicroseconds(1580);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(520);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1600);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(600);
  delayMicroseconds(560);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(1580);
    pulseIR(580);
  delayMicroseconds(540);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1600);
    pulseIR(580);
  delayMicroseconds(480);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(38980);
    pulseIR(8860);
  delayMicroseconds(2120);
    pulseIR(620);  }
    
    
    
    
        void DawnDusk()                         //F ixture Initialize Dawn/Dusk Mode
{ Serial.println("Dawn/Dusk Mode Initialized");
    pulseIR(8840);
  delayMicroseconds(4320);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(600);
  delayMicroseconds(480);
    pulseIR(620);
  delayMicroseconds(480);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(620);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(520);
    pulseIR(520);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(620);
  delayMicroseconds(1580);
    pulseIR(540);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1660);
    pulseIR(600);
  delayMicroseconds(1560);
    pulseIR(520);
  delayMicroseconds(480);
    pulseIR(600);
  delayMicroseconds(1580);
    pulseIR(600);
  delayMicroseconds(1600);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(500);
    pulseIR(540);
  delayMicroseconds(540);
    pulseIR(600);
  delayMicroseconds(560);
    pulseIR(520);
  delayMicroseconds(500);
    pulseIR(580);
  delayMicroseconds(1580);
    pulseIR(580);
  delayMicroseconds(540);
    pulseIR(520);
  delayMicroseconds(560);
    pulseIR(580);
  delayMicroseconds(1660);
    pulseIR(580);
  delayMicroseconds(1580);
    pulseIR(580);
  delayMicroseconds(1620);
    pulseIR(540);
  delayMicroseconds(38980);
    pulseIR(8860);
  delayMicroseconds(2120);
    pulseIR(620);  }
 
#27 ·
Now we're ready to automate a light!

Still editing, instructions incoming!
Is the code you posted intended to be a library file?
 
#33 ·
The Arduino is sending. The factory receiver on the fixture receives. The Arduino acts just like the factory remote, it just does it on its own without you standing there pressing buttons. The receiver on the Arduino is just for decoding unknown signals; it isnt needed once you have the protocol and codes.
 
#37 ·
Exactly... I guess it's more accurate to say that I'm automating the remote as opposed to automating the light. I'm hoping this kind of approach is more appealing to those who might not want to open the light fixture up and void their warranty. I already have a 24" LED+ in pieces on my workbench though :hihi:
 
#36 ·
Since there's been some question as to how this operates, here's a picture of mine right now running the light on my 55g. You can see I just have it aimed at the factory IR receiver on the Current fixture. The remote still works if you want to manually change the mode, but if you don't touch it, the Arduino automatically changes modes according to the times you set up in the sketch.

The box underneath the Arduino and Current receiver is an old DSL WiFi box that I have gutted to use as a case for this project.

Note that the range right now is around 2 feet, I only have it this close for accuracy during testing. In the near future, I hope to extend the range to around 10 feet (the same as the factory remote).

 
#38 ·
Very interesting indeed. Once upon a time, I dabbled a bit with home built PCBs using Eagle software and the photo method. I think adding a RTC is a great idea. I also like the idea of single PCB. But in lieu of that, you could also just do a simple PCB for the IR emitter and whatever other electronics you add to the project and place everything inside a project box to keep it simple and clean.

I wander if you could wire the Arduino in between the fixture's IR receiver and the fixture, allowing the remote to continue to work as well. That way you could hide the controller in a cabinet or something and still use the remote when you wanted too. And you wouldn't need the emitter.

Now I have to order one so I can monkey with it.
 
#39 ·
The remote still works, until another command is sent from the Arduino, at which point it defaults to the programming again. It would be easy to put the Arduino inline between the receiver and light fixture if you don't mind cutting the factory wires to the receiver, allowing you to hide the controller and still have the receiver exposed. The Arduino outputs 5V on this channel, so you'd just have to make sure 5V is what the fixture needs. I'm pretty sure most PWM signals are 5V, so it should be good.
 
#44 ·
I've got to hand it too you, Indychus, this is an awesome little DIY project. My wife is already cussing you, since I've just found my next "spousal substitute" as she labels my projects.

I'm going to have to do the IR end of it because it's just too cool not too. I can think of all kinds of uses, aside from controlling the light fixture, where I can use this device. If nothing else I can turn on the bedroom TV at 3:00AM a couple times a week to freak the wife out. lol!

I'll probably still end up with one hard wired into the fixture though, so if you reverse engineer the fixture you have torn apart on your bench, be sure and let us know!
 
#46 ·
This is a terrific DIY! Way cheaper than buying a $100 controller. (You should link to this in your signature)

I think I'm going to bite the bullet and try this out over the weekend. I'm a huge tech geek but I'm sure I'll have tons of questions... since this will be my first fun with Arduino.

Thanks for sharing all the details.
 
#47 ·
Glad you've enjoyed it so far... I'm working on a random function to kick it into t-storm mode randomly right now, and hopefully RTC will be here today so I can get that going soon. It still has a long way to go!

Sent from my HTC One X using Tapatalk 4 Beta
 
This is an older thread, you may not receive a response, and could be reviving an old thread. Please consider creating a new thread.
Top