Christmas Boot Light Display

In the neighborhood that I live in, we are issued a plywood boot on a metal stake and are requested to decorate it to our liking and display during the Christmas season between Thanksgiving week and January 1 of each year. I decided to make our boot a Raspberry Pi project, adding chase lights around the edge of the boot and “glue on” decorations in the central part of the boot. The boot was painted with a candy cane theme, and the lights rear mounted with wiring, connections and control devices on the back.

The electronics part of this project consisted of a Raspberry Pi Model B version 2, commercial 5VDC power supply, some cheap 3VDC LED lights (red, red, red and blue), various NPN transistors, resistors, capacitors for the LED drivers. The LED lights came from Dollar Tree ($1.00 each string), and the remaining electronics from standard retail electronics outlets.

The operating system used is Raspbian Jessie (current when I built the project in 2016). The optional java development and runtime environment was added as well as Pi4J (https://pi4j.com/).

The Raspberry Pi Model B is actually an old device, obtained in a crazy purchasing binge in 2012 when the device first came out. Alas, I have a few more.  This model of Raspberry Pi comes with thinwire ethernet (the “blue cable”) which I used for development and debugging but does not come with wifi, which would be used when the boot is on display outside. I added a wifi dongle for wifi access. The hostname my daughter assigned to the boot is “Brogan”.

The project schematic is shown in Figure 1.

Brogan Schematic

Figure 1. Schematic and block diagram of electronics portion of Christmas boot

There are 4 LED circuits on Brogan: 3 red circuits comprising chase lights and one blue circuit that runs along the sole of the boot. Each of the driver circuits (the hardware) is identical (see the schematic), and for development/debugging, I built each circuit with a fixed single LED on the main circuit board and switchable side circuit for the main display. The lights normally run from 17:00 to 22:00 each night, with a half second “heartbeat” otherwise on the blue circuit each minute the rest of the day. I normally plug in and turn on the boot once after Thanksgiving and leave it running continuously 24 X 7 until I shut it down in January. Any timing events are handled by the Raspberry Pi (as long as we have wifi connectivity). I don’t need to put the whole thing on a timer or manually start it or shut down on a daily basis. I think every year we’ve gone on a multi-day trip while it was running happily at home.

While the display is running (ie. each evening), circuit 4 is “on steady” for most of each minute, with a flash off and on again at the top of each 60 second cycle. For circuits 1 to 3, the chase lights are implemented according to Figure 2.

Red LED timing

Figure 2. Timing chart for “Red” LED circuits 1 to 3.

The lights pause each minute and flash, then the circuit manager logic syncs up for the next minute of operation. At the top of each minute, the circuit manager determines whether to continue with another minute of display (ie. between 17:00 and 22:00) or wait 58 seconds for the the next minute. The “Actor” parallel programming pattern is used to dispatch commands, resulting in a separate control thread for each LED circuit. [I am fond of this pattern and wrote an article about it here.]

Source Code


RunChristmasBoot.sh

#!/bin/bash
# Hopefully this will run the Christmas Boot driver at system boot time
# 2016/11/19 HAB
# Initial version

_now=$(date +"%Y_%m_%d")
_file="/opt/Christmasboot/log_$_now.log"
echo "Starting Christmas Boot Driver, logging to $_file..."

cd /opt/Christmasboot
sudo java -classpath .:classes:/opt/pi4j/lib/'*' ChristmasBootDriver &>> "$_file" &

exit 0

ChristmasBootDriver.java

//: $CVSHeader$
//: $Log$
//:
/**
 * @author Harvey
 *
 */

/*
 * ChristmasBootDriver.java
 *
 * Copyright 2016 Harvey Brydon <bitwise@brydon.net>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import com.pi4j.system.SystemInfo;

//import com.pi4j.platform.PlatformManager;  // Newer pi4j version has CPU temperature

//Compile:
// javac -classpath .:classes:/opt/pi4j/lib/'*' -d . ChristmasBootDriver.java
// Run:
// sudo java -classpath .:classes:/opt/pi4j/lib/'*' ChristmasBootDriver

/**
 * @author Harvey
 *
 */
class ChristmasBootDriver
{
    public ChristmasBootDriver()
    {
        cm = new CircuitManager();
        cm.Start();
    }

    public void LogToStdOut(String str)
    {
        DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss ");
        Date date = new Date();
        System.out.print(dateFormat.format(date));
        System.out.println(str);
    }

    private enum HourState_t {
        HS_UNKNOWN, HS_RUNNING, HS_PULSING
    }

    // This API never terminates, except in case of error
    protected void RunClockLoop()
    {
        HourState_t hs = HourState_t.HS_UNKNOWN;
        try
        {
            int previousSecond = 100;
            while(true)
            {
                Calendar c = Calendar.getInstance();
                int hour = c.get(Calendar.HOUR);
                int hourday = c.get(Calendar.HOUR_OF_DAY);
                 if(previousSecond > 60)
                 {
                     System.out.format("Initial Hour is %d, Hour_of_day is %d %n", hour, hourday);
                 }
                int minute = c.get(Calendar.MINUTE);
                int second = c.get(Calendar.SECOND);
                if(second < previousSecond) { //if(hour > 16 && hour < 22) if(hourday > 16 && hourday < 22)
                    {
                        cm.Send(new CctMgrMsg(CctMgrMsg.MgrRequest_t.CM_RUN));
                        if(HourState_t.HS_RUNNING != hs)
                        {
                            System.out.format("[R] Hour is %d, hourday is %d %n", hour, hourday);
                            System.out.format("%02d:%02d:%02d Switching to RUNNING state%n", hour, minute, second);
                            hs = HourState_t.HS_RUNNING;
                        }
                    }
                    else
                    {
                        cm.Send(new CctMgrMsg(
                                CctMgrMsg.MgrRequest_t.CM_HEARTBEAT));
                        if(HourState_t.HS_PULSING != hs)
                        {
                            System.out.format("[P] Hour is %d, hourday is %d %n", hour, hourday);
                            System.out.format("%02d:%02d:%02d Switching to PULSING state%n", hour, minute, second);
                            hs = HourState_t.HS_PULSING;
                        }
                    }
                    Thread.sleep(59000); // 59 seconds
                }
                else
                {
                    Thread.sleep(1005); // smidgen over 1 second
                }
                previousSecond = second;
            }
        }
        catch(InterruptedException e)
        {
            System.err.println(e.getMessage());
            cm.Send(new CctMgrMsg(CctMgrMsg.MgrRequest_t.CM_SHUTDOWN));
            cm.Join();
            System.exit(0);
        }
    }

    public static void main(String[] args)
    {
        ChristmasBootDriver cbd = new ChristmasBootDriver();
        cbd.LogToStdOut("ChristmasBootDriver Starting");

        // We need a pi4j update to get support for CPU temperature...
        // try{System.out.println("CPU Temperature : " +
        // SystemInfo.getCpuTemperature());}
        // catch(UnsupportedOperationException ex){}

        cbd.RunClockLoop();
        cbd.LogToStdOut("ChristmasBootDriver Terminating"); // Ummm ... We
                                                            // should never get
                                                            // here...
    }

    private CircuitManager cm;
}

CircuitManager.java

//: $CVSHeader$
//: $Log$
//:

/*
 * CircuitManager.java
 *
 * Copyright 2016 Harvey Brydon
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

/**
 * @author Harvey Brydon <bitwise@brydon.net>
 *
 */

import com.pi4j.io.gpio.RaspiPin;
import net.brydon.HBJParLib.AActor;
import net.brydon.HBJParLib.BaseMessage;

// Message class which contains the Circuit Manager requests.
class CctMgrMsg extends BaseMessage
{
    /// Definition of Circuit Manager requests
    public enum MgrRequest_t {
        CM_RUN, CM_HEARTBEAT, CM_ON, CM_OFF, CM_SHUTDOWN
    }

    public CctMgrMsg(MgrRequest_t req)
    {
        m_request = req;
    }

    /// Get Request code
    public MgrRequest_t GetRequest()
    {
        return m_request;
    }

    private final MgrRequest_t m_request;
};


public class CircuitManager extends AActor
{
    public CircuitManager()
    {
        circuit1 = new LEDCircuitDriver(RaspiPin.GPIO_03);
        circuit2 = new LEDCircuitDriver(RaspiPin.GPIO_04);
        circuit3 = new LEDCircuitDriver(RaspiPin.GPIO_05);
        circuit4 = new LEDCircuitDriver(RaspiPin.GPIO_06);
        // [Note: make sure that caller calls Start() before using]
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.brydon.HBJParLib.AActor#Start()
     */
    @Override
    public void Start()
    {
        // [common problem; don't forget to start the actors]
        circuit1.Start();
        circuit2.Start();
        circuit3.Start();
        circuit4.Start();

        super.Start();
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.brydon.HBJParLib.AActor#Exit()
     */
    @Override
    protected void Exit()
    {
        circuit1.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_SHUTDOWN));
        circuit2.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_SHUTDOWN));
        circuit3.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_SHUTDOWN));
        circuit4.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_SHUTDOWN));
        circuit1.Join();
        circuit2.Join();
        circuit3.Join();
        circuit4.Join();

        super.Exit();
    }

    @Override
    protected void Process(BaseMessage msg)
    {
        CctMgrMsg pMsg1 = (CctMgrMsg) msg;
        switch(pMsg1.GetRequest())
        {
        case CM_RUN: // Run the lights once (one minute)
            RunMinuteLoop();
            break;
        case CM_HEARTBEAT: // Flash circuit #4 for half a second
            RunHeartBeat();
            break;
        case CM_ON: // Turn all circuits on
            TurnAllOn();
            break;
        case CM_OFF:  // Turn all circuits off
            TurnAllOff();
            break;
        case CM_SHUTDOWN: // Shutdown the actors; prepare for app exit
        default:
            Exit();
            break;
        }
    }

    /// Flash circuit #4 for half a second, turn all circuits off
    private void RunHeartBeat()
    {
        circuit4.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_PULSE, 500));
        TurnAllOff();
    }

    /// Run the light sequence for one minute
    private void RunMinuteLoop()
    {
        try
        {
            TurnAllOff();
            Thread.sleep(1000);
            TurnAllOn();
            Thread.sleep(1000);

            TurnAllOff();
            Thread.sleep(1000);
            TurnAllOn();
            Thread.sleep(1000);

            TurnAllOff();
            Thread.sleep(1000);
            TurnAllOn();
            Thread.sleep(1000);

            for(int i = 0; i < 17; ++i)
            {
                // Three second sequence for each circuit
                circuit1.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 1000));
                circuit1.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));
                circuit1.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 1000));
                circuit1.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));

                circuit2.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));
                circuit2.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 1000));
                circuit2.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));
                circuit2.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 1000));

                circuit3.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 500));
                circuit3.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));
                circuit3.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 1000));
                circuit3.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_WAIT, 500));
                circuit3.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 500));

                circuit4.Send(new LEDDriverMessage(
                        LEDDriverMessage.DriverRequests_t.LED_PULSE, 3000));
            }
            TurnAllOn();
            Thread.sleep(2000);
            TurnAllOff();
            // For a 60 second interval, we have provided 59 seconds of entertainment; let
            // everything catch up with the lights off for the last second...
        }
        catch(InterruptedException e)
        { // ignore
        }
    }

    /// Turn all circuits on
    private void TurnAllOn()
    {
        circuit1.Send(
                new LEDDriverMessage(LEDDriverMessage.DriverRequests_t.LED_ON));
        circuit2.Send(
                new LEDDriverMessage(LEDDriverMessage.DriverRequests_t.LED_ON));
        circuit3.Send(
                new LEDDriverMessage(LEDDriverMessage.DriverRequests_t.LED_ON));
        circuit4.Send(
                new LEDDriverMessage(LEDDriverMessage.DriverRequests_t.LED_ON));
    }

    /// Turn all circuits off
    private void TurnAllOff()
    {
        circuit1.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_OFF));
        circuit2.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_OFF));
        circuit3.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_OFF));
        circuit4.Send(new LEDDriverMessage(
                LEDDriverMessage.DriverRequests_t.LED_OFF));
    }

    private final LEDCircuitDriver circuit1;
    private final LEDCircuitDriver circuit2;
    private final LEDCircuitDriver circuit3;
    private final LEDCircuitDriver circuit4;
}

