Programmable Macro Pad

by tinyboatproductions in Circuits > Arduino

10184 Views, 53 Favorites, 0 Comments

Programmable Macro Pad

Making a Macro Pad
DSC_0151_00002.jpg
61+K6UTHKUL._SL1000_.jpg
DSC_0156_00007.jpg
DSC_0155_00006.jpg
DSC_0157_00008.jpg

I wanted to build a programable macro pad after seeing so many out there. One of my main goals was to avoid using multiplexing on the switches. I didn't want to increase complexity more than I needed to, or order any diodes.

Another big reason to make this was so that I could completely design how it looked. There are lots of options out there for tons of different macro pads but I wanted one that felt like my own. The case is something that if you want to go wild and re-design I think it is a great opportunity especially if you are new to 3D modeling and printing. As I'm not using a circuit board you are really free to move any of the components where every your heart takes you. Or you can use the same case I did, I've liked it on the next page.

Supplies

Parts

(1) Teensy LC (a Pro Micro would also work but it would be tight on pins)

(10) MX Keyboard Switches (if you design your own case you can use any switch you want)

(10) Key Caps

(1) Rotary Encoder (Can be with click)

(4) Single Color LEDs

(1) Resistor, appropriate size for LED

(1) Micro USB Cord

Wire

Solder

Tools

Soldering Iron

Wire Cutters/Strippers

3D printer (optional)

Hot Glue Gun (optional)

Sand Paper (optional)

Make the Case

If you are printing a case start your case prints now.

If you are designing your own case I would recommend print just the parts that will interact with components. Things like the holes for the LEDs to make sure they are the right size to hold the the LED securely. Also the mountings for the switches, make sure they fit tightly so that they don't rattle around when you use them.

Here is a link to the case that I made for this project. I have also uploaded an STEP file. This means you can open the model in a program like Fusion360 or Solidworks, (others work to but I have only worked with those), and see the entire design process/structure and make changes to the dimensions that you need.

Put Everything in the Case

Because these are case mounted switches the case needs to be ready, or at least the top of the case, before you can move onto the next step.

Snap all the switches into place along with the LEDs. If your encoder came with headers you'll want to clip them apart and then de-solder each pin to leave an open place to connect the wires.

Soldering

Curicut.png
DSC_0159_00010.jpg
DSC_0162_00013.jpg

For wiring I've included the wiring diagram that I used for this project, I've left off the pin numbers as you can use what ever feels right for your project, any pin will work for any of the buttons/LEDs/encoder. The only pin I would avoid using in the Teensy is pin 13, it is tied to the onboard LED.

