Skip to content

Instantly share code, notes, and snippets.

@mistic100
Last active May 25, 2026 18:47
Show Gist options
  • Select an option

  • Save mistic100/90f497f09066c74cb504050a5e39fc19 to your computer and use it in GitHub Desktop.

Select an option

Save mistic100/90f497f09066c74cb504050a5e39fc19 to your computer and use it in GitHub Desktop.
/**
* 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