LEDCircuitDriver.java

//: $CVSHeader$
//: $Log$
//:

/*
 * LEDCircuitDriver.java
 *
 * Copyright 2016 Harvey Brydon
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */

/**
 * @author Harvey Brydon <bitwise@brydon.net>
 *
 */

import com.pi4j.io.gpio.GpioController;
import com.pi4j.io.gpio.GpioFactory;
import com.pi4j.io.gpio.GpioPinDigitalOutput;
import com.pi4j.io.gpio.Pin;
import com.pi4j.io.gpio.PinState;
import net.brydon.HBJParLib.AActor;
import net.brydon.HBJParLib.BaseMessage;


// Message class which contains the driver requests.
class LEDDriverMessage extends BaseMessage
{
    /// Definition of LED Driver Circuit requests
    public enum DriverRequests_t {
        LED_PULSE, LED_WAIT, LED_ON, LED_OFF, LED_SHUTDOWN
    }

    public LEDDriverMessage(DriverRequests_t req, int iTime)
    {
        m_request = req;
        m_time = iTime;
    }

    public LEDDriverMessage(DriverRequests_t req)
    {
        m_request = req;
        m_time = 0;
    }

    /// Get Request code
    DriverRequests_t GetRequest()
    {
        return m_request;
    }

    /// Get time value
    int GetTimeInterval()
    {
        return m_time;
    }

