Two fading LED working in tandem

Hey guys, I wanted to share a simple fading under-counter LED strip example, that will be a work in process as I add as I make improvements…

I know that this has been done to death, but my goal was to have:

a) two strips respond to one command
b) nice, smooth fades to new setpoints, and
c) have them transition simultaneously.

So, I used two Photons working together to control independently powered LED strips illuminating kitchen cabinets. I need only change the Master settings and the Slave adjusts in tandem. It looks like this using my home automation controller to transmit the target setpoints via a Spark Function:

So, when I send a new target setpoint to the master, the master broadcasts a UDP package with the new level. The slave (on the same LAN) receives the new setpoint and begins its fade to the new setpoint. Because the UDP is so fast, any latency is virtually undetectable. It works well so far.

Here is the current code:

master.ino file:

#include "Fade.h"
#include <application.h>

//#define DEBUG_ON  // Comment out to surpress serial output

#ifdef DEBUG_ON
#define DEBUG_PRINT(x) Serial.print(x)
#define DEBUG_PRINTLN(x) Serial.println(x)
#define SERIAL_START(x) Serial.begin(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINTLN(x)
#define SERIAL_START(x)
#endif

#define POWER_PIN D4     // Toggles LEDs on/off
#define FADE_UP_PIN D6   // increases LED fade
#define FADE_DOWN_PIN D2 // decreases LED fade
#define FADE_BUTTON_INCREMENT (255/10)  // modify denominator to step up/down in larger/smaller increments
#define PWM_PIN  A4      // output pin to your MOSFET

const int dawnDuskOffset = 45; // you can automatically have your device set norms for Day and Night states (minutes)
const int RESET_TIME = 4;
const int RECOVERY_BRIGHTNESS = 100; // when you power cycle it will recover to RECOVERY_BRIGHTNESS.
const int buttonPin[] = {POWER_PIN, FADE_UP_PIN, FADE_DOWN_PIN};  // array for the pushbuttons

struct deviceTime {
  int Hour;
  int Minute;
};

//default values for sunrise/sunset
deviceTime sunset = {
  18,0};
deviceTime sunrise = {
   6,0};
//
char message[40] = "No Time Values Received";
unsigned long webhookTimer;
int oldValue[3];
int currentLevel = 0;
int lastHour;
const int UDP_TX_PACKET_MAX_SIZE = 4;
const IPAddress slaveIP(10,0,1,255);
const unsigned int localPort = 2222;
UDP Udp;
unsigned long udpHeartbeatTimer = millis();

//extern "C" char* itoa(int a, char* buffer, unsigned char radix);

bool isDaytime;

unsigned long lastPress;

Fade ledStrip(PWM_PIN, 15); // Create the Fade object, set the PWM pin and set the time between increments in milliseconds

