├── .gitignore ├── README.md ├── ble_button_box.ino ├── images ├── AdafruitHuzzah32PinDiagram.png └── wiring.jpg └── stl ├── button box.stl ├── faceplate.stl ├── funky switch knob.stl ├── knob with d hole.stl └── multi-direction-switch-clamp.stl /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluetooth Button Box 2 | 3 | This is code that I run on a Huzzah32 Feather from Adafruit. It lives inside of the button box of my sim racing wheel. This gives me 3 encoders, a "funky switch", & 20+ buttons, all wirelessly. The Huzzah 32 ESP32 board has a BMS built in, and will charge the lithium cell whether the board is powered or not. I use a magnetic USB-Micro-B connector for quick connect and disconnect for charging. with ~1400 mAh in the cell I can get close to 24 hours of continuous on time. 4 | 5 | * Originally forked from [Magnus Thome's implementation](https://github.com/MagnusThome/ESP32-BLE-Gamepad). 6 | * Uses [John Main's code from www.best-microcontroller-projects.com](https://www.best-microcontroller-projects.com/rotary-encoder.html). 7 | * Huzzah32 Battery Level code adapted from [Jon Froehlich's contrubution](https://github.com/makeabilitylab/arduino/commit/0b58c0b3b3194f50a81efa4008637b5f90e681fb). 8 | 9 | ## Parts List 10 | 11 | * Micro controller [Adafruit HUZZAH32 – ESP32 Feather Board](https://www.adafruit.com/product/3405) (other ESP32 boards should work fine too) 12 | * Ball spring detent encoders: [PEC11H-4020F-S0016](https://www.mouser.com/ProductDetail/652-PEC11H4020FS0016/) (most 6mm encoders will fit fine) 13 | * Multi direction switch [RKJXT1F42001](https://www.mouser.com/ProductDetail/688-RKJXT1F42001/) (funky switch) 14 | * 12MM Tactile Switch 260GF 7.3MM [653-B3F-4155](https://www.adafruit.com/product/1009) 15 | * i2c display module [SSD1306 (128x32)](https://www.amazon.com/gp/product/B08L7QW7SR) 16 | * 3.7V Lipo cell with JST PH 2-Pin connector. 17 | *CHECK POLARITY OF THE CONNECTOR!* De-pin and swap the connector's pins if necessary: [Adafruit](https://www.adafruit.com/product/2011) or [Amazon](https://www.amazon.com/gp/product/B08FD39Y5R) 18 | * 3D printed parts in the [STL directory](./stl); you'll probably design your own enclosure though. 19 | * Power switch (SPST). This is used to ground the `EN` pin which shuts off the 3.3V regulator but leaves the charging circuit powered. 20 | 21 | 22 | ### Prerequisites 23 | 24 | * Install the Arduino IDE 25 | * Install the board definitions for the ESP32 26 | * Install the needed libraries (listed below) 27 | 28 | ```cpp 29 | #include // https://github.com/adafruit/Adafruit-GFX-Library 30 | #include // https://github.com/adafruit/Adafruit_SSD1306 31 | #include // https://github.com/madhephaestus/ESP32Encoder/ 32 | #include // https://github.com/Chris--A/Keypad 33 | #include // https://github.com/lemmingDev/ESP32-BLE-Gamepad 34 | ``` 35 | 36 | 37 | ### Wiring 38 | 39 | ![Huzzah 32 Wiring Diagram](./images/wiring.jpg) 40 | 41 | #### Button Matrix 42 | 43 | You can use one button matrix for all of your push buttons, your shifters, and the encoder push function. Wiring it this way is more difficult/confusing than simply using one GPIO per button, but it's far more efficient (uses fewer pins). 44 | 45 | **A note about Pin `13`** I was not able to use GPIO `13` for the button matrix (I was experiencing glitching in the matrix with all the other buttons). You may have more luck using it. I swapped the pin over to GPIO `4` afterwards. 46 | 47 | There should be no reason you couldn't add more rows or columns to the matrix using GPI `36`, `39`, `34`, and GPIO `13`; if they are avaialble in your final wiring plan. I have not tested this. 48 | 49 | #### Power Switch 50 | 51 | You have two options. You can either connect your switch between `EN` and `GND` or you can just add the switch between the positive lead of your Lipo cell. Using the `EN` pin is easier, but there could be reasons you want to switch it at the power source. This is up to you. 52 | 53 | #### Encoders 54 | 55 | There should be no reason you couldn't add two more encoders to the layout (if you wanted to) using GPI `36`, `39`, `34`, and GPIO `13`; if they are avaialble in your final wiring plan. I have not tested this. 56 | 57 | #### OLED Screen & Custom i2c Pins 58 | 59 | I adde the OLED screen after the encoders and buttons were already mapped & built. At the time I had no idea I would have wanted to add an i2c device, so I used the Huzzah's default i2c pins for the button matrix (GPIOs `22` & `23`). With the ESP 32, you can just re-assign these in the `Wire.begin()`, so in the provided code, I have assinged them to GPIOs `25` & `26`. 60 | 61 | ```cpp 62 | // Custom i2c pins and address for Adafruit OLED library 63 | Wire.begin(CUSTOM_I2C_SDA, CUSTOM_I2C_SCL, SCREEN_ADDRESS); 64 | ``` 65 | 66 | ----- 67 | 68 | ### Board Settings 69 | 70 | These are my VS Code settings for interfacing with the board (your port will be different): 71 | ```json 72 | { 73 | "port": "COM6", // find your Port and use it here 74 | "board": "esp32:esp32:featheresp32", 75 | "configuration": "FlashFreq=80,UploadSpeed=921600,DebugLevel=none,PartitionScheme=default", 76 | "sketch": "ble_button_box.ino", 77 | "output": "../build" 78 | } 79 | ``` 80 | 81 | ### Button Matrix 82 | 83 | I'm not using any diodes, but you can implement them if you need to. I am using a 5x4 matrix for a total of 20 buttons: 84 | 85 | ```cpp 86 | #define ROWS 5 87 | #define COLS 4 88 | uint8_t rowPins[ROWS] = {15, 32, 14, 22, 23}; 89 | uint8_t colPins[COLS] = {4, 12, 27, 33}; 90 | ``` 91 | 92 | * 5 buttons for the funky switch (4 directions and push) 93 | * 2 buttons for shifters (microswitches) 94 | * 2 buttons for the encoders (push) 95 | * 11 face buttons (push) 96 | 97 | You need to define the HID button numbers you want the matrix buttons to identify as. In my example I am just counting up from `1..20` (you can tell that I counted incorrectly and skipped `4` while starting from `0`; no matter). 98 | 99 | ```cpp 100 | byte keymap[ROWS][COLS] = { 101 | { 0, 1, 2, 3}, 102 | { 5, 6, 7, 8}, 103 | { 9,10,11,12}, 104 | {13,14,15,16}, 105 | {17,18,19,20} 106 | }; 107 | ``` 108 | 109 | ### Funky Switch Issues 110 | 111 | A note about the funky switch; you may notice that I'm doing some strange stuff here: 112 | 113 | ```cpp 114 | #define FUNKY_DIR_COUNT 4 115 | #define FUNKY_DIR_HOLDOFF_TIME 300 // 0.3sec 116 | unsigned long funkyDirectionLastHoldoff[4] = {0,0,0,0}; 117 | char funkyCenterCode = 0; 118 | char funkyDirectionCodes[FUNKY_DIR_COUNT] = {13, 9, 5, 17}; 119 | bool handlingFunkySwitch = false; 120 | ``` 121 | 122 | The Alps funky switch presses the center push for every direction press; it's hardwired that way. In order to code around this, I am checking to see if one of the four directions is pressed (the funky switch ended up being the first column [`0`, `5`, `9`, `13`, `17`] in my wiring). The array defines the `up`, `left`, `right`, `down` directions and the code iterates through it all later on. 123 | 124 | If one of the cardinal directions is being pressed, we send _only_ that button code and stop processing the matrix. 125 | 126 | We also need to have a holdoff time implemented to prevent bouncing. 127 | 128 | ```cpp 129 | // Loops through the funky switch directions and checks to see if the push is being triggered 130 | // along with another direction. If it is, we will flag it for skipping the normal press 131 | // routine. We also press the _intended_ direction only. 132 | for (int i = 0; i < FUNKY_DIR_COUNT; i++) { 133 | // if center not handling press, try directions. 134 | if(customKeypad.findInList((char) funkyCenterCode) != -1 && customKeypad.findInList((char) funkyDirectionCodes[i]) != -1) { 135 | if(now - funkyDirectionLastHoldoff[i] > FUNKY_DIR_HOLDOFF_TIME) { 136 | pressKey((char) funkyDirectionCodes[i]); 137 | funkyDirectionLastHoldoff[i] = now; // last press send 138 | } 139 | if (funkyDirectionLastHoldoff[i]==0) funkyDirectionLastHoldoff[i] = 1; // SAFEGUARD WRAP AROUND OF millis() (WHICH IS TO 0) SINCE holdoff[i]==0 HAS A SPECIAL MEANING ABOVE 140 | handlingFunkySwitch = true; 141 | } 142 | } 143 | ``` 144 | 145 | I'm sure there's a more elegant way to handle this, but I'm just not seeing it. 146 | 147 | 148 | ### Encoders 149 | 150 | You will need to assign pins and "buttons" for your encoders too! 151 | 152 | ```cpp 153 | ////////////////////////////////////////////////// 154 | // John Main "Robust Rotary encoder" Setup 155 | ////////////////////////////////////////////////// 156 | // The `PEC11H-4020F-S0016` encoders I used from the BOM are 157 | // very noisy and cause issues and double counts when used 158 | // with the ESP32Encoder library. John's code works well! 159 | #define ENCODER_COUNT 2 160 | uint8_t uppPin[ENCODER_COUNT] = {16, 18}; // A pins 161 | uint8_t dwnPin[ENCODER_COUNT] = {19, 5}; // B pins 162 | uint8_t encCount[ENCODER_COUNT] = {0, 0}; 163 | uint8_t encValue[ENCODER_COUNT] = {0, 0}; 164 | uint8_t encPrevNextCode[ENCODER_COUNT] = {0, 0}; 165 | uint16_t encStore[ENCODER_COUNT]= {0, 0}; 166 | uint8_t encoderUpp[ENCODER_COUNT] = {23,25}; // Up buttons 167 | uint8_t encoderDwn[ENCODER_COUNT] = {24,26}; // Down buttons 168 | static int8_t rotEncTable[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0}; 169 | 170 | 171 | 172 | ////////////////////////////////////////////////// 173 | // ESP32Encoder Setup 174 | ////////////////////////////////////////////////// 175 | // The `RKJXT1F42001` encoder works really well with the 176 | // ESP32Encoder library, and does not work very well with 177 | // the implemented "Robust Rotary encoder" solution. 178 | unsigned long funkyEncoderHoldoff = 0; 179 | int32_t funkyEncoderPrevCenter = 0; 180 | uint8_t funkyEncoderUppPin = 21; // A pin 181 | uint8_t funkyEncoderDwnPin = 17; // B pin 182 | uint8_t funkyEncoderUpp = 22; // Up button 183 | uint8_t funkyEncoderDwn = 21; // Down button 184 | #define FUNKY_HOLDOFF_TIME 30 185 | ESP32Encoder funkyEncoder; 186 | ``` 187 | 188 | Here the encoders are listed as columns. In this example, `16`, `19` are one encoder, and `18`, `5` are another, etc. 189 | 190 | Likewise, when the encoder attached to `16`, `19` is rolled up or down, it'll press the gamepad buttons: `23`, `24`. These can be assigned to whatever you need them to be. 191 | 192 | 193 | ### Battery Level 194 | 195 | Battery voltage and percentage reading is implemented. 196 | 197 | ```cpp 198 | getBatteryVoltage(); 199 | getBatteryPercent(); 200 | ``` 201 | 202 | Currently it sends the percent reading to the Bluetooth host every 5 minutes. The display code will later be updated to display this upon a button press combination (or perhaps holding a specific button for a few seconds). 203 | 204 | Depending on your board, cell, and multimeter, you may need to adjust the vRef value. 205 | ```cpp 206 | const float vRef = 1.048; // should be 1.1V but may need to be calibrated 207 | ``` 208 | -------------------------------------------------------------------------------- /ble_button_box.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include // https://github.com/adafruit/Adafruit-GFX-Library 3 | #include // https://github.com/adafruit/Adafruit_SSD1306 4 | #include // https://github.com/madhephaestus/ESP32Encoder/ 5 | #include // https://github.com/Chris--A/Keypad 6 | #include // https://github.com/lemmingDev/ESP32-BLE-Gamepad 7 | 8 | 9 | 10 | ////////////////////////////////////////////////// 11 | // Adjustment Setup 12 | ////////////////////////////////////////////////// 13 | bool debounceAdjustMode = false; 14 | 15 | 16 | 17 | ////////////////////////////////////////////////// 18 | // Battery Polling Intervals 19 | ////////////////////////////////////////////////// 20 | long batteryUpdateInterval = 300000; // every 5 minutes 21 | // long batteryUpdateInterval = 1000; // 1 Hz (testing/fun) 22 | long prevBatteryUpdate = 290000; // 10 seconds from goal 23 | 24 | 25 | 26 | ////////////////////////////////////////////////// 27 | // OLED Screen Setup 28 | ////////////////////////////////////////////////// 29 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 30 | #define SCREEN_HEIGHT 32 // OLED display height, in pixels 31 | #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) 32 | #define SCREEN_ADDRESS 0x3C 33 | #define CUSTOM_I2C_SDA 25 34 | #define CUSTOM_I2C_SCL 26 35 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 36 | 37 | 38 | 39 | ////////////////////////////////////////////////// 40 | // BLE Gamepad Setup 41 | ////////////////////////////////////////////////// 42 | BleGamepad bleGamepad("BLE Sim Buttons", "Arduino"); 43 | 44 | 45 | 46 | ////////////////////////////////////////////////// 47 | // Custom Keypad Matrix Setup 48 | ////////////////////////////////////////////////// 49 | #define ROWS 5 50 | #define COLS 4 51 | uint8_t rowPins[ROWS] = {15, 32, 14, 22, 23}; 52 | uint8_t colPins[COLS] = {4, 12, 27, 33}; 53 | byte keymap[ROWS][COLS] = { // buttons 54 | { 0, 1, 2, 3}, 55 | { 5, 6, 7, 8}, 56 | { 9,10,11,12}, 57 | {13,14,15,16}, 58 | {17,18,19,20} 59 | }; 60 | unsigned long keypadHoldoff[ROWS * COLS + 1] = {0}; 61 | unsigned short keypadHoldoffTime = 125; 62 | Keypad customKeypad = Keypad( makeKeymap(keymap), rowPins, colPins, ROWS, COLS); 63 | 64 | 65 | 66 | ////////////////////////////////////////////////// 67 | // John Main "Robust Rotary encoder" Setup 68 | ////////////////////////////////////////////////// 69 | // The `PEC11H-4020F-S0016` encoders I used from the BOM are 70 | // very noisy and cause issues and double counts when used 71 | // with the ESP32Encoder library. John's code works well! 72 | #define ENCODER_COUNT 2 73 | uint8_t uppPin[ENCODER_COUNT] = {16, 18}; // A pins 74 | uint8_t dwnPin[ENCODER_COUNT] = {19, 5}; // B pins 75 | uint8_t encCount[ENCODER_COUNT] = {0, 0}; 76 | uint8_t encValue[ENCODER_COUNT] = {0, 0}; 77 | uint8_t encPrevNextCode[ENCODER_COUNT] = {0, 0}; 78 | uint16_t encStore[ENCODER_COUNT]= {0, 0}; 79 | uint8_t encoderUpp[ENCODER_COUNT] = {23,25}; // Up buttons 80 | uint8_t encoderDwn[ENCODER_COUNT] = {24,26}; // Down buttons 81 | static int8_t rotEncTable[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0}; 82 | 83 | 84 | 85 | ////////////////////////////////////////////////// 86 | // ESP32Encoder Setup 87 | ////////////////////////////////////////////////// 88 | // The `RKJXT1F42001` encoder works really well with the 89 | // ESP32Encoder library, and does not work very well with 90 | // the implemented "Robust Rotary encoder" solution. 91 | unsigned long funkyEncoderHoldoff = 0; 92 | int32_t funkyEncoderPrevCenter = 0; 93 | uint8_t funkyEncoderUppPin = 21; // A pin 94 | uint8_t funkyEncoderDwnPin = 17; // B pin 95 | uint8_t funkyEncoderUpp = 22; // Up button 96 | uint8_t funkyEncoderDwn = 21; // Down button 97 | #define FUNKY_HOLDOFF_TIME 30 98 | ESP32Encoder funkyEncoder; 99 | 100 | 101 | 102 | ////////////////////////////////////////////////// 103 | // Funky Switch Direction Logic Setup 104 | ////////////////////////////////////////////////// 105 | #define FUNKY_DIR_COUNT 4 106 | #define FUNKY_DIR_HOLDOFF_TIME 300 // 0.3sec 107 | unsigned long funkyDirectionLastHoldoff[4] = {0,0,0,0}; 108 | char funkyCenterCode = 0; 109 | char funkyDirectionCodes[FUNKY_DIR_COUNT] = {13, 9, 5, 17}; 110 | bool handlingFunkySwitch = false; 111 | 112 | 113 | 114 | void setup() { 115 | Serial.begin(115200); 116 | 117 | // Custom i2c pins and address for Adafruit OLED library 118 | Wire.begin(CUSTOM_I2C_SDA, CUSTOM_I2C_SCL, SCREEN_ADDRESS); 119 | 120 | // Initialize the OLED display 121 | display.begin(); 122 | 123 | // Setup encoder logic for "John Main" encoders: 124 | // https://esp32.com/viewtopic.php?f=19&t=2881&start=30; 125 | for (uint8_t i=0; ifunkyEncoderPrevCenter && keypadHoldoffTime + increment <= maxValue) { 190 | keypadHoldoffTime += increment; 191 | } 192 | if (cntr= minValue) { 193 | keypadHoldoffTime -= increment; 194 | } 195 | // Display current debounce value 196 | displayDebounceMessage(keypadHoldoffTime); 197 | } else { 198 | // Normal funky encoder functionality 199 | if (cntr>funkyEncoderPrevCenter) { sendKey(funkyEncoderUpp); } 200 | if (cntr FUNKY_HOLDOFF_TIME) { 208 | funkyEncoderPrevCenter = funkyEncoder.getCount(); 209 | funkyEncoderHoldoff = 0; 210 | } 211 | } 212 | 213 | if (customKeypad.getKeys()) { 214 | // Loops through the funky switch directions and checks to see if the push is being triggered 215 | // along with another direction. If it is, we will flag it for skipping the normal press 216 | // routine. We also press the _intended_ direction only. 217 | for (int i = 0; i < FUNKY_DIR_COUNT; i++) { 218 | // if center not handling press, try directions. 219 | if(customKeypad.findInList((char) funkyCenterCode) != -1 && customKeypad.findInList((char) funkyDirectionCodes[i]) != -1) { 220 | if(now - funkyDirectionLastHoldoff[i] > FUNKY_DIR_HOLDOFF_TIME) { 221 | pressKey((char) funkyDirectionCodes[i], now); 222 | funkyDirectionLastHoldoff[i] = now; // last press send 223 | } 224 | if (funkyDirectionLastHoldoff[i]==0) funkyDirectionLastHoldoff[i] = 1; // SAFEGUARD WRAP AROUND OF millis() (WHICH IS TO 0) SINCE holdoff[i]==0 HAS A SPECIAL MEANING ABOVE 225 | handlingFunkySwitch = true; 226 | } 227 | } 228 | 229 | // Scan the whole key list and find changed keys. 230 | for (int i=0; i < LIST_MAX; i++) { 231 | if (customKeypad.key[i].stateChanged) { // Only find keys that have changed state. 232 | switch (customKeypad.key[i].kstate) { // Report active key state : IDLE, PRESSED, HOLD, or RELEASED 233 | case PRESSED: 234 | if(handlingFunkySwitch == false){ 235 | pressKey(customKeypad.key[i].kchar, now); 236 | } 237 | break; 238 | case HOLD: 239 | if(handlingFunkySwitch == false){ 240 | holdKey(customKeypad.key[i].kchar); 241 | } 242 | break; 243 | case RELEASED: 244 | case IDLE: 245 | releaseKey(customKeypad.key[i].kchar); 246 | break; 247 | } 248 | 249 | } 250 | } 251 | } 252 | 253 | // Send battery level to host periodically 254 | if(now - prevBatteryUpdate > batteryUpdateInterval) { 255 | prevBatteryUpdate = now; 256 | int batteryLevel = getBatteryPercent(); 257 | float batteryVoltage = getBatteryVoltage(); 258 | 259 | Serial.print("battery: \t"); 260 | Serial.println((String)batteryLevel); 261 | 262 | if(bleGamepad.isConnected()) { 263 | bleGamepad.setBatteryLevel(batteryLevel); 264 | } 265 | 266 | if(!debounceAdjustMode) { 267 | displayBatteryStats(); 268 | } 269 | } 270 | 271 | // No longer looking for strange funky switch logic 272 | handlingFunkySwitch = false; 273 | } 274 | 275 | void sendKey(uint8_t key) { 276 | uint32_t gamepadbutton = pow(2,key); // CONVERT TO THE BINARY MAPPING GAMEPAD KEYS USE 277 | Serial.print("pulse\t"); 278 | Serial.println(key); 279 | if(bleGamepad.isConnected()) { 280 | bleGamepad.press(gamepadbutton); 281 | delay(100); // there needs to be a press duration... 282 | bleGamepad.release(gamepadbutton); 283 | } 284 | } 285 | 286 | void pressKey(uint8_t key, unsigned long now) { 287 | if(now - keypadHoldoff[key] > keypadHoldoffTime) { 288 | keypadHoldoff[key] = now; 289 | 290 | // Disable Adjustment Mode 291 | if(debounceAdjustMode && key == 15) { 292 | debounceAdjustMode = false; 293 | displayBatteryStats(); 294 | return; 295 | } 296 | 297 | // Handle regular button presses 298 | uint32_t gamepadbutton = pow(2,key); 299 | Serial.print("press\t"); 300 | Serial.println(key); 301 | if(bleGamepad.isConnected()) { 302 | bleGamepad.press(gamepadbutton); 303 | } 304 | } 305 | } 306 | 307 | void holdKey(uint8_t key) { 308 | uint32_t gamepadbutton = pow(2,key); 309 | Serial.print("hold\t"); 310 | Serial.println(key); 311 | 312 | // Enable Adjustment Mode 313 | if(!debounceAdjustMode && key == 15) { 314 | debounceAdjustMode = true; 315 | displayDebounceMessage(keypadHoldoffTime); 316 | return; 317 | } 318 | } 319 | 320 | void releaseKey(uint8_t key) { 321 | // Handle regular button releases 322 | uint32_t gamepadbutton = pow(2,key); 323 | Serial.print("release\t"); 324 | Serial.println(key); 325 | if(bleGamepad.isConnected()) { 326 | bleGamepad.release(gamepadbutton); 327 | } 328 | } 329 | 330 | // https://www.best-microcontroller-projects.com/rotary-encoder.html 331 | // A vald CW or CCW move returns 1, invalid returns 0. 332 | int8_t readRotary(uint8_t DATA_PIN, uint8_t CLK_PIN, uint8_t i) { 333 | static int8_t rotEncTable[] = {0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0}; 334 | 335 | encPrevNextCode[i] <<= 2; 336 | if (digitalRead(DATA_PIN)) encPrevNextCode[i] |= 0x02; 337 | if (digitalRead(CLK_PIN)) encPrevNextCode[i] |= 0x01; 338 | encPrevNextCode[i] &= 0x0f; 339 | 340 | // If valid then store as 16 bit data. 341 | if (rotEncTable[encPrevNextCode[i]] ) { 342 | encStore[i] <<= 4; 343 | encStore[i] |= encPrevNextCode[i]; 344 | //if (encStore[i]==0xd42b) return 1; 345 | //if (encStore[i]==0xe817) return -1; 346 | if ((encStore[i]&0xff)==0x2b) return -1; 347 | if ((encStore[i]&0xff)==0x17) return 1; 348 | } 349 | return 0; 350 | } 351 | 352 | int getBatteryPercent() { 353 | const int maxDisplayed = 100; // 100% (higher values are constrained) 354 | const int minDisplayed = 0; // 0% or flat (we'll probably never get there) 355 | const float maxBatteryVoltage = 4.2; // Max LiPoly voltage of a 3.7 battery is 4.2 356 | const float minBatteryVoltage = 3.3; // cutoff voltage (3.2, 3.3 to be safer) 357 | float voltageLevel = getBatteryVoltage(); 358 | float usablePercent = ((voltageLevel - minBatteryVoltage) / (maxBatteryVoltage - minBatteryVoltage)) * ((maxDisplayed - minDisplayed) + minDisplayed); 359 | return constrain((int)usablePercent, minDisplayed, maxDisplayed); 360 | } 361 | 362 | // If you read voltage values higher than 4.2, and are seeing a difference 363 | // between the value on your multimeter, adjust the vRef. Something between 364 | // 1.0 and 1.1 should be where you're aiming for. 365 | // You can go as deep as you want here: 366 | // https://esp32.com/viewtopic.php?f=19&t=2881&start=30; 367 | // the simple fix below works for me though. 368 | float getBatteryVoltage() { 369 | const float vrefCalibration = 0.9417; 370 | const float vRef = 1.1; // should be 1.1V but may need to be calibrated above 371 | const float maxAnalogVal = 4095.0; // defines the range of the ADC calculation 372 | return (analogRead(35) / maxAnalogVal) * 2 * (vRef * vrefCalibration) * 3.3; // calculate voltage level 373 | } 374 | 375 | void displayBatteryStats() { 376 | int batteryLevel = getBatteryPercent(); 377 | float batteryVoltage = getBatteryVoltage(); 378 | 379 | display.clearDisplay(); 380 | display.setTextSize(2); 381 | display.setTextColor(SSD1306_WHITE); 382 | display.setCursor(0, 0); 383 | display.println((String)batteryVoltage + "V"); 384 | display.println((String)batteryLevel + "%"); 385 | display.display(); 386 | } 387 | 388 | void displayDebounceMessage(unsigned short holdoff) { 389 | display.clearDisplay(); 390 | display.setTextSize(2); 391 | display.setTextColor(SSD1306_WHITE); 392 | display.setCursor(0, 0); 393 | display.println("Debounce"); 394 | display.println((String)holdoff + "ms"); 395 | display.display(); 396 | } -------------------------------------------------------------------------------- /images/AdafruitHuzzah32PinDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/images/AdafruitHuzzah32PinDiagram.png -------------------------------------------------------------------------------- /images/wiring.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/images/wiring.jpg -------------------------------------------------------------------------------- /stl/button box.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/stl/button box.stl -------------------------------------------------------------------------------- /stl/faceplate.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/stl/faceplate.stl -------------------------------------------------------------------------------- /stl/funky switch knob.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/stl/funky switch knob.stl -------------------------------------------------------------------------------- /stl/knob with d hole.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/stl/knob with d hole.stl -------------------------------------------------------------------------------- /stl/multi-direction-switch-clamp.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamiehs/ble-button-box/3059bf47bb0094fed68083bf5f95be9a8ec22e23/stl/multi-direction-switch-clamp.stl --------------------------------------------------------------------------------