This post is about the wireless module that receives data from the base station described in Wireless Audio Visualizer – Part I and visualizes it on two VU-meters and a TFT display.
Since the AI-Thinker AudioKit base station from Part I does most of the heavy lifting, all that’s left for the wireless module is a rather simple visualization of the received data. That’s why the code for my setup (below) probably needs no further explanation, even if you decide to use different components.
My own choice of components for this module (see picture) were entirely determined by what I had lying on the shelf, and by the size of my lunch box, that I was prepared to sacrifice for the good cause. Otherwise, using an €52 M5Stack Core 2 would be a very expensive overkill (and I don’t even use its entire display). Even the cheapest ESP32 and a small display can do the job. Besides, these panel meters are also available with an LED backlight, what would make the DC converter in my setup obsolete. That said, my lunch box and the M5Stack seemed to be just made for each other and I definitely prefer the warm glow of incandescent bulbs over LEDs.
An even cheaper solution is emulating the VU-meters on two displays, for instance TTGO T-Display boards, that I used for my first YouTube video.
So it all depends on how much money you want to spend and what you want your module to show (and how). Note that using a different M5Stack core module could be a problem. Both my Gray and Fire have their speaker hard-wired to one of the DAC pins, so you would have to physically remove it. Which is probably a good idea anyway, as at least the Gray will click with every call to analogRead(). Nice looks, poor electronic design.
Wiring of analog panel meters
All analog panel VU-meters that I have come across so far have similar specs, but I’m far from an electronics expert, so if you use my wiring, make sure that their resistance is about 650Ω and their current limit 0.5 mA max.
As my code drives these meters from the ESP32’s 3.3V DACs (GPIO 25 & 26), we need to add current limiting resistors in series. For meters with the above specs, their value R follows from 3.3 = 0.0005 * (R + 650), which means that we would need two 6K resistors. For my module, however, I used the wiring scheme from an earlier (currently archived) post, using a 4K7 resistor and a flyback diode for each meter, and it works great.
My code has a variable maxDac that takes its value (in the [50, 255] range) from a potentiometer reading:
1 |
maxDac = map(analogRead(35), 0, 4095, 50, 255); |
For testing whether your resistor’s value is safe for your type of meter, you can temporarily hard-code the value of maxDac, starting with a low value, or play with the range of the map command.
If you use a display without touch and want one of the spectrum variants to be displayed instead of the default logo / battery indicator, just set the value of variable spectrum in the code to 1 for a leaking peak spectrum and to 2 for the 3-color version.
Finally, you’ll need to upload 6 small jpg files to SPIFFS. They can be found here.
Here is the complete code for an M5Stack Core 2, with analog panel meters connected to pins 25 & 26 and a 10K potentiometer connected to pin 35 (‘sensitivity’ control).
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 |
/* * Wireless Audio Visualizer - Wireless Module (on M5Stack Core 2) * 2022 © paulF (afterBlink) * * See https://www.youtube.com/watch?v=mZC5sKY97VI */ #include "esp_now.h" #include "WiFi.h" #include "FS.h" #include "M5Core2.h" #include "SPIFFS.h" uint8_t maxDac; unsigned long t, t2, batcheck; const uint8_t n_bins = 32; typedef struct sent_struct { uint16_t l; // links uint16_t r; // rechts uint8_t m[n_bins]; } sent_struct; sent_struct myData; int oldval[n_bins]; int peak[n_bins]; const uint8_t bar_w = 6; const uint8_t bar_h = 120; const uint8_t red_h = 20; const uint8_t orange_h = 20; const uint8_t green_h = 80; uint16_t bar[bar_h * bar_w]; uint8_t spectrum = 0; // 0 = logo / battery indicator; 1 = leaking peak spectrum; 2 = 3-color spectrum void fill_bar() { for (int i = 0; i < bar_w * green_h; i++) bar[i] = TFT_GREEN; for (int i = bar_w * green_h; i < bar_w * (green_h + orange_h); i++) bar[i] = TFT_YELLOW; for (int i = bar_w * (green_h + orange_h); i < bar_w * bar_h; i++) bar[i] = TFT_RED; } void zero_peaks() { for (int p = 0; p < n_bins; p++) { peak[p] = 0; } } void drawbin(uint8_t bnr) { int val = myData.m[bnr]; val = min(val / 2, 120); val = max(val, oldval[bnr] - 4); oldval[bnr] = val; M5.Lcd.pushImage(8 * (30 - bnr), 10, 6, val, bar); M5.Lcd.fillRect(8 * (30 - bnr), val + 10, 6, 132 - val, TFT_BLACK); } void drawbin2(uint8_t bnr) { int val = myData.m[bnr]; val = min(val / 2, 120); val = max(val, oldval[bnr] - 3); oldval[bnr] = val; M5.Lcd.fillRect(8 * (30 - bnr), 10, 6, val, TFT_DARKGREY); M5.Lcd.fillRect(8 * (30 - bnr), val + 10, 6, 132 - val, TFT_BLACK); if ((peak[bnr] - 2) > val ) { M5.Lcd.fillRect(8 * (30 - bnr), peak[bnr] + 10, 6, 2, TFT_LIGHTGREY); peak[bnr] = peak[bnr] - 2; } else { peak[bnr] = val; } } void OnDataRecv(const uint8_t * mac, const uint8_t *incomingData, int len) { memcpy(&myData, incomingData, sizeof(myData)); } void touchAction(Event & e) { if (strcmp(e.objName(), "background") == 0) { M5.Lcd.fillScreen(TFT_BLACK); if (spectrum == 2) t2 = 0; spectrum = (spectrum + 1) % 3; } } void setup() { //Serial.begin(115200); maxDac = map(analogRead(35), 0, 4095, 50, 255); M5.begin(true, false, true, false); M5.Axp.SetLcdVoltage(2800); M5.Buttons.addHandler(touchAction, E_TAP); if (!SPIFFS.begin(true)) { Serial.println("SPIFFS Mount Failed"); return; } M5.Lcd.setRotation(2); // portrait M5.Lcd.fillScreen(TFT_BLACK); M5.Lcd.drawJpgFile(SPIFFS, "/logo.jpg", 66, 85); M5.Lcd.setSwapBytes(true); for (int v = 0; v < n_bins; v++) { oldval[v] = 0; } fill_bar(); zero_peaks(); batcheck = 30000; WiFi.mode(WIFI_MODE_STA); Serial.println(WiFi.macAddress()); //Init ESP-NOW if (esp_now_init() != ESP_OK) { Serial.println("Error initializing ESP-NOW"); return; } esp_now_register_recv_cb(OnDataRecv); t = millis(); t2 = 0; } void loop() { dacWrite(25, map(myData.l >> 8, 0, 255, 0, maxDac)); dacWrite(26, map(myData.r >> 8, 0, 255, 0, maxDac)); if (spectrum == 0 && (millis() - t2) > batcheck) { float batVoltage = M5.Axp.GetBatVoltage(); float batPercentage = ( batVoltage < 3.2 ) ? 0 : ( batVoltage - 3.2 ) * 100; int hulp = (int)(batPercentage / 25.0 + 0.49); switch (hulp) { case 0: M5.Lcd.drawJpgFile(SPIFFS, "/logo0.jpg", 66, 85); break; case 1: M5.Lcd.drawJpgFile(SPIFFS, "/logo25.jpg", 66, 85); break; case 2: M5.Lcd.drawJpgFile(SPIFFS, "/logo50.jpg", 66, 85); break; case 3: M5.Lcd.drawJpgFile(SPIFFS, "/logo75.jpg", 66, 85); break; case 4: M5.Lcd.drawJpgFile(SPIFFS, "/logo100.jpg", 66, 85); break; default: M5.Lcd.drawJpgFile(SPIFFS, "/logo.jpg", 66, 85); break; } t2 = millis(); } if (spectrum == 1) { for (uint8_t f = 1; f < n_bins - 1; f++) { drawbin2(f); } } if (spectrum == 2) { for (uint8_t f = 1; f < n_bins - 1; f++) { drawbin(f); } } if ((millis() - t) > 100) { maxDac = map(analogRead(35), 0, 4095, 50, 255); t = millis(); } M5.update(); delay(5); } |