void setup()
{
  SERIAL_START(115200);
 
  pinMode(D7,OUTPUT);
  Spark.function("setDimLevel" , incomingDimCommand);
  Spark.variable("currentLevel", &currentLevel, INT);
  Spark.variable("SunriseTime", &message, STRING);
  Spark.variable("IsDaytime", &isDaytime, INT);
  Spark.subscribe("hook-response/fl_sun_time", sunTimeHandler, MY_DEVICES);
  Udp.begin(localPort);
  int startupLevel = 255;
  if (EEPROM.read(1))
  {
    startupLevel = 51;
  }
  sendFadeTargetAsUDP(startupLevel);
  EEPROM.write(1,0);
  for (int i = 0; i < 3; i++)
  {
    pinMode(buttonPin[i], INPUT_PULLUP);
  }
  DEBUG_PRINTLN("Publishing Sunrise/Sunset WebHook...");
  Spark.publish("fl_sun_time");
  char buffer[30] = "";
  sprintf(buffer, "Sunrise: %02d:%02d, Sunset: %02d:%02d", sunrise.Hour, sunrise.Minute, sunset.Hour, sunset.Minute);
  strcpy (message, buffer);
  pinMode(PWM_PIN, OUTPUT);
  ledStrip.write(startupLevel);
  char myMessage[60] = "";
  sprintf(myMessage, "Kitchen Dimmer Reset: now set to %d%%", map(ledStrip.getSetpoint(), 0, 255, 0, 100));
  Spark.publish("pushover", String(myMessage), 60, PRIVATE); // Spark WebHook!!!
}
//
void loop()
{
  ledStrip.update();
  bool daylightSavings = IsDST(Time.day(), Time.month(), Time.weekday());
  Time.zone(daylightSavings? -4 : -5);
  isDaytime = IsDAYTIME();
  currentLevel = ledStrip.read();
  int currentHour = Time.hour();
  if (lastHour == RESET_TIME && lastHour != currentHour)
  {
    EEPROM.write(1,1);
    System.reset();
  }
  lastHour = currentHour;
  if (millis() - webhookTimer > 60 * 60 * 1000UL)
  {
    DEBUG_PRINTLN("Publishing Sunrise/Sunset WebHook...");
    Spark.publish("fl_sun_time");
    DEBUG_PRINT("Requested new Sunrise/Sunset times...");
    webhookTimer = millis();
  }
  if (Udp.parsePacket())
  {
    char packetBuffer[UDP_TX_PACKET_MAX_SIZE] = "";
    Udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);
    DEBUG_PRINT("UDP Echo recieved:");
    DEBUG_PRINTLN(packetBuffer);
    if(strstr(packetBuffer, "getTarget"))
    {
      sendFadeTargetAsUDP(currentLevel);
    }
  }
  if (millis() - udpHeartbeatTimer >= 500UL)
  {
    sendHeartBeatAsUDP("HB");
    udpHeartbeatTimer = millis();
    digitalWrite(D7,!digitalRead(D7));
  }
  for (int i = 0; i < 3; i++)
  {
    int value = digitalRead(buttonPin[i]);
    if (value != oldValue[i] && value == LOW && millis() - lastPress > 50)
    {
      lastPress = millis();
      int fadeTarget = ledStrip.read();
      switch (i)
      {
        case 0:
          fadeTarget = fadeTarget? 0 : 255;
          DEBUG_PRINT("POWER Button pressed; new value:");
          DEBUG_PRINTLN(fadeTarget);
          break;
        case 1:
          fadeTarget = min(fadeTarget + FADE_BUTTON_INCREMENT, 255);
          /*if (fadeTarget > 255 - FADE_BUTTON_INCREMENT)
          {
            fadeTarget = 255;
          }*/
          DEBUG_PRINT("UP Button pressed; new value:");
          DEBUG_PRINTLN(fadeTarget);
          break;
        case 2:
          fadeTarget = max(fadeTarget - FADE_BUTTON_INCREMENT, 0);
          /*if (fadeTarget < FADE_BUTTON_INCREMENT)
          {
            fadeTarget = 0;
          }*/
          DEBUG_PRINT("DOWN Button pressed; new value:");
          DEBUG_PRINTLN(fadeTarget);
          break;
        default:
          // Nothing to do here...
          ;
      }
      sendFadeTargetAsUDP(fadeTarget);
      ledStrip.write(fadeTarget);
    }
    oldValue[i] = value;
  }
}
//
bool IsDAYTIME()
{
  int rise_time = tmConvert_t(Time.year(), Time.month(), Time.day(), sunrise.Hour, sunrise.Minute, 0);
  int set_time = tmConvert_t(Time.year(), Time.month(), Time.day(), sunset.Hour, sunset.Minute, 0);
  int now_time = tmConvert_t(Time.year(), Time.month(), Time.day(), Time.hour(), Time.minute(), Time.second());
  return (now_time > (rise_time + (dawnDuskOffset * 60)) && now_time < set_time - (dawnDuskOffset * 60));
}
//
inline time_t tmConvert_t(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss)
{
  struct tm t;
  t.tm_year = YYYY-1900;
  t.tm_mon = MM;
  t.tm_mday = DD;
  t.tm_hour = hh;
  t.tm_min = mm;
  t.tm_sec = ss;
  t.tm_isdst = 0;
  time_t t_of_day = mktime(&t);
  return t_of_day;
}
//
void sunTimeHandler(const char * event, const char * data)
{
  String sunriseReturn = String(data);
  char sunriseBuffer[40] = "";
  sunriseReturn.toCharArray(sunriseBuffer, 40); // example: \"5~37~20~30~\"
  sunrise.Hour = atoi(strtok(sunriseBuffer, "\"~"));
  sunrise.Minute = atoi(strtok(NULL, "~"));
  sunset.Hour = atoi(strtok(NULL, "~"));
  sunset.Minute = atoi(strtok(NULL, "~"));
  char buffer[40] = "";
  sprintf(buffer, "Sunrise: %02d:%02d, Sunset: %02d:%02d", sunrise.Hour, sunrise.Minute, sunset.Hour, sunset.Minute);
  strcpy (message, buffer);
}
//
bool IsDST(int dayOfMonth, int month, int dayOfWeek)
{
  if (month < 3 || month > 11)
  {
    return false;
  }
  if (month > 3 && month < 11)
  {
    return true;
  }
  int previousSunday = dayOfMonth - (dayOfWeek - 1);
  if (month == 3)
  {
    return previousSunday >= 8;
  }
  return previousSunday <= 0;
}
//
int incomingDimCommand(String params)
{
  int value = params.toInt();
  if (value == -1) // i.e. -1 to toggle
  {
    value = (ledStrip.read()? 0 : 100);
  }
  else
  {
    value = constrain(value, 0, 100);
  }
  sendFadeTargetAsUDP(map(value, 0, 100, 0, 255));
  ledStrip.write(map(value, 0, 100, 0, 255));
  DEBUG_PRINT("I just set incoming dim level to ");
  DEBUG_PRINT(value);
  DEBUG_PRINTLN("%");
  char myMessage[60] = "";
  sprintf(myMessage, "Kitchen Dimmer now set to %d%%", value);
  Spark.publish("pushover", String(myMessage), 60, PRIVATE); // Spark WebHook!!!
  return value;
}
void sendFadeTargetAsUDP(int slaveTarget)
{
  DEBUG_PRINT("sending message via UDP, message = ");
  char udpString[UDP_TX_PACKET_MAX_SIZE] = "";
  itoa(slaveTarget, udpString, 10); // base 10
  DEBUG_PRINTLN(udpString);
  Udp.beginPacket(slaveIP, 2222);
  Udp.write(udpString);
  Udp.endPacket();
}
void sendHeartBeatAsUDP(const char* slaveTarget)
{
  Udp.beginPacket(slaveIP, 2222);
  Udp.write(slaveTarget);
  Udp.endPacket();
}