    private final DriverRequests_t m_request;
    private final int              m_time;
}
 
 
/// Actor class implementing an LED circuit driver.
/// Each instance of the class drives one circuit.
class LEDCircuitDriver extends AActor
{

    /// Constructor for the class. Input parameter is P1 header pin number (Pi4J
    /// numbering)
    public LEDCircuitDriver(Pin iPinNumber)
    {
        // Provision circuit pin, turn it off by default and at shutdown state
        m_circuit = gpio.provisionDigitalOutputPin(iPinNumber, "MyLED",
                PinState.LOW);
        m_circuit.setShutdownOptions(true, PinState.LOW);
        // [Note: make sure that caller calls Start() before using]
    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * net.brydon.HBJParLib.Actor.AActor#Process(net.brydon.HBJParLib.Actor.
     * BaseMessage)
     */
    /// The main message processing method for this class. We have to handle two
    /// things here: the main payload activity for the class and termination
    /// processing. This method is called from the base class whenever a message
    /// is pulled from the queue.
    @Override
    protected void Process(BaseMessage msg)
    {
        LEDDriverMessage pMsg = (LEDDriverMessage) msg;

        switch(pMsg.GetRequest())
        {
        case LED_PULSE:
            m_circuit.pulse(pMsg.GetTimeInterval(), true);
            break;
        case LED_WAIT:
            try
            {
                Thread.sleep(pMsg.GetTimeInterval());
            }
            catch(InterruptedException ex)
            {
                // do nothing
            }
            break;
        case LED_ON:
            m_circuit.high();
            break;
        case LED_OFF:
            m_circuit.low();
            break;
        case LED_SHUTDOWN:
            m_circuit.low();
            Exit();
        default:
        }
    }