The basic run down is I ran a bare wire around to all the components and connected it to the ground on the Teensy. Then added a wire between each component and the corresponding wire on the Teensy. Take your time, but know if you mix some of them up its easy to fix in the code later (I swapped some of the wires, you'll be able to see in the code where I had to swap pins).

By the time you are done you will have a nice nest of wires that can be tucked away in the case.

Code

Everyone's favorite part: code.

The code I used is below, I think its easy to follow but I also wrote it so I am biased. If you are using the Teensy, you will need to go through a few extra steps to get programing from the Arduino IDE. I followed this tutorial to get it set. If you need some reasons this is a great board check out this video.

Essentially it boils down to a few simple steps for each component.

  1. Definitions
  2. Define/Include the libraries
  3. Define all the pins
  4. Create the encoder as an object.
  5. Setup
  6. Setup each of the buttons
  7. Setup the encoder
  8. Start the Keyboard, so you can send keyboard inputs
  9. Loop
  10. Check to see if one of the buttons is pressed
  11. if a button is pressed check what layout is active
  12. Use the button number and the layout number to send the right key combination
  13. The encoder works the same way, just wait for input and do the right thing

The next step is a more in depth walk through of the code if you want more information on how everything works.

This could run faster in theory by using interrupts on all the buttons and the encoder but I have not noticed any lag or missed inputs using just polling in a loop. But feel free to go wild here too.

For the macros, the key strokes that will be sent, you can really put any thing here: text, key combinations, keys that aren't even on your keyboard (think F13-F24).

The code below is basically just the framework for this project. You will need to add what key strokes you want for most of the buttons. I've left a few in there for simple things like save, undo, and copy/paste. For a full list of key you can send check out this resource for the Keyboard.h library. Note that you can use the windows media keys but they are not included in the list.

You can use the code as is or add your own macros too. When you are ready upload the code to the board and you should be ready to rock.

#include <Bounce2.h>
#include <Keyboard.h>
#include <Encoder.h>

/*
 * Sketch made for my 3x3 macro pad with an encoder and state button. The pad can cycle through 4 states so a total of 36 commands can be set on it.
 * The encoder is used for the media, the rotation is for volume, CW for volume up and CCW for volume down. The button will be used to play/pause on 
 * a short press and a long (500ms) for skip track.
 * If you add more buttons or states, you will need to updated the pin arrays, as well as add the options in the massive switch/case in the loop.
 */
 
//Set up the button grid
const int numButtons = 9;
const int buttonPins[numButtons] = {4,3,2,7,6,5,10,9,8}; //Array of button pins in an order that makes sense to me
//Set up all the buttons as bounce objects
Bounce buttons[] = {Bounce(buttonPins[0],10),Bounce(buttonPins[1],10),Bounce(buttonPins[2],10),Bounce(buttonPins[3],10),Bounce(buttonPins[4],10),Bounce(buttonPins[5],10),Bounce(buttonPins[6],10),Bounce(buttonPins[7],10),Bounce(buttonPins[8],10)};

//Set up the states
const int numStates = 4;
const int statePins[numStates] = {16,17,18,19}; //State LED pins, in order that makes sense
const int statePin = 11;
Bounce stateButton = Bounce(statePin,10); //Set up the state buttons as a bounce object
int state = 0; //Set the current state

//Set up the encoder
const int encoderButtonPin = 12;
Encoder volumeKnob(14,15); //Set up the encoder as an Encoder object
Bounce encoderButton = Bounce(encoderButtonPin,10); //Set up to the encoder button as a Bounce object
int timeLimit = 500; //Cut off time for play/pause or skip

int ledState = LOW;


void setup() {
  Serial.begin(9600);
  Keyboard.begin(); //Start the Keyboard object
  for(int i = 0; i < numButtons; i++){
    pinMode(buttonPins[i], INPUT_PULLUP);
  }

  pinMode(statePin, INPUT_PULLUP);
  pinMode(encoderButtonPin, INPUT_PULLUP);
  
  //Cycle through the state pins as a startup animation
  for(int i = 0; i < numStates; i++) {
    pinMode(statePins[i],OUTPUT);
    digitalWrite(statePins[i],HIGH);
    delay(250);
    digitalWrite(statePins[i],LOW);
  }
  digitalWrite(statePins[state],HIGH);

  
  //Turn the buildin LED off - used for debugging
  //pinMode(13,OUTPUT);
  //digitalWrite(13,ledState);
}

long positionLeft = -999;

void loop() {
  //check all buttons
  for(int j = 0; j < numButtons; j++){
    if(buttons[j].update()){
      if(buttons[j].fallingEdge()){
        //Toggle the builtin LED - used for ensuring buttons are working
        //ledState = !ledState;
        //Serial.write("Button");
        //digitalWrite(13,ledState);

        //use the current state and the button number to find the command needed and send it.
        switch (state) {
          case 0: //Layout 1
            switch (j) {
              case 0: //copy
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('c');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: //paste
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('v');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 2: 
                
                break;
              case 3: //all
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('a');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 1: //Layout 2
            switch (j) {
              case 0: //undo
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('z');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: 
                break;
              case 2:          
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 2: //Layout 3
            switch (j) {
              case 0: // escape
                Keyboard.press(KEY_ESC);
                Keyboard.releaseAll();
                break; 
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6:
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 3: //Layout 4
            switch (j) {
              case 0: 
                break;
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6:
                break;
              case 7: 
                break;
              case 8: 
                break;
            }
            break;
        }
      }
    } 
  }

  //state change button pressed
  if(stateButton.update()){
    if(stateButton.fallingEdge()){
      digitalWrite(statePins[state],LOW); //turn off current LED
      state++;
      if(state >= numStates) {
        state = 0;
      }
      digitalWrite(statePins[state],HIGH);
      Serial.print(state);
    }
  }

  //encoder play pause
  if(encoderButton.update()) {
    if(encoderButton.fallingEdge()) {
      int fall = millis();
      while(!encoderButton.update()){}
      if(encoderButton.risingEdge()){
        int rise = millis();
        Serial.println(rise - fall);
        if(rise - fall > timeLimit){
          Keyboard.press(KEY_MEDIA_NEXT_TRACK);
        } else {
          Keyboard.press(KEY_MEDIA_PLAY_PAUSE);
        }
      }
      Keyboard.releaseAll();
    }
  }
  
  //encoder volume changing
  long newLeft;
  newLeft = volumeKnob.read();
  if(newLeft != positionLeft){
    if((newLeft - positionLeft) > 0) {
      //volumeup
      Serial.println("volume up");
      Keyboard.press(KEY_MEDIA_VOLUME_INC);
    } else {
      //volumedown
      Serial.println("volume down");
      Keyboard.press(KEY_MEDIA_VOLUME_DEC);
    }
    Keyboard.releaseAll();
    delay(200);
    positionLeft = newLeft;
  }
}

Pro Micro Code

Here is code if you are building this project using a Pro Micro rather than a Teensy.

The main difference comes in in the encoder programing, instead of using Keyboard, you need to use Consumer

Also instead of using the Keyboard.h library, you need to use HID-Project.h


#include <Bounce2.h>
#include <Encoder.h>
#include "HID-Project.h"


/*
 * Sketch made for my 3x3 macro pad with an encoder and state button. The pad can cycle through 4 states so a total of 36 commands can be set on it.
 * The encoder is used for the media, the rotation is for volume, CW for volume up and CCW for volume down. The button will be used to play/pause on 
 * a short press and a long (500ms) for skip track.
 * If you add more buttons or states, you will need to updated the pin arrays, as well as add the options in the massive switch/case in the loop.
 */
 
//Set up the button grid
const int numButtons = 9;
const int buttonPins[numButtons] = {4,3,2,7,6,5,10,9,8}; //Array of button pins in an order that makes sense to me
//Set up all the buttons as bounce objects
Bounce buttons[] = {Bounce(buttonPins[0],10),Bounce(buttonPins[1],10),Bounce(buttonPins[2],10),Bounce(buttonPins[3],10),Bounce(buttonPins[4],10),Bounce(buttonPins[5],10),Bounce(buttonPins[6],10),Bounce(buttonPins[7],10),Bounce(buttonPins[8],10)};


//Set up the states
const int numStates = 4;
const int statePins[numStates] = {16,17,18,19}; //State LED pins, in order that makes sense
const int statePin = 11;
Bounce stateButton = Bounce(statePin,10); //Set up the state buttons as a bounce object
int state = 0; //Set the current state


//Set up the encoder
const int encoderButtonPin = 12;
Encoder volumeKnob(14,15); //Set up the encoder as an Encoder object
Bounce encoderButton = Bounce(encoderButtonPin,10); //Set up to the encoder button as a Bounce object
int timeLimit = 500; //Cut off time for play/pause or skip


int ledState = LOW;




void setup() {
  Serial.begin(9600);
  Keyboard.begin(); //Start the Keyboard object
  for(int i = 0; i < numButtons; i++){
    pinMode(buttonPins[i], INPUT_PULLUP);
  }


  pinMode(statePin, INPUT_PULLUP);
  pinMode(encoderButtonPin, INPUT_PULLUP);
  
  //Cycle through the state pins as a startup animation
  for(int i = 0; i < numStates; i++) {
    pinMode(statePins[i],OUTPUT);
    digitalWrite(statePins[i],HIGH);
    delay(250);
    digitalWrite(statePins[i],LOW);
  }
  digitalWrite(statePins[state],HIGH);


  
  //Turn the buildin LED off - used for debugging
  //pinMode(13,OUTPUT);
  //digitalWrite(13,ledState);
}


long positionLeft = -999;


void loop() {
  //check all buttons
  for(int j = 0; j < numButtons; j++){
    if(buttons[j].update()){
      if(buttons[j].fallingEdge()){
        //Toggle the builtin LED - used for ensuring buttons are working
        //ledState = !ledState;
        //Serial.write("Button");
        //digitalWrite(13,ledState);


        //use the current state and the button number to find the command needed and send it.
        switch (state) {
          case 0: //Layout 1
            switch (j) {
              case 0: //copy
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('c');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: //paste
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('v');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 2: 
                
                break;
              case 3: //all
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('a');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 1: //Layout 2
            switch (j) {
              case 0: //undo
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('z');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: 
                break;
              case 2:          
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 2: //Layout 3
            switch (j) {
              case 0: // escape
                Keyboard.press(KEY_ESC);
                Keyboard.releaseAll();
                break; 
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6:
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 3: //Layout 4
            switch (j) {
              case 0: 
                break;
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6:
                break;
              case 7: 
                break;
              case 8: 
                break;
            }
            break;
        }
      }
    } 
  }


  //state change button pressed
  if(stateButton.update()){
    if(stateButton.fallingEdge()){
      digitalWrite(statePins[state],LOW); //turn off current LED
      state++;
      if(state >= numStates) {
        state = 0;
      }
      digitalWrite(statePins[state],HIGH);
      Serial.print(state);
    }
  }


  //encoder play pause
  if(encoderButton.update()) {
    if(encoderButton.fallingEdge()) {
      int fall = millis();
      while(!encoderButton.update()){}
      if(encoderButton.risingEdge()){
        int rise = millis();
        //Serial.println(rise - fall);
        if(rise - fall > timeLimit){
          Consumer.press(MEDIA_NEXT);
        } else {
          Consumer.press(MEDIA_PLAY_PAUSE);
        }
      }
      Keyboard.releaseAll();
    }
  }
  
  //encoder volume changing
  long newLeft;
  newLeft = volumeKnob.read();
  if(newLeft != positionLeft){
    if((newLeft - positionLeft) > 0) {
      //volumeup
      Serial.println("volume up");
      Consumer.press(MEDIA_VOLUME_UP);
    } else {
      //volumedown
      Serial.println("volume down");
      Consumer.press(MEDIA_VOLUME_DOWN);
    }
    Keyboard.releaseAll();
    delay(200);
    positionLeft = newLeft;
  }
}

Code Walk Through

Here is a more indepth walk through code for this macropad and how it works. As just a warning I have no qualification to teach people how to code, the majority of what I know is what I can remember for the code 101 classes that I took years ago.
The main thing I know about programming for Arduino and any of the Arduino like boards is that the set is about 75% of the code. The more care you take when setting, wiring/defining your pins, up the more likely you are to succeed.

The code below is mainly in 2 parts, setup and loop; just like with any Arduino program.

First up, why write code when we can use someone else's. One of the few programming things I know is: don’t waste time writing code someone has already written better. That’s a long way to say: here are the libraries we are going to use. When working with Teensy we need to be a bit careful with what libraries that we use as not all Arduino libraries work well with the Teensy. For the buttons/debouncing of the buttons I am using Bounce2, for sending the keyboard inputs we are using Keyboard, and for the encoder we are using Encoder.

#include <bounce2.h><Bounce2.h>
#include <Keyboard.h><keyboard.h>
#include <Encoder.h><p><encoder.h><br></encoder.h></p><encoder.h></encoder.h></keyboard.h></bounce2.h>

Now that we have some libraries in our code we need to start coding our pins. A tip here is to only have your pin number 1 place, everywhere else have a variable with that value stored. That way if something were to go wrong and you need to change the pin number, you aren’t hunting through a pile of code for every time you used a 14.

For this code I decided to use arrays. In this set up arrays are an easy method to keep track of all of the buttons we are using. It also makes the code expandable. If you were to add 2 or 3 more buttons you just have to change the numButtons and add the pin numbers to the end of the array. The way the code is written they will be checked without anything else being added (the code will crash if you pressed any of the new buttons but it would work until that point). Mainly that’s what we are doing here in the arrays. Put the buttons in an order that makes sense to you. To me it made sense to number the buttons starting the top left corner going across and then down.

 
//Set up the button grid
const int numButtons = 9;
const int buttonPins[numButtons] = {4,3,2,7,6,5,10,9,8}; //Array of button pins in an order that makes sense to me

Next up, using the libraries that we added. Right now we have an array of pin numbers, one element for each button. Which is fine and we could use it and write our own debouncing code, but, why bother, someone else already has, and it’s better than what I could write. Here we create an object for each button, an object is basically just like a little box where we store all the information for a thing; in this case a button. The object keeps track of the pin number and takes care of the debouncing. I’ve mentioned debouncing a few times. I'll explain what it is a bit. With any mechanical switch there is a time at the beginning and the end of a press where the button is kinda connecting. This can lead to the button being read as pressed multiple times really fast, which we don't want. We want the code to recognize one press when we press the button 1 time. The Bounce2 library takes care of this, but we need to tell Bouce2 to do all of that. That’s what the code does here. It also uses the array we just set up with the pin numbers. This line adds a bounce object to the array for each button we have.

//Set up all the buttons as bounce objects
Bounce buttons[] = {Bounce(buttonPins[0],10),Bounce(buttonPins[1],10),Bounce(buttonPins[2],10),Bounce(buttonPins[3],10),Bounce(buttonPins[4],10),Bounce(buttonPins[5],10),Bounce(buttonPins[6],10),Bounce(buttonPins[7],10),Bounce(buttonPins[8],10)};

Next up the state LEDs. This section is very similar to the array for the button pins. Again we add the number of states first, then add the pins to an array to keep track of the. Here because we don't need to set up any objects for the LEDs to work, the numStates can be changed and the pin number added to the array and the new LED will work.

//Set up the states
const int numStates = 4;
const int statePins[numStates] = {16,17,18,19}; //State LED pins, in order that makes sense

This section sets up a single pin which is kept separate from the buttons above. I keep this button separate as I find it easier to think of and take care of it on its own. We also tell the code what state to start in, in this case state 0. A reminder that arrays start at 0 in C++ not 1. So the first element in an array is the 0th element, not the 1st.

const int statePin = 11;
Bounce stateButton = Bounce(statePin,10); //Set up the state buttons as a bounce object
int state = 0; //Set the current state

Ok we are nearly ready to start writing fun code. But we still need to get the encoder set up. If you really want to learn about how encoders work do it, they are a clever piece of tech that is very interesting. As a quick overview, there are about 1 billion different types, but they all work by telling small differences in rotation, that difference will tell us which direction the encoder is turning and how far. Again, we aren't going to write much code here, as we have the Encoder library. So below we do a combination of what we have done already, setup the encoder button as a button object. Then set up the encoder as an Encoder object. Finally, I added a time limit, I want to use the encoder button as both play/pause and skip. So I add the timeLimit as how long I need to hold the button to skip to the next song rather than play/pause. A good note here is that times in Arduino are in milliseconds, so 500ms is half a second.

//Set up the encoder
const int encoderButtonPin = 12;
Encoder volumeKnob(14,15); //Set up the encoder as an Encoder object
Bounce encoderButton = Bounce(encoderButtonPin,10); //Set up to the encoder button as a Bounce object
int timeLimit = 500; //Cut off time for play/pause or skip

I used this when first writing the code to debug the code. So that I can turn an LED on and off when I press the button. This is the code that is running in the test circuit in video. This line isn’t needed anymore but it doesn't hurt to leave it.

int ledState = LOW;

Nearly there, setup() time. First thing I do in most of my projects is to add a serial connection so that I can use printing to the monitor to debug my code. That's the first line. Next we start the Keyboard object. This is very similar to the objects we have set up before but it doesnt need any information to start, we just have to tell it to start. Just Keyboard.begin() and it's good to go.

void setup() {
  Serial.begin(9600);
  Keyboard.begin(); //Start the Keyboard object

Now we need to set up the buttons as inputs. We are using the internal pullup resistors here too with the buttons so we don't have to add them to our circuit. Here is where we can reap the benefits of using arrays. We can simply write the pinMode code once and just loop through the array and it’s good to go.

  for(int i = 0; i < numButtons; i++){
    pinMode(buttonPins[i], INPUT_PULLUP);
  }

The state button and the encoder button each get their own line. This is the same pinMode line as above just with the right button.

  pinMode(statePin, INPUT_PULLUP);
  pinMode(encoderButtonPin, INPUT_PULLUP);

Now we do a similar thing with the LEDS. and also add a little animation because it's fun. Set up the LED as an output then flip it on, wait a bit, then back off.

  //Cycle through the state pins as a startup animation
  for(int i = 0; i < numStates; i++) {
    pinMode(statePins[i],OUTPUT);
    digitalWrite(statePins[i],HIGH);
    delay(250);
    digitalWrite(statePins[i],LOW);
  } 

Now we just turn on the current state LED so we can start off on the right state. If we excluded this line when the code starts no LEDs would light up until you change the state.

  digitalWrite(statePins[state],HIGH);

Here is a bit more of my debugging leftovers. Uncomment this for a flashing LED when you press a button

  //Turn the buildin LED off - used for debugging
  //pinMode(13,OUTPUT);
  //digitalWrite(13,ledState);
}

It is evident I don't really know what I'm doing here. This line is needed by the Encoder. I have no idea what it does. I assume it keeps track of the position.

long positionLeft = -999;

Now to the stuff that does stuff: the loop(). The first section loops over the buttons. For each button check if the button has been pressed. The first if, the .update(), will tell the code that the button has been pressed. The next line, .fallingEdge(), detects when the button pin goes from high to low, meaning the button has been pressed. The Bounce2 library takes care of all the hard stuff and just tells us the button has been pressed.

void loop() {
  //check all buttons
  for(int j = 0; j < numButtons; j++){
    if(buttons[j].update()){
      if(buttons[j].fallingEdge()){
        //Toggle the builtin LED - used for ensuring buttons are working
        //ledState = !ledState;
        //Serial.write("Button");
        //digitalWrite(13,ledState);

So at this point we know a few things. Which button has been pressed, in this instance the variable j and we know our state. Now we can use that information to find out what commands to send to the computer. First we will find the right layout using the state variable. I am using a switch{case}, but you could use a series of if{} else if {} but a switch case is a lot cleaner and easier to follow. For the switch case we define each possible case, for the given variable. So for the state, it is possible for it to be a 0, a 1, a 2, or a 3, so there is a case for each, remember the state array starts at 0. (you can also add a default case to capture any weird things that can get in but I like to live on the edge. Code now crash later). Within each of the state cases we also have a switch case based on j, where j is the button number we are on. Again each has a 0 through 8 case. With each of those cases is where we put our commands to send to the computer.

        //use the current state and the button number to find the command needed and send it.
        switch (state) {
          case 0: //Layout 1
            switch (j) { 

Let’s take a look at the first command. Here we have the copy command, which I would guess you are familiar with, you are programming after all, the control + ‘c’ key. We just need to break it down into single commands and put each on a line. First we press the control key, then press the ‘c’ key. Then delay for 100 milliseconds, this gives the computer enough time to recognize the command, the Teensy can blaze through code so i just want to make sure we dont send it too fast. A huge note here, the .press() command acts like holding down the key. So we need to release the keys otherwise it will be like we are just holding those keys down forever and it would bind up the computer. We can then use .releaseAll() to let go of the keys we have pressed. You could release each key on its own but just doing all of them is easiest. The breaks here are not strictly necessary but they ensure that the code will pop back out of the switch case and continue on. But this is basically how the code works for each function key.

case 0: //copy
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('c');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: //paste
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('v');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 2: 
                
                break;
              case 3: //all
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('a');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 1: //Layout 2
            switch (j) {
              case 0: //undo
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('z');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 1: 
                break;
              case 2:          
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6: // save
                Keyboard.press(KEY_RIGHT_CTRL);
                Keyboard.press('s');
                delay(100);
                Keyboard.releaseAll();
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 2: //Layout 3
            switch (j) {
              case 0: // escape
                Keyboard.press(KEY_ESC);
                Keyboard.releaseAll();
                break; 
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: //
                break;
              case 5:
                break;
              case 6:
                break;
              case 7:
                break;
              case 8:
                break;
            }
            break;
          case 3: //Layout 4
            switch (j) {
              case 0: 
                break;
              case 1: 
                break;
              case 2: 
                break;
              case 3: 
                break;
              case 4: 
                break;
              case 5: 
                break;
              case 6:
                break;
              case 7: 
                break;
              case 8: 
                break;
            }
            break;
        }
      }
    } 
  }

State button time baabby. Another benefit of having the state button as its own thing is we can set specific code for it without having to put it in 4 times (once for each state). We basically do the same thing for this button as we did for the other buttons. We check the button with .update() then check to see if the line has gone low, to mean it has been pressed using .fallingEdge(). We also do a few other things here. We turn off the current state LED. Then we increment the state using state++, this is the same as using state = state + 1. So if the state was 2 it would now be 3. But this does mean that when the state is 3 the value will now be 4, which is outside of the number we defined earlier (remember that the array starts at 0 so the maximum spot is 3 not 4). To fix this, whe just check if the state is greater than or equal to the number of states we defined, if it is, we set it back to 0. Then we turn on the new state LED. also a debug line here, just print the state to make sure its increased properly.

//state change button pressed
  if(stateButton.update()){
    if(stateButton.fallingEdge()){
      digitalWrite(statePins[state],LOW); //turn off current LED
      state++;
      if(state >= numStates) {
        state = 0;
      }
      digitalWrite(statePins[state],HIGH);
      Serial.print(state);
    }
  }

Oh boy, now the encoder. For the button, we are doing the same thing as before with .update() and .fallingEdge(). To find out the button has been pressed we add a step here because we want this button to do multiple things. A timer is started, basically when the button is pressed start counting. The while() look waits until there is an update on the encoder button (be careful using a while loop like this, this is ok because the button needs to be released at some point, so its fine to wait, but this will bind up your code until you let go of the button) and when the button is released we stop counting. Then we find the difference between the two and if the time is greater than the time limit we defined waaaaay up there. If it's longer than the time, we send the next track command, if not the play/pause command. Also note here that the .releaseAll() is near the end rather than after each line, this is not ideal but it works.

 //encoder play pause
  if(encoderButton.update()) {
    if(encoderButton.fallingEdge()) {
      int fall = millis();
      while(!encoderButton.update()){}
      if(encoderButton.risingEdge()){
        int rise = millis();
        Serial.println(rise - fall);
        if(rise - fall > timeLimit){
          Keyboard.press(KEY_MEDIA_NEXT_TRACK);
        } else {
          Keyboard.press(KEY_MEDIA_PLAY_PAUSE);
        }
      }
      Keyboard.releaseAll();
    }
  } 

We are almost done here. Just the volume control left. This is nearly identical to the example code, we just add the commands we want to send. This is not the most ideal implementation of an encoder, as we aren't using interrupts but, as with a lot of things in this code, it works, it's just a bit slow, which is a shame on such a powerful board. I really don’t know what’s going on here.

//encoder volume changing
  long newLeft;
  newLeft = volumeKnob.read();
  if(newLeft != positionLeft){
    if((newLeft - positionLeft) > 0) {
      //volumeup
      Serial.println("volume up");
      Keyboard.press(KEY_MEDIA_VOLUME_INC);
    } else {
      //volumedown
      Serial.println("volume down");
      Keyboard.press(KEY_MEDIA_VOLUME_DEC);
    }
    Keyboard.releaseAll();
    delay(200);
    positionLeft = newLeft;
  }
}

That's a rundown of the code. It’s definitely not the most simple code but I think that if you take it line by line it can be followed. Another tip I would have is to look at each component on its own. Like the LEDs to start with, then get the state button, and build on that.

Test It Out

DSC_0152_00003.jpg

Now all that's left is to pop the keycaps and encoder knob on and get to testing it out. Lean what works and what doesn't. And now if you find your self wishing it was fewer key strokes on your computer now you can fix it.

Feel free to ask any questions that you have I'll try the best I can to help out!