slave.ino file:

#include "Fade.h"
#include <application.h>

#define COMPILED_FILENAME __FILE__

#define UDP_TX_PACKET_MAX_SIZE 250

#define DEBUG_ON   // Comment out to supress serial output

#define USING_UDP  // Comment out to switch to Spark.publish() & Spark.subscribe()

#ifdef DEBUG_ON
#define DEBUG_PRINT(x) Serial.print(x)
#define DEBUG_PRINT2(x,y) Serial.print(x,y)
#define DEBUG_PRINTLN(x) Serial.println(x)
#define SERIAL_START(x) Serial.begin(x)
#else
#define DEBUG_PRINT(x)
#define DEBUG_PRINT2(x,y)
#define DEBUG_PRINTLN(x)
#define SERIAL_START(x)
#endif

const char * programVersion = COMPILED_FILENAME;

const int LED_PIN = D1;
const int RECOVERY_BRITENESS = 255/2;

int currentLevel = 0;

#ifdef USING_UDP
  unsigned int localPort = 2222;
  UDP udp;
  unsigned long lastHeartbeatTime = millis();
  const IPAddress masterIP = {10,0,1,255};
#else
  #define PARTICLE_MASTER_ID_NO "55ff6e065075555336270287"
#endif

Fade ledStrip(LED_PIN, 15);
//
void setup()
{
  SERIAL_START(115200);

  pinMode(D7, OUTPUT);
  pinMode(D1, OUTPUT);
  digitalWrite(D7, HIGH);
  DEBUG_PRINTLN("Retreiving saved dim level...");
  Spark.function("setDimLevel" , incomingDimCommand);
  Spark.variable("currentLevel", &currentLevel, INT);
  Spark.variable("version", programVersion, STRING);  // for display of program version
  ledStrip.write(RECOVERY_BRITENESS);
#ifdef USING_UDP
  udp.begin(localPort);
#else
  Spark.function("setDimLevel" , incomingDimCommand);
  Spark.subscribe("newDimValue", masterRequest, PARTICLE_MASTER_ID_NO );
  Spark.publish("dim_slave", String(currentLevel));
#endif
  lastHeartbeatTime = millis();
  char myMessage[60] = "";
  sprintf(myMessage, "Dimmer Slave Reset: now set to %d%%", map(ledStrip.setPoint(), 0, 255, 0, 100));
  Spark.publish("pushover", String(myMessage), 60, PRIVATE); // Spark WebHook!!!
#ifdef USING_UDP
  udp.beginPacket(masterIP, 2222);
  udp.write("getTarget");
  udp.endPacket();
#endif
}
//
void loop()
{
  ledStrip.update();
  currentLevel = ledStrip.read();
#ifdef USING_UDP
  if (udp.parsePacket())
  {
    char packetBuffer[UDP_TX_PACKET_MAX_SIZE] = "";
    udp.read(packetBuffer, UDP_TX_PACKET_MAX_SIZE);
    if (strstr(packetBuffer, "HB"))  // I expect a heartbeat or a new fade level
    {
      lastHeartbeatTime = millis();
      DEBUG_PRINT("<3 ");
      digitalWrite(D7, !digitalRead(D7));
    }
    else
    {
      int udpValue = atoi(packetBuffer);
      ledStrip.write(udpValue);
      DEBUG_PRINT("New Dim Level Recieved... setting to: ");
      DEBUG_PRINTLN(udpValue);
    }
  }
  if ((millis() - lastHeartbeatTime > 45 * 1000UL) && (ledStrip.read()))
  {
    DEBUG_PRINTLN("Master is off....");
    //ledStrip.write(0);
    //digitalWrite(D7, LOW);
    char myMessage[80] = "";
    sprintf(myMessage, "No Signal from Master...");
    Spark.publish("pushover", String(myMessage), 60, PRIVATE); // Spark WebHook!!!
  }
#endif
}
//

