#include <Wire.h>
#include <Mouse.h>
#include <Keyboard.h>
#include <EEPROM.h>
#define AS5600_ADDR 0x36
#define RAW_ANGLE_HIGH 0x0C
#define RAW_ANGLE_LOW 0x0D
#define NUM_BUTTONS 4
const uint8_t buttonPins[NUM_BUTTONS] = {7, 6, 5, 4};
#define NAME_LEN 16
#define COMBO_LEN 32
#define RECORD_SIZE (NAME_LEN + COMBO_LEN + 2)
#define CONFIG_SIZE (NUM_BUTTONS * RECORD_SIZE + 1)
#define MAX_ANGLE 4096
#define SCROLL_STEP 300
char buttonNames[NUM_BUTTONS][NAME_LEN + 1];
char buttonCombos[NUM_BUTTONS][COMBO_LEN + 1];
uint8_t buttonKeyCount[NUM_BUTTONS];
uint8_t buttonKeyCodes[NUM_BUTTONS][5];
bool isStringMode[NUM_BUTTONS];
bool buttonStates[NUM_BUTTONS] = {false};
int8_t scrollDirection = -1;
int32_t totalAngle = 0;
int32_t lastRawAngle = 0;
void setup() {
Serial.begin(115200);
Wire.begin();
Mouse.begin();
Keyboard.begin();
for (uint8_t i = 0; i < NUM_BUTTONS; i++) pinMode(buttonPins[i], INPUT_PULLUP);
loadConfig();
delay(200);
}
void loop() {
handleEncoderScroll();
handleSerial();
handleButtons();
delay(10);
}
void handleEncoderScroll() {
int32_t rawAngle = readRawAngle();
int32_t delta = calculateAngleDelta(rawAngle, lastRawAngle);
totalAngle += delta;
if (abs(totalAngle) >= SCROLL_STEP) {
int steps = totalAngle / SCROLL_STEP;
Mouse.move(0, 0, steps * scrollDirection);
totalAngle %= SCROLL_STEP;
}
lastRawAngle = rawAngle;
}
int32_t readRawAngle() {
Wire.beginTransmission(AS5600_ADDR);
Wire.write(RAW_ANGLE_HIGH);
Wire.endTransmission();
Wire.requestFrom(AS5600_ADDR, 2);
uint16_t high = Wire.read();
uint16_t low = Wire.read();
return (high << 8) | low;
}
int32_t calculateAngleDelta(int32_t current, int32_t last) {
int32_t delta = current - last;
if (delta > MAX_ANGLE / 2) delta -= MAX_ANGLE;
if (delta < -MAX_ANGLE / 2) delta += MAX_ANGLE;
return delta;
}
void handleButtons() {
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
bool pressed = digitalRead(buttonPins[i]) == LOW;
if (pressed && !buttonStates[i]) {
if (isStringMode[i]) {
Keyboard.print(buttonCombos[i] + 4); // skip "STR:"
} else {
for (uint8_t k = 0; k < buttonKeyCount[i]; k++) {
Keyboard.press(buttonKeyCodes[i][k]);
}
}
buttonStates[i] = true;
} else if (!pressed && buttonStates[i]) {
if (!isStringMode[i]) {
for (uint8_t k = 0; k < buttonKeyCount[i]; k++) {
Keyboard.release(buttonKeyCodes[i][k]);
}
}
buttonStates[i] = false;
}
}
}
void handleSerial() {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd == "GET_CONFIG") {
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
Serial.print(buttonNames[i]);
Serial.print("|");
Serial.print(buttonCombos[i]);
Serial.print(";");
}
Serial.print("SCROLL_DIR=");
Serial.println(scrollDirection);
} else if (cmd.startsWith("SET_CONFIG ")) {
String payload = cmd.substring(11);
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
int sep = payload.indexOf(';');
String entry = (sep != -1) ? payload.substring(0, sep) : payload;
int pipe = entry.indexOf('|');
if (pipe != -1) {
entry.substring(0, pipe).toCharArray(buttonNames[i], NAME_LEN);
entry.substring(pipe + 1).toCharArray(buttonCombos[i], COMBO_LEN);
}
if (sep == -1) {
payload = "";
break;
}
payload = payload.substring(sep + 1);
}
// Handle SCROLL_DIR=±1
if (payload.startsWith("SCROLL_DIR=")) {
int val = payload.substring(11).toInt();
if (val == 1 || val == -1) {
scrollDirection = val;
}
}
saveConfig();
Serial.println("CONFIG SAVED");
}
}
}
void saveConfig() {
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
int base = i * RECORD_SIZE;
for (uint8_t j = 0; j < NAME_LEN; j++) EEPROM.update(base + j, buttonNames[i][j]);
EEPROM.update(base + NAME_LEN, '\0');
for (uint8_t j = 0; j < COMBO_LEN; j++) EEPROM.update(base + NAME_LEN + 1 + j, buttonCombos[i][j]);
EEPROM.update(base + NAME_LEN + 1 + COMBO_LEN, '\0');
parseCombo(i);
}
EEPROM.update(NUM_BUTTONS * RECORD_SIZE, scrollDirection);
}
void loadConfig() {
const char* defaults[] = {"ESC", "ENTER", "SPACE", "SHIFT"};
for (uint8_t i = 0; i < NUM_BUTTONS; i++) {
int base = i * RECORD_SIZE;
for (uint8_t j = 0; j < NAME_LEN; j++) {
char c = EEPROM.read(base + j);
buttonNames[i][j] = (c == 0xFF || !isPrintable(c)) ? 0 : c;
}
buttonNames[i][NAME_LEN] = '\0';
for (uint8_t j = 0; j < COMBO_LEN; j++) {
char c = EEPROM.read(base + NAME_LEN + 1 + j);
buttonCombos[i][j] = (c == 0xFF || !isPrintable(c)) ? 0 : c;
}
buttonCombos[i][COMBO_LEN] = '\0';
if (buttonCombos[i][0] == '\0') {
strcpy(buttonNames[i], (String("Button") + (i + 1)).c_str());
strcpy(buttonCombos[i], defaults[i]);
}
parseCombo(i);
}
scrollDirection = EEPROM.read(NUM_BUTTONS * RECORD_SIZE);
if (scrollDirection != 1 && scrollDirection != -1) scrollDirection = 1;
}
void parseCombo(uint8_t index) {
isStringMode[index] = false;
buttonKeyCount[index] = 0;
if (strncmp(buttonCombos[index], "STR:", 4) == 0) {
isStringMode[index] = true;
return;
}
char combo[COMBO_LEN + 1];
strncpy(combo, buttonCombos[index], COMBO_LEN);
combo[COMBO_LEN] = '\0';
char* token = strtok(combo, "+");
while (token && buttonKeyCount[index] < 5) {
if (strcasecmp(token, "CTRL") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_CTRL;
else if (strcasecmp(token, "SHIFT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_SHIFT;
else if (strcasecmp(token, "ALT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_ALT;
else if (strcasecmp(token, "GUI") == 0 || strcasecmp(token, "WIN") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_LEFT_GUI;
else if (strcasecmp(token, "ESC") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_ESC;
else if (strcasecmp(token, "TAB") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_TAB;
else if (strcasecmp(token, "BACKSPACE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_BACKSPACE;
else if (strcasecmp(token, "PRIOR") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_PAGE_UP;
else if (strcasecmp(token, "NEXT") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_PAGE_DOWN;
else if (strcasecmp(token, "CAPS_LOCK") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_CAPS_LOCK;
else if (strcasecmp(token, "DELETE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_DELETE;
else if (strcasecmp(token, "ENTER") == 0 || strcasecmp(token, "RETURN") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = KEY_RETURN;
else if (strcasecmp(token, "SPACE") == 0) buttonKeyCodes[index][buttonKeyCount[index]++] = ' ';
else if (strlen(token) == 1) buttonKeyCodes[index][buttonKeyCount[index]++] = token[0];
token = strtok(NULL, "+");
}
}