Lighting the stairs

 

Software

The basics

The basic goal of the software running on the Trinket M0 is to detect when one of the PIR sensors detects motion and then turn on the LED strip. Of course, just turning on the LEDs is a little boring, especially when we have individually addressable RGB pixels, a light level sensor, two PIRs, and a mode switch. So what might we do to make the lighting more interesting?

Since there is a PIR at the top and bottom of the stairs, we know which end of the stairs was entered. One simiple animation is to incrementally turn on the pixels from the entered end of the stairs to the exit end of the stairs. So if the top PIR trips, light the strip from the top descending to the bottom. Or vice versa. And, with a notion of the light level in the stairwell, we could choose an appropriate color and/or or brightness. For example, if it stairwell was dark, a medium brightness RED might be appropriate (better for night vision). White might be a better choice when the stairwell is moderately bright. And if the stairwell is fairly well lit, perhaps the LEDs are not required at all.

By using the mode switch, we could choose to have lighting patterns that are just kind of fun. A rainbow swirl, fade-in/fade-out of a random color, etc. Whatever we could imagine.

Arduino vs CircuitPython

At this point in project, I've written a few versions of the code. They are as follows:

  1. Proof of principle code that eventually evolved in the version originally posted on GitHub
  2. An attempt to translate that proof of principle code into CircuitPython. This failed miserably as I couldn't get the code to load (memory issues) on the Trinket M0.
  3. An Arduino based, object-oriented, re-write of the original code
  4. A CircuitPython translation of the Arduino o-o code. This required a Feather M4 express to run.
  5. A cut down version of the M4 code that did work on a Trinket M0. This code is limited to a single animation.

The following discussion refers to the Arduino object-oriented code base. The discussion is also relevant to the CircuitPython code since it was derived from the Arduino code base.

Writing the software

I like to have insight into what the code I write is actually doing (in constrast to what I thought I wrote). When the USB cable is attached, printing text to the console is very helpful. But eventually, that tether is gone and I might still want some insight. Fortunately, this project has lots of indicator LEDs available. There's the red LED (standard pin 13) and the single DotStar on the Trinket M0. There are also as many as 150 LEDs on the LED strip.

I appropriated the first and last LED of the strip to use as an indicator for when the top or bottom PIR is indicating motion. The DotStar is used to indicate what state the software is in, and the red LED is used to indicate that the code is operating (blinks on and off).

DotStar Color Description
Red Top PIR went high. Waiting for bottom PIR to trigger.
Green Bottom PIR went high. Waiting for top PIR to trigger.
Blue Top, then bottom PIRs have triggered. Waiting for PIRs low or timeout.
Pale blue Top PIR went HIGH. Timed out waiting for bottom PIR to go HIGH.
Magenta Bottom PIR went HIGH, then top PIR went HIGH. Waiting for PIRs low or timeout.
Yellow Bottom PIR went HIGH. Timed out waiting for top PIR to go HIGH.
Black Idle state (returns to this color after 30 seconds)

Trinket M0 DotStar indicator colors

PIR handling

The handling of the PIR has changed since the original code base. The original code took pains to make sure the PIR didn't appear to be in the HIGH state for too long. The new code base has code to make sure the PIR is HIGH for a minimum time period before reacting to it. This behavior is encapsulated within the PIR object's read() function.

In initial testing, it became clear that sometimes the PIR will trigger when someone walks by with no intention of using the stairs. So one PIR would trigger and the second one never did. Code was added that basically times out this situation. The timeout can be adjusted. The #define of timeBetweenPIRTriggers controls this timeout.

Early use of the installation infrequently saw the LEDs light up for no apparent reason. But, eventually there seemed to be some correlation with wireless events in the house (like HomeKit lights turning on). The suspicion was that interference was causing the long run to the PIR at the top of the stairs to act like bit of an antenna and causing a brief period of time when the PIR appeared to be in the HIGH state.

LED animations

Originally I used the standard technique of using delay() between steps in the LED strip animations. This works for simple animations, but makes it difficult to detect changes in the PIR output. At least detecting the changes in a main loop. And, as I wanted to have longer running animations, delay() was no longer an option.

In the object oriented versions of the code, the Animation object (and derivatives) have the following functions:

setup()

The setup() code waits a bit to let the Serial.begin() get set. The code then prints out some information about the configuration of the code. This was a useful reminder to me when debugging.

An animation is done by startupShowColors() followed by a loop that waits until both PIRs are not reporting activity. Early testing showed that the PIR would assert motion detection for a while after power on.

The initialization of the PIR objects and Animation objects is done in-line earlier in the code stairway.ino file.

loop()

The basic function of the main loop is to watch for changes in the PIRs and then, under the right conditions, run an animation on the LED strip. Here is a state diagram of the basic main loop.

