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