    /// global context for Pi4j's gpio instance
    private static final GpioController gpio = GpioFactory.getInstance();

    /// Connection to the pin/circuit that we manage in this actor object
    private final GpioPinDigitalOutput m_circuit;
}


AActor.java

//: $CVSHeader: HBCode/HBJParLib/src/main/java/net/brydon/HBJParLib/AActor.java,v 1.1 2013/03/15 00:57:24 harvey Exp $
//: $Log: AActor.java,v $
//: Revision 1.1  2013/03/15 00:57:24  harvey
//: Initial Actor code
//:
//

package net.brydon.HBJParLib;

import java.util.concurrent.LinkedBlockingQueue;

// Class AActor comprises an abstract base class for an Actor Programming Model object.
// This class contains support code to manage a thread for this object and perform
// message processing, termination and other related 'Actor' tasks.
//
// To be used, a derived class must be defined which implements the Process() method.
// This method must detect a termination message and shutdown the actor; otherwise
// act on the messages sent to the object through the message queue.
//
// Messages are defined in class BaseMessage or a class derived from it.

/// Abstract base class for Actor objects.  Derive your actor object from this class.
public abstract class AActor extends Thread
{
    /// Definition of actor states
    public enum ActorState_t { AS_CREATED, AS_RUNNING, AS_STOPPED }