Basic main loop state diagram

Of course there is a bit more going on when the code transitions from one state to another. For one thing, the DotStar is set to a specific color to indicate the current state as described above.

The two PIR objects are read() are read at the top of loop(). The executeStateMachine implements the diagram seen above. executeStateMachine has some helper functions (like firstSensorTripped) to simplify the implementation.

The last part of loop() simply calls Continue() on the currentAnimation, and calls indicatorContinue() and blinkActive() to keep the DotStar current and blink LED13 to show the main loop is running.

Miscellany

The function fadingMode() is a wrapper around just reading the pin for the mode switch. This was done to make it easier to have a debug mode when no switch was physically connected.

The function getLightLevel() is used to get the current reading from the photoresistor. However, if the LEDs are ON, the reading may not reflect the ambient light level. If the LEDs are ON (checks to see if currentAnimation.Active() is true, then getLightLevel() returns the last reading from when the LEDs were OFF. The function also tracks the minimum and maximum light level readings. Once a day, these readings are used to re-normalize light levels (processLightLevels()) and then are reset to start the process again. Not ideal, but better than nothing as the values from the photoresistor can vary quite a bit. The functions colorBasedOnConditions() and chooseAnimation() use the light level.

Animation

The Animation class is the base class for all animations. However, not all methods are virtual. The class is currently defined like this:

 class Animation {
  public:
    Animation(const char * animationName, float animationTime, int firstOffset=1, int lastOffset=-1);
    virtual void Start(bool topToBottom, uint32_t colorToUse);  // Initate animation
    virtual void Continue() = 0;          // keep animation going
    virtual void Finish(bool topToBottom);  // initiate completion of animation
    bool Active();                        // is the animation currently active?
    void printSelf();                     // print animation name and _animationStepIncrement

    uint32_t randomColor();               // returns a random color from colors[] table (.cpp file)

  protected:
    const char * _name = "Base class";    // name of animation
    int _firstLED;                        // address of 1st pixel in strip we can use
    int _lastLED;                         // address of last pixel in strip we can use
    bool _active;                         // true if animation is currently displaying something
    uint32_t _colorToUse;                 // suggested color to use
    bool _topToBottom;                    // animation clue, start animation from top
    unsigned long _lastUpdateTime;        // last time (millis) that animation was updated
    float _animationTime;                 // amount of time (float seconds) the animation should take
    int _animationStepIncrement;          // time (millis) between updates
    int _lastColorIndex;                  // used by randomColor()
 
    // setAllPixelsTo is like .fill() except limited (inclusively) from _firstLED to _lastLED
    void setAllPixelsTo(uint32_t aColor, bool doShow=true);
};
The current derived classes are as follows:

Function name Description
ColorWipe Fills the strip from one end to the other with a single color. Uses the topToBottom clue.
ColorSwirl Fills the strip with a rainbow of color. Animates (swirls) the colors until stopped.
FadeToColor Fills the strip with a single color and then fades in to full brightness. Calling Finish() initiates a fade out to black.
Marquee Does a marquee style animation for a specified color. Animates until stopped. Uses the topToBottom clue. The object variable marqueeQuanta controls the number of ON LEDs between OFF LEDs.
Startup Sequentially fills the strip with the next color found in colors. Uses the topToBottom clue.
Twinkle Fills the strip by randomly selecting a pixel and color until the entire strip is on. Once the strip is completely ON, then it randomly selects LEDs to turn off and back on to create a "twinkle" effect. Finish() initiates randomly turning off LEDs until all are off (black).
ZipLine Moves a single pixel of a specified color from top to bottom and back again until stopped. Uses the topToBottom clue.
ZipLineInverse Derived from ZipLine. Lights the entire strip in a specified color then moves a single black pixel from top to bottom and back again until stopped. Uses the topToBottom clue.
Zip2 Derived from ZipLine. Lights a single pixel at the top and bottom of the strip and rapidly moves them back and forth.
Zip2Inverse Derived from Zip2. Lights the entire strip in a specified color then moves a black pixel from both the top and bottom toward the other and back again.
ZipR Derived from ZipLine. Like ZipLine except that it changes the LED color every few steps.

Source code

The source code for the project is available on here on github. It is the first code I've written using C++ style object oriented code. So there are almost certainly things that should be implemented in a different way. The same is true for the object-oriented CircuitPython code.

One thing that caused problems for me was handling the neopixel object. Originally I thought I would pass it in to Animation constructors. But when I did this, it appeared that memory was getting corrupted when I attempted to use the neopixel object from my animation objects. I eventually gave up and resorted to declaring the pixels object as an external to the Animations. Seems a little clunky, but it works reliably.