#ifndef USING_UDP


void masterRequest(const char *event, const char *data)
{
  String params = String(data);
  int value = params.toInt();
  value = constrain(value, 0, 100);
  ledStrip.write(map(value, 0, 100, 0, 255));
  Spark.publish("dim_slave_Subscribe", String(currentLevel));// test
  DEBUG_PRINT("Set incoming dim level to");
  DEBUG_PRINTLN(value);
}

#endif

int incomingDimCommand(String params)
{
  int value = params.toInt();
  if (value == -1) // i.e. -1 to toggle
  {
    value = (ledStrip.read()? 0 : 100);
  }
  else
  {
    value = constrain(value, 0, 100);
  }
  ledStrip.write(map(value, 0, 100, 0, 255));
  DEBUG_PRINT("I just set incoming dim level to ");
  DEBUG_PRINT(value);
  DEBUG_PRINTLN("%");
  char myMessage[60] = "";
  sprintf(myMessage, "Kitchen Dimmer Slave now set to %d%%", value);
  Spark.publish("pushover", String(myMessage), 60, PRIVATE); // Spark WebHook!!!
  return value;
}

my non-blocking fade library header:

#ifndef Fade_h
#define Fade_h

#include "application.h"

class Fade
{
  public:
    Fade() {};
    Fade(int pin, uint32_t timeStep = 15, uint8_t min = 0, uint8_t max = 255);
    void write(int to);
    void update();
    void update(uint32_t time);
    uint8_t read();
    uint32_t readSpeed();
    uint32_t writeSpeed(uint32_t time);
    uint8_t setPoint();
  private:
    uint8_t _min;
    uint8_t _max;
    uint8_t _targetFade;
    uint8_t _pwmRate;
    uint32_t _time;
    uint32_t _last;
    uint8_t _pin;
};

#endif

and the implementation file:

#include <application.h>

#include "Fade.h"


Fade::Fade(int pin, uint32_t timeStep, uint8_t min, uint8_t max)
{
  _pin = pin;
  _time = timeStep;
  _min = min;
  _max = max;
  pinMode(_pin, OUTPUT);
  analogWrite(_pin, _min);
  _pwmRate = _min;
}

void Fade::write(int to)
{
  _targetFade = (uint8_t) constrain(to, _min, _max);

  this->update();
}

void Fade::update()
{
  this->update(millis());
}

void Fade::update(uint32_t time)
{
  if (time - _time > _last)
  {
    _last = time;
    if (_pwmRate > _targetFade) analogWrite(_pin, --_pwmRate);
    if (_pwmRate < _targetFade) analogWrite(_pin, ++_pwmRate);
  }
}

uint8_t Fade::setPoint()
{
  return _targetFade;
}

uint8_t Fade::read()
{
  return _pwmRate;
}

uint32_t Fade::readSpeed()
{
  return _time;
}

uint32_t Fade::writeSpeed(uint32_t time)
{
  _time = time;
}

Next I plan to add in a motion sensor that I can use to bring up/down the levels when people enter/exit the kitchen. I want it to respond differently based on time of day.

3 Likes

Nice work! Looks really good

1 Like