Andrew Dodd

 Home

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.