Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cpb.config.json
out/
33 changes: 33 additions & 0 deletions Example SD Card/config.yaml
Original file line number Diff line number Diff line change
@@ -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]]
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 `<Settings>` 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 `<LED_Mode>` 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 `<LED_Mode>` 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

Expand All @@ -112,7 +143,7 @@ Then, there is a `<WheelMode>` and `<WheelKey>` tag. `<WheelKey>` can be any key
Next is a `<MacroButtons>` tag that holds all of our Macro buttons for the profile.

Each macro button looks like this:
```
```xml
<MacroButton>
<Action>0,68</Action>
<Action>0,0</Action>
Expand All @@ -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.
28 changes: 15 additions & 13 deletions Software/MacroPad/MacroPad.ino
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -215,8 +216,6 @@ void setup1(){ //core 1

initialiseSD();

loadSettings("/config.xml");

calculateColourMultiplier();

u8g2.begin();
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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();
}

Expand Down Expand Up @@ -454,4 +456,4 @@ int readLastProfile(){
}

return (uint8_t)file.parseInt();
}
}
78 changes: 78 additions & 0 deletions Software/MacroPad/yamlData.ino
Original file line number Diff line number Diff line change
@@ -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;
}