Last active
May 12, 2023 01:15
-
-
Save BOSSoNe0013/a4b44349dfb8e1a2ab98b089495b2b09 to your computer and use it in GitHub Desktop.
Code for my macro keyboard mod https://www.thingiverse.com/make:914894
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
#### | |
## 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(); | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add Adalight serial interface so LEDs colors can managed with OpenRGB