Skip to content

Instantly share code, notes, and snippets.

@BOSSoNe0013
Last active May 12, 2023 01:15
Show Gist options
  • Save BOSSoNe0013/a4b44349dfb8e1a2ab98b089495b2b09 to your computer and use it in GitHub Desktop.
Save BOSSoNe0013/a4b44349dfb8e1a2ab98b089495b2b09 to your computer and use it in GitHub Desktop.
Code for my macro keyboard mod https://www.thingiverse.com/make:914894
/*
####
## VM's keyboard
####
Board: Arduino Micro
Tested with versions:
Arduino IDE 1.8.12
Libraries (and version used to develop the project):
ClickEncoder - https://github.com/0xPIT/encoder
TimerOne v1.1 - https://github.com/PaulStoffregen/TimerOne
HID-Project v2.6.1 - https://github.com/NicoHood/HID
Base on project from Prusa - Volume Control Knob - https://blog.prusaprinters.org/3d-print-an-oversized-media-control-volume-knob-arduino-basics/
Macro keyboard with rotary encoder. Offers 8 charry MX buttons and one rotary encoder with click.
The functions of buttons can be set by defining the arrya key.
If you are unable to upload the code, then briefly connect RST pin to GND when you see "Uploading" in Arduino IDE
If using the switch it will change the function of pressing in the knob
Actions for rotary encoder are later in the code (search for ROTARY_ACTIONS)
*/
#include <ClickEncoder.h>
#include <TimerOne.h>
#define HID_CUSTOM_LAYOUT
#define LAYOUT_FRENCH
#include <HID-Project.h>
#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
#include <avr/power.h> // Required for 16 MHz Adafruit Trinket
#endif
// Which pin on the Arduino is connected to the NeoPixels?
#define PIN 16 // On Trinket or Gemma, suggest changing this to 1
// How many NeoPixels are attached to the Arduino?
#define NUM_LEDS 8
#define NUMBER_OF_KEYS 9 // Count of keys in the keyboard
#define MAX_COMBINATION_KEYS 4 // Maximum number of key codes that can be pressed at the same time (does dont correspond to actually pressed keys)
#define MAX_SEQUENCE_KEYS 16 // Maximum length of key combination sequence (that means first you send CTRL + Z (1. combination), then SHIFT + ALT + X (2. combination), then A (3. combination) ... )
#define DEBOUNCING_MS 20 // wait in ms when key can oscilate
#define FIRST_REPEAT_CODE_MS 500 // after FIRST_REPEAT_CODE_MS ,s if key is still pressed, start sending the command again
#define REPEAT_CODE_MS 150 // when sending command by holding down key, wait this long before sending command egain
// Rotary encoder connections
#define ENCODER_CLK A0
#define ENCODER_DT A1
#define ENCODER_SW A2
#define serialRate 115200
Adafruit_NeoPixel pixels(NUM_LEDS, PIN, NEO_GRB + NEO_KHZ800);
// Defining types
enum TKeyState {INACTIVE, DEBOUNCING, ACTIVE, HOLDING}; // Key states - INACTIVE -> DEBOUNCING -> ACTIVE -> HOLDING -> INACTIVE
// -> INACTIVE
enum TKeyType {KEYBOARD, MOUSE, CONSUMER, SYSTEM, MODIFIER}; // Types of key codes - simulating keyboard, mouse, multimedia or modifier that alters the rotary encoder behavior
typedef struct TActions {
uint16_t durationMs;
uint16_t key[MAX_COMBINATION_KEYS];
} TAction;
typedef struct TKeys {
uint8_t pin;
enum TKeyType type;
enum TKeyState state;
uint32_t stateStartMs;
uint16_t modificatorKeys[MAX_COMBINATION_KEYS];
TAction action[MAX_SEQUENCE_KEYS];
} TKey;
// Different types expect different actions:
// KEYBOARD - keys + modifiers, for example: {.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {KEY_LEFT_SHIFT}, .action = {{.durationMs = 100, .key = {KEY_H}}, {.durationMs = 100, .key = {KEY_I}}}}
// CONSUMER - only keys, for example: {.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {} , .action = {{.durationMs = 100, .key = {MEDIA_VOLUME_MUTE, CONSUMER_BRIGHTNESS_UP}}, {.durationMs = 100, .key = {CONSUMER_CALCULATOR}}}}
// SYSTEM - only 1 key, for example: {.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {} , .action = {{.durationMs = 100, .key = {HID_SYSTEM_SLEEPP}}}}
// MODIFIER - nothing for example: {.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {} , .action = {}}
// MOUSE - only keys for example: {.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {} , .action = {{.durationMs = 10100, .key = {10100, 10000, 9800}}, {.durationMs = 200, .key = {MOUSE_LEFT, MOUSE_MIDDLE}}}} = first move 100px in X-direction and scroll -200px and wait 100ms, then click left and middle mouse button and wait 200ms
// if durationMs >= 10000; then in key is tripplet- delta movement in X, Y and SCROLL (zero is mapped to 10000, so 9800 is -200px) - {{.durationMs = 10200, .key = {10000, 10000, 10300}} - scroll 300px and wait 200ms
// if durationMs < 10000; then in key are mouse keys to press - {{.durationMs = 150, .key = {MOUSE_LEFT, MOUSE_MIDDLE}} - press left and middle mouse buttons for 150ms
// Supported key commands:
// KEYBOARD - c:\Users\XXX\Documents\Arduino\libraries\HID-Project\src\KeyboardLayouts\ImprovedKeylayouts.h - see section enum KeyboardKeycode : uint8_t
// CONSUMER - c:\Users\XXX\Documents\Arduino\libraries\HID-Project\src\HID-APIs\ConsumerAPI.h - see section enum ConsumerKeycode : uint16_t
// SYSTEM - c:\Users\XXX\Documents\Arduino\libraries\HID-Project\src\HID-APIs\SystemAPI.h - see section enum SystemKeycode : uint8_t
// MOUSE - c:\Users\XXX\Documents\Arduino\libraries\HID-Project\src\HID-APIs\MouseAPI.h - see defines - MOUSE_LEFT, MOUSE_RIGHT, MOUSE_MIDDLE
// Define actions for your keys
TKey key[NUMBER_OF_KEYS] = {
{.pin = 2, .type = MODIFIER, .state = INACTIVE, .stateStartMs = 0}, // switch
{.pin = 3, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F16}}, {.durationMs = 100, .key = {KEYPAD_1}}}},
{.pin = 4, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F17}}, {.durationMs = 100, .key = {KEYPAD_2}}}},
{.pin = 5, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F18}}, {.durationMs = 100, .key = {KEYPAD_3}}}},
{.pin = 6, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F19}}, {.durationMs = 100, .key = {KEYPAD_4}}}},
{.pin = 7, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F20}}, {.durationMs = 100, .key = {KEYPAD_5}}}},
{.pin = 8, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F21}}, {.durationMs = 100, .key = {KEYPAD_6}}}},
{.pin = 9, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F22}}, {.durationMs = 100, .key = {KEY_UP_ARROW}}}},
{.pin = 10, .type = KEYBOARD, .state = INACTIVE, .stateStartMs = 0, .modificatorKeys = {}, .action = {{.durationMs = 100, .key = {KEY_F23}}, {.durationMs = 100, .key = {KEY_DOWN_ARROW}}}},
};
// Actions for rotary encoder are later in the code (search for ROTARY_ACTIONS)
// Adalight sends a "Magic Word" (defined in /etc/boblight.conf) before sending the pixel data
uint8_t prefix[] = {'A', 'd', 'a'}, hi, lo, chk, i;
// global variables
ClickEncoder *encoder; // variable representing the rotary encoder
int16_t last, value; // variables for current and last rotation value
uint8_t globalModifier; // when holding down key with MODIFIER type, this is set to true - can be used to change the behaviour of other keys or rotary encoder
void setup() {
#if defined(__AVR_ATtiny85__) && (F_CPU == 16000000)
clock_prescale_set(clock_div_1);
#endif
pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
Consumer.begin(); // Initializes the media keyboard
Keyboard.begin();
encoder = new ClickEncoder(ENCODER_DT, ENCODER_CLK, ENCODER_SW,4); // Initializes the rotary encoder with the mentioned pins
for (uint8_t i = 0; i < NUMBER_OF_KEYS; i++) pinMode(key[i].pin, INPUT_PULLUP);
Timer1.initialize(1000); // Initializes the timer, which the rotary encoder uses to detect rotation - 1000us = 1ms
Timer1.attachInterrupt(timerIsr);
last = 0;
globalModifier = false;
for (uint8_t i = 0; i < 3; i++) {
lightsOn(HIGH);
}
Serial.begin(serialRate);
// Send "Magic Word" string to host
Serial.print("Ada\n");
}
uint8_t modifierState = HIGH;
void loop() {
processAdaLightMessages();
// read the key's states and if one is pressed, execute the associated command
for (uint8_t i = 0; i < NUMBER_OF_KEYS; i++) {
uint8_t keyState = digitalRead(key[i].pin);
if (key[i].type == MODIFIER) {
if (modifierState != keyState) {
modifierState = keyState;
lightsOn(keyState);
globalModifier = keyState == LOW;
}
}
if ((key[i].state == INACTIVE) && (keyState == LOW)) {
key[i].state = DEBOUNCING;
key[i].stateStartMs = millis();
} else if (key[i].state == DEBOUNCING) {
if (keyState == HIGH) key[i].stateStartMs = millis();
else if ((millis() - key[i].stateStartMs) > DEBOUNCING_MS) {
key[i].state = ACTIVE;
processKey(i);
}
} else if (key[i].state == ACTIVE) {
if (keyState == HIGH) {
key[i].state = INACTIVE;
key[i].stateStartMs = millis();
} else if ((millis() - key[i].stateStartMs) > FIRST_REPEAT_CODE_MS) {
key[i].state = HOLDING;
key[i].stateStartMs = millis();
processKey(i);
}
} else if (key[i].state == HOLDING) {
if (keyState == HIGH) {
key[i].state = INACTIVE;
key[i].stateStartMs = millis();
} else if ((millis() - key[i].stateStartMs) > REPEAT_CODE_MS) {
key[i].stateStartMs = millis();
processKey(i);
}
}
}
// ROTARY_ACTIONS
// Turning the rotary encoder
value += encoder->getValue();
// This part of the code is responsible for the actions when you rotate the encoder
if (value != last) { // New value is different than the last one, that means to encoder was rotated
uint16_t diff = abs(value - last);
if (globalModifier) { // The rotary encoder is used as volume control or with the Modifier key as a scroll
ConsumerKeycode cmd = (last < value) ? MEDIA_FAST_FORWARD : MEDIA_REWIND;
for (uint8_t i = 0; i < diff; i++) Consumer.write(cmd);
} else {
ConsumerKeycode cmd = (last < value) ? MEDIA_VOLUME_UP : MEDIA_VOLUME_DOWN;
for (uint8_t i = 0; i < diff; i++) Consumer.write(cmd);
}
last = value; // Refreshing the "last" varible for the next loop with the current value
}
// Pressing the rotary encoder - detects single and double clicks
// This next part handles the rotary encoder BUTTON
ClickEncoder::Button b = encoder->getButton(); // Asking the button for it's current state
if (b != ClickEncoder::Open) { // If the button is unpressed, we'll skip to the end of this if block
switch (b) {
case ClickEncoder::Clicked: // Button was clicked once
// SurfaceDial.press(); // Replace this line to have a different function when clicking button once
Consumer.write(MEDIA_PLAY_PAUSE);
break;
case ClickEncoder::DoubleClicked: // Button was double clicked
Consumer.write(MEDIA_RECORD); // Replace this line to have a different function when double-clicking
break;
}
}
//delay(10); // I think this is not needed
}
// Capture rotary encoder pulses
void timerIsr() {
encoder->service();
}
// Execute key commands
uint8_t processKey(uint8_t keyIndex) {
TKey *lkey = &key[keyIndex];
if (lkey->type == KEYBOARD) {
if (globalModifier) {
// Press modificators
for (uint8_t i = 0; i < MAX_COMBINATION_KEYS; i++) {
if (lkey->modificatorKeys[i]) Keyboard.press((KeyboardKeycode) lkey->modificatorKeys[i]);
else break;
}
}
uint8_t actionIndex = 0;
if (globalModifier) {
TAction *laction = &lkey->action[1];
if (laction->key[0]) actionIndex = 1;
}
if (globalModifier ) {
TAction *laction = &lkey->action[actionIndex];
if (laction->key[0]) Keyboard.press((KeyboardKeycode) laction->key[0]);
if (laction->durationMs) delay(laction->durationMs);
if (laction->key[0]) Keyboard.release((KeyboardKeycode) laction->key[0]);
}
else {
TAction *laction = &lkey->action[0];
if (laction->key[0]) Keyboard.press((KeyboardKeycode) laction->key[0]);
if (laction->durationMs) delay(laction->durationMs);
if (laction->key[0]) Keyboard.release((KeyboardKeycode) laction->key[0]);
}
Keyboard.releaseAll();
} else if (lkey->type == CONSUMER) {
for (uint8_t i = 0; i < MAX_SEQUENCE_KEYS; i++) {
TAction *laction = &lkey->action[i];
if ((laction->durationMs) || (laction->key[0])) {
//press keys
for (uint8_t j = 0; j < MAX_COMBINATION_KEYS; j++) {
if (laction->key[j]) Consumer.press((ConsumerKeycode) laction->key[j]);
else break;
}
// wait
if (laction->durationMs) delay(laction->durationMs);
//release keys
for (uint8_t j = 0; j < MAX_COMBINATION_KEYS; j++) {
if (laction->key[j]) Consumer.release((ConsumerKeycode) laction->key[j]);
else break;
}
} else {
break;
}
}
Consumer.releaseAll();
}
}
void processAdaLightMessages() {
if(Serial.available() > 0) {
for(i = 0; i < sizeof prefix; ++i) {
if(prefix[i] == Serial.read()) {
if(i != (sizeof prefix - 1)) {
continue;
}
}
else {
break;
}
if(i == (sizeof prefix - 1)) {
while (!Serial.available()) ;;
hi=Serial.read();
while (!Serial.available()) ;;
lo=Serial.read();
while (!Serial.available()) ;;
chk=Serial.read();
if (chk == (hi ^ lo ^ 0x55)) {
for (uint8_t i = 0; i < NUM_LEDS; i++) {
byte r, g, b;
while(!Serial.available());
r = Serial.read();
while(!Serial.available());
g = Serial.read();
while(!Serial.available());
b = Serial.read();
pixels.setPixelColor(i, pixels.Color(r, g, b));
}
pixels.show(); // Send the updated pixel colors to the hardware.
}
}
}
}
}
void lightsMode1() {
pixels.clear(); // Set all pixel colors to 'off'
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
// Top row
pixels.setPixelColor(4, pixels.Color(0, 0, 255)); // F13
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(5, pixels.Color(127, 0, 255)); // F14
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(6, pixels.Color(255, 0, 127)); // F15
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(7, pixels.Color(255, 0, 0)); // F16
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
// Bottom row
pixels.setPixelColor(3, pixels.Color(0, 255, 127)); // F17
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(2, pixels.Color(0, 255, 0)); // F18
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(1, pixels.Color(255, 255, 0)); // F19
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(0, pixels.Color(255, 127, 0)); // F20
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
}
void lightsMode2() {
pixels.clear(); // Set all pixel colors to 'off'
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
// Top row
pixels.setPixelColor(4, pixels.Color(0, 0, 255)); // F13
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(5, pixels.Color(0, 0, 191)); // F14
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(6, pixels.Color(0, 0, 127)); // F15
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(7, pixels.Color(0, 0, 63)); // F16
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
// Bottom row
pixels.setPixelColor(3, pixels.Color(255, 0, 0)); // F17
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(2, pixels.Color(191, 0, 0)); // F18
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(1, pixels.Color(127, 0, 0)); // F19
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
pixels.setPixelColor(0, pixels.Color(63, 0, 0)); // F20
pixels.show(); // Send the updated pixel colors to the hardware.
delay(100);
}
void lightsOn(uint8_t state) {
if (state == LOW) {
lightsMode2();
}
else {
lightsMode1();
}
}
@BOSSoNe0013
Copy link
Author

Add Adalight serial interface so LEDs colors can managed with OpenRGB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment