LED Flash Patterns...for the last time - Part 2
So where to next? The code from the end of the last post is a long way from anything useful, but the point is to illustrate an idea more than deliver a library.
We should, however, have the goal of some sort of “Led Manager” in mind, that can encapsulate some of this behaviour around managing LEDs and the patterns we want them to show.
A LED Manager, how it might look
One of my favourite Eric Evans quotes is (paraphased) “client code ignores the implementation, developers do not”. In light of this, here is a high level overview of what the client code that uses the “Led Manager” might look like…glossing over some of the details/implementation.
We would have a few “states” or “modes”, such as:
enum {
Ok = 0,
Warning = 1,
Error = 2,
};
And we would register the patterns we want to show against each state:
void initialise() {
...other initialisation...
// Ok -> A short green flash every 1 sec
LedMgr_addPattern(Ok, "G ", ...some other details);
// Warning -> A double yellow flash every 1 sec
LedMgr_addPattern(Warning, "YY YY ", ...some other details);
// Error -> A short red double flash, twice
LedMgr_addPattern(Error, "R R R R ", ...some other details);
...other initialisation...
}
And then the runtime would trigger these various states, based on what “state” or “mode” is appropriate:
void checkStatus() {
int errorCount = getErrorCount();
if (errorCount < 3) {
LedMgr_activate(Ok); // We good, one for each hand
} else if (errorCount < 14) {
LedMgr_activate(Warning); // Getting hard to juggle
} else {
LedMgr_activate(Error); // ZOMG!!!
}
}
So at a high level, the details of “what” the flash patterns will be (the strings); and the details about “when” they will be active (the LedMgr_activate()
calls); and the mechanism for co-ordinating them (the use of a shared enum) are all supplied into the Led Manager. These items are all “application” level code, that would change in line the functionality of a product. While the Led Manager interface is mostly about “system” capability.
An implementation
In keeping with the last post, I will provide an implementation for an Arduino as a single sketch. This is getting a little long for one sketch, but hopefully will be manageable.
The general design is:
- Use the TIMER1 to create a 125ms ISR trigger
- Use the ISR to update the LED based on the active state
- Register the state to pattern mapping in the
setup()
call - Change the active state in the main
loop()
call
The limitations of the design are:
- It only manages the patterns for one LED
- It doesn’t really do colours yet (we need more than one LED for colour!)
- It doesn’t enforce the isolation of the module boundaries
The sketch
Hopefully this sketch is somewhat self explanatory. The first half is the “Led Manager”, and the second half is an application that cycles through the “beats” of the previous post. I think that even despite it being three times longer, it is still very approachable, espectially the application setup()
and loop()
functions.
I really advise either loading this up and watching the LED flash…or starting to read at the setup()
and loop()
functions.
Also available as an INO file here.
// LED Pattern Demo - Part 2
// Building an isolated module "concept"
/**********************************************
* START of LED Management Module
**********************************************/
enum {
// The maximum number of patterns that can be registered (to avoid memory
// management)
MAX_PATTERNS = 5,
// Set prescale to 64
PRESCALE_VALUE = (1 << CS10) | (1 << CS11),
// Preload timer value to give 8Hz rollover:
// - 64 prescaler -> 16000000 Hz / 64 / 8Hz = 31250
TIMER_PRELOAD_VALUE = 31250,
};
/**
* The function signature called by the Led Manager to drive the "led" to a
* state.
*
* @param color The color that the LED should be set to
* @param arg The user provided value when the pattern was registered
*/
typedef int (*LedManager_UpdateFunc)(char color, void *arg);
// An internal type to store the pattern information
typedef struct {
// The external "value" that correspondes to this pattern. Storing this allows
// the "outside" caller to determine what the values provided to
// LedMgr_activate() are
long value;
// A pointer to a string (hopefully immutable / in flash) that represents this
// pattern
char *pattern;
// The driver function to call to update the LED module
LedManager_UpdateFunc handler;
// The argument to be handed back to the driver function
void *arg;
} pattern_bucket;
// Our internal store of the available patterns
static pattern_bucket patterns[MAX_PATTERNS] = {0};
// The active pattern (NB: Not necessarily set)
static pattern_bucket *current_pattern = NULL;
/*
* This ISR is responsible for driving the LED change. Possibly a little risky
* in an ISR, but should be ok for this sketch.
*/
ISR(TIMER1_OVF_vect) // interrupt service routine for overflow
{
// Reset value to make sure this happens again in 125ms
TCNT1 = TIMER_PRELOAD_VALUE;
static int counter = 0;
if (current_pattern != NULL) {
int len = strlen(current_pattern->pattern);
char val = current_pattern->pattern[counter % len];
current_pattern->handler(val, current_pattern->arg);
}
counter++;
}
/*
* Activate the pattern registered with the provided value.
*
* If the value does not match any registered pattern, disable the active
* pattern
*/
void LedMgr_activate(long value) {
for (int i = 0; i < MAX_PATTERNS; i++) {
if (patterns[i].value == value) {
current_pattern = &patterns[i];
return;
}
}
current_pattern = NULL; // no patterns match, disable current pattern
}
/*
* Adds a new pattern
*
* @param value An external value to use as the "key" for activating this
* pattern
* @param pattern The pattern, as a string, in an encoding understood by the
* provided UpdateFunc
* @param handler A function that can respond to each character in the pattern
* and drive the "LED" correctly
* @param arg An external parameter to pass through to the handler
*
* @retval 0 if successful
* @retval -1 if unable to add the pattern
*/
int LedMgr_addPattern(long value, char const *const pattern,
LedManager_UpdateFunc handler, void *arg) {
for (int i = 0; i < MAX_PATTERNS; i++) {
pattern_bucket *p = &patterns[i];
// If the current bucket is empty, use it
if (p->pattern == NULL) {
p->value = value;
p->pattern = pattern;
p->handler = handler;
p->arg = arg;
return 0;
}
// If this bucket is already used AND the value is the same, overwrite it
if (p->value == value) {
p->pattern = pattern;
p->handler = handler;
p->arg = arg;
return 0;
}
}
// Not enough buckets left
return -1;
}
/**********************************************
* END of LED Management Module
**********************************************/
/**********************************************
* START of Application
**********************************************/
enum State {
FOUR_TO_THE_FLOOR,
SWUNG_BEAT,
THREE_FOUR,
};
/**
* Drive the provided pin to the provided pattern element
*
* @param color the pattern element
* @param arg an int representing the pin
*/
int onOffDrive(char color, void *arg) {
int pin = (int)arg;
// Notice that we support many chars for "OFF"
if (color == ' ' || color == '-' || color == '_') {
digitalWrite(pin, LOW);
} else {
digitalWrite(pin, HIGH);
}
}
void setup() {
Serial.begin(9600);
pinMode(LED_BUILTIN, OUTPUT);
// Register the patterns against the states..........along with some details
// --> see here how the LED_BUILTIN
// is supplied so that the LedMgr
// can pass it back to the
// onOffDrive function
LedMgr_addPattern(FOUR_TO_THE_FLOOR, "X__", onOffDrive, LED_BUILTIN);
LedMgr_addPattern(SWUNG_BEAT, "XXX X X ", onOffDrive, LED_BUILTIN);
LedMgr_addPattern(THREE_FOUR, "X---", onOffDrive, LED_BUILTIN);
// Setup Timer1 to create an 8Hz ISR trigger (if CPU Freq is 16 MHz)
noInterrupts(); // disable all interrupts
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = TIMER_PRELOAD_VALUE; // preload timer
TCCR1B |= PRESCALE_VALUE;
TIMSK1 |= (1 << TOIE1); // enable timer overflow interrupt ISR
interrupts(); // enable all interrupts
}
void loop() {
delay(9000); // change every 9 secs
static long nextBeat = 0;
switch (nextBeat) {
case 0:
LedMgr_activate(FOUR_TO_THE_FLOOR);
Serial.println("four to the floor");
nextBeat = 1;
break;
case 1:
LedMgr_activate(SWUNG_BEAT);
Serial.println("feel the swing");
nextBeat = 2;
break;
default:
LedMgr_activate(THREE_FOUR);
Serial.println("3/4 beat");
nextBeat = 0;
break;
}
}
But what about colours?
The “LED Manager” is starting to take shape, but the original goal was to simplify the description of LED flash patterns with colours…so where are they? Perhaps at this point you can see that adding colour support should actually be quite straight forward with the existing LedMgr
implementation. However, I will save that for the next post.