Coding a sketch for easy reconfiguration by a newbie

I am building a USB footswitch macro keyboard for a friend similar to my own and am trying to make it possible for him to change or add macros with minimum learning curve. So far I’ve moved all of the macro specific stuff to a separate tab so he can modify that file without being overly concerned about breaking things. He might become interested in delving deeper into Arduino fiddling so I’ve tried to keep all the code as transparent as possible.

I’m posting a single switch version for brevity but the final unit will have 2 - 4 switches.

The main .ino file:

#define SKETCH_VERSION "OneKey 007"
#include "onekey.h"
/* OneKey is a customizable single button USB macro keyboard.
   STATUS: Added macro sequence capability/example, tested OK

   Creating a macro and assigning it to a switch can be done
   on the macros tab.

   Hardware config:
     Any Arduino supported by the HID library
     One SPDT momentary switch:
                  o—— NC → pin 2
      ↓ GND ——o /
                  o—— NO → pin 3
*/
#define ACTIVE LOW
const byte SWnc = 2;
const byte SWno = 3;
extern macroPointer SWmacro;

void setup() {
  pinMode(SWnc, INPUT_PULLUP);
  pinMode(SWno, INPUT_PULLUP);
  Keyboard.begin();
  Mouse.begin();
  Serial.begin(9600);
}

void loop() {
  debugVersion();
  static bool SWon;
  bool SWch = false;
  if (SWon && (digitalRead(SWnc) == ACTIVE) or
      !SWon && (digitalRead(SWno) == ACTIVE)) {
    SWch = true;
    SWon = !SWon;
    debugSwitch(SWon); //delete after wiring test
  }
  //SWmacro(SWon, SWch); //uncomment after wiring test
}

void debugSwitch(bool on) {
  if (on) {
    Serial.println("switch on");
  }
  else {
    Serial.println("switch off");
  }
}

The .h file:

/* onekey.h - supporty/ugly bits for OneKey */
#include <Keyboard.h>
#include <Mouse.h>
#define KEY_SCROLL_LOCK 0xCF
#define REPEAT true

typedef void (*macroPointer)(bool, bool);

class millisTimer {
    bool repeat;
    unsigned long interval;
    unsigned long start;
    bool running;
  public:
    millisTimer(unsigned long i = 0, bool r = false):
      repeat(r),
      interval(i),
      start(r ? millis() - i : millis()),
      running(true)
    {}
    operator bool () {
      if (running && millis() - start >= interval) {
        if (repeat) start = millis();
        else running = false;
        return true;
      }
      return false;
    }
    bool restart(unsigned long i = 0) {
      bool finished = running && millis() - start >= interval;
      if (i) interval = i;
      start = repeat ? millis() - i : millis();
      running = true;
      return finished;
    }
};
/* millisTimer is a trivial non-blocking timer.
   It returns false until the specified interval has expired.
   In REPEAT mode it will return true on the first test and
   will automatically restart the timer whenever it returns true.

   Example 1:
   {
     static millisTimer myTimer {200};
     if (myTimer) {
       // do a thing once if 200ms has passed
     }
     if (some_trigger_value) {
       myTimer.restart(150);
     }
   }
   Example 2:
   {
     static millisTimer myTimer {500, REPEAT};
     if (myTimer) {
       // do things repeatedly, not more often than
       // twice per second
     }
   }

   Caution example: if (myTimer && buttonPressed)
   This will probably result in undesired behaviour.
   Generally it's best to only test the timer after all
   related conditions have been tested.
*/

void debugVersion() {
  // for when I forget which sketch I uploaded :-)
  static millisTimer tmr {5000, REPEAT};
  if (tmr) Serial.println(SKETCH_VERSION);
}

The macros.ino file: In the second post (it’s pretty comment heavy)

Can anyone spot any obvious blunders or a better way to ease the experience?

I’m wondering if the ComboSequence macro can be simplified with lambdas but I haven’t put any work into trying that yet.

The macros.ino file:

/* Keyboard/mouse macros for use with OneKey.

   Assign a macro to a switch at the end of this file.
   Macros Should be of this basic format:

void tkUniqueMacroName(bool on, bool ch) {
  //macro content examples:
  if (ch) {
    //Do a thing when the switch has just changed state.
    if (on) {
      //Note that this condition is nested inside the if (ch).
      //Do a thing once when the button is pressed.
    }
    else {
      // We're still nested so...
      // Do a thing once when the button is released.
    }
  }
  if (on) {
    // Do a thing repeatedly while the switch is on.
    // This could happen many thousand times per second.
    // Be careful.
  }
}

*/

// The following examples are ready to use macros

/* Targeting reticle:
   On button press the ability is activated. Then the
   targeting cursor is positioned by mouse. On button
   release ability is fired at targeted location.
   Fun Fact: Doing two completely different things at
   keypress and keyrelease is something most macro
   keyboards can't do.
*/
void tkTargetReticle(bool on, bool ch) {
  if (ch) {
    if (on) {
      Keyboard.write('[');
    }
    else {
      Mouse.click();
    }
  }
}

/* Toggle modifier key:
   Allows secondary in-game macro behaviours for a time
   without requiring the key to be held down.
*/
void tkToggleAlt(bool on, bool ch) {
  if (on && ch) {
    static bool toggle;
    toggle = !toggle;
    if (toggle) {
      Keyboard.press(KEY_LEFT_ALT);
    }
    else {
      Keyboard.release(KEY_LEFT_ALT);
    }
  }
}

