Posted 21 January 2026
My wife recently asked me if I could build a ‘Solfeggio’ tone generator that she could listen to as a sleep aid. A quick net search yielded the following:
Solfeggio frequencies are a set of specific tones believed to promote healing, relaxation, and balance. The primary nine frequencies range from 174 Hz to 963 Hz, each associated with different benefits, such as emotional healing, stress reduction, and chakra alignment.
174 Hz: Pain relief and grounding.
285 Hz: Healing tissue and organs.
396 Hz: Liberating guilt and fear.
417 Hz: Facilitating change and undoing situations.
528 Hz: Transformation and DNA repair.
639 Hz: Enhancing communication and relationships.
741 Hz: Awakening intuition and problem-solving.
852 Hz: Returning to spiritual order.
As a result of a previous project to create a GPS-enabled DashCam Power Switch, I thought this would be a perfect ‘Cheap Yellow Display project. The Cheap Yellow Display (CYD) is a ESP32-2432S028R development board integrated with a 240×320 pixel color touch-sensitive display. In the case of my DashCam project, this meant that I only needed a GPS module, a low-Rds MOSFET, and a bit of circuitry to make it all work – neat. For this tone generation project, it looked like I might be able to do the whole thing with just a CWD and a pair of BT earbuds, as the ESP32-2432S028R dev board incorporates BT capability. The wife already had a pair of BT earbuds, and I had acquired two more CYD modules after it worked so well for my DashCam, so I was all set.
As I did with the DashCam project, I started consulting with Grok right away. If you haven’t yet taken advantage of this – you should – Not only can Grok code in any language, it is also mind-bogglingly fast for internet searches on particular topics. So I started out with this conversation:
Me: I want to build a rechargeable battery-operated tone generator using the ESP32/CYD configuration. I would like the battery to be the same (or smaller) width/length as the CYD. The input to the charging circuit would be +5V from the Type-C connector on the CYD. What battery and charging circuit would you suggest?
Grok: For a rechargeable battery-operated tone generator using the ESP32-based Cheap Yellow Display (CYD), which measures approximately 50mm x 86mm, you’ll want a compact 3.7V LiPo battery that fits within those dimensions for easy integration (e.g., mounted behind the board). The tone generator can use the ESP32’s built-in DAC (8-bit resolution) or PWM for audio output, connected to a small speaker or amp. Power management involves charging the battery via the CYD’s USB-C (which supplies 5V) and boosting the battery voltage to 5V for the board when unplugged, as the CYD expects 5V input on its power pins.Suggested BatteryA 3.7V 1000mAh LiPo battery with JST connector, such as a 603450 cell or equivalent.
Why this? It provides 3-5 hours of runtime for a low-power tone generator (ESP32 + display draws ~200-300mA at 5V under load; enable deep sleep modes to extend life). It’s widely available, rechargeable, and includes built-in protection (PCM) against overcharge/discharge. Smaller options like 300-500mAh (e.g., 402030 at 40mm x 20mm x 3mm) work if runtime isn’t critical, but 1000mAh balances size and capacity for ESP32 projects. ezsbc.shop +3 Avoid larger capacities (e.g., 3000mAh) if they exceed ~65mm x 35mm, as they may not fit neatly.
Dimensions: Approximately 60mm x 34mm x 5mm (fits well within 86mm x 50mm, leaving room for wiring).
Suggested Charging Circuit: Use a TP4056-based module with integrated battery protection (BMS).
- Why this? It’s a simple, inexpensive ($1-2) 1A charger for single-cell LiPo batteries, accepting 5V input directly from the CYD’s USB-C (via wires to the module’s IN+ / IN- pads). It handles charging to 4.2V safely and includes overcharge/over-discharge protection. When USB-C is plugged in, it charges the battery; when unplugged, the module’s OUT+ / OUT- pads provide battery voltage (3.7V)
- Additional component needed: Pair it with a small boost converter like the MT3608 (adjustable to 5V output) connected to the TP4056’s OUT pads. This boosts the 3.7V battery voltage to 5V for the CYD’s 5V pin (avoid direct 3.7V connection, as the board’s regulator expects 5V for stable operation). github.com +2 Add a switch on the boost output for power control.
There was some back-and-forth to nail down the actual modules I would need, but pretty soon I had the battery (li-ion 603450), charger (TP4056) and DC-DC converter (MT3608) on order from Amazon. The reason I went with separate charger and up-converter modules instead of the IP5306 all-in-one is due the IP5306 requiring a minimum load current to remain active, and this would conflict with my plan to put the ESP32 to sleep during idle times to extend battery life.
After getting the hardware issues resolved, the next step was the ESP32 software. I had Grok develop the software using Arduino code, with the following specs:
Me: Referencing our discussion of my ESP32 CYD Sinewave Tone Generator Project, I would like to see your take on Arduino code for the ESP32, with the following specs:
- Non-K&R bracket convention (beginning bracket on new line)
- Nine (9) touch-sensitive radio buttons for the ‘Solfeggio frequencies’ (https://www.bing.com/search?q=complete+list+of+solfeggio+frequencies&FORM=R5FD1).
- A volume slider (horizontal at the bottom or vertical on the side)
- Tone output directed to Bluetooth transmitter
- Sleep mode after 10 minutes from coming out of sleep mode
- Battery charge indicator based on A/D measurements of the LiPo battery voltage
- Whatever other obvious requirement that I have failed to mention
Within a few seconds, Grok produced the following code:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
// ESP32 CYD Sinewave Tone Generator for Solfeggio Frequencies // Uses TFT_eSPI for display, XPT2046_Touchscreen for touch, arduino-audio-tools for audio generation and Bluetooth A2DP source // Assumes TFT_eSPI User_Setup.h configured for CYD: ILI9341, pins as per Random Nerd Tutorials // Bluetooth output to paired device (e.g., hearing aids or speaker) // Battery voltage on GPIO35 with 2:1 divider (adjust calibration as needed) // Deep sleep after 10 min inactivity, wake on touch IRQ (GPIO27 low) // Libraries (install via Arduino Library Manager) #include <TFT_eSPI.h> #include <XPT2046_Touchscreen.h> #include <AudioTools.h> #include <BluetoothA2DPSource.h> // Display and Touch TFT_eSPI tft = TFT_eSPI(); #define XPT2046_IRQ 27 #define XPT2046_MOSI 23 #define XPT2046_MISO 19 #define XPT2046_SCK 18 #define XPT2046_CS 22 SPIClass touchscreenSPI = SPIClass(VSPI); XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ); // Audio BluetoothA2DPSource a2dp_source; SineWaveGenerator<int16_t> sineWave; GeneratedSoundStream<int16_t> soundStream(2, 44100); // Stereo, 44.1kHz AudioInfo audioInfo(44100, 2, 16); // Solfeggio Frequencies (Hz) const float frequencies[9] = {174, 285, 396, 417, 528, 639, 741, 852, 963}; int selectedFreqIndex = -1; // None selected initially // UI Dimensions (240x320 portrait) const int screenWidth = 240; const int screenHeight = 320; const int buttonSize = 70; const int buttonSpacing = 10; const int sliderHeight = 30; const int sliderWidth = screenWidth - 20; const int sliderX = 10; const int sliderY = screenHeight - sliderHeight - 10; // Volume (0-127) int volume = 64; // Battery const int batteryPin = 35; const float minVoltage = 3.0; // LiPo low const float maxVoltage = 4.2; // LiPo full const float dividerRatio = 2.0; // 2:1 divider // Sleep unsigned long lastActivityTime = 0; const unsigned long sleepTimeout = 10UL * 60 * 1000; // 10 minutes void setup() { Serial.begin(115200); // Display tft.init(); tft.setRotation(0); // Portrait tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); // Touch touchscreenSPI.begin(XPT2046_SCK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); ts.begin(touchscreenSPI); ts.setRotation(0); // Bluetooth a2dp_source.start("CYD Tone Generator"); // Device name soundStream.begin(audioInfo); sineWave.begin(audioInfo, 0); // Initial freq 0 (silent) // Audio pipeline a2dp_source.set_stream_reader(&soundStream); // Battery pin pinMode(batteryPin, INPUT); // Wake setup (for after deep sleep) esp_sleep_enable_gpio_wakeup(); gpio_wakeup_enable((gpio_num_t)XPT2046_IRQ, GPIO_INTR_LOW_LEVEL); // Initial draw drawUI(); updateBatteryIndicator(); lastActivityTime = millis(); } void loop() { checkTouch(); checkSleep(); // Generate audio if frequency selected if (selectedFreqIndex != -1) { sineWave.setFrequency(frequencies[selectedFreqIndex]); sineWave.setAmplitude(volume / 127.0f); // 0-1 range soundStream.add(sineWave); } else { sineWave.setFrequency(0); // Silent } delay(10); // Light loop } void drawUI() { // Draw 3x3 buttons for frequencies int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.drawRect(x, y, buttonSize, buttonSize, TFT_WHITE); tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } // Volume slider tft.drawRect(sliderX, sliderY, sliderWidth, sliderHeight, TFT_WHITE); updateSlider(); // Battery indicator (top right) tft.setCursor(screenWidth - 50, 10); tft.print("Bat:"); } void updateSlider() { int fillWidth = map(volume, 0, 127, 0, sliderWidth); tft.fillRect(sliderX + 1, sliderY + 1, fillWidth - 1, sliderHeight - 2, TFT_GREEN); tft.fillRect(sliderX + fillWidth, sliderY + 1, sliderWidth - fillWidth - 1, sliderHeight - 2, TFT_BLACK); } void updateBatteryIndicator() { float adcValue = analogRead(batteryPin) / 4095.0 * 3.3 * dividerRatio; // Calibrate as needed int percent = map(adcValue * 100, minVoltage * 100, maxVoltage * 100, 0, 100); percent = constrain(percent, 0, 100); tft.setCursor(screenWidth - 30, 10); tft.printf("%3d%%", percent); } void checkTouch() { if (ts.tirqTouched() && ts.touched()) { TS_Point p = ts.getPoint(); // Calibrate touch (adjust based on your CYD, typical range) int x = map(p.x, 200, 3700, 0, screenWidth); int y = map(p.y, 200, 3700, 0, screenHeight); lastActivityTime = millis(); // Check buttons int row = (y - buttonSpacing) / (buttonSize + buttonSpacing); int col = (x - buttonSpacing) / (buttonSize + buttonSpacing); if (row >= 0 && row < 3 && col >= 0 && col < 3) { int index = row * 3 + col; if (index != selectedFreqIndex) { selectedFreqIndex = index; // Redraw buttons to highlight selected (simple: text bold or color) redrawButtons(); } else { selectedFreqIndex = -1; // Toggle off redrawButtons(); } } // Check slider if (y >= sliderY && y <= sliderY + sliderHeight && x >= sliderX && x <= sliderX + sliderWidth) { volume = map(x - sliderX, 0, sliderWidth, 0, 127); volume = constrain(volume, 0, 127); updateSlider(); } } } void redrawButtons() { int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.fillRect(x + 1, y + 1, buttonSize - 2, buttonSize - 2, (i == selectedFreqIndex) ? TFT_BLUE : TFT_BLACK); tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } } void checkSleep() { if (millis() - lastActivityTime > sleepTimeout) { // Prepare for sleep tft.fillScreen(TFT_BLACK); // Optional esp_deep_sleep_start(); } } |
Not only did Grok produce initial code, but it helped me work through some subtle issues with the ‘AudioTools’ library as I had not used this library before. Grok would offer code, then I would run (or try to run) and report back on any errors encountered. Pretty soon I had code that compiled cleanly and ran, and then it was a case of exercising the code and reporting any debug output back to Grok. This went around for a while, but we eventually got to working code. I learned that while Grok is very fast, he’s not always all that accurate. For anything other than the most trivial programming task, the human at the other end of the conversation must have relevant experience. At one point in the debugging process, Grok was recommending some hardware changes on the CYD board, but I knew from prior experience with the DashCam project that no such mods were required for detecting user touches on the touch-screen. So I was able to provide Grok with a small working program to do this and Grok was happy to incorporate the new information into the complete program. This, however, brought up another ‘gotcha’. Grok takes input from the human, thinks for a few seconds, and then disgorges another complete program – without any accompanying explanation. So, I said “what was the problem with the previous code?” and got a pretty comprehensive answer- yay.
Grok really doesn’t do a very good job of troubleshooting or code debug at all – that’s where the human’s engineering experience comes in. At one point I tried to describe the ‘cut the world in two’ troubleshooting technique I have been using for decades, but I’m not sure if Grok has any ability to remember things like that, or any ability to apply them, so I suspect that was a waste of time. Grok will continue to produce code without explanation if you let it – kind of a ‘random walk’ shotgun approach AFAICT. At the end of this hours-long session, I got kind of frustrated with this behavior and terminated the session. I put up a DM to @xAI, but never got a response.
Eventually, over a 2-3 day period we got a working program that would connect via BT to my wife’s earbuds and play the tone (with some ‘reverb’ added, even!) selected by tapping a button on the CYD screen.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 |
// ESP32 CYD Sinewave Tone Generator for Solfeggio Frequencies // Uses TFT_eSPI for display, XPT2046_Touchscreen for touch, I2S for audio output to PCM5102 // Assumes TFT_eSPI User_Setup.h configured for CYD: ILI9341, pins as per Random Nerd Tutorials // Wired output to PCM5102 DAC (pins: BCK=22, WS=27, DATA=26) // Deep sleep after 20 min inactivity, wake on touch IRQ (GPIO36); state persisted in NVS // Libraries (install via Arduino Library Manager) #include <TFT_eSPI.h> #include <XPT2046_Touchscreen.h> #include <driver/i2s.h> #include <math.h> // For sin and PI #include <Preferences.h> // For NVS storage // Touch pin definitions (VSPI with custom pins) #define TOUCH_CS 33 #define TOUCH_IRQ 36 #define TOUCH_PRESSURE_THRESHOLD 100 // Minimum z (pressure) to consider a valid touch (filter noise) #define TOUCH_PRESS_HIGH 150 // Higher threshold for initial press detection #define TOUCH_PRESS_LOW 50 // Lower threshold for release detection (hysteresis) #define TOUCH_CLK 25 #define TOUCH_MISO 39 #define TOUCH_MOSI 32 // Touch calibration constants const int TS_MINX = 3831, TS_MINY = 308, TS_MAXX = 294, TS_MAXY = 3798; // Display and Touch TFT_eSPI tft = TFT_eSPI(); SPIClass touchscreenSPI = SPIClass(VSPI); // Use VSPI bus XPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ); // I2S pin configuration #define I2S_BCK_PIN 22 // Bit clock #define I2S_WS_PIN 27 // Word select (LRCK) #define I2S_DATA_PIN 26 // Data out (DIN) // Audio parameters #define SAMPLE_RATE 44100 #define BITS_PER_SAMPLE 16 #define CHANNELS 2 // Stereo #define BUFFER_SIZE 1024 // Increased for smoother playback (adjust if needed) // Solfeggio Frequencies (Hz) const float frequencies[9] = { 174, 285, 396, 417, 528, 639, 741, 852, 963 }; int selectedFreqIndex = -1; // None selected initially // UI Dimensions (240x320 portrait) const int screenWidth = 240; const int screenHeight = 320; const int buttonSize = 70; const int buttonSpacing = 10; const int sliderHeight = 30; // Increased for finger usability const int sliderSpacing = 5; const int sliderWidth = screenWidth - 20; const int sliderX = 10; const int leftVolSliderY = 245; // Start after buttons (end ~240) const int rightVolSliderY = leftVolSliderY + sliderHeight + sliderSpacing; // Audio controls float leftVol = 0.5f; // Left ear volume (0-1) float rightVol = 0.5f; // Right ear volume (0-1) // Battery indicator const int batteryPin = 35; const float minVoltage = 3.0; // LiPo low const float maxVoltage = 4.2; // LiPo full const float dividerRatio = 2.0; // 2:1 divider int batRectWidth = 30; int batRectHeight = 5; // Deep sleep (after inactivity) unsigned long lastActivityTime = 0; const unsigned long sleepTimeout = 20UL * 60 * 1000; // 20 minutes // RTC data for surviving deep sleep RTC_DATA_ATTR int bootCount = 0; // Global buffer for samples int16_t audioBuffer[BUFFER_SIZE * CHANNELS]; // NVS for state persistence Preferences prefs; // Touch state for hysteresis bool isPressed = false; // Function to generate audio data (sine wave with separate L/R volumes) void generateAudioBuffer(float frequency) { static float phase = 0.0; const float phaseIncrement = 2.0 * M_PI * frequency / SAMPLE_RATE; for (int i = 0; i < BUFFER_SIZE; i++) { int16_t sample = (int16_t)(sin(phase) * 32767.0); // Raw sine wave full amplitude // Apply separate volumes to L/R audioBuffer[2 * i] = (int16_t)(sample * leftVol); // Left channel audioBuffer[2 * i + 1] = (int16_t)(sample * rightVol); // Right channel phase += phaseIncrement; if (phase >= 2.0 * M_PI) phase -= 2.0 * M_PI; } } // Method to print the reason by which ESP32 has been awaken from sleep void print_wakeup_reason() { esp_sleep_wakeup_cause_t wakeup_reason; wakeup_reason = esp_sleep_get_wakeup_cause(); switch (wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT0: Serial.println("Wakeup caused by external signal using RTC_IO"); break; case ESP_SLEEP_WAKEUP_EXT1: Serial.println("Wakeup caused by external signal using RTC_CNTL"); break; case ESP_SLEEP_WAKEUP_TIMER: Serial.println("Wakeup caused by timer"); break; case ESP_SLEEP_WAKEUP_TOUCHPAD: Serial.println("Wakeup caused by touchpad"); break; case ESP_SLEEP_WAKEUP_ULP: Serial.println("Wakeup caused by ULP program"); break; default: Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); break; } } void setup() { Serial.begin(115200); touchscreenSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS); ts.begin(touchscreenSPI); ts.setRotation(1); tft.init(); tft.invertDisplay(1); tft.setRotation(2); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); pinMode(batteryPin, INPUT); // Increment boot number and print it every reboot ++bootCount; Serial.println("Boot number: " + String(bootCount)); // Print the wakeup reason for ESP32 print_wakeup_reason(); // Load state from NVS prefs.begin("cyd_tone", false); selectedFreqIndex = prefs.getInt("freqIndex", -1); leftVol = prefs.getFloat("leftVol", 0.5f); rightVol = prefs.getFloat("rightVol", 0.5f); prefs.end(); // Configure I2S i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = (i2s_bits_per_sample_t)BITS_PER_SAMPLE, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = 0, .dma_buf_count = 8, .dma_buf_len = BUFFER_SIZE, .use_apll = true, // Better clock accuracy for audio .tx_desc_auto_clear = true, .fixed_mclk = 0 }; i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCK_PIN, .ws_io_num = I2S_WS_PIN, .data_out_num = I2S_DATA_PIN, .data_in_num = I2S_PIN_NO_CHANGE }; // Install and start I2S driver if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK) { Serial.println("I2S driver install failed"); while (1); } if (i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) { Serial.println("I2S pin set failed"); while (1); } Serial.println("I2S initialized."); // Configure external wake up on GPIO36 (TOUCH_IRQ, wakes on low level when touched) esp_sleep_enable_ext0_wakeup((gpio_num_t)TOUCH_IRQ, 0); // 0 for low level Serial.println("[DEBUG] Wake setup done"); drawUI(); Serial.printf("In setup(): Just before call to updateBatteryIndicator()\n"); updateBatteryIndicator(); lastActivityTime = millis(); } void loop() { // Prioritize audio float currentFreq = (selectedFreqIndex >= 0) ? frequencies[selectedFreqIndex] : 0.0f; generateAudioBuffer(currentFreq); // Write buffer to I2S size_t bytesWritten; i2s_write(I2S_NUM_0, audioBuffer, sizeof(audioBuffer), &bytesWritten, portMAX_DELAY); if (bytesWritten != sizeof(audioBuffer)) { Serial.println("I2S write incomplete"); } checkTouch(); // Check touch after audio to minimize interruption updateBatteryIndicator(); if (millis() - lastActivityTime > sleepTimeout) { // Save state to NVS before sleep prefs.begin("cyd_tone", false); prefs.putInt("freqIndex", selectedFreqIndex); prefs.putFloat("leftVol", leftVol); prefs.putFloat("rightVol", rightVol); prefs.end(); tft.fillScreen(TFT_BLACK); // Optional clear before sleep Serial.println("Going to deep sleep now"); esp_deep_sleep_start(); // Deep sleep; resets on wake } } // drawUI function void drawUI() { tft.fillScreen(TFT_BLACK); // Clear screen // Draw 3x3 buttons for frequencies int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.drawRect(x, y, buttonSize, buttonSize, TFT_WHITE); tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } // Left Volume slider (cyan) with label tft.setTextSize(1); tft.setCursor(sliderX - 30, leftVolSliderY + (sliderHeight / 2) - 4); tft.print("L Vol"); tft.setTextSize(2); tft.drawRect(sliderX, leftVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); int leftFillWidth = static_cast<int>(leftVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, leftVolSliderY + 1, leftFillWidth, sliderHeight - 2, TFT_CYAN); tft.fillRect(sliderX + 1 + leftFillWidth, leftVolSliderY + 1, sliderWidth - 2 - leftFillWidth, sliderHeight - 2, TFT_BLACK); // Right Volume slider (green) with label tft.setTextSize(1); tft.setCursor(sliderX - 30, rightVolSliderY + (sliderHeight / 2) - 4); tft.print("R Vol"); tft.setTextSize(2); tft.drawRect(sliderX, rightVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); int rightFillWidth = static_cast<int>(rightVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, rightVolSliderY + 1, rightFillWidth, sliderHeight - 2, TFT_GREEN); tft.fillRect(sliderX + 1 + rightFillWidth, rightVolSliderY + 1, sliderWidth - 2 - rightFillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(0, 0, batRectWidth, batRectHeight, TFT_WHITE); redrawButtons(); // Apply selected state } void updateBatteryIndicator() { Serial.printf("In updateBatteryIndicator()\n"); int adcCount = analogRead(batteryPin); float adcValue = adcCount / 4095.0 * 3.3 * dividerRatio; // Calibrate as needed int percent = map(adcValue * 100, minVoltage * 100, maxVoltage * 100, 0, 100); percent = constrain(percent, 0, 100); int fillWidth = percent * batRectWidth / 100; int start_y = 10; //tft.fillRect(0, 0, batRectWidth, batRectHeight, TFT_BLACK); // Clear previous tft.fillRect(0, start_y, batRectWidth, batRectHeight, TFT_BLACK); // Clear previous if (percent <= 20) { tft.fillRect(0, start_y, fillWidth, batRectHeight, TFT_RED); } else if (percent > 20 && percent <= 50) { tft.fillRect(0, start_y, fillWidth, batRectHeight, TFT_ORANGE); } else if (percent > 50) { tft.fillRect(0, start_y, fillWidth, batRectHeight, TFT_GREEN); } tft.drawRect(0, start_y, batRectWidth, batRectHeight, TFT_WHITE); } // checkTouch function with hysteresis and state management void checkTouch() { TS_Point p = ts.getPoint(); if (!isPressed) { if (p.z > TOUCH_PRESS_HIGH) { isPressed = true; processTouch(p, true); // Initial press = true } } else { if (p.z < TOUCH_PRESS_LOW) { isPressed = false; // Process touch end if needed (not for this app) } else { processTouch(p, false); // Drag, initial press = false } } delay(20); // Reduced debounce for responsiveness } // Helper to process touch (called on press start or drag) void processTouch(TS_Point p, bool initialPress) { int x = map(p.y, TS_MINY, TS_MAXY, 0, screenWidth); // Inverted X int y = map(p.x, TS_MINX, TS_MAXX, 0, screenHeight); // Inverted Y Serial.printf("Mapped X: %d, Y: %d, Z: %d\n", x, y, p.z); // Check buttons first (only on initial press) if (initialPress) { int row = (y - buttonSpacing) / (buttonSize + buttonSpacing); int col = (x - buttonSpacing) / (buttonSize + buttonSpacing); if (row >= 0 && row < 3 && col >= 0 && col < 3) { int index = row * 3 + col; Serial.printf("Button pressed: %d\n", index); // Debug if (index != selectedFreqIndex) { selectedFreqIndex = index; } else { selectedFreqIndex = -1; // Toggle off } redrawButtons(); return; // Exit to avoid checking sliders } } // Check left volume slider (allow drag, initial or ongoing) if (y >= leftVolSliderY - 5 && y <= leftVolSliderY + sliderHeight + 5 && x >= sliderX && x <= sliderX + sliderWidth) { leftVol = static_cast<float>(x - sliderX) / sliderWidth; leftVol = constrain(leftVol, 0.0f, 1.0f); Serial.printf("Left Vol adjusted to: %.2f\n", leftVol); // Debug int fillWidth = static_cast<int>(leftVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, leftVolSliderY + 1, fillWidth, sliderHeight - 2, TFT_CYAN); tft.fillRect(sliderX + 1 + fillWidth, leftVolSliderY + 1, sliderWidth - 2 - fillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(sliderX, leftVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); // Redraw border } // Check right volume slider (allow drag, initial or ongoing) if (y >= rightVolSliderY - 5 && y <= rightVolSliderY + sliderHeight + 5 && x >= sliderX && x <= sliderX + sliderWidth) { rightVol = static_cast<float>(x - sliderX) / sliderWidth; rightVol = constrain(rightVol, 0.0f, 1.0f); Serial.printf("Right Vol adjusted to: %.2f\n", rightVol); // Debug int fillWidth = static_cast<int>(rightVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, rightVolSliderY + 1, fillWidth, sliderHeight - 2, TFT_GREEN); tft.fillRect(sliderX + 1 + fillWidth, rightVolSliderY + 1, sliderWidth - 2 - fillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(sliderX, rightVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); // Redraw border } lastActivityTime = millis(); // Reset inactivity timer on touch } // redrawButtons function void redrawButtons() { int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.fillRect(x + 1, y + 1, buttonSize - 2, buttonSize - 2, (i == selectedFreqIndex) ? TFT_BLUE : TFT_BLACK); tft.drawRect(x, y, buttonSize, buttonSize, TFT_WHITE); // Ensure border tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } } |
This program worked fine, but had a few problems:
- The ESP32 ‘deep sleep’ mode was problematic. The mode itself worked fine (and dropped the current drain down to just a few mA, but when the system came back awake, it wouldn’t reconnect to the BT earbuds. We eventually wound up using the ‘light sleep’ mode, which would reliably reconnect to the earbuds, but drew significantly more current (less battery life).
- The earbuds themselves were an issue. When/if the system failed to reconnect, then only way to do so was to put the earbuds back in their charging container and force them back into ‘pairing’ mode so the tone generator box could ‘see’ them again, and this was a PITA.
These issues led my wife to ask if I could change the project to use wired earbuds rather than wireless. This made the design both simpler and more complex. Simpler because now ‘sleep’ wouldn’t be an issue as the wired earbuds eliminated the need to connect via BT. More complex, because the CYD has no native stereo output, so an additional module to convert I2S protocol stereo audio data to human-audible stereo sound was required, and in turn this required finding a third output-enabled GPIO line (the CYD configuration uses most of the ESP32’s GPIO lines to run the display), and this required cutting a board trace and running a jumper to access GPIO26.
Here’s my statement to Grok:
Me: referencing our CYD Tone Generator project, I want to modify it to use plug-in wired earbuds rather than BT. I believe this will require outputting the audio on two GPIO lines (22 & 27)? and implementing an audio amplifier and a volume control. Can you recommend a good audio amplifier package for this?
Grok immediately identified a really nice module:
Grok: I recommend the PCM5102A I2S DAC module (often labeled as “PCM5102 DAC Decoder” or similar). It’s a compact, affordable breakout board widely used in ESP32 audio projects for its simplicity and good sound quality (32-bit, up to 384kHz sampling). Many versions include a built-in 3.5mm headphone jack, making it plug-and-play for wired earbuds without additional soldering for the connector.
Why this one?
Directly interfaces with ESP32 I2S output (no need for the internal DAC on GPIO 25/26).
Provides line-level analog output that can drive most earbuds (16–32Ω impedance) at reasonable volumes without distortion—users commonly report success with direct connection for low-power applications like tones.
Low noise, high SNR (112dB), and minimal external components needed.
Cheap and available: ~$5–10 on Amazon, AliExpress, or electronics sites like Adafruit/SparkFun (search for “PCM5102 I2S DAC module”).
If your earbuds are very sensitive or you need more headroom, some variants include an integrated headphone amplifier (e.g., based on LM4880M—look for “PCM5102 with headphone amp” on Tindie or AliExpress for ~$10–15).
Connections (assuming standard I2S remapping in code):
VCC: 3.3V from ESP32.
GND: Shared ground.
BCLK (bit clock): Connect to your GPIO 22 (or remap as needed).
LRCK/WS (word select/left-right clock): You’ll likely need a third GPIO (e.g., 25 or 26) for this—add it if not already in your setup.
DIN/SDIN (data in): Connect to your GPIO 27.
FMT, FLT, DMP, SCL: Ground these (or leave default for standard I2S format).
XMT/XSMT: 3.3V (unmutes the output).
Analog out: Directly to 3.5mm jack for earbuds.
So I got a couple of the PCM5102A I2S DAC modules from Amazon, and wired them up according to Grok’s instructions. 3.3V, Gnd, BCLK, DIN all came from CN1 on the CYD, but I had to cut a trace to free GPIO26 from it’s normal connection through an audio amp to the ‘speaker’ connector so I could use it for LRCK. See below for the details:



Here’s the program:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 |
/* Name: CYD_ToneGen_DeepSleep_LibConflict_Reverb.ino Created: 1/20/2026 4:30:19 PM Author: FRANK_XPS_9530\Frank */ // ESP32 CYD Sinewave Tone Generator for Solfeggio Frequencies // Uses TFT_eSPI for display, XPT2046_Touchscreen for touch, I2S for audio output to PCM5102 // Assumes TFT_eSPI User_Setup.h configured for CYD: ILI9341, pins as per Random Nerd Tutorials // Wired output to PCM5102 DAC (pins: BCK=22, WS=27, DATA=26) // Deep sleep after 20 min inactivity, wake on touch IRQ (GPIO36); state persisted in NVS // Libraries (install via Arduino Library Manager) #include <TFT_eSPI.h> #include <XPT2046_Touchscreen.h> #include <driver/i2s.h> #include <math.h> // For sin and PI #include <Preferences.h> // For NVS storage // Touch pin definitions (VSPI with custom pins) #define TOUCH_CS 33 #define TOUCH_IRQ 36 #define TOUCH_PRESSURE_THRESHOLD 100 // Minimum z (pressure) to consider a valid touch (filter noise) #define TOUCH_PRESS_HIGH 150 // Higher threshold for initial press detection #define TOUCH_PRESS_LOW 50 // Lower threshold for release detection (hysteresis) #define TOUCH_CLK 25 #define TOUCH_MISO 39 #define TOUCH_MOSI 32 // Touch calibration constants const int TS_MINX = 3831, TS_MINY = 308, TS_MAXX = 294, TS_MAXY = 3798; // Display and Touch TFT_eSPI tft = TFT_eSPI(); SPIClass touchscreenSPI = SPIClass(VSPI); // Use VSPI bus XPT2046_Touchscreen ts(TOUCH_CS, TOUCH_IRQ); // I2S pin configuration #define I2S_BCK_PIN 22 // Bit clock #define I2S_WS_PIN 27 // Word select (LRCK) #define I2S_DATA_PIN 26 // Data out (DIN) // Audio parameters #define SAMPLE_RATE 44100 #define BITS_PER_SAMPLE 16 #define CHANNELS 2 // Stereo #define BUFFER_SIZE 1024 // Increased for smoother playback (adjust if needed) const float masterGain = 0.5f; //01/20/26 Jo Pref: 0.5 = half previous max volume; tune to 0.4, 0.6, etc. //reverb constants #define DELAY_SAMPLES 22050 // ~0.5 s delay at 44.1 kHz int16_t delayBuffer[DELAY_SAMPLES] = { 0 }; int delayPtr = 0; const float reverbDecay = 0.4f; // 0.0–1.0 → how fast echo fades const float reverbFeedback = 0.35f; // 0.0–0.6 → how much echo feeds back (keep low to avoid ringing) // Solfeggio Frequencies (Hz) const float frequencies[9] = { 174, 285, 396, 417, 528, 639, 741, 852, 963 }; int selectedFreqIndex = -1; // None selected initially // UI Dimensions (240x320 portrait) const int screenWidth = 240; const int screenHeight = 320; const int buttonSize = 70; const int buttonSpacing = 10; const int sliderHeight = 25; // Reduced to fit on screen const int sliderSpacing = 10; // Adjusted spacing const int sliderWidth = screenWidth - 20; const int sliderX = 10; const int leftVolSliderY = 255; // Adjusted to match touch top y~260, avoid button overlap const int rightVolSliderY = leftVolSliderY + sliderHeight + sliderSpacing; // 255+25+10=290 to 315 (fits within 320) // Audio controls float leftVol = 0.5f; // Left ear volume (0-1) float rightVol = 0.5f; // Right ear volume (0-1) // Battery indicator (digital) const int batteryPin = 35; int batRectWidth = 30; int batRectHeight = 5; // Deep sleep (after inactivity) unsigned long lastActivityTime = 0; const unsigned long sleepTimeout = 20UL * 60 * 1000; // 20 minutes // RTC data for surviving deep sleep RTC_DATA_ATTR int bootCount = 0; // Global buffer for samples int16_t audioBuffer[BUFFER_SIZE * CHANNELS]; // NVS for state persistence Preferences prefs; // Touch state for hysteresis bool isPressed = false; //Function to generate audio data (sine wave with separate L/R volumes) //01/20/26 mod to halve volume for Jo pref void generateAudioBuffer(float frequency) { static float phase = 0.0; const float phaseIncrement = 2.0 * M_PI * frequency / SAMPLE_RATE; for (int i = 0; i < BUFFER_SIZE; i++) { int16_t sample = (int16_t)(sin(phase) * 32767.0); // full-scale dry sine // Apply L/R volumes first (slider 0–1) int32_t left = (int32_t)(sample * leftVol); int32_t right = (int32_t)(sample * rightVol); // Apply master gain (halves overall level without affecting slider mapping) left = (int32_t)(left * masterGain); right = (int32_t)(right * masterGain); // Clip to 16-bit range audioBuffer[2 * i] = (int16_t)constrain(left, INT16_MIN, INT16_MAX); audioBuffer[2 * i + 1] = (int16_t)constrain(right, INT16_MIN, INT16_MAX); phase += phaseIncrement; if (phase >= 2.0 * M_PI) phase -= 2.0 * M_PI; } } // Method to print the reason by which ESP32 has been awaken from sleep void print_wakeup_reason() { esp_sleep_wakeup_cause_t wakeup_reason; wakeup_reason = esp_sleep_get_wakeup_cause(); switch (wakeup_reason) { case ESP_SLEEP_WAKEUP_EXT0: Serial.println("Wakeup caused by external signal using RTC_IO"); break; case ESP_SLEEP_WAKEUP_EXT1: Serial.println("Wakeup caused by external signal using RTC_CNTL"); break; case ESP_SLEEP_WAKEUP_TIMER: Serial.println("Wakeup caused by timer"); break; case ESP_SLEEP_WAKEUP_TOUCHPAD: Serial.println("Wakeup caused by touchpad"); break; case ESP_SLEEP_WAKEUP_ULP: Serial.println("Wakeup caused by ULP program"); break; default: Serial.printf("Wakeup was not caused by deep sleep: %d\n", wakeup_reason); break; } } void setup() { Serial.begin(115200); touchscreenSPI.begin(TOUCH_CLK, TOUCH_MISO, TOUCH_MOSI, TOUCH_CS); ts.begin(touchscreenSPI); ts.setRotation(1); tft.init(); tft.invertDisplay(1); tft.setRotation(2); tft.fillScreen(TFT_BLACK); tft.setTextColor(TFT_WHITE, TFT_BLACK); tft.setTextSize(2); pinMode(batteryPin, INPUT); // Increment boot number and print it every reboot ++bootCount; Serial.println("Boot number: " + String(bootCount)); // Print the wakeup reason for ESP32 print_wakeup_reason(); // Load state from NVS prefs.begin("cyd_tone", false); selectedFreqIndex = prefs.getInt("freqIndex", -1); //leftVol = prefs.getFloat("leftVol", 0.5f); //rightVol = prefs.getFloat("rightVol", 0.5f); leftVol = prefs.getFloat("leftVol", 0.1f);//Jo pref rightVol = prefs.getFloat("rightVol", 0.1f);//Jo pref prefs.end(); // Configure I2S i2s_config_t i2s_config = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = SAMPLE_RATE, .bits_per_sample = (i2s_bits_per_sample_t)BITS_PER_SAMPLE, .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = 0, .dma_buf_count = 8, .dma_buf_len = BUFFER_SIZE, .use_apll = true, // Better clock accuracy for audio .tx_desc_auto_clear = true, .fixed_mclk = 0 }; i2s_pin_config_t pin_config = { .bck_io_num = I2S_BCK_PIN, .ws_io_num = I2S_WS_PIN, .data_out_num = I2S_DATA_PIN, .data_in_num = I2S_PIN_NO_CHANGE }; // Install and start I2S driver if (i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL) != ESP_OK) { Serial.println("I2S driver install failed"); while (1); } if (i2s_set_pin(I2S_NUM_0, &pin_config) != ESP_OK) { Serial.println("I2S pin set failed"); while (1); } Serial.println("I2S initialized."); // Configure external wake up on GPIO36 (TOUCH_IRQ, wakes on low level when touched) esp_sleep_enable_ext0_wakeup((gpio_num_t)TOUCH_IRQ, 0); // 0 for low level Serial.println("[DEBUG] Wake setup done"); drawUI(); updateBatteryIndicator(); lastActivityTime = millis(); } void loop() { // Prioritize audio float currentFreq = (selectedFreqIndex >= 0) ? frequencies[selectedFreqIndex] : 0.0f; generateAudioBuffer(currentFreq); // Write buffer to I2S size_t bytesWritten; i2s_write(I2S_NUM_0, audioBuffer, sizeof(audioBuffer), &bytesWritten, portMAX_DELAY); if (bytesWritten != sizeof(audioBuffer)) { Serial.println("I2S write incomplete"); } checkTouch(); // Check touch after audio to minimize interruption updateBatteryIndicator(); if (millis() - lastActivityTime > sleepTimeout) { // Save state to NVS before sleep prefs.begin("cyd_tone", false); prefs.putInt("freqIndex", selectedFreqIndex); prefs.putFloat("leftVol", leftVol); prefs.putFloat("rightVol", rightVol); prefs.end(); tft.fillScreen(TFT_BLACK); // Optional clear before sleep Serial.println("Going to deep sleep now"); esp_deep_sleep_start(); // Deep sleep; resets on wake } } // drawUI function void drawUI() { tft.fillScreen(TFT_BLACK); // Clear screen // Draw 3x3 buttons for frequencies int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.drawRect(x, y, buttonSize, buttonSize, TFT_WHITE); tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } // Left Volume slider (cyan) with label tft.setTextSize(1); tft.setCursor(sliderX - 30, leftVolSliderY + (sliderHeight / 2) - 4); tft.print("L"); tft.setTextSize(2); tft.drawRect(sliderX, leftVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); int leftFillWidth = static_cast<int>(leftVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, leftVolSliderY + 1, leftFillWidth, sliderHeight - 2, TFT_CYAN); tft.fillRect(sliderX + 1 + leftFillWidth, leftVolSliderY + 1, sliderWidth - 2 - leftFillWidth, sliderHeight - 2, TFT_BLACK); // Right Volume slider (green) with label tft.setTextSize(1); tft.setCursor(sliderX - 30, rightVolSliderY + (sliderHeight / 2) - 4); tft.print("R"); tft.setTextSize(2); tft.drawRect(sliderX, rightVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); int rightFillWidth = static_cast<int>(rightVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, rightVolSliderY + 1, rightFillWidth, sliderHeight - 2, TFT_GREEN); tft.fillRect(sliderX + 1 + rightFillWidth, rightVolSliderY + 1, sliderWidth - 2 - rightFillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(0, 0, batRectWidth, batRectHeight, TFT_WHITE); redrawButtons(); // Apply selected state } // updateBatteryIndicator function using digitalRead void updateBatteryIndicator() { int batLevel = digitalRead(batteryPin); // 1 = HIGH (good), 0 = LOW (low) //Serial.println("Battery digital: " + String(batLevel)); // Debug tft.fillRect(0, 0, batRectWidth, batRectHeight, TFT_BLACK); // Clear previous if (batLevel == 0) { tft.fillRect(0, 0, batRectWidth, batRectHeight, TFT_RED); // Low } else { tft.fillRect(0, 0, batRectWidth, batRectHeight, TFT_GREEN); // Good } tft.drawRect(0, 0, batRectWidth, batRectHeight, TFT_WHITE); } // checkTouch function with hysteresis and state management void checkTouch() { TS_Point p = ts.getPoint(); if (!isPressed) { if (p.z > TOUCH_PRESS_HIGH) { isPressed = true; processTouch(p, true); // Initial press = true } } else { if (p.z < TOUCH_PRESS_LOW) { isPressed = false; // Process touch end if needed (not for this app) } else { processTouch(p, false); // Drag, initial press = false } } delay(20); // Reduced debounce for responsiveness } // Helper to process touch (called on press start or drag) void processTouch(TS_Point p, bool initialPress) { int x = map(p.y, TS_MINY, TS_MAXY, 0, screenWidth); // Inverted X int y = map(p.x, TS_MINX, TS_MAXX, 0, screenHeight); // Inverted Y //Serial.printf("Mapped X: %d, Y: %d, Z: %d\n", x, y, p.z); // Check buttons first (only on initial press) if (initialPress) { int row = (y - buttonSpacing) / (buttonSize + buttonSpacing); int col = (x - buttonSpacing) / (buttonSize + buttonSpacing); if (row >= 0 && row < 3 && col >= 0 && col < 3) { int index = row * 3 + col; //Serial.printf("Button pressed: %d\n", index); // Debug if (index != selectedFreqIndex) { selectedFreqIndex = index; } else { selectedFreqIndex = -1; // Toggle off } redrawButtons(); return; // Exit to avoid checking sliders } } // Check left volume slider (allow drag, initial or ongoing) if (y >= leftVolSliderY - 5 && y <= leftVolSliderY + sliderHeight + 5 && x >= sliderX && x <= sliderX + sliderWidth) { leftVol = static_cast<float>(x - sliderX) / sliderWidth; leftVol = constrain(leftVol, 0.0f, 1.0f); //Serial.printf("Left Vol adjusted to: %.2f\n", leftVol); // Debug int fillWidth = static_cast<int>(leftVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, leftVolSliderY + 1, fillWidth, sliderHeight - 2, TFT_CYAN); tft.fillRect(sliderX + 1 + fillWidth, leftVolSliderY + 1, sliderWidth - 2 - fillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(sliderX, leftVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); // Redraw border } // Check right volume slider (allow drag, initial or ongoing) if (y >= rightVolSliderY - 5 && y <= rightVolSliderY + sliderHeight + 5 && x >= sliderX && x <= sliderX + sliderWidth) { rightVol = static_cast<float>(x - sliderX) / sliderWidth; rightVol = constrain(rightVol, 0.0f, 1.0f); int fillWidth = static_cast<int>(rightVol * (sliderWidth - 2)); tft.fillRect(sliderX + 1, rightVolSliderY + 1, fillWidth, sliderHeight - 2, TFT_GREEN); tft.fillRect(sliderX + 1 + fillWidth, rightVolSliderY + 1, sliderWidth - 2 - fillWidth, sliderHeight - 2, TFT_BLACK); tft.drawRect(sliderX, rightVolSliderY, sliderWidth, sliderHeight, TFT_WHITE); // Redraw border } lastActivityTime = millis(); // Reset inactivity timer on touch } // redrawButtons function void redrawButtons() { int row = 0; int col = 0; for (int i = 0; i < 9; i++) { int x = buttonSpacing + col * (buttonSize + buttonSpacing); int y = buttonSpacing + row * (buttonSize + buttonSpacing); tft.fillRect(x + 1, y + 1, buttonSize - 2, buttonSize - 2, (i == selectedFreqIndex) ? TFT_BLUE : TFT_BLACK); tft.drawRect(x, y, buttonSize, buttonSize, TFT_WHITE); // Ensure border tft.setCursor(x + 10, y + 25); tft.printf("%.0f", frequencies[i]); col++; if (col > 2) { col = 0; row++; } } } |
This all worked really well, except for one last major ‘gotcha’. We used GPIO35 (input only) to monitor the battery voltage, and implemented a small ‘slider’ style battery charge indicator at the very top of the display. A call to ‘analogRead()’ in loop() measured battery voltage, and this value was depicted by the length and color of the ‘slider’ display. Unfortunately it turned out that the ESP32 library required to run the PCM5102A DAC module conflicted with the Arduino library used for ‘analogRead()’ calls. As hard as this was to believe, it was real, and neither Grok nor I could figure any way around it. So, we wound up using ‘digitalRead()’ instead (which obviously will only return a ‘0’ or ‘1’) and displaying either a full-length green slider (battery good) or a full-length red slider (battery bad).
And the schematic:
