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.
At this point in project, I've written a few versions of the code. They are as follows:
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.
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
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.
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:
Animation()
- Constructor. Parameters include name (mostly used for debugging) and time period for the animation.Start()
- Start the animation. Parameters are topToBottom
which the animation can use as a clue, and colorToUse
, again a suggestion for the animation.Continue()
- The bulk of the animation is typically implemented in this function. It should be called periodically, usually often from the main loop.Finish()
- Starts the completion of the animation. The animation may complete here, or it may rely on calls to Continue()
to step through the process of finishing the animation. The topToBottom
parameter again can be used as a clue for the animation.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.
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. |
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.