From 9ae096ccfc28d8326c00d0c830d7972c51196151 Mon Sep 17 00:00:00 2001 From: Dennis Decoene Date: Fri, 9 Jan 2026 22:47:50 +0100 Subject: [PATCH] Add support for YAML configuration - Added a custom lightweight YAML stream parser in `yamlData.ino`. - Replicated the original project's memory-efficient "seek-and-find" logic to avoid external dependencies and high RAM usage. - Updated `MacroPad.ino` to automatically detect and prefer `config.yaml` over `config.xml`. - Updated `README.md` with instructions for the new YAML format. - Maintained full backward compatibility with existing XML configurations. --- .gitignore | 2 + Example SD Card/config.yaml | 33 ++++++++++++++ README.md | 43 ++++++++++++++++--- Software/MacroPad/MacroPad.ino | 28 ++++++------ Software/MacroPad/yamlData.ino | 78 ++++++++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 19 deletions(-) create mode 100644 .gitignore create mode 100644 Example SD Card/config.yaml create mode 100644 Software/MacroPad/yamlData.ino diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4775e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cpb.config.json +out/ \ No newline at end of file diff --git a/Example SD Card/config.yaml b/Example SD Card/config.yaml new file mode 100644 index 0000000..ed03fa9 --- /dev/null +++ b/Example SD Card/config.yaml @@ -0,0 +1,33 @@ +Settings: + LED_Mode: Bands + LED_Primary: [255, 0, 0] + LED_Secondary: [0, 0, 255] + Clicky_P: 0.5 + Clicky_I: 0.0 + Twist_P: 0.65 + Twist_I: 0.2 + Momentum_P: 0.3 + Momentum_I: 0.0 + +Profiles: + - name: "Example 1" + WheelMode: Clicky + WheelKey: 0 + Buttons: + - label: "Button 1", actions: [[0, 49], [0, 0], [0, 0]] + - label: "Button 2", actions: [[0, 50], [0, 0], [0, 0]] + - label: "Button 3", actions: [[0, 51], [0, 0], [0, 0]] + - label: "Button 4", actions: [[0, 52], [0, 0], [0, 0]] + - label: "Button 5", actions: [[0, 53], [0, 0], [0, 0]] + - label: "Button 6", actions: [[0, 54], [0, 0], [0, 0]] + + - name: "Media Control" + WheelMode: Momentum + WheelKey: 0 + Buttons: + - label: "Prev", actions: [[0, 177], [0, 0], [0, 0]] + - label: "Play", actions: [[0, 179], [0, 0], [0, 0]] + - label: "Next", actions: [[0, 176], [0, 0], [0, 0]] + - label: "Mute", actions: [[0, 173], [0, 0], [0, 0]] + - label: "Vol-", actions: [[0, 174], [0, 0], [0, 0]] + - label: "Vol+", actions: [[0, 175], [0, 0], [0, 0]] \ No newline at end of file diff --git a/README.md b/README.md index f850805..7eaeae2 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ A 6 button macropad with a display for button labels and a mouse knob with haptic feedback! -[Project Video Link](https://youtu.be/bNUKRJQjuvQ) +[Project Video Link](https://youtu.be/bNUKRJQjubQ) #### Features - 6 Programmable Macro Buttons - 128x64 OLED display for button labels and Icons - Support for up to 256 profiles for a total of 1536 Macros! -- Easy XML configuration, no special drivers required! +- Support for YAML or XML configuration, no special drivers required! - Macro button combinations can be configured with up to 3 simultaneous buttons or 3 seperate button presses with configurable delays between them. - Micro SD Storage for button labels and config files. - Haptic feedback mouse wheel with three different modes. Clicky, Twist and Momentum @@ -92,11 +92,42 @@ You will also need to have your SD card set up correctly in order to use the mac Copy the entire contents of the "Example SD Card" folder onto you SD card to begin with to ensure everything is working before you start working on your own files. -### XML Config +### Configuration (YAML or XML) + +The firmware will check for `config.yaml` first. If it is not found, it will look for `config.xml`. + +#### YAML Config (Recommended) +Create a `config.yaml` file on your SD card. It is much easier to read and edit than XML. + +```yaml +Settings: + LED_Mode: Bands + LED_Primary: [255, 0, 0] + LED_Secondary: [0, 0, 255] + Clicky_P: 0.5 + Clicky_I: 0.0 + Twist_P: 0.65 + Twist_I: 0.2 + Momentum_P: 0.3 + Momentum_I: 0.0 + +Profiles: + - name: "Example 1" + WheelMode: Clicky + WheelKey: 0 + Buttons: + - label: "Button 1", actions: [[0, 49], [0, 0], [0, 0]] + - label: "Button 2", actions: [[0, 50], [0, 0], [0, 0]] + - label: "Button 3", actions: [[0, 51], [0, 0], [0, 0]] + - label: "Button 4", actions: [[0, 52], [0, 0], [0, 0]] + - label: "Button 5", actions: [[0, 53], [0, 0], [0, 0]] + - label: "Button 6", actions: [[0, 54], [0, 0], [0, 0]] +``` +#### XML Config In the `` tag of the XML file you will find all of the settings for the LED's, along with the P and I tuning values for the various wheel modes. -There are 6 acceptable inpts for the `` tag. If you spell the words incorrectly the commands won't work, so it would be a good idea to copy and paste from here: +There are 6 acceptable inputs for the `` tag. If you spell the words incorrectly the commands won't work, so it would be a good idea to copy and paste from here: Breath, Bands, Halo, Rainbow, Solid, Off @@ -112,7 +143,7 @@ Then, there is a `` and `` tag. `` can be any key Next is a `` tag that holds all of our Macro buttons for the profile. Each macro button looks like this: -``` +```xml 0,68 0,0 @@ -123,4 +154,4 @@ Each macro button looks like this: There will be 6 of these sections per profile. Each action has two values, the first is the delay (in milliseconds) to perform before the action, followed by the keycode you wish to press (using the same website I linked earlier). Setting both of these values to 0 for any of the three actions will mean nothing happens for that action. Label is simply the name that will appear on screen for that button. -And that's it! just replicate that first example profile as many times as you like (up to 256 times, anyway) and each one will create a new profile that you can store your macros in. +And that's it! just replicate that first example profile as many times as you like (up to 256 times, anyway) and each one will create a new profile that you can store your macros in. \ No newline at end of file diff --git a/Software/MacroPad/MacroPad.ino b/Software/MacroPad/MacroPad.ino index ab13b9b..2ace1ad 100644 --- a/Software/MacroPad/MacroPad.ino +++ b/Software/MacroPad/MacroPad.ino @@ -59,6 +59,7 @@ const int _CS = 5; const int _SCK = 2; bool sdDetected = false; +bool useYaml = false; // Added for YAML support U8G2_SSD1309_128X64_NONAME0_1_4W_SW_SPI u8g2(U8G2_R0, 28, 22, 6, 7, 8); @@ -215,8 +216,6 @@ void setup1(){ //core 1 initialiseSD(); - loadSettings("/config.xml"); - calculateColourMultiplier(); u8g2.begin(); @@ -256,7 +255,7 @@ void buttonRead(){ //Read button inputs and set state arrays. if(profilePlusStarted){ if(activeProfile < totalProfiles - 1){ activeProfile++; - loadProfile("/config.xml", activeProfile); + if(useYaml) loadProfileYaml("/config.yaml", activeProfile); else loadProfile("/config.xml", activeProfile); storeLastProfile(); Serial.print("Stored last profile = "); Serial.println(activeProfile); @@ -272,14 +271,12 @@ void buttonRead(){ //Read button inputs and set state arrays. } else if(profileMinusStarted && profileChangeTimer + 100 < millis()){ profileSelectMenu = true; profileChangeTimer = millis(); - //delay(500); - //Serial.println("Trigger Menu"); } } else { if(profileMinusStarted){ if(activeProfile > 0 ){ activeProfile--; - loadProfile("/config.xml", activeProfile); + if(useYaml) loadProfileYaml("/config.yaml", activeProfile); else loadProfile("/config.xml", activeProfile); storeLastProfile(); Serial.print("Stored last profile = "); Serial.println(activeProfile); @@ -308,7 +305,7 @@ void buttonRead(){ //Read button inputs and set state arrays. profileSelectMenu = false; profileMinusStarted = false; profilePlusStarted = false; - loadProfile("/config.xml", activeProfile); + if(useYaml) loadProfileYaml("/config.yaml", activeProfile); else loadProfile("/config.xml", activeProfile); storeLastProfile(); Serial.print("Stored last profile = "); Serial.println(activeProfile); @@ -330,17 +327,22 @@ void initialiseSD(){ Serial.println("SD Initialised!"); } - totalProfiles = countProfiles("/config.xml"); - if(debug){ - Serial.print("Profiles found: "); - Serial.println(totalProfiles); + // Detection logic for YAML vs XML + if (SD.exists("/config.yaml")) { + useYaml = true; + totalProfiles = countProfilesYaml("/config.yaml"); + loadSettingsYaml("/config.yaml"); + } else { + useYaml = false; + totalProfiles = countProfiles("/config.xml"); + loadSettings("/config.xml"); } activeProfile = readLastProfile(); Serial.print("Active Profile = "); Serial.println(activeProfile); - loadProfile("/config.xml", activeProfile); + if(useYaml) loadProfileYaml("/config.yaml", activeProfile); else loadProfile("/config.xml", activeProfile); loadButtonIcons(); } @@ -454,4 +456,4 @@ int readLastProfile(){ } return (uint8_t)file.parseInt(); -} +} \ No newline at end of file diff --git a/Software/MacroPad/yamlData.ino b/Software/MacroPad/yamlData.ino new file mode 100644 index 0000000..1d2bbe3 --- /dev/null +++ b/Software/MacroPad/yamlData.ino @@ -0,0 +1,78 @@ +// Helper for YAML strings +const char* yamlClean(String s) { + s.trim(); + if (s.startsWith("\"") || s.startsWith("'")) s = s.substring(1, s.length() - 1); + return s.c_str(); +} + +uint16_t countProfilesYaml(const char *filename) { + File file = SD.open(filename); + if (!file) return 0; + uint16_t count = 0; + file.find("Profiles:"); + while (file.find("- name:")) { + String n = file.readStringUntil('\n'); + n.trim(); + if (n.startsWith("\"")) n = n.substring(1, n.length() - 1); + // Note: Using author's original indexing logic + strncpy(profileNames[count], n.c_str(), maxProfiles); + count++; + } + file.close(); + return count; +} + +bool loadSettingsYaml(const char *filename) { + File file = SD.open(filename); + if (!file) return false; + if (!file.find("Settings:")) return false; + if (file.find("LED_Mode:")) { + String m = file.readStringUntil('\n'); + ledMode = parseLEDMode(yamlClean(m)); + } + if (file.find("LED_Primary: [")) { + primaryColour[0] = file.parseInt(); primaryColour[1] = file.parseInt(); primaryColour[2] = file.parseInt(); + } + if (file.find("LED_Secondary: [")) { + secondaryColour[0] = file.parseInt(); secondaryColour[1] = file.parseInt(); secondaryColour[2] = file.parseInt(); + } + if (file.find("Clicky_P:")) Clicky_P = file.parseFloat(); + if (file.find("Clicky_I:")) Clicky_I = file.parseFloat(); + if (file.find("Twist_P:")) Twist_P = file.parseFloat(); + if (file.find("Twist_I:")) Twist_I = file.parseFloat(); + if (file.find("Momentum_P:")) Momentum_P = file.parseFloat(); + if (file.find("Momentum_I:")) Momentum_I = file.parseFloat(); + file.close(); + return true; +} + +bool loadProfileYaml(const char *filename, uint16_t index) { + File file = SD.open(filename); + if (!file) return false; + file.find("Profiles:"); + for (uint16_t i = 0; i <= index; i++) { + if (!file.find("- name:")) { file.close(); return false; } + } + String n = file.readStringUntil('\n'); + strncpy(profileName, yamlClean(n), sizeof(profileName)); + if (file.find("WheelMode:")) { + String wm = file.readStringUntil('\n'); + wheelMode = parseWheelMode(yamlClean(wm)); + } + if (file.find("WheelKey:")) wheelAction = (uint8_t)file.parseInt(); + memset(macroAction, 0, sizeof(macroAction)); + memset(macroDelay, 0, sizeof(macroDelay)); + for (uint8_t btn = 0; btn < 6; btn++) { + if (!file.find("label:")) break; + String lbl = file.readStringUntil(','); + strncpy(buttonLabel[btn], yamlClean(lbl), sizeof(buttonLabel[btn])); + file.find("actions: ["); + for (uint8_t act = 0; act < 3; act++) { + file.find("["); + macroDelay[btn][act] = (uint16_t)file.parseInt(); + macroAction[btn][act] = (uint8_t)file.parseInt(); + } + } + file.close(); + return true; +} \ No newline at end of file