    /// Constructor for the class
    public AActor()
    {
        m_state = ActorState_t.AS_CREATED;
        m_messages = new LinkedBlockingQueue();
    }

    
    /// Start actor execution
    public void Start()
    {
        // The Actor should only be started once (and
        // also not restarted after termination)
        if(ActorState_t.AS_CREATED == m_state)
        {
            m_state = ActorState_t.AS_RUNNING;
            start();
        }
    }

    
    /// Send a message to the actor
    public void Send(BaseMessage message)  // TODO: This needs to be synchronized...
    {
        try
        {
            m_messages.put(message);
        }
        catch(final InterruptedException ex)
        {
            Thread.currentThread().interrupt();
            throw new RuntimeException("Unexpected interruption");
        }
    }

    
    /// Wait for the actor to complete execution.  This method is
    /// normally called by an external thread to wait for the thread
    /// owned by this actor to complete.
    public void Join()
    {
        try
        {
            super.join();
        }
        catch(final InterruptedException ex)
        {
            // Do nothing...
        }
    }

    
    /// Get current actor state
    /// This method can be called by the actor or an external thread
    public ActorState_t GetState()
    {
        return m_state;
    }

    
    ///// Get message count indicator
    //protected unsigned int GetMessageCount()
    //{ return m_messages.unsafe_size(); }

    
    /// Method implemented in derived class to perform message processing.
    /// This method is normally called by the actor processing logic.
    protected abstract void Process(BaseMessage msg);

    
    /// Terminate processing on the Actor.
    /// This method is normally called by the derived actor class to
    /// signal execution termination.
    protected void Exit()
    {
        m_state = ActorState_t.AS_STOPPED;
    }

    
    /// Actor message processing loop.
    /// This method is normally called only by the thread message processing.
    /// It is public only because of questionable Java inheritance rules,
    /// and should really be private.
    @Override
    public void run() // [should really be private]
    {
        while(ActorState_t.AS_RUNNING == m_state)
        {
            try
            {
                final BaseMessage psOMsg = m_messages.take();
                Process(psOMsg);
            }
            catch(final InterruptedException ex)
            {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Unexpected interruption");
            }
        }
    }


    private volatile ActorState_t    m_state;    // current object state
    private LinkedBlockingQueue m_messages; // message queue
}

BaseMessage.java

//: $CVSHeader: HBCode/HBJParLib/src/main/java/net/brydon/HBJParLib/BaseMessage.java,v 1.1 2013/03/15 00:57:25 harvey Exp $
//: $Log: BaseMessage.java,v $
//: Revision 1.1  2013/03/15 00:57:25  harvey
//: Initial Actor code
//:
//

package net.brydon.HBJParLib;

/// Class BaseMessage is used by the Actor class.  See class AActor for usage.

/// Base class for message processing.  Derive your message object from this class.
public abstract class BaseMessage
{
    /// Constructor for the class
    public BaseMessage()
    {
    }
}