/* Push-to-talk:
   I use scroll-lock as my PTT keybind in voice comms
   because this macro makes the scroll-lock indicator
   on my keyboard indicate when I'm transmitting :-D
*/
void tkPushToTalk(bool on, bool ch) {
  if (ch) {
    if (on) {
      Keyboard.press(KEY_SCROLL_LOCK);
    }
    else {
      Keyboard.release(KEY_SCROLL_LOCK);
      Keyboard.write(KEY_SCROLL_LOCK);
    }
  }
}

/* Spam the spacebar:
   because sometimes you just gotta
   This example demonstrates using a timer to
   do something repeatedly, in this case tapping
   the space bar every 50 milliseconds
*/
void tkSpamSpaceBar(bool on, bool ch) {
  if (on) {
    static millisTimer tmr {50, REPEAT};
    if (tmr) {
      Keyboard.write(' ');
    }
  }
}
/* Combo Sequence:
   Cases requiring a sequence of timed events can be
   handled as shown here. In this example the sequence
   is started by a button press event but if it's
   still actively progressing a sequence the event
   will be ignored. The sequence will proceed to 
   completion regardless of the state of the button
   that started it.
   The use of a non-blocking timer allows for the
   continued operation of other buttons and macros
   while this sequence is running. In corollary,
   any other operation that delays the main loop
   for significant time may cause this sequence
   to be unacceptably delayed.
*/
void tkComboSequence(bool on, bool ch) {
  static int progress;
  static millisTimer tmr;
  if (ch && on && !progress) {
    progress++;
  }
  switch (progress) {
    case 0 :
      break;
    case 1 :
      Keyboard.write('T');
      tmr.restart(300);
      progress++;
      break;
    case 2 :
      if (tmr) {
        Keyboard.write('e');
        tmr.restart();
        progress++;
      }
      break;
    case 3 :
      if (tmr) {
        Keyboard.write('x');
        tmr.restart();
        progress++;
      }
      break;
    case 4 :
      if (tmr) {
        Keyboard.write('t');
        tmr.restart(800);
        progress++;
      }
      break;
    case 5 :
      if (tmr) {
        Keyboard.write(KEY_BACKSPACE);
        tmr.restart(300);
        progress++;
      }
      break;
    case 6 :
      if (tmr) {
        Keyboard.write(KEY_BACKSPACE);
        tmr.restart(600);
        progress++;
      }
      break;
    case 7 :
      if (tmr) {
        Keyboard.write('s');
        tmr.restart();
        progress++;
      }
      break;
    case 8 :
      if (tmr) {
        Keyboard.write('t');
        tmr.restart(1200);
        progress++;
      }
      break;
    case 9 :
      if (tmr) {
        Keyboard.print(" lol");
        tmr.restart(800);
        progress++;
      }
      break;
    case 10 :
      if (tmr) {
        Keyboard.press(KEY_LEFT_SHIFT);
        Keyboard.write('1');
        Keyboard.write('1');
        Keyboard.release(KEY_LEFT_SHIFT);
        Keyboard.write('1');
        Keyboard.write('1');
        Keyboard.write(KEY_RETURN);
        progress = 0;
      }
      break;
  }
}

/* Do nothing:
   Use this to disable a switch if neccessary.
*/
void tkDoNothing(bool on, bool ch) {
  //nothing
}

// ^^ This is a good spot to insert additional macros.
// All macro definitions need to be declared prior to this section
// Assign a macro to a switch here:

macroPointer SWmacro = tkPushToTalk;

I'd use descriptive variable names. ch means nothing to me.

The system of commenting/uncommenting debugSwitch and SWmacro seems not so user friendly. Instead, I'd do something like:

const boolean debugMode = true;  // Set to false after the wiring test

pert: ch means nothing to me.

Fair point. I like my names to be as short as possible while still being obvious in context. Even in the context of the status of a switch 'ch' is probably too brief but it think 'on' is pretty clear. I've changed it to 'changed'.

Regarding the debug functions and uncommenting I've realized it's better to leave the debug code in for easy troubleshooting down the road. I wrapped the debug code in 'if (Serial)' so it does nothing if the serial port isn't open. The wiring test will be done by me, as will the uncommenting, so that's sorted :-)

Thanks for your input.

I figured out a lambda array solution that makes a timed sequence macro easier to work with I think.

In the header:

struct timedSequence {
  void (*lambda)();
  unsigned long delayAfter;
};

The macro:

void tkLambdaSequenceDemo(bool on, bool changed) {
  static int progress;
  static millisTimer tmr;
  static const timedSequence sequence[] {
    []{Keyboard.write('T');}, 300,
    []{Keyboard.write('e');}, 300,
    []{Keyboard.write('x');}, 300,
    []{Keyboard.write('t');}, 800,
    []{Keyboard.write(KEY_BACKSPACE);}, 300,
    []{Keyboard.write(KEY_BACKSPACE);}, 600,
    []{Keyboard.write('s');}, 600,
    []{Keyboard.write('t');}, 1200,
    []{Keyboard.print(" lol");}, 800,
    []{
      Keyboard.press(KEY_LEFT_SHIFT);
      Keyboard.write('1');
      Keyboard.write('1');
      Keyboard.release(KEY_LEFT_SHIFT);
      Keyboard.write('1');
      Keyboard.write('1');
      Keyboard.write(KEY_RETURN);
    }, 1
  };
  if (tmr && (progress || changed && on)) {
    sequence[progress].lambda();
    tmr.restart(sequence[progress].delayAfter);
    if (++progress >= sizeof(sequence) / sizeof(*sequence)) {
      progress = 0;
    }
  }
}