Last active
May 25, 2026 18:47
-
-
Save mistic100/90f497f09066c74cb504050a5e39fc19 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| /** | |
| * Firmware for my 5 keys + 1 encoder macropad | |
| * Link: https://photos.strangeplanet.fr/index.php?/category/269-macropad_mai_2026 | |
| * | |
| * Supports multiple profiles. | |
| * The encoder is used to switch profile and configure the colors | |
| * (long press: brightness -> short press: color 1 -> short press: color 2 -> short press: idle) | |
| * | |
| * BOM: | |
| * - RP2040 Zero | |
| * - 5 MX keys | |
| * - EC11 encoder | |
| * - 5mm adressable LED strip (24 LEDs) | |
| */ | |
| #include <Arduino.h> | |
| #include <Button2.h> | |
| #include <FastLED.h> | |
| #include <Preferences.h> | |
| #include <Keyboard.h> | |
| // Pin mapping | |
| #define PIN_GROUND 10 | |
| #define PIN_KEY_A 13 | |
| #define PIN_KEY_B 12 | |
| #define PIN_KEY_C 14 | |
| #define PIN_KEY_D 11 | |
| #define PIN_KEY_E 9 | |
| #define PIN_ENCODER_A 7 | |
| #define PIN_ENCODER_B 6 | |
| #define PIN_ENCODER_BUTTON 8 | |
| #define PIN_LED_DATA 29 | |
| // Device Sizing | |
| #define NUM_PROFILES 3 | |
| #define NUM_KEYS 5 | |
| #define LED_COUNT 24 | |
| #define BACK_LED_COUNT 3 | |
| // Timing constants | |
| #define MODE_ENTER_LONG_PRESS_MS 500 | |
| #define ENCODER_SCAN_INTERVAL_MS 1 | |
| #define SETTINGS_SAVE_DELAY_MS 5000 | |
| // LED defaults and limits | |
| #define DEFAULT_BRIGHTNESS 96 | |
| #define MIN_BRIGHTNESS 8 | |
| #define MAX_BRIGHTNESS 255 | |
| #define BRIGHTNESS_STEP 8 | |
| // Profile indicator LEDS | |
| const uint8_t PROFILE_INDICATORS[NUM_PROFILES][2] = { | |
| {6, 8}, | |
| {11, 12}, | |
| {15, 17}, | |
| }; | |
| // Modifier bitmask values for KeyAction.modifiers | |
| #define MOD_NONE 0x00 | |
| #define MOD_CTRL 0x01 | |
| #define MOD_SHIFT 0x02 | |
| #define MOD_ALT 0x04 | |
| // Encoder transition filter | |
| #define ENCODER_INVALID 2 | |
| #define ENCODER_STEPS_PER_DETENT 4 | |
| // Preferences storage keys | |
| #define PREF_NAMESPACE "macropad" | |
| #define PREF_KEY_PROFILE "profile" | |
| #define PREF_KEY_BRIGHTNESS "brightness" | |
| const char* PREF_KEY_COLOR[NUM_PROFILES][2] = { | |
| {"color1", "color12"}, | |
| {"color2", "color22"}, | |
| {"color3", "color32"}, | |
| }; | |
| enum SystemMode : uint8_t | |
| { | |
| MODE_NORMAL = 0, | |
| MODE_BRIGHTNESS_CONFIG, | |
| MODE_COLOR1_CONFIG, | |
| MODE_COLOR2_CONFIG, | |
| }; | |
| struct KeyAction | |
| { | |
| uint8_t keycode; | |
| uint8_t modifiers; | |
| }; | |
| KeyAction profiles[NUM_PROFILES][NUM_KEYS] = { | |
| { | |
| {KEY_F1, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F2, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F3, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F4, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F5, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| }, | |
| { | |
| {KEY_F6, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F7, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F8, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F9, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F10, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| }, | |
| { | |
| {KEY_F11, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F12, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F13, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F14, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| {KEY_F15, MOD_CTRL | MOD_SHIFT | MOD_ALT}, | |
| }, | |
| }; | |
| // Quadratic transition matrix: [prevState][newState] -> -1, +1, ENCODER_INVALID, or 0. | |
| const int8_t encoderTransition[4][4] = { | |
| {0, -1, 1, ENCODER_INVALID}, | |
| {1, 0, ENCODER_INVALID, -1}, | |
| {-1, ENCODER_INVALID, 0, 1}, | |
| {ENCODER_INVALID, 1, -1, 0}, | |
| }; | |
| CRGB leds[LED_COUNT]; | |
| Preferences preferences; | |
| Button2 keyAButton; | |
| Button2 keyBButton; | |
| Button2 keyCButton; | |
| Button2 keyDButton; | |
| Button2 keyEButton; | |
| Button2 encoderButton; | |
| SystemMode mode = MODE_NORMAL; | |
| uint8_t activeProfile = 0; | |
| uint8_t ledBrightness = DEFAULT_BRIGHTNESS; | |
| uint8_t ledColor[NUM_PROFILES][2] = { | |
| {0, 0}, // red | |
| {96, 96}, // green | |
| {160, 160}, // blue | |
| }; | |
| bool ledDirty = true; | |
| uint32_t encoderLastScanMs = 0; | |
| uint8_t encoderPrevState = 0; | |
| int8_t encoderAccumulated = 0; | |
| bool settingsDirty = false; | |
| uint32_t lastSettingsChangeMs = 0; | |
| void setMode(SystemMode newMode) | |
| { | |
| if (mode != newMode) | |
| { | |
| mode = newMode; | |
| ledDirty = true; | |
| } | |
| } | |
| void markSettingsDirty() | |
| { | |
| settingsDirty = true; | |
| lastSettingsChangeMs = millis(); | |
| } | |
| void saveSettingsNow() | |
| { | |
| preferences.putUChar(PREF_KEY_PROFILE, activeProfile); | |
| preferences.putUChar(PREF_KEY_BRIGHTNESS, ledBrightness); | |
| for (uint8_t i = 0; i < NUM_PROFILES; i++) | |
| { | |
| preferences.putUChar(PREF_KEY_COLOR[i][0], ledColor[i][0]); | |
| preferences.putUChar(PREF_KEY_COLOR[i][1], ledColor[i][1]); | |
| } | |
| } | |
| void flushSettingsIfNeeded(uint32_t nowMs) | |
| { | |
| if (!settingsDirty) | |
| { | |
| return; | |
| } | |
| if ((nowMs - lastSettingsChangeMs) < SETTINGS_SAVE_DELAY_MS) | |
| { | |
| return; | |
| } | |
| saveSettingsNow(); | |
| settingsDirty = false; | |
| } | |
| void loadSettings() | |
| { | |
| activeProfile = preferences.getUChar(PREF_KEY_PROFILE, 0); | |
| if (activeProfile >= NUM_PROFILES) | |
| { | |
| activeProfile = 0; | |
| } | |
| ledBrightness = preferences.getUChar(PREF_KEY_BRIGHTNESS, DEFAULT_BRIGHTNESS); | |
| if (ledBrightness < MIN_BRIGHTNESS) | |
| { | |
| ledBrightness = MIN_BRIGHTNESS; | |
| } | |
| if (ledBrightness > MAX_BRIGHTNESS) | |
| { | |
| ledBrightness = MAX_BRIGHTNESS; | |
| } | |
| for (uint8_t i = 0; i < NUM_PROFILES; i++) | |
| { | |
| ledColor[i][0] = preferences.getUChar(PREF_KEY_COLOR[i][0], ledColor[i][0]); | |
| ledColor[i][1] = preferences.getUChar(PREF_KEY_COLOR[i][1], ledColor[i][1]); | |
| } | |
| } | |
| uint8_t readEncoderState() | |
| { | |
| const uint8_t a = static_cast<uint8_t>(digitalRead(PIN_ENCODER_A)); | |
| const uint8_t b = static_cast<uint8_t>(digitalRead(PIN_ENCODER_B)); | |
| return static_cast<uint8_t>((a << 1U) | b); | |
| } | |
| void sendKeyAction(const KeyAction &action) | |
| { | |
| if (action.keycode == 0) | |
| { | |
| return; | |
| } | |
| if ((action.modifiers & MOD_CTRL) != 0) | |
| { | |
| Keyboard.press(KEY_LEFT_CTRL); | |
| } | |
| if ((action.modifiers & MOD_SHIFT) != 0) | |
| { | |
| Keyboard.press(KEY_LEFT_SHIFT); | |
| } | |
| if ((action.modifiers & MOD_ALT) != 0) | |
| { | |
| Keyboard.press(KEY_LEFT_ALT); | |
| } | |
| Keyboard.press(action.keycode); | |
| delay(5); | |
| Keyboard.releaseAll(); | |
| } | |
| void keyAPressedHandler(Button2 &) { sendKeyAction(profiles[activeProfile][0]); } | |
| void keyBPressedHandler(Button2 &) { sendKeyAction(profiles[activeProfile][1]); } | |
| void keyCPressedHandler(Button2 &) { sendKeyAction(profiles[activeProfile][2]); } | |
| void keyDPressedHandler(Button2 &) { sendKeyAction(profiles[activeProfile][3]); } | |
| void keyEPressedHandler(Button2 &) { sendKeyAction(profiles[activeProfile][4]); } | |
| void encoderButtonPressHandler(Button2 &) | |
| { | |
| switch (mode) | |
| { | |
| case MODE_BRIGHTNESS_CONFIG: | |
| setMode(MODE_COLOR1_CONFIG); | |
| break; | |
| case MODE_COLOR1_CONFIG: | |
| setMode(MODE_COLOR2_CONFIG); | |
| break; | |
| case MODE_COLOR2_CONFIG: | |
| setMode(MODE_NORMAL); | |
| break; | |
| } | |
| } | |
| void encoderButtonLongClickHandler(Button2 &) | |
| { | |
| if (mode == MODE_NORMAL) | |
| { | |
| setMode(MODE_BRIGHTNESS_CONFIG); | |
| } | |
| } | |
| void applyEncoderStep(int8_t step) | |
| { | |
| if (step == 0) | |
| { | |
| return; | |
| } | |
| switch (mode) | |
| { | |
| case MODE_NORMAL: | |
| { | |
| int8_t next = static_cast<int8_t>(activeProfile) + (step > 0 ? 1 : -1); | |
| if (next < 0) | |
| { | |
| next = NUM_PROFILES - 1; | |
| } | |
| else if (next >= NUM_PROFILES) | |
| { | |
| next = 0; | |
| } | |
| activeProfile = static_cast<uint8_t>(next); | |
| markSettingsDirty(); | |
| ledDirty = true; | |
| break; | |
| } | |
| case MODE_BRIGHTNESS_CONFIG: | |
| { | |
| int16_t nextValue = static_cast<int16_t>(ledBrightness) + (step > 0 ? BRIGHTNESS_STEP : -BRIGHTNESS_STEP); | |
| if (nextValue < MIN_BRIGHTNESS) | |
| { | |
| nextValue = MIN_BRIGHTNESS; | |
| } | |
| if (nextValue > MAX_BRIGHTNESS) | |
| { | |
| nextValue = MAX_BRIGHTNESS; | |
| } | |
| ledBrightness = static_cast<uint8_t>(nextValue); | |
| markSettingsDirty(); | |
| ledDirty = true; | |
| break; | |
| } | |
| case MODE_COLOR1_CONFIG: | |
| { | |
| ledColor[activeProfile][0] += (step > 0 ? 10 : -10); | |
| markSettingsDirty(); | |
| ledDirty = true; | |
| break; | |
| } | |
| case MODE_COLOR2_CONFIG: | |
| { | |
| ledColor[activeProfile][1] += (step > 0 ? 10 : -10); | |
| markSettingsDirty(); | |
| ledDirty = true; | |
| break; | |
| } | |
| } | |
| } | |
| void processEncoder(const uint32_t nowMs) | |
| { | |
| if ((nowMs - encoderLastScanMs) < ENCODER_SCAN_INTERVAL_MS) | |
| { | |
| return; | |
| } | |
| encoderLastScanMs = nowMs; | |
| const uint8_t state = readEncoderState(); | |
| if (state == encoderPrevState) | |
| { | |
| return; | |
| } | |
| const int8_t transition = encoderTransition[encoderPrevState][state]; | |
| encoderPrevState = state; | |
| if (transition == ENCODER_INVALID) | |
| { | |
| encoderAccumulated = 0; | |
| return; | |
| } | |
| if (transition == 0) | |
| { | |
| return; | |
| } | |
| encoderAccumulated += transition; | |
| if (encoderAccumulated >= ENCODER_STEPS_PER_DETENT) | |
| { | |
| encoderAccumulated = 0; | |
| applyEncoderStep(1); | |
| } | |
| else if (encoderAccumulated <= -ENCODER_STEPS_PER_DETENT) | |
| { | |
| encoderAccumulated = 0; | |
| applyEncoderStep(-1); | |
| } | |
| } | |
| void renderLedsIfNeeded() | |
| { | |
| if (!ledDirty) | |
| { | |
| return; | |
| } | |
| auto color1 = CHSV(ledColor[activeProfile][0], 255, 255); | |
| auto color2 = CHSV(ledColor[activeProfile][1], 255, 255); | |
| fill_solid(leds, BACK_LED_COUNT, color1); | |
| fill_gradient(leds, BACK_LED_COUNT, color1, LED_COUNT-BACK_LED_COUNT-1, color2); | |
| fill_solid(leds+LED_COUNT-BACK_LED_COUNT-1, BACK_LED_COUNT, color2); | |
| if (mode == MODE_NORMAL) | |
| { | |
| for (uint8_t i = PROFILE_INDICATORS[activeProfile][0]; i <= PROFILE_INDICATORS[activeProfile][1]; ++i) | |
| { | |
| leds[i] = CRGB::White; | |
| } | |
| } | |
| FastLED.setBrightness(ledBrightness); | |
| FastLED.show(); | |
| ledDirty = false; | |
| } | |
| void setup() | |
| { | |
| pinMode(PIN_GROUND, OUTPUT); | |
| digitalWrite(PIN_GROUND, LOW); | |
| pinMode(PIN_ENCODER_A, INPUT_PULLUP); | |
| pinMode(PIN_ENCODER_B, INPUT_PULLUP); | |
| keyAButton.begin(PIN_KEY_A); | |
| keyBButton.begin(PIN_KEY_B); | |
| keyCButton.begin(PIN_KEY_C); | |
| keyDButton.begin(PIN_KEY_D); | |
| keyEButton.begin(PIN_KEY_E); | |
| encoderButton.begin(PIN_ENCODER_BUTTON); | |
| encoderButton.setLongClickTime(MODE_ENTER_LONG_PRESS_MS); | |
| keyAButton.setPressedHandler(keyAPressedHandler); | |
| keyBButton.setPressedHandler(keyBPressedHandler); | |
| keyCButton.setPressedHandler(keyCPressedHandler); | |
| keyDButton.setPressedHandler(keyDPressedHandler); | |
| keyEButton.setPressedHandler(keyEPressedHandler); | |
| encoderButton.setPressedHandler(encoderButtonPressHandler); | |
| encoderButton.setLongClickDetectedHandler(encoderButtonLongClickHandler); | |
| preferences.begin(PREF_NAMESPACE); | |
| loadSettings(); | |
| encoderPrevState = readEncoderState(); | |
| FastLED.addLeds<WS2812B, PIN_LED_DATA, GRB>(leds, LED_COUNT); | |
| FastLED.clear(true); | |
| Keyboard.begin(); | |
| renderLedsIfNeeded(); | |
| } | |
| void loop() | |
| { | |
| const uint32_t nowMs = millis(); | |
| keyAButton.loop(); | |
| keyBButton.loop(); | |
| keyCButton.loop(); | |
| keyDButton.loop(); | |
| keyEButton.loop(); | |
| encoderButton.loop(); | |
| processEncoder(nowMs); | |
| flushSettingsIfNeeded(nowMs); | |
| renderLedsIfNeeded(); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment