├── .gitignore ├── .vscode └── extensions.json ├── README.md ├── data ├── capture_cv_chart.js ├── capture_cv_meter.js ├── capture_frequency.js ├── chart.min.js ├── chartjs-plugin-streaming.min.js ├── cv_chart.html ├── cv_meter.html ├── favicon.ico ├── freq_counter.html ├── index.html └── style.css ├── docs ├── block.png ├── capture_8s_manual.png ├── capture_gated.png ├── capture_gated_2000Hz.png ├── capture_gated_400Hz.png ├── cv_meter.png ├── esp32_meter_schematic.pdf ├── freq_counter.png ├── hardware.jpg └── home_page.png ├── lib └── README ├── littlefsbuilder.py ├── mklittlefs ├── no_ota.csv ├── platformio.ini └── src ├── config.h ├── freq_counter.cpp ├── freq_counter.h ├── ina226.cpp ├── ina226.h ├── main.cpp ├── nv_data.cpp ├── nv_data.h ├── wifi_cfg.cpp └── wifi_cfg.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 MULTI-METER 2 | 3 | ESP32 development board used to implement WiFi multi-meter with following functions : 4 | 5 | * Capture and display load bus voltage and load current, in meter mode or chart recorder mode. Uses INA226 sensor. 6 | * [Frequency counter with range 1Hz to 40MHz and 1Hz resolution, and independent oscillator (signal generator).](https://blog.eletrogate.com/esp32-frequencimetro-de-precisao) 7 | * Uses websockets for control and data communication between browser and ESP32. 8 | 9 | Tested with Chrome and Opera browsers on Ubuntu 20.04 LTS. 10 | 11 | ## Configuration 12 | 13 | If not configured, the meter boots up as a stand-alone WiFi Access Point (SSID `ESP32_METER`, no password required) and web server. 14 | 15 | Connect to this WiFi Access Point, then open the home page `http://192.168.4.1` in a browser. 16 | If your OS has mDNS support, use the url `http://meter.local`. 17 | 18 | MacOS has built-in support for mDNS. On Windows, install Bonjour. On Ubuntu, install Avahi. 19 | 20 |

21 | 22 |

23 | 24 | Full meter functionality is available in this stand-alone mode. 25 | 26 | However, it is more convenient for the meter to connect as a station to an existing WiFi Access Point. 27 | 28 | On the home page you can specify an external WiFi Access Point SSID and password. 29 | Submit the WiFi credentials and restart the ESP32. 30 | Now it will connect as a station to the configured Access Point and then start the web server. 31 | The web page is at `http://meter.local` as before. 32 | 33 | If you do not have OS mDNS support, and you still want to use the meter in station mode, you will need a serial debug connection to the ESP32. 34 | Check the serial monitor log on reboot to get the station IP address assigned to the meter by the external WiFi Access Point. 35 | 36 | # Current & Voltage Display 37 | 38 |

39 | 40 |

41 | 42 | Full-scale bus voltage = ~40V. 43 | 44 | There are two ranges for current measurement : 45 | * HIGH : Full-scale 1638mA, with a resolution of 50uA. 46 | * LOW : Full-scale 78mA, with a resolution of 2.4uA. 47 | 48 | Meter configuration, capture and display functions are available via a web page. 49 | 50 | There are two display modes : 51 | * Meter display with current and voltage readings updated every second 52 | * Scrollable chart display with manually triggered or gated capture 53 | 54 | 55 | ## Current & Voltage Meter 56 | 57 | DUT current and voltage readings are updated every second. 58 | 59 | 60 | 61 | The meter uses a fixed sampling configuration : 62 | * Vbus ADC conversion time 332uS 63 | * Shunt ADC conversion time 332uS 64 | * Averages 0.4 seconds of samples for every meter reading 65 | 66 | Select the current scale from one of the following options : 67 | * HI : Full-scale 1638.35mA, resolution 50uA 68 | * LO : Full-scale 78.017mA, resolution 2.4uA 69 | * Auto 70 | 71 | If you select `Auto`, samples are first captured with the LO range setting, as this has higher resolution. 72 | If any sample is off-scale, the averaging process is restarted with HI range configured. 73 | 74 | Do not select the `LO` or `Auto` option if the expected current is high and the DUT cannot tolerate the voltage drop across the 1ohm `LO` current range shunt resistor. 75 | 76 | For any selected option, if the final averaging process used off-scale samples, the current display background will flash orange as a warning. 77 | 78 | ## Current & Voltage Chart 79 | 80 | There are two capture options : manually triggered with a selected capture interval, and gated capture. 81 | 82 | Sample rate options : 83 | * 2000Hz : no sample averaging, vbus ADC conversion time 204uS, shunt ADC conversion time 204uS. 84 | * 1000Hz : no sample averaging, vbus ADC conversion time 332uS, shunt ADC conversion time 332uS. 85 | * 400Hz : averaging 4 samples, vbus ADC conversion time 140uS, shunt ADC conversion time 140uS. 86 | 87 | Current scale options : 88 | * HI : Full-scale 1638.35mA, 50uA resolution 89 | * LO : Full-scale 78.017mA, 2.4uA resolution 90 | 91 | In Chart Display mode, current auto-ranging is not possible due to the high sampling rate. 92 | 93 | Click on the legends `ma` and `V` above the plot to toggle the display of the corresponding data. 94 | 95 | Use the scroll bar sliders to display a subset of the captured data. 96 | 97 | 98 | ## Manually triggered capture with selected time interval 99 | 100 | Up to 16000 samples can be captured with a single trigger. So the maximum capture interval depends on the selected sampling rate. 101 | 102 | * max 8 seconds capture @ 2000Hz 103 | * max 16 seconds capture @ 1000Hz 104 | * max 40 seconds capture @ 400Hz 105 | 106 | ## Gated Capture 107 | 108 | The external gate signal is input via an opto-isolator. 109 | This gate signal can come from the Device Under Test (DUT) or other external trigger. 110 | 111 | Once triggered, capture will start when the gate input goes high. 112 | 113 | Capture will end when the gate input goes low, or 16000 samples have been captured. 114 | 115 | 116 | ## Example D.U.T. 117 | 118 | The DUT is an ESP32 development board running an Internet connectivity test. 119 | 120 | The power supply is a fully-charged 18650 Li-Ion battery. 121 | 122 | On reset, the DUT ESP32 executes the following sequence : 123 | 1. Connects to a WiFi Internet Access Point as a station 124 | 2. Connects to a Network Time Protocol (NTP) server to get local time 125 | 3. Enters deep sleep for 5 seconds 126 | 4. Restarts 127 | 128 | The DUT ESP32 is periodically active for approximately 2.4 seconds, and in deep-sleep for 5 seconds. 129 | 130 | The DUT ESP32 uses a GPIO pin to set the meter gate signal high on restart, and resets the gate signal just before going to sleep. 131 | 132 | ## Manually triggered capture 133 | 134 | This is an example of an 8-second manually triggered capture. The capture was triggered approximately half-way during the DUT ESP32 deep-sleep interval. 135 | 136 |

137 | 138 |

139 | 140 | We can see that the DUT load current is ~10mA when the ESP32 is in deep-sleep. This residual current is due to the USB-UART IC and LDO regulator on the DUT ESP32 development board. 141 | 142 | When the DUT ESP32 is active, we can see high current pulses (> 450mA) corresponding to WiFi radio transmission bursts. 143 | 144 | ## Gated capture 145 | 146 | This is an example of gated capture. It records the load current & voltage only while the DUT ESP32 is active. 147 | 148 |

149 | 150 |

151 | 152 | # Choosing Sample Rates 153 | 154 | ## Example : Sample Rate = 400Hz 155 | Sampling at 400Hz will result in less noise due to sample averaging. It also allows you to capture longer intervals. 156 | 157 | However, it may not capture brief current pulses or record accurate maximum/minimum values. 158 | 159 |

160 | 161 |

162 | 163 | ## Example : Sample Rate = 2000Hz 164 | 165 |

166 | 167 |

168 | 169 | The measurements are noisier, but it captures all the current pulses due to WiFi transmission bursts. 170 | 171 | Maximum and minimum current values are more accurately captured. 172 | 173 | # Frequency Counter & Oscillator 174 | 175 | 176 | 177 | ## Frequency Counter 178 | * 3.3V TTL level frequency counter input on GPIO 34. 179 | * 1Hz to 40MHz range with 1Hz resolution. 180 | 181 | ## Oscillator 182 | * Independent oscillator output on GPIO 33. 183 | * For the screen snapshot above, the oscillator output pin was connected to the frequency counter pin, and the oscillator frequency set to 13429Hz. 184 | 185 | # Build Environment 186 | * Ubuntu 20.04 LTS amdx64 187 | * Visual Studio Code with PlatformIO plugin using Arduino framework targeting `esp32dev` board. 188 | * The file `platformio.ini` specifies the framework packages, ESP32 configuration, and external libraries. 189 | * External libraries used : 190 | * EspAsyncWebServer 191 | * AsyncTCP 192 | * ArduinoJson 193 | * Chart.js v3.7.0 194 | 195 | # Hardware 196 | 197 | 198 | 199 | 200 | * [Circuit Schematic (PDF)](docs/esp32_meter_schematic.pdf) 201 | * Any ESP32 development board with on-board USB-UART. 202 | * INA226 current sensor 203 | * A pi-filter network + low-noise MIC5205 LDO voltage regulator provides the power supply for the INA226. 204 | * High-side current metering. Si4925 dual PMOSFET, 2N7002 NMOSFETs are used to switch shunt resistors for low and high scale current measurements. 205 | * PC817 opto-coupler for gated measurement. 206 | * 0.05ohm 1% shunt resistor for HIGH current scale (0 - 1638mA, 50uA resolution) 207 | * 1.0 ohm 1% additional shunt resistor for LOW current scale (0 - 78mA, 2.4uA resolution). 208 | * SS56 schottky diode, protects the 1.0 ohm shunt resistor when the LOW current scale is selected. 209 | * Frequency counter input signal (3.3V TTL level) on GPIO 34. 210 | * Independent Oscillator output signal on GPIO 33. 211 | 212 | # Credits 213 | * [Range switching with FET switches](https://www.youtube.com/watch?v=xSEYPP5Xsi0) 214 | * [Javascript scrolling chart](https://stackoverflow.com/questions/35854244/how-can-i-create-a-horizontal-scrolling-chart-js-line-chart-with-a-locked-y-axis) 215 | * [Frequency Counter & Oscillator](https://blog.eletrogate.com/esp32-frequencimetro-de-precisao) 216 | 217 | 218 | -------------------------------------------------------------------------------- /data/capture_cv_chart.js: -------------------------------------------------------------------------------- 1 | // Chart Initialization 2 | 3 | let c = document.getElementById("myChart"); 4 | let Ctxt = document.getElementById("myChart").getContext("2d"); 5 | 6 | let timeMs = 0.0; 7 | let periodMs = 0.5; 8 | let iScale = 0.05; 9 | let vScale = 0.00125; 10 | let Time = []; 11 | let Data_mA = []; 12 | let Data_V = []; 13 | 14 | for(let inx = 0; inx < 1000; inx++){ 15 | Time.push(periodMs*inx); 16 | Data_mA.push(0); 17 | Data_V.push(0); 18 | } 19 | 20 | var ChartInst; 21 | 22 | function new_chart() { 23 | ChartInst = new Chart(Ctxt, { 24 | type: "line", 25 | data: { 26 | labels: Time, 27 | datasets: [{ 28 | label: 'mA', 29 | yAxisID: 'mA', 30 | backgroundColor: "rgb(209, 20, 61)", 31 | borderColor: "rgb(209, 20, 61)", 32 | data: Data_mA, 33 | cubicInterpolationMode: 'monotone', // maxima/minima stay at sample points 34 | }, 35 | { 36 | label: 'V', 37 | yAxisID: 'V', 38 | backgroundColor: "rgb(34, 73, 228)", 39 | borderColor: "rgb(34, 73, 228)", 40 | data: Data_V, 41 | cubicInterpolationMode: 'monotone', 42 | }], 43 | }, 44 | options: { 45 | animation: { 46 | duration: 0 47 | }, 48 | responsive : true, 49 | maintainAspectRatio: false, 50 | borderWidth : 1, 51 | pointRadius : 0, 52 | scales: { 53 | mA : { 54 | type: 'linear', 55 | position: 'left', 56 | ticks : { 57 | color: "rgb(209, 20, 61)" 58 | } 59 | }, 60 | V : { 61 | type: 'linear', 62 | position: 'right', 63 | ticks : { 64 | color: "rgb(34, 73, 228)" 65 | } 66 | } 67 | } 68 | }, 69 | }); 70 | } 71 | 72 | // Chart Handling 73 | 74 | function init_sliders() { 75 | let sliderSections = document.getElementsByClassName("range-slider"); 76 | for (let i = 0; i < sliderSections.length; i++) { 77 | let sliders = sliderSections[i].getElementsByTagName("input"); 78 | for (let j = 0; j < sliders.length; j++) { 79 | if (sliders[j].type === "range") { 80 | sliders[j].oninput = update_chart; 81 | sliders[j].value = (j == 0 ? 0.0 : Time.length*periodMs); 82 | sliders[j].min = 0.0; 83 | sliders[j].max = parseFloat(Time.length)*periodMs; 84 | // Manually trigger event first time to display values 85 | sliders[j].oninput(); 86 | } 87 | } 88 | } 89 | } 90 | 91 | function update_chart() { 92 | // Get slider values 93 | let slides = document.getElementsByTagName("input"); 94 | let min = parseFloat(slides[0].value); 95 | let max = parseFloat(slides[1].value); 96 | // Neither slider will clip the other, so make sure we determine which is larger 97 | if (min > max) { 98 | let tmp = max; 99 | max = min; 100 | min = tmp; 101 | } 102 | 103 | let time_slice = []; 104 | let data_mA_slice = []; 105 | let data_V_slice = []; 106 | 107 | let min_index = min/periodMs; 108 | let max_index = max/periodMs; 109 | 110 | time_slice = JSON.parse(JSON.stringify(Time)).slice(min_index, max_index); 111 | data_mA_slice = JSON.parse(JSON.stringify(Data_mA)).slice(min_index, max_index); 112 | data_V_slice = JSON.parse(JSON.stringify(Data_V)).slice(min_index, max_index); 113 | 114 | ChartInst.data.labels = time_slice; 115 | ChartInst.data.datasets[0].data = data_mA_slice; 116 | ChartInst.data.datasets[1].data = data_V_slice; 117 | ChartInst.update(0); // no animation 118 | 119 | let iAvg = 0.0; 120 | let vAvg = 0.0; 121 | let iMax = -9999999.0; 122 | let iMin = 9999999.0; 123 | let vMax = -9999999.0; 124 | let vMin = 9999999.0; 125 | for(let t = 0; t < time_slice.length ; t++){ 126 | let i = parseFloat(data_mA_slice[t]); 127 | let v = parseFloat(data_V_slice[t]); 128 | iAvg = iAvg + i; 129 | vAvg = vAvg + v; 130 | if (i > iMax) iMax = i; 131 | if (v > vMax) vMax = v; 132 | if (i < iMin) iMin = i; 133 | if (v < vMin) vMin = v; 134 | } 135 | iAvg = iAvg/time_slice.length; 136 | vAvg = vAvg/time_slice.length; 137 | 138 | let displayElement = document.getElementsByClassName("rangeValues")[0]; 139 | displayElement.innerHTML = "[" + min + "," + max + "]mS"; 140 | document.getElementById("istats").innerHTML = 141 | "avg : " + iAvg.toFixed(3) + "mA
" + 142 | "min : " + iMin.toFixed(3) + "mA
" + 143 | "max : " + iMax.toFixed(3) + "mA"; 144 | document.getElementById("vstats").innerHTML = 145 | "avg : " + vAvg.toFixed(3) + "V
" + 146 | "min : " + vMin.toFixed(3) + "V
" + 147 | "max : " + vMax.toFixed(3) + "V"; 148 | } 149 | 150 | 151 | // WebSocket Initialization 152 | 153 | let gateway = `ws://${window.location.hostname}/ws`; 154 | var websocket; 155 | 156 | 157 | window.addEventListener('load', on_window_load); 158 | 159 | function on_window_load(event) { 160 | new_chart(); 161 | init_sliders(); 162 | init_web_socket(); 163 | init_capture_buttons(); 164 | } 165 | 166 | window.onbeforeunload = function() { 167 | websocket.onclose = function () {}; // disable onclose handler first 168 | websocket.close(); 169 | } 170 | 171 | 172 | // WebSocket handling 173 | 174 | function init_web_socket() { 175 | console.log('Trying to open a WebSocket connection...'); 176 | websocket = new WebSocket(gateway); 177 | websocket.binaryType = "arraybuffer"; 178 | websocket.onopen = on_ws_open; 179 | websocket.onclose = on_ws_close; 180 | websocket.onmessage = on_ws_message; 181 | } 182 | 183 | function on_ws_open(event) { 184 | console.log('Connection opened'); 185 | } 186 | 187 | function on_ws_close(event) { 188 | console.log('Connection closed'); 189 | setTimeout(init_web_socket, 2000); 190 | } 191 | 192 | 193 | function on_ws_message(event) { 194 | let view = new Int16Array(event.data); 195 | if ((view.length == 1) && (view[0] == 1234)){ 196 | document.getElementById("led").innerHTML = "
"; 197 | } 198 | else 199 | if ((view.length > 3) && (view[0] == 1111)){ 200 | // new capture tx start 201 | periodMs = parseFloat(view[1])/1000.0; 202 | iScale = view[2] == 0 ? 0.05 : 0.002381; 203 | ChartInst.destroy(); 204 | timeMs = 0.0; 205 | Time = []; 206 | Data_mA = []; 207 | Data_V = []; 208 | let len = (view.length - 3) / 2; 209 | for(let t = 0; t < len; t++){ 210 | Time.push(timeMs); 211 | let ima = view[2*t+3] * iScale; 212 | let v = view[2*t+4] * vScale; 213 | Data_mA.push(ima); 214 | Data_V.push(v); 215 | timeMs += periodMs; 216 | } 217 | // ready to receive next data packet 218 | websocket.send("x"); 219 | new_chart(); 220 | init_sliders(); 221 | update_chart(); 222 | } 223 | else 224 | if ((view.length > 1) && (view[0] == 2222)){ 225 | let len = (view.length - 1) / 2; 226 | for(let t = 0; t < len; t++){ 227 | Time.push(timeMs); 228 | let ima = view[2*t+1] * iScale; 229 | let v = view[2*t+2] * vScale; 230 | Data_mA.push(ima); 231 | Data_V.push(v); 232 | timeMs += periodMs; 233 | } 234 | // ready to receive next data packet 235 | websocket.send("x"); 236 | init_sliders(); 237 | update_chart(); 238 | } 239 | else 240 | if ((view.length == 1) && (view[0] == 3333)){ 241 | // tx complete 242 | init_sliders(); 243 | //update_chart(); 244 | document.getElementById("led").innerHTML = "
"; 245 | } 246 | } 247 | 248 | // Button handling 249 | 250 | function init_capture_buttons() { 251 | document.getElementById("capture").addEventListener("click", on_capture_click); 252 | document.getElementById("captureGated").addEventListener("click", on_capture_gated_click); 253 | } 254 | 255 | function on_capture_click(event) { 256 | let cfgIndex = document.getElementById("cfgInx").value; 257 | let captureSeconds = document.getElementById("captureSecs").value; 258 | let scale = document.getElementById("scale").value; 259 | let jsonObj = {}; 260 | jsonObj["action"] = "cv_capture"; 261 | jsonObj["cfgIndex"] = cfgIndex; 262 | jsonObj["captureSecs"] = captureSeconds.toString(); 263 | jsonObj["scale"] = scale; 264 | websocket.send(JSON.stringify(jsonObj)); 265 | // set capture led to red, indicate capturing 266 | document.getElementById("led").innerHTML = "
"; 267 | } 268 | 269 | 270 | function on_capture_gated_click(event) { 271 | let cfgIndex = document.getElementById("cfgInx").value; 272 | let scale = document.getElementById("scale").value; 273 | let jsonObj = {}; 274 | jsonObj["action"] = "cv_capture"; 275 | jsonObj["cfgIndex"] = cfgIndex; 276 | // set capture seconds to 0 for gated capture 277 | jsonObj["captureSecs"] = "0"; 278 | jsonObj["scale"] = scale; 279 | websocket.send(JSON.stringify(jsonObj)); 280 | // set capture led to yellow, indicate waiting for gate 281 | document.getElementById("led").innerHTML = "
"; 282 | } 283 | 284 | 285 | function on_sample_rate_change(selectObject) { 286 | let value = selectObject.value; 287 | let docobj = document.getElementById("captureSecs"); 288 | if (value == "0") { 289 | docobj.max = "8"; 290 | if (docobj.value > 8) docobj.value = 8; 291 | } 292 | else 293 | if (value == "1") { 294 | docobj.max = "16"; 295 | if (docobj.value > 16) docobj.value = 16; 296 | } 297 | else 298 | if (value == "2") { 299 | docobj.max = "40"; 300 | if (docobj.value > 40) docobj.value = 40; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /data/capture_cv_meter.js: -------------------------------------------------------------------------------- 1 | 2 | let iScale = 0.05; 3 | let vScale = 0.00125; 4 | let offScale = 0; 5 | let i, v, bgColor; 6 | 7 | function update_meter() { 8 | document.getElementById("ma").innerHTML = i.toFixed(3) + " mA"; 9 | if (offScale == 1) { 10 | document.getElementById("ma").style.backgroundColor = 'orange'; 11 | } 12 | else { 13 | document.getElementById("ma").style.backgroundColor = bgColor; 14 | } 15 | document.getElementById("volts").innerHTML = v.toFixed(3) + " V"; 16 | } 17 | 18 | // WebSocket Initialization 19 | 20 | let gateway = `ws://${window.location.hostname}/ws`; 21 | var websocket; 22 | 23 | window.addEventListener('load', on_window_load); 24 | 25 | function on_window_load(event) { 26 | bgColor = document.getElementById("ma").style.backgroundColor; 27 | init_web_socket(); 28 | } 29 | 30 | function trigger_capture() { 31 | let scale = document.getElementById("scale").value; 32 | websocket.send("m"+scale); 33 | } 34 | 35 | var periodicTrigger = setInterval(trigger_capture, 1000); 36 | 37 | window.onbeforeunload = function() { 38 | clearInterval(periodicTrigger); 39 | websocket.onclose = function () {}; // disable onclose handler first 40 | websocket.close(); 41 | } 42 | 43 | // WebSocket handling 44 | 45 | function init_web_socket() { 46 | console.log('Trying to open a WebSocket connection...'); 47 | websocket = new WebSocket(gateway); 48 | websocket.binaryType = "arraybuffer"; 49 | websocket.onopen = on_ws_open; 50 | websocket.onclose = on_ws_close; 51 | websocket.onmessage = on_ws_message; 52 | } 53 | 54 | function on_ws_open(event) { 55 | console.log('Connection opened'); 56 | } 57 | 58 | function on_ws_close(event) { 59 | console.log('Connection closed'); 60 | setTimeout(init_web_socket, 2000); 61 | } 62 | 63 | 64 | function on_ws_message(event) { 65 | let view = new Int16Array(event.data); 66 | if ((view.length == 5) && (view[0] == 4444)){ 67 | iScale = (view[1] == 0 ? 0.05 : 0.002381); 68 | i = view[2] * iScale; 69 | v = view[3] * vScale; 70 | offScale = view[4]; 71 | update_meter(); 72 | } 73 | // acknowledge packet 74 | websocket.send("x"); 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /data/capture_frequency.js: -------------------------------------------------------------------------------- 1 | 2 | let freqHz; 3 | 4 | function update_frequency() { 5 | document.getElementById("hz").innerHTML = freqHz + " Hz"; 6 | } 7 | 8 | // WebSocket Initialization 9 | 10 | let gateway = `ws://${window.location.hostname}/ws`; 11 | var websocket; 12 | 13 | window.addEventListener('load', on_window_load); 14 | 15 | function on_window_load(event) { 16 | init_web_socket(); 17 | } 18 | 19 | function trigger_capture() { 20 | websocket.send("f"); 21 | } 22 | 23 | var periodicTrigger = setInterval(trigger_capture, 1000); 24 | 25 | window.onbeforeunload = function() { 26 | clearInterval(periodicTrigger); 27 | websocket.onclose = function () {}; // disable onclose handler first 28 | websocket.close(); 29 | } 30 | 31 | // WebSocket handling 32 | 33 | function init_web_socket() { 34 | console.log('Trying to open a WebSocket connection...'); 35 | websocket = new WebSocket(gateway); 36 | websocket.binaryType = "arraybuffer"; 37 | websocket.onopen = on_ws_open; 38 | websocket.onclose = on_ws_close; 39 | websocket.onmessage = on_ws_message; 40 | } 41 | 42 | function on_ws_open(event) { 43 | console.log('Connection opened'); 44 | } 45 | 46 | function on_ws_close(event) { 47 | console.log('Connection closed'); 48 | setTimeout(init_web_socket, 2000); 49 | } 50 | 51 | 52 | function on_ws_message(event) { 53 | let view = new Int32Array(event.data); 54 | if ((view.length == 2) && (view[0] == 5555)){ 55 | freqHz = view[1]; 56 | update_frequency(); 57 | } 58 | // acknowledge packet 59 | websocket.send("x"); 60 | } 61 | 62 | 63 | function on_osc_freq_change(selectObject) { 64 | let value = selectObject.value; 65 | let jsonObj = {}; 66 | jsonObj["action"] = "oscfreq"; 67 | jsonObj["freqhz"] = value; 68 | websocket.send(JSON.stringify(jsonObj)); 69 | } 70 | -------------------------------------------------------------------------------- /data/chartjs-plugin-streaming.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * chartjs-plugin-streaming v2.0.0 3 | * https://nagix.github.io/chartjs-plugin-streaming 4 | * (c) 2017-2021 Akihiko Kusanagi 5 | * Released under the MIT license 6 | */ 7 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).ChartStreaming=t(e.Chart,e.Chart.helpers)}(this,(function(e,t){"use strict";function o(e,t,o){return Math.min(Math.max(e,t),o)}function n(e,o){const n=e.options.realtime,a=e.chart.options.plugins.streaming;return t.valueOrDefault(n[o],a[o])}function a(e,{x:o,y:n},{xAxisID:a,yAxisID:i}){const s={};return t.each(o,(e=>{s[e]={axisId:a}})),t.each(n,(e=>{s[e]={axisId:i}})),s}const i="undefined"==typeof window?t.noop:window.cancelAnimationFrame;function s(e){const t=e.frameRequestID;t&&(i.call(window,t),delete e.frameRequestID)}function r(e){const t=e.refreshTimerID;t&&(clearInterval(t),delete e.refreshTimerID,delete e.refreshInterval)}function l(e,o,n){e.refreshTimerID||(e.refreshTimerID=setInterval((()=>{const n=t.callback(o);e.refreshInterval===n||isNaN(n)||(r(e),l(e,o,n))}),n||0),e.refreshInterval=n||0)}function c(e,o,n){return o="number"==typeof o?o:e.parse(o),t.isFinite(o)?{value:e.getPixelForValue(o),transitionable:!0}:{value:n}}function d(){const t=e.registry.getElement("boxAnnotation"),o=e.registry.getElement("lineAnnotation"),n=e.registry.getElement("pointAnnotation"),a=t.prototype.resolveElementProperties,i=o.prototype.resolveElementProperties,s=n.prototype.resolveElementProperties;t.prototype.resolveElementProperties=function(e,t){return function(e,t,o){const{scales:n,chartArea:a}=t,{xScaleID:i,yScaleID:s,xMin:r,xMax:l,yMin:d,yMax:u}=o,m=n[i],p=n[s],{top:f,left:h,bottom:g,right:y}=a,x=e.$streaming={};if(m){const e=c(m,r,h),t=c(m,l,y),o=e.value>t.value;e.transitionable&&(x[o?"x2":"x"]={axisId:i}),t.transitionable&&(x[o?"x":"x2"]={axisId:i}),e.transitionable!==t.transitionable&&(x.width={axisId:i,reverse:e.transitionable})}if(p){const e=c(p,d,f),t=c(p,u,g),o=e.value>t.value;e.transitionable&&(x[o?"y2":"y"]={axisId:s}),t.transitionable&&(x[o?"y":"y2"]={axisId:s}),e.transitionable!==t.transitionable&&(x.height={axisId:s,reverse:e.transitionable})}}(this,e,t),a.call(this,e,t)},o.prototype.resolveElementProperties=function(e,t){const o=e.chartArea;e.chartArea=function(e,t,o){const{scales:n,chartArea:a}=t,{scaleID:i,value:s}=o,r=n[i],{top:l,left:d,bottom:u,right:m}=a,p=e.$streaming={};if(r){const e=r.isHorizontal();return c(r,s).transitionable&&(p[e?"x":"y"]={axisId:i},p[e?"x2":"y2"]={axisId:i}),e?{top:l,bottom:u}:{left:d,right:m}}const{xScaleID:f,yScaleID:h,xMin:g,xMax:y,yMin:x,yMax:b}=o,v=n[f],I=n[h],D={};if(v){const e=c(v,g),t=c(v,y);e.transitionable?p.x={axisId:f}:D.left=d,t.transitionable?p.x2={axisId:f}:D.right=m}if(I){const e=c(I,x),t=c(I,b);e.transitionable?p.y={axisId:h}:D.top=l,t.transitionable?p.y2={axisId:h}:D.bottom=u}return D}(this,e,t);const n=i.call(this,e,t);return e.chartArea=o,n},n.prototype.resolveElementProperties=function(e,t){return function(e,t,o){const n=t.scales,{xScaleID:a,yScaleID:i,xValue:s,yValue:r}=o,l=n[a],d=n[i],u=e.$streaming={};l&&c(l,s).transitionable&&(u.x={axisId:a});d&&c(d,r).transitionable&&(u.y={axisId:i})}(this,e,t),s.call(this,e,t)}}const u={x:["x","caretX"],y:["y","caretY"]};function m(...e){const t=this,o=t.getActiveElements()[0];if(o){const e=t._chart.getDatasetMeta(o.datasetIndex);t.$streaming=a(0,u,e)}else t.$streaming={};t.constructor.prototype.update.call(t,...e)}const p=new WeakMap;function f(e){const{originalScaleOptions:o}=function(e){let t=p.get(e);return t||(t={originalScaleOptions:{}},p.set(e,t)),t}(e),a=e.scales;return t.each(a,(e=>{const t=e.id;o[t]||(o[t]={duration:n(e,"duration"),delay:n(e,"delay")})})),t.each(o,((e,t)=>{a[t]||delete o[t]})),o}function h(e,t,a,i){const{chart:s,axis:r}=e,{minDuration:l=0,maxDuration:c=1/0,minDelay:d=-1/0,maxDelay:u=1/0}=i&&i[r]||{},m=e.options.realtime,p=n(e,"duration"),h=n(e,"delay"),g=o(p*(2-t),l,c);let y,x;return f(s),y=e.isHorizontal()?(e.right-a.x)/(e.right-e.left):(e.bottom-a.y)/(e.bottom-e.top),x=h+y*(p-g),m.duration=g,m.delay=o(x,d,u),g!==e.max-e.min}function g(e,t,a){const{chart:i,axis:s}=e,{minDelay:r=-1/0,maxDelay:l=1/0}=a&&a[s]||{},c=n(e,"delay")+(e.getValueForPixel(t)-e.getValueForPixel(0));return f(i),e.options.realtime.delay=o(c,r,l),!0}function y(e,o){const n=o.$streaming;if(n.zoomPlugin!==e){const a=n.resetZoom=o.resetZoom;!function(e){e.zoomFunctions.realtime=h,e.panFunctions.realtime=g}(e),o.resetZoom=e=>{!function(e){const o=f(e);t.each(e.scales,(e=>{const t=e.options.realtime;if(t){const n=o[e.id];n?(t.duration=n.duration,t.delay=n.delay):(delete t.duration,delete t.delay)}}))}(o),a(e)},n.zoomPlugin=e}}function x(e){const t=e.$streaming;t.zoomPlugin&&(e.resetZoom=t.resetZoom,function(e){p.delete(e)}(e),delete t.resetZoom,delete t.zoomPlugin)}const b={millisecond:{common:!0,size:1,steps:[1,2,5,10,20,50,100,250,500]},second:{common:!0,size:1e3,steps:[1,2,5,10,15,30]},minute:{common:!0,size:6e4,steps:[1,2,5,10,15,30]},hour:{common:!0,size:36e5,steps:[1,2,3,6,12]},day:{common:!0,size:864e5,steps:[1,2,5]},week:{common:!1,size:6048e5,steps:[1,2,3,4]},month:{common:!0,size:2628e6,steps:[1,2,3]},quarter:{common:!1,size:7884e6,steps:[1,2,3,4]},year:{common:!0,size:3154e7}},v=Object.keys(b);function I(e,o,n){if(n){if(n.length){const{lo:a,hi:i}=t._lookup(n,o);e[n[a]>=o?n[a]:n[i]]=!0}}else e[o]=!0}const D=["pointBackgroundColor","pointBorderColor","pointBorderWidth","pointRadius","pointRotation","pointStyle","pointHitRadius","pointHoverBackgroundColor","pointHoverBorderColor","pointHoverBorderWidth","pointHoverRadius","backgroundColor","borderColor","borderSkipped","borderWidth","hoverBackgroundColor","hoverBorderColor","hoverBorderWidth","hoverRadius","hitRadius","radius","rotation"];function k(e,o,n){const a=e.$animations||{};t.each(e.$streaming,((i,s)=>{if(i.axisId===o){const o=i.reverse?-n:n,r=a[s];t.isFinite(e[s])&&(e[s]-=o),r&&(r._from-=o,r._to-=o)}}))}class w extends e.TimeScale{constructor(e){super(e),this.$realtime=this.$realtime||{}}init(e,o){const a=this;super.init(e,o),l(a.$realtime,(()=>{const e=a.chart,o=n(a,"onRefresh");return t.callback(o,[e],a),function(e){const{chart:o,id:a,max:i}=e,s=n(e,"duration"),r=n(e,"delay"),l=n(e,"ttl"),c=n(e,"pause"),d=Date.now()-(isNaN(l)?s+r:l);let u,m,p,f;t.each(o.data.datasets,((e,n)=>{const s=o.getDatasetMeta(n),r=a===s.xAxisID?"x":a===s.yAxisID&&"y";if(r){const a=s.controller,h=e.data,g=h.length;if(c){for(u=0;u{t.isArray(e[o])&&e[o].splice(m,p)})),t.each(e.datalabels,(e=>{t.isArray(e)&&e.splice(m,p)})),"object"!=typeof h[0]&&(f={start:m,count:p}),t.each(o._active,((e,t)=>{e.datasetIndex===n&&e.index>=m&&(e.index>=m+p?e.index-=p:o._active.splice(t,1))}),null,!0)}})),f&&o.data.labels.splice(f.start,f.count)}(a),e.update("quiet"),n(a,"refresh")}))}update(e,o,a){const i=this,{$realtime:r,options:l}=i,{bounds:c,offset:d,ticks:u}=l,{autoSkip:m,source:p,major:f}=u,h=f.enabled;n(i,"pause")?s(r):(r.frameRequestID||(r.head=Date.now()),function(e,o){if(!e.frameRequestID){const n=()=>{const a=e.nextRefresh||0,i=Date.now();if(a<=i){const n=t.callback(o),a=1e3/(Math.max(n,0)||30),s=e.nextRefresh+a||0;e.nextRefresh=s>i?s:i+a}e.frameRequestID=t.requestAnimFrame.call(window,n)};e.frameRequestID=t.requestAnimFrame.call(window,n)}}(r,(()=>{const e=i.chart,o=e.$streaming;return function(e){const{chart:o,id:a,$realtime:i}=e,s=n(e,"duration"),r=n(e,"delay"),l=e.isHorizontal(),c=l?e.width:e.height,d=Date.now(),u=o.tooltip,m=function(e){const t=e.$streaming.annotationPlugin;if(t){const o=t._getState(e);return o&&o.elements||[]}return[]}(o);let p=c*(d-i.head)/s;l===!!e.options.reverse&&(p=-p),t.each(o.data.datasets,((e,t)=>{const n=o.getDatasetMeta(t),{data:i=[],dataset:s}=n;for(let e=0,t=i.length;el.shift(),set:t.noop}),Object.defineProperty(e,"max",{get:()=>r.shift(),set:t.noop});const c=super.buildTicks();return delete e.min,delete e.max,e.min=s,e.max=i,c}calculateLabelRotation(){const e=this.options.ticks,t=e.maxRotation;e.maxRotation=e.minRotation||0,super.calculateLabelRotation(),e.maxRotation=t}fit(){const e=this,t=e.options;super.fit(),t.ticks.display&&t.display&&e.isHorizontal()&&(e.paddingLeft=3,e.paddingRight=3,e._handleMargins())}draw(e){const o=this,{chart:n,ctx:a}=o,i=o.isHorizontal()?{left:e.left,top:0,right:e.right,bottom:n.height}:{left:0,top:e.top,right:n.width,bottom:e.bottom};o._gridLineItems=null,o._labelItems=null,t.clipArea(a,i),super.draw(e),t.unclipArea(a)}destroy(){const e=this.$realtime;s(e),r(e)}_generate(){const e=this,o=e._adapter,a=n(e,"duration"),i=n(e,"delay"),s=n(e,"refresh"),r=e.$realtime.head-i,l=r-a,c=e._getLabelCapacity(l),{time:d,ticks:u}=e.options,m=d.unit||function(e,t,o,n){const a=o-t,i=v.length;for(let t=v.indexOf(e);t1e5*f)throw new Error(l+" and "+r+" are too far apart with stepSize of "+f+" "+m);k=R,g&&p&&!y&&!d.round&&(k=+o.startOf(k,p),k=+o.add(k,~~((R-k)/(x.size*f))*f,m));const $="data"===u.source&&e.getDataTimestamps();for(w=0;ke-t)).map((e=>+e))}}w.id="realtime",w.defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},realtime:{},ticks:{autoSkip:!1,source:"auto",major:{enabled:!0}}},e.defaults.describe("scale.realtime",{_scriptable:e=>"onRefresh"!==e});e.defaults.set("transitions",{quiet:{animation:{duration:0}}});const R={x:["x","cp1x","cp2x"],y:["y","cp1y","cp2y"]};function $(o){const n=this;"quiet"===o&&t.each(n.data.datasets,((t,o)=>{n.getDatasetMeta(o).controller._setStyle=function(t,o,n,a){e.DatasetController.prototype._setStyle.call(this,t,o,"quiet",a)}})),e.Chart.prototype.update.call(n,o),"quiet"===o&&t.each(n.data.datasets,((e,t)=>{delete n.getDatasetMeta(t).controller._setStyle}))}function E(e){const t=e.$streaming;e.render(),t.lastMouseEvent&&setTimeout((()=>{const o=t.lastMouseEvent;o&&e._eventHandler(o)}),0)}const M=[{id:"streaming",version:"2.0.0",beforeInit(e){const o=e.$streaming=e.$streaming||{render:E},n=o.canvas=e.canvas,a=o.mouseEventListener=n=>{const a=t.getRelativePosition(n,e);o.lastMouseEvent={type:"mousemove",chart:e,native:n,x:a.x,y:a.y}};n.addEventListener("mousedown",a),n.addEventListener("mouseup",a)},afterInit(e){e.update=$},beforeUpdate(o){const{scales:n,elements:a}=o.options,i=o.tooltip;t.each(n,(({type:e})=>{"realtime"===e&&(a.line.capBezierPoints=!1)})),i&&(i.update=m);try{!function(e,t){const o=t.$streaming;if(o.annotationPlugin!==e){const t=e.afterUpdate;d(),o.annotationPlugin=e,e.afterUpdate=(e,o,n)=>{const a=o.mode,i=n.animation;"quiet"===a&&(n.animation=!1),t.call(this,e,o,n),"quiet"===a&&(n.animation=i)}}}(e.registry.getPlugin("annotation"),o)}catch(e){!function(e){delete e.$streaming.annotationPlugin}(o)}try{y(e.registry.getPlugin("zoom"),o)}catch(e){x(o)}},beforeDatasetUpdate(e,o){const{meta:n,mode:a}=o;if("quiet"===a){const{controller:e,$animations:o}=n;o&&o.visible&&o.visible._active&&(e.updateElement=t.noop,e.updateSharedOptions=t.noop)}},afterDatasetUpdate(e,t){const{meta:o,mode:n}=t,{data:i=[],dataset:s,controller:r}=o;for(let e=0,t=i.length;e{e instanceof w&&e.destroy()}))},defaults:{duration:1e4,delay:0,frameRate:30,refresh:1e3,onRefresh:null,pause:!1,ttl:void 0},descriptors:{_scriptable:e=>"onRefresh"!==e}},w];return e.Chart.register(M),M})); 8 | -------------------------------------------------------------------------------- /data/cv_chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Current & Voltage Chart 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Current & Voltage Chart

12 |
13 | 14 |
15 |
16 |
Current 17 |

iStats

18 |
19 |
20 |
21 | 22 |

23 |

24 | 25 | 26 | 27 |
28 |
29 |
30 |
Voltage 31 |

vStats

32 |
33 |
34 |
35 |

36 |

37 | 38 | 39 | 40 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
41 | 46 |
55 | 59 |
62 |
63 |
64 |
Capture Seconds
77 |
78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /data/cv_meter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Current & Voltage Meter 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Current & Voltage Meter

13 |
14 |
15 |
16 | 17 | 18 | 19 | 26 | 27 |
20 | 25 |
28 |
29 |
30 |

31 |
32 | 33 |

34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /data/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/data/favicon.ico -------------------------------------------------------------------------------- /data/freq_counter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Frequency Counter & Oscillator 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Frequency Counter & Oscillator

13 |
14 | 15 |

16 | 17 |

0 Hz

18 |

19 | 20 | 21 | 22 | 23 | 24 |
Oscillator Frequency (Hz)
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ESP32 Multi-Meter 5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 |

ESP32 Multi-Meter

13 |
14 | 15 |

16 | 17 |

18 | Firmware Revision : %FW_REV% 19 |
20 | 21 |

22 |

25 |

26 | 27 | 30 | 31 | 32 |

33 |

36 | 37 |

38 | 39 |

40 |
WiFi Access Point Credentials 41 |
42 | 43 |
44 | 45 | 46 | 47 |
SSID
Password
48 |
49 |

50 |

51 | 52 |
53 | 54 |
55 |
56 |
57 | 58 |

59 | 60 |

61 | 62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | max-width: 90%; 3 | margin:auto auto; 4 | font-family:arial; 5 | font-size:1.0em; 6 | text-align:left; 7 | color:black; 8 | background-color:#d6d0bf; 9 | } 10 | 11 | .center { 12 | margin: auto; 13 | width : 80%; 14 | justify-items: center; 15 | } 16 | 17 | .wifi { 18 | margin: auto; 19 | width : 600px; 20 | padding: 10px 10px; 21 | display: flex; 22 | justify-content: center; 23 | text-align: center; 24 | } 25 | 26 | a:link{ 27 | color:white; 28 | font-size:1.0em; 29 | text-decoration:none; 30 | } 31 | 32 | a:hover{ 33 | color:white; 34 | font-size:1.0em; 35 | text-decoration:none; 36 | } 37 | 38 | section { 39 | font-size:1.0em; 40 | } 41 | 42 | canvas { 43 | background-color: rgba(0,0,0,0.1); 44 | } 45 | 46 | h1{ 47 | color:white; 48 | border-radius:0.25em; 49 | text-align:center; 50 | font-size:1.0em; 51 | padding:0.2em 0.2em; 52 | background:#558ED5; 53 | } 54 | 55 | h2{ 56 | color:black; 57 | font-size:0.8em; 58 | } 59 | 60 | 61 | table{ 62 | font-family:arial,sans-serif; 63 | font-size:1.0em; 64 | border-collapse:collapse; 65 | margin: auto; 66 | } 67 | 68 | td { 69 | text-align: left; 70 | } 71 | 72 | 73 | input[type="number"] { 74 | clear: both; 75 | float: left; 76 | width: 50px; 77 | } 78 | 79 | .cfg-select { 80 | clear: both; 81 | float: left; 82 | width: 360px; 83 | } 84 | 85 | .scale-select { 86 | clear: both; 87 | float: left; 88 | width: 210px; 89 | } 90 | 91 | .sample-select { 92 | clear: both; 93 | float: left; 94 | width: 100px; 95 | } 96 | 97 | 98 | .form-submit { 99 | margin-bottom:1rem; 100 | border-radius:0.25em; 101 | padding:0.3em 0.3em; 102 | background:#0066A2; 103 | margin:auto; 104 | color:white; 105 | width:150px; 106 | font-size:1.0em; 107 | } 108 | 109 | p { 110 | font-size:1.0em; 111 | } 112 | 113 | .istats { 114 | color: rgb(209, 20, 61); 115 | grid-area: 1 / 1 / span 1 / span 1; 116 | } 117 | .chrt { 118 | width: 1200px; 119 | height: 600px; 120 | margin-bottom: 20px; 121 | grid-area: 1 / 2 / span 1 / span 1; 122 | } 123 | .vstats { 124 | color: rgb(34, 73, 228); 125 | grid-area: 1 / 3 / span 1 / span 1; 126 | } 127 | 128 | .capture { 129 | margin-top: 40px; 130 | align-items: center; 131 | padding: 10px 10px; 132 | justify-content: center; 133 | text-align: center; 134 | } 135 | 136 | .grid-container { 137 | display: grid; 138 | width : 100%; 139 | height : 60%; 140 | grid-template-columns: auto auto auto; 141 | grid-gap: 10px; 142 | padding: 10px; 143 | justify-items: center; 144 | } 145 | 146 | .grid-container > div { 147 | text-align: center; 148 | padding: 20px 50px; 149 | font-size: 1.0em; 150 | } 151 | 152 | section.range-slider { 153 | position: relative; 154 | width: 1200px; 155 | height: 80px; 156 | margin: auto; 157 | text-align: center; 158 | } 159 | 160 | section.range-slider input[type="range"] { 161 | pointer-events: none; 162 | position: absolute; 163 | -webkit-appearance: none; 164 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 165 | border: none; 166 | border-radius: 4px; 167 | background: #efefef; 168 | box-shadow: inset 0 1px 0 0 #cdc6c6, inset 0 -1px 0 0 #d9d4d4; 169 | -webkit-box-shadow: inset 0 1px 0 0 #cdc6c6, inset 0 -1px 0 0 #d9d4d4; 170 | overflow: hidden; 171 | left: 0; 172 | top: 30px; 173 | width: 1200px; 174 | outline: none; 175 | height: 24px; 176 | padding: 0px; 177 | } 178 | 179 | section.range-slider input[type="range"]::-webkit-slider-thumb { 180 | pointer-events: all; 181 | position: relative; 182 | z-index: 1; 183 | outline: 0; 184 | -webkit-appearance: none; 185 | width: 24px; 186 | height: 24px; 187 | border: none; 188 | border-radius: 4px; 189 | background: gray; 190 | } 191 | 192 | button { 193 | border-radius:0.25em; 194 | background: #0066A2; 195 | padding:0.3em 1.0em; 196 | color:white; 197 | font-size:1.0em; 198 | } 199 | 200 | button:active { 201 | transform: translateY(2px); 202 | } 203 | 204 | .current { 205 | margin: auto; 206 | text-align: center; 207 | font-size : 8.0em; 208 | color: rgb(209, 20, 61); 209 | } 210 | 211 | .voltage { 212 | margin: auto; 213 | text-align: center; 214 | font-size : 8.0em; 215 | color: rgb(34, 73, 228); 216 | } 217 | 218 | .frequency { 219 | margin: auto; 220 | text-align: center; 221 | font-size : 8.0em; 222 | color: black; 223 | } 224 | 225 | .led-box { 226 | height: 24px; 227 | width: 24px; 228 | margin: auto; 229 | justify-content: center; 230 | } 231 | 232 | .led-red { 233 | margin: 0 0; 234 | width: 24px; 235 | height: 24px; 236 | background-color: #F00; 237 | border-radius: 50%; 238 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #441313 0 -1px 9px, rgba(255, 0, 0, 0.5) 0 2px 12px; 239 | } 240 | 241 | .led-yellow { 242 | margin: 0 0; 243 | width: 24px; 244 | height: 24px; 245 | background-color: #FF0; 246 | border-radius: 50%; 247 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #808002 0 -1px 9px, #FF0 0 2px 12px; 248 | } 249 | 250 | .led-green { 251 | margin: 0 0; 252 | width: 24px; 253 | height: 24px; 254 | background-color: #ABFF00; 255 | border-radius: 50%; 256 | box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; 257 | } 258 | 259 | -------------------------------------------------------------------------------- /docs/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/block.png -------------------------------------------------------------------------------- /docs/capture_8s_manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/capture_8s_manual.png -------------------------------------------------------------------------------- /docs/capture_gated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/capture_gated.png -------------------------------------------------------------------------------- /docs/capture_gated_2000Hz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/capture_gated_2000Hz.png -------------------------------------------------------------------------------- /docs/capture_gated_400Hz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/capture_gated_400Hz.png -------------------------------------------------------------------------------- /docs/cv_meter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/cv_meter.png -------------------------------------------------------------------------------- /docs/esp32_meter_schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/esp32_meter_schematic.pdf -------------------------------------------------------------------------------- /docs/freq_counter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/freq_counter.png -------------------------------------------------------------------------------- /docs/hardware.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/hardware.jpg -------------------------------------------------------------------------------- /docs/home_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/docs/home_page.png -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /littlefsbuilder.py: -------------------------------------------------------------------------------- 1 | Import("env") 2 | env.Replace( MKSPIFFSTOOL=env.get("PROJECT_DIR") + '/mklittlefs' ) -------------------------------------------------------------------------------- /mklittlefs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/har-in-air/ESP32_MULTI_METER/1154841d68ea1e3558cb1f4679da0e9a0a67413a/mklittlefs -------------------------------------------------------------------------------- /no_ota.csv: -------------------------------------------------------------------------------- 1 | # Name, Type, SubType, Offset, Size, Flags 2 | nvs, data, nvs, 0x9000, 0x5000, 3 | otadata, data, ota, 0xe000, 0x2000, 4 | app0, app, ota_0, 0x10000, 0x200000, 5 | spiffs, data, spiffs, 0x210000,0x1F0000, 6 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:esp32dev] 12 | platform = https://github.com/platformio/platform-espressif32.git#feature/idf-master 13 | platform_packages = framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32 14 | 15 | board = esp32dev 16 | framework = arduino 17 | board_build.f_cpu = 240000000L 18 | 19 | board_build.partitions = no_ota.csv 20 | upload_port = /dev/ttyUSB* 21 | upload_speed = 921600 22 | monitor_port = /dev/ttyUSB* 23 | monitor_speed = 115200 24 | build_type = debug 25 | monitor_filters = esp32_exception_decoder 26 | build_flags = 27 | -DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG 28 | lib_deps = 29 | ArduinoJSON 30 | AsyncTCP 31 | https://github.com/me-no-dev/ESPAsyncWebServer.git 32 | 33 | extra_scripts = ./littlefsbuilder.py -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H_ 2 | #define CONFIG_H_ 3 | 4 | #define CORE_0 0 5 | #define CORE_1 1 6 | 7 | #define pinFET1 18 // switch in the 1.0ohm shunt 8 | #define pinFET05 19 // switch in the 0.05ohm shunt 9 | #define pinSDA 22 // I2C interface to INA226 10 | #define pinSCL 21 // -do- 11 | #define pinGate 4 // external current monitor gate signal 12 | #define pinAlert 5 13 | 14 | #define pinLED 14 15 | 16 | 17 | #define MODE_CURRENT_VOLTAGE 11 18 | #define MODE_FREQUENCY 22 19 | #define MODE_INVALID 33 20 | 21 | 22 | typedef struct { 23 | // input 24 | uint16_t cfg; 25 | int scale; 26 | int nSamples; 27 | uint32_t periodUs; 28 | // output 29 | float sampleRate; // Hz 30 | float vavg; // volts 31 | float vmax; 32 | float vmin; 33 | float iavgma; // mA 34 | float imaxma; 35 | float iminma; 36 | } CV_MEASURE_t; 37 | 38 | 39 | typedef struct { 40 | int frequencyHz; 41 | } FREQ_MEASURE_t; 42 | 43 | 44 | typedef struct { 45 | int mode; 46 | union { 47 | CV_MEASURE_t cv_meas; 48 | FREQ_MEASURE_t f_meas; 49 | } m; 50 | } MEASURE_t; 51 | 52 | extern volatile MEASURE_t Measure; 53 | extern volatile int16_t* Buffer; 54 | 55 | 56 | #endif -------------------------------------------------------------------------------- /src/freq_counter.cpp: -------------------------------------------------------------------------------- 1 | 2 | // Modified from : 3 | // BLOG Eletrogate 4 | // ESP32 Frequency Meter 5 | // https://blog.eletrogate.com/esp32-frequencimetro-de-precisao 6 | // Rui Viana and Gustavo Murta august/2020 7 | #include 8 | #include // Library STDIO 9 | #include // Library ESP32 LEDC 10 | #include 11 | #include // Library ESP32 PCNT 12 | #include "config.h" 13 | #include "freq_counter.h" 14 | 15 | static const char* TAG = "freq_meter"; 16 | 17 | #define PCNT_COUNT_UNIT PCNT_UNIT_0 // Set Pulse Counter Unit - 0 18 | #define PCNT_COUNT_CHANNEL PCNT_CHANNEL_0 // Set Pulse Counter channel - 0 19 | 20 | #define PCNT_INPUT_SIG_IO GPIO_NUM_34 // Set Pulse Counter input - Freq Meter Input GPIO 34 21 | #define LEDC_HS_CH0_GPIO GPIO_NUM_33 // Saida do LEDC - gerador de pulsos - GPIO_33 22 | #define PCNT_INPUT_CTRL_IO GPIO_NUM_35 // Set Pulse Counter Control GPIO pin - HIGH = count up, LOW = count down 23 | #define OUTPUT_CONTROL_GPIO GPIO_NUM_32 // Timer output control port - GPIO_32 24 | #define PCNT_H_LIM_VAL overflow // Overflow of Pulse Counter 25 | 26 | volatile bool FreqReadyFlag = false; 27 | volatile bool FreqCaptureFlag = false; 28 | volatile int FrequencyHz = 0; // frequency value 29 | volatile SemaphoreHandle_t FreqSemaphore; 30 | 31 | uint32_t OscFreqHz = 23456; // 1Hz to 40MHz 32 | bool OscFreqFlag = false; 33 | 34 | 35 | uint32_t overflow = 20000; // Max Pulse Counter value 36 | int16_t pulses = 0; // Pulse Counter value 37 | uint32_t multPulses = 0; // Quantidade de overflows do contador PCNT 38 | uint32_t sample_time = 999955; // sample time of 1 second to count pulses 39 | uint32_t mDuty = 0; // Duty value 40 | uint32_t resolution = 0; // Resolution value 41 | char buf[32]; // Buffer 42 | 43 | esp_timer_create_args_t create_args; // Create an esp_timer instance 44 | esp_timer_handle_t timer_handle; // Create an single timer 45 | 46 | portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED; // portMUX_TYPE to do synchronism 47 | 48 | 49 | static void read_PCNT(void *p); // Read Pulse Counter 50 | static char *ultos_recursive(unsigned long val, char *s, unsigned radix, int pos); // Format an unsigned long (32 bits) into a string 51 | static char *ltos(long val, char *s, int radix); // Format an long (32 bits) into a string 52 | static void init_PCNT(void); // Initialize and run PCNT unit 53 | static void IRAM_ATTR pcnt_intr_handler(void *arg); // Counting overflow pulses 54 | static void init_osc_freq (); // Initialize Oscillator to test Freq Meter 55 | static void init_frequency_meter (); 56 | 57 | 58 | // Initialize Oscillator to test Freq Meter 59 | static void init_osc_freq() { 60 | resolution = (log (80000000 / OscFreqHz) / log(2)) / 2 ; // Calc of resolution of Oscillator 61 | if (resolution < 1) resolution = 1; // set min resolution 62 | // Serial.println(resolution); // Print 63 | mDuty = (pow(2, resolution)) / 2; // Calc of Duty Cycle 50% of the pulse 64 | // Serial.println(mDuty); // Print 65 | 66 | ledc_timer_config_t ledc_timer = {}; // LEDC timer config instance 67 | 68 | ledc_timer.duty_resolution = ledc_timer_bit_t(resolution); // Set resolution 69 | ledc_timer.freq_hz = OscFreqHz; // Set Oscillator frequency 70 | ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE; // Set high speed mode 71 | ledc_timer.timer_num = LEDC_TIMER_0; // Set LEDC timer index - 0 72 | ledc_timer_config(&ledc_timer); // Set LEDC Timer config 73 | ledc_channel_config_t ledc_channel = {}; // LEDC Channel config instance 74 | 75 | ledc_channel.channel = LEDC_CHANNEL_0; // Set HS Channel - 0 76 | ledc_channel.duty = mDuty; // Set Duty Cycle 50% 77 | ledc_channel.gpio_num = LEDC_HS_CH0_GPIO; // LEDC Oscillator output GPIO 33 78 | ledc_channel.intr_type = LEDC_INTR_DISABLE; // LEDC Fade interrupt disable 79 | ledc_channel.speed_mode = LEDC_HIGH_SPEED_MODE; // Set LEDC high speed mode 80 | ledc_channel.timer_sel = LEDC_TIMER_0; // Set timer source of channel - 0 81 | ledc_channel_config(&ledc_channel); // Config LEDC channel 82 | } 83 | 84 | 85 | // Counting overflow pulses 86 | static void IRAM_ATTR pcnt_intr_handler(void *arg) { 87 | portENTER_CRITICAL_ISR(&timerMux); // disabling the interrupts 88 | multPulses++; // increment Overflow counter 89 | PCNT.int_clr.val = BIT(PCNT_COUNT_UNIT); // Clear Pulse Counter interrupt bit 90 | portEXIT_CRITICAL_ISR(&timerMux); // enabling the interrupts 91 | } 92 | 93 | 94 | // Initialize and run PCNT unit 95 | static void init_PCNT(void) { 96 | pcnt_config_t pcnt_config = { }; // PCNT unit instance 97 | 98 | pcnt_config.pulse_gpio_num = PCNT_INPUT_SIG_IO; // Pulse input GPIO 34 - Freq Meter Input 99 | pcnt_config.ctrl_gpio_num = PCNT_INPUT_CTRL_IO; // Control signal input GPIO 35 100 | pcnt_config.unit = PCNT_COUNT_UNIT; // Unidade de contagem PCNT - 0 101 | pcnt_config.channel = PCNT_COUNT_CHANNEL; // PCNT unit number - 0 102 | pcnt_config.counter_h_lim = PCNT_H_LIM_VAL; // Maximum counter value - 20000 103 | pcnt_config.pos_mode = PCNT_COUNT_INC; // PCNT positive edge count mode - inc 104 | pcnt_config.neg_mode = PCNT_COUNT_INC; // PCNT negative edge count mode - inc 105 | pcnt_config.lctrl_mode = PCNT_MODE_DISABLE; // PCNT low control mode - disable 106 | pcnt_config.hctrl_mode = PCNT_MODE_KEEP; // PCNT high control mode - won't change counter mode 107 | pcnt_unit_config(&pcnt_config); // Initialize PCNT unit 108 | 109 | pcnt_counter_pause(PCNT_COUNT_UNIT); // Pause PCNT unit 110 | pcnt_counter_clear(PCNT_COUNT_UNIT); // Clear PCNT unit 111 | 112 | pcnt_event_enable(PCNT_COUNT_UNIT, PCNT_EVT_H_LIM); // Enable event to watch - max count 113 | pcnt_isr_register(pcnt_intr_handler, NULL, 0, NULL); // Setup Register ISR handler 114 | pcnt_intr_enable(PCNT_COUNT_UNIT); // Enable interrupts for PCNT unit 115 | 116 | pcnt_counter_resume(PCNT_COUNT_UNIT); // Resume PCNT unit - starts count 117 | } 118 | 119 | 120 | // Read Pulse Counter 121 | static void read_PCNT(void *p){ 122 | gpio_set_level(OUTPUT_CONTROL_GPIO, 0); // Stop counter - output control LOW 123 | pcnt_get_counter_value(PCNT_COUNT_UNIT, &pulses); // Read Pulse Counter value 124 | xSemaphoreGive(FreqSemaphore); 125 | } 126 | 127 | 128 | static void init_frequency_meter (){ 129 | init_osc_freq(); // Initialize Oscillator 130 | init_PCNT(); // Initialize and run PCNT unit 131 | 132 | gpio_pad_select_gpio(OUTPUT_CONTROL_GPIO); // Set GPIO pad 133 | gpio_set_direction(OUTPUT_CONTROL_GPIO, GPIO_MODE_OUTPUT); // Set GPIO 32 as output 134 | 135 | create_args.callback = read_PCNT; // Set esp-timer argument 136 | esp_timer_create(&create_args, &timer_handle); // Create esp-timer instance 137 | 138 | gpio_matrix_in(PCNT_INPUT_SIG_IO, SIG_IN_FUNC226_IDX, false); // Set GPIO matrin IN - Freq Meter input 139 | } 140 | 141 | #if 0 142 | // Format an unsigned long (32 bits) into a string 143 | static char *ultos_recursive(unsigned long val, char *s, unsigned radix, int pos) { 144 | int c; 145 | if (val >= radix) { 146 | s = ultos_recursive(val / radix, s, radix, pos + 1); 147 | } 148 | c = val % radix; 149 | c += (c < 10 ? '0' : 'a' - 10); 150 | *s++ = c; 151 | if (pos % 3 == 0) { 152 | *s++ = ','; 153 | } 154 | return s; 155 | } 156 | 157 | 158 | // Format an long (32 bits) into a string 159 | static char *ltos(long val, char *s, int radix) { 160 | if (radix < 2 || radix > 36) { 161 | s[0] = 0; 162 | } 163 | else { 164 | char *p = s; 165 | if (radix == 10 && val < 0) { 166 | val = -val; 167 | *p++ = '-'; 168 | } 169 | p = ultos_recursive(val, p, radix, 0) - 1; 170 | *p = 0; 171 | } 172 | return s; 173 | } 174 | #endif 175 | 176 | void frequency_task(void* pvParam){ 177 | ESP_LOGI(TAG, "frequency_task running on core %d with priority %d", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 178 | 179 | FreqSemaphore = xSemaphoreCreateBinary(); 180 | init_frequency_meter(); 181 | pcnt_counter_clear(PCNT_COUNT_UNIT); // Clear Pulse Counter 182 | esp_timer_start_once(timer_handle, sample_time); // Initialize High resolution timer (1 sec) 183 | gpio_set_level(OUTPUT_CONTROL_GPIO, 1); // Set enable PCNT count 184 | while (1) { 185 | xSemaphoreTake(FreqSemaphore, portMAX_DELAY); 186 | FrequencyHz = (pulses + (multPulses * overflow)) / 2 ; // Calculation of FrequencyHz 187 | FreqReadyFlag = true; 188 | //Serial.printf("Frequency : %d Hz\n", FrequencyHz); // Print FrequencyHz with commas 189 | 190 | multPulses = 0; // Clear overflow counter 191 | // Put your function here, if you want 192 | //vTaskDelay(10); 193 | // Put your function here, if you want 194 | 195 | pcnt_counter_clear(PCNT_COUNT_UNIT); // Clear Pulse Counter 196 | esp_timer_start_once(timer_handle, sample_time); // Initialize High resolution timer (1 sec) 197 | gpio_set_level(OUTPUT_CONTROL_GPIO, 1); // Set enable PCNT count 198 | 199 | if (OscFreqFlag == true) { 200 | OscFreqFlag = false; 201 | init_osc_freq(); 202 | } 203 | } 204 | vTaskDelete(NULL); 205 | } -------------------------------------------------------------------------------- /src/freq_counter.h: -------------------------------------------------------------------------------- 1 | #ifndef FREQ_METER_H_ 2 | #define FREQ_METER_H_ 3 | 4 | extern volatile bool FreqReadyFlag; 5 | extern volatile bool FreqCaptureFlag; 6 | extern volatile int FrequencyHz; 7 | 8 | #define MSG_TX_FREQUENCY 5555 9 | 10 | extern uint32_t OscFreqHz; 11 | extern bool OscFreqFlag; 12 | 13 | void frequency_task(void* pvParam); 14 | 15 | #endif -------------------------------------------------------------------------------- /src/ina226.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "config.h" 4 | #include "nv_data.h" 5 | #include "ina226.h" 6 | 7 | #define i32(x) ((int32_t)(x)) 8 | 9 | static const char* TAG = "ina226"; 10 | 11 | volatile bool DataReadyFlag = false; 12 | volatile bool MeterReadyFlag = false; 13 | volatile bool GateOpenFlag = false; 14 | volatile bool CVCaptureFlag = false; 15 | volatile bool EndCaptureFlag = false; 16 | volatile bool LastPacketAckFlag = false; 17 | 18 | 19 | const CONFIG_t Config[NUM_CFG] = { 20 | // chart capture configurations 21 | { 0x4000 | (0 << 9) | (1 << 6) | (1 << 3), 500}, // avg 1, v 204uS, s 204uS, period 500uS 22 | { 0x4000 | (0 << 9) | (2 << 6) | (2 << 3), 1000}, // avg 1, v 332uS, s 332uS, period 1000uS 23 | { 0x4000 | (1 << 9) | (1 << 6) | (1 << 3), 2500}, // avg 4, v 204uS, s 204uS, period 2500uS 24 | // meter capture configuration 25 | { 0x4000 | (4 << 9) | (4 << 6) | (4 << 3), 1064000} // avg 128, v 1.1mS, s 1.1mS, period 557.57mS 26 | }; 27 | 28 | 29 | static void switch_scale(int scale); 30 | 31 | // switch on one of the FETs, other is turned off 32 | // For SCALE_HI, shunt resistor is 0.05 ohms 33 | // For SCALE_LO, shunt resistor is 1.05 ohms 34 | static void switch_scale(int scale) { 35 | digitalWrite(pinFET1, scale == SCALE_HI ? LOW : HIGH); 36 | digitalWrite(pinFET05, scale == SCALE_HI ? HIGH : LOW); 37 | } 38 | 39 | 40 | void ina226_write_reg(uint8_t regAddr, uint16_t data) { 41 | Wire.beginTransmission(INA226_I2C_ADDR); 42 | Wire.write(regAddr); 43 | Wire.write((uint8_t)((data>>8) & 0x00FF)); 44 | Wire.write((uint8_t)(data & 0x00FF)); 45 | Wire.endTransmission(); 46 | } 47 | 48 | 49 | uint16_t ina226_read_reg(uint8_t regAddr) { 50 | Wire.beginTransmission(INA226_I2C_ADDR); 51 | Wire.write(regAddr); 52 | Wire.endTransmission(false); // restart 53 | Wire.requestFrom(INA226_I2C_ADDR, 2); 54 | uint8_t buf[2]; 55 | buf[0] = Wire.read(); 56 | buf[1] = Wire.read(); 57 | // msbyte, then lsbyte 58 | uint16_t res = ((uint16_t)buf[1]) | (((uint16_t)buf[0])<<8); 59 | return res; 60 | } 61 | 62 | // Shunt ADC resolution is 2.5uV/lsb 63 | // Vshunt = (ShuntADCSample * 2.5) uV 64 | // I = Vshunt/Rshunt 65 | 66 | // SCALE_HI : Rshunt = 0.05ohm 67 | // Resolution = 2.5uV/0.05ohms = 50uA = 0.05mA 68 | // I = (ShuntADCSample * 2.5)/0.05 = (ShuntADCSample * 50) uA = (ShuntADCSample * 0.05)mA 69 | // Full scale = (32767 * 50)uA = 1638350 uA = 1638.35mA 70 | 71 | // SCALE_LO : Rshunt = 1.05 ohm shunt resistor 72 | // Resolution = 2.5uV/1.05ohms = 2.38uA 73 | // I = (ShuntADCSample * 2.5)/1.05 = (ShuntADCSample * 2.381) uA = (ShuntADCSample * 0.002381)mA 74 | // Full scale = (32767 * 2.381) uA = 78016.67 uA = 78.017 mA 75 | 76 | // Bus ADC resolution is 1.25mV/lsb 77 | // Full scale = (32767 * 1.25) mV = 40.95875V 78 | 79 | bool ina226_capture_oneshot(volatile MEASURE_t &measure, volatile int16_t* buffer, bool manualScale) { 80 | uint16_t reg_bus, reg_shunt; 81 | switch_scale(measure.m.cv_meas.scale); 82 | // conversion ready -> alert pin goes low 83 | ina226_write_reg(REG_MASK, 0x0400); 84 | // configure for one-shot bus and shunt adc conversion 85 | buffer[0] = MSG_TX_CV_METER; 86 | buffer[1] = measure.m.cv_meas.scale; 87 | // configure for one-shot bus and shunt adc conversion 88 | ina226_write_reg(REG_CFG, measure.m.cv_meas.cfg | 0x0003); 89 | // throw away first sample 90 | // pinAlert pulled up to 3v3, active low on conversion complete 91 | while (digitalRead(pinAlert) == HIGH); 92 | // read shunt and bus ADCs 93 | reg_shunt = ina226_read_reg(REG_SHUNT); 94 | reg_bus = ina226_read_reg(REG_VBUS); 95 | 96 | uint32_t tstart = micros(); 97 | ina226_write_reg(REG_CFG, measure.m.cv_meas.cfg | 0x0003); 98 | while (digitalRead(pinAlert) == HIGH); 99 | uint32_t tend = micros(); 100 | // measure the INA226 total conversion time for shunt and bus adcs 101 | uint32_t us = tend - tstart; 102 | // read shunt and bus ADCs 103 | reg_shunt = ina226_read_reg(REG_SHUNT); 104 | reg_bus = ina226_read_reg(REG_VBUS); 105 | int16_t shunt_i16 = (int16_t)reg_shunt; 106 | bool offScale = ((shunt_i16 == 32767) || (shunt_i16 == -32768)) ? true : false; 107 | buffer[2] = shunt_i16; 108 | buffer[3] = (int16_t)reg_bus; 109 | buffer[4] = offScale ? 1 : 0; 110 | measure.m.cv_meas.iavgma = (measure.m.cv_meas.scale == SCALE_HI)? shunt_i16*0.05f : shunt_i16*0.002381f; 111 | measure.m.cv_meas.iminma = measure.m.cv_meas.iavgma; 112 | measure.m.cv_meas.imaxma = measure.m.cv_meas.iavgma; 113 | measure.m.cv_meas.vavg = reg_bus*0.00125f; 114 | measure.m.cv_meas.vmax = measure.m.cv_meas.vavg; 115 | measure.m.cv_meas.vmin = measure.m.cv_meas.vavg; 116 | measure.m.cv_meas.sampleRate = 1000000.0f/(float)us; 117 | ESP_LOGI(TAG,"OneShot : [0x%04X scale=%d] %dus %dHz %.1fV %.3fmA\n", measure.m.cv_meas.cfg, measure.m.cv_meas.scale, us, (int)(measure.m.cv_meas.sampleRate+0.5f), measure.m.cv_meas.vavg, measure.m.cv_meas.iavgma); 118 | if (manualScale == true) { 119 | MeterReadyFlag = true; 120 | } 121 | else { 122 | MeterReadyFlag = !offScale; 123 | } 124 | return !offScale; // return false for off-scale current reading 125 | } 126 | 127 | 128 | bool ina226_capture_averaged_sample(volatile MEASURE_t &measure, volatile int16_t* buffer, bool manualScale) { 129 | int16_t data_i16; // shunt and bus readings 130 | int32_t savg, bavg; // averaging accumulators 131 | uint16_t reg_bus, reg_shunt; 132 | buffer[0] = MSG_TX_CV_METER; 133 | buffer[1] = measure.m.cv_meas.scale; 134 | switch_scale(measure.m.cv_meas.scale); 135 | // conversion ready -> alert pin goes low 136 | ina226_write_reg(REG_MASK, 0x0400); 137 | // continuous bus and shunt conversion 138 | ina226_write_reg(REG_CFG, measure.m.cv_meas.cfg | 0x0007); 139 | 140 | // throw away first sample 141 | // pinAlert pulled up to 3v3, active low on conversion complete 142 | while (digitalRead(pinAlert) == HIGH); 143 | // read shunt and bus ADCs 144 | reg_shunt = ina226_read_reg(REG_SHUNT); 145 | reg_bus = ina226_read_reg(REG_VBUS); 146 | 147 | uint32_t tstart = micros(); 148 | int inx = 0; 149 | savg = bavg = 0; 150 | // 0.4s averaging, worst case need to re-measure with high-scale => 0.8 seconds 151 | int numSamples = 400000/(int)measure.m.cv_meas.periodUs; 152 | bool offScale = false; 153 | while (inx < numSamples){ 154 | uint32_t t1 = micros(); 155 | // pinAlert pulled up to 3v3, active low on conversion complete 156 | while (digitalRead(pinAlert) == HIGH); 157 | // read shunt and bus ADCs 158 | reg_shunt = ina226_read_reg(REG_SHUNT); 159 | reg_bus = ina226_read_reg(REG_VBUS); 160 | 161 | data_i16 = (int16_t)reg_shunt; 162 | if ((data_i16 == 32767) || (data_i16 == -32768)) { 163 | offScale = true; 164 | } 165 | savg += i32(data_i16); 166 | 167 | data_i16 = (int16_t)reg_bus; 168 | bavg += i32(data_i16); 169 | while ((micros() - t1) < measure.m.cv_meas.periodUs); 170 | inx++; 171 | } 172 | uint32_t us = micros() - tstart; 173 | measure.m.cv_meas.sampleRate = (1000000.0f*(float)numSamples)/(float)us; 174 | // convert shunt adc reading to mA 175 | savg = savg / numSamples; 176 | measure.m.cv_meas.iavgma = (measure.m.cv_meas.scale == SCALE_HI)? savg*0.05f : savg*0.002381f; 177 | // convert bus adc reading to V 178 | bavg = bavg / numSamples; 179 | measure.m.cv_meas.vavg = bavg*0.00125f; 180 | buffer[2] = (int16_t)savg; 181 | buffer[3] = (int16_t)bavg; 182 | buffer[4] = offScale == true ? 1 : 0; 183 | // vload = vbus 184 | ESP_LOGI(TAG,"CV Meter sample : %s %.1fV %.3fmA\n", measure.m.cv_meas.scale == SCALE_LO ? "LO" : "HI", measure.m.cv_meas.vavg, measure.m.cv_meas.iavgma); 185 | if (manualScale == true) { 186 | MeterReadyFlag = true; 187 | } 188 | else { 189 | MeterReadyFlag = !offScale; 190 | } 191 | return !offScale; 192 | } 193 | 194 | 195 | void ina226_capture_buffer_triggered(volatile MEASURE_t &measure, volatile int16_t* buffer) { 196 | int16_t smax, smin, bmax, bmin, data_i16; // shunt and bus readings 197 | int32_t savg, bavg; // averaging accumulators 198 | uint16_t reg_bus, reg_shunt; 199 | smax = bmax = -32768; 200 | smin = bmin = 32767; 201 | savg = bavg = 0; 202 | int samplesPerSecond = 1000000/(int)measure.m.cv_meas.periodUs; 203 | switch_scale(measure.m.cv_meas.scale); 204 | // conversion ready -> alert pin goes low 205 | ina226_write_reg(REG_MASK, 0x0400); 206 | // continuous bus and shunt conversion 207 | ina226_write_reg(REG_CFG, measure.m.cv_meas.cfg | 0x0007); 208 | 209 | // throw away first sample 210 | // pinAlert pulled up to 3v3, active low on conversion complete 211 | while (digitalRead(pinAlert) == HIGH); 212 | // read shunt and bus ADCs 213 | reg_shunt = ina226_read_reg(REG_SHUNT); 214 | reg_bus = ina226_read_reg(REG_VBUS); 215 | 216 | uint32_t tstart = micros(); 217 | // packet header with start packet ID, 218 | // sample period in mS, and current scale 219 | buffer[0] = MSG_TX_START; 220 | buffer[1] = (int16_t)measure.m.cv_meas.periodUs; 221 | buffer[2] = measure.m.cv_meas.scale; 222 | int offset = 3; 223 | EndCaptureFlag = false; 224 | int inx = 0; 225 | while (inx < measure.m.cv_meas.nSamples){ 226 | uint32_t t1 = micros(); 227 | int bufIndex = offset + 2*inx; 228 | // pinAlert pulled up to 3v3, active low on conversion complete 229 | while (digitalRead(pinAlert) == HIGH); 230 | // read shunt and bus ADCs 231 | reg_shunt = ina226_read_reg(REG_SHUNT); 232 | reg_bus = ina226_read_reg(REG_VBUS); 233 | 234 | data_i16 = (int16_t)reg_shunt; 235 | buffer[bufIndex] = data_i16; 236 | savg += i32(data_i16); 237 | if (data_i16 > smax) smax = data_i16; 238 | if (data_i16 < smin) smin = data_i16; 239 | 240 | data_i16 = (int16_t)reg_bus; 241 | buffer[bufIndex+1] = data_i16; 242 | bavg += i32(data_i16); 243 | if (data_i16 > bmax) bmax = data_i16; 244 | if (data_i16 < bmin) bmin = data_i16; 245 | // break data buffer into packets with samplesPerSecond samples 246 | if (((inx+1) % samplesPerSecond) == 0) { 247 | // continued packet header ID for next socket transmission 248 | buffer[bufIndex+2] = MSG_TX; 249 | offset++; 250 | TxSamples = samplesPerSecond; 251 | EndCaptureFlag = (inx == (measure.m.cv_meas.nSamples-1)) ? true : false; 252 | DataReadyFlag = true; // ready to transmit the last `samplesPerSecond` samples 253 | } 254 | while ((micros() - t1) < measure.m.cv_meas.periodUs); 255 | inx++; 256 | } 257 | uint32_t us = micros() - tstart; 258 | measure.m.cv_meas.sampleRate = (1000000.0f*(float)measure.m.cv_meas.nSamples)/(float)us; 259 | // convert shunt adc reading to mA 260 | savg = savg / measure.m.cv_meas.nSamples; 261 | if (measure.m.cv_meas.scale == SCALE_HI) { 262 | measure.m.cv_meas.iavgma = savg*0.05f; 263 | measure.m.cv_meas.imaxma = smax*0.05f; 264 | measure.m.cv_meas.iminma = smin*0.05f; 265 | } 266 | else { 267 | measure.m.cv_meas.iavgma = savg*0.002381f; 268 | measure.m.cv_meas.imaxma = smax*0.002381f; 269 | measure.m.cv_meas.iminma = smin*0.002381f; 270 | } 271 | // convert bus adc reading to V 272 | bavg = bavg / measure.m.cv_meas.nSamples; 273 | measure.m.cv_meas.vavg = bavg*0.00125f; 274 | measure.m.cv_meas.vmax = bmax*0.00125f; 275 | measure.m.cv_meas.vmin = bmin*0.00125f; 276 | // vload = vbus 277 | ESP_LOGI(TAG,"CV Buffer Triggered : 0x%04X %s %.1fHz %.1fV %.3fmA\n", measure.m.cv_meas.cfg, measure.m.cv_meas.scale == SCALE_LO ? "LO" : "HI", measure.m.cv_meas.sampleRate, measure.m.cv_meas.vavg, measure.m.cv_meas.iavgma); 278 | } 279 | 280 | 281 | 282 | void ina226_capture_buffer_gated(volatile MEASURE_t &measure, volatile int16_t* buffer) { 283 | int16_t smax, smin, bmax, bmin, data_i16; // shunt and bus readings 284 | int32_t savg, bavg; // averaging accumulators 285 | uint16_t reg_bus, reg_shunt; 286 | smax = bmax = -32768; 287 | smin = bmin = 32767; 288 | savg = bavg = 0; 289 | int samplesPerSecond = 1000000/(int)measure.m.cv_meas.periodUs; 290 | switch_scale(measure.m.cv_meas.scale); 291 | // conversion ready -> alert pin goes low 292 | ina226_write_reg(REG_MASK, 0x0400); 293 | // continuous bus and shunt conversion 294 | ina226_write_reg(REG_CFG, measure.m.cv_meas.cfg | 0x0007); 295 | // throw away first sample 296 | // pinAlert pulled up to 3v3, active low on conversion complete 297 | while (digitalRead(pinAlert) == HIGH); 298 | // read shunt and bus ADCs 299 | reg_shunt = ina226_read_reg(REG_SHUNT); 300 | reg_bus = ina226_read_reg(REG_VBUS); 301 | 302 | // packet header with start packet ID, 303 | // sample period in mS, and current scale 304 | buffer[0] = MSG_TX_START; 305 | buffer[1] = (int16_t)measure.m.cv_meas.periodUs; 306 | buffer[2] = measure.m.cv_meas.scale; 307 | int offset = 3; 308 | int numSamples = 0; 309 | EndCaptureFlag = false; 310 | // gate signal is active low 311 | while (digitalRead(pinGate) == HIGH); 312 | GateOpenFlag = true; 313 | uint32_t tstart = micros(); 314 | while((digitalRead(pinGate) == LOW) && (numSamples < MaxSamples)) { 315 | uint32_t t1 = micros(); 316 | int bufIndex = offset + 2*numSamples; 317 | // pinAlert pulled up to 3v3, active low on conversion complete 318 | while (digitalRead(pinAlert) == HIGH); 319 | // read shunt and bus ADCs 320 | reg_shunt = ina226_read_reg(REG_SHUNT); 321 | reg_bus = ina226_read_reg(REG_VBUS); 322 | 323 | data_i16 = (int16_t)reg_shunt; 324 | buffer[bufIndex] = data_i16; 325 | savg += i32(data_i16); 326 | if (data_i16 > smax) smax = data_i16; 327 | if (data_i16 < smin) smin = data_i16; 328 | 329 | data_i16 = (int16_t)reg_bus; 330 | buffer[bufIndex+1] = data_i16; 331 | bavg += i32(data_i16); 332 | if (data_i16 > bmax) bmax = data_i16; 333 | if (data_i16 < bmin) bmin = data_i16; 334 | // break data buffer into packets with samplesPerSecond samples 335 | if (((numSamples+1) % samplesPerSecond) == 0) { 336 | // continued packet header ID for next socket transmission 337 | buffer[bufIndex+2] = MSG_TX; 338 | offset++; 339 | TxSamples = samplesPerSecond; 340 | DataReadyFlag = true; 341 | } 342 | while ((micros() - t1) < measure.m.cv_meas.periodUs); 343 | numSamples++; 344 | } 345 | uint32_t us = micros() - tstart; 346 | TxSamples = numSamples % samplesPerSecond; 347 | EndCaptureFlag = true; 348 | DataReadyFlag = true; 349 | measure.m.cv_meas.nSamples = numSamples; 350 | measure.m.cv_meas.sampleRate = (1000000.0f*(float)numSamples)/(float)us; 351 | // convert shunt adc reading to mA 352 | savg = savg / numSamples; 353 | if (measure.m.cv_meas.scale == SCALE_HI) { 354 | measure.m.cv_meas.iavgma = savg*0.05f; 355 | measure.m.cv_meas.imaxma = smax*0.05f; 356 | measure.m.cv_meas.iminma = smin*0.05f; 357 | } 358 | else { 359 | measure.m.cv_meas.iavgma = savg*0.002381f; 360 | measure.m.cv_meas.imaxma = smax*0.002381f; 361 | measure.m.cv_meas.iminma = smin*0.002381f; 362 | } 363 | // convert bus adc reading to V 364 | bavg = bavg / numSamples; 365 | measure.m.cv_meas.vavg = bavg*0.00125f; 366 | measure.m.cv_meas.vmax = bmax*0.00125f; 367 | measure.m.cv_meas.vmin = bmin*0.00125f; 368 | // vload = vbus 369 | ESP_LOGI(TAG,"CV Buffer Gated : %.3fsecs 0x%04X %s %.1fHz %.1fV %.3fmA\n", (float)us/1000000.0f, measure.m.cv_meas.cfg, measure.m.cv_meas.scale == SCALE_LO ? "LO" : "HI", measure.m.cv_meas.sampleRate, measure.m.cv_meas.vavg, measure.m.cv_meas.iavgma); 370 | } 371 | 372 | 373 | void ina226_reset() { 374 | ESP_LOGI(TAG,"INA226 system reset"); 375 | // system reset, bit self-clears 376 | ina226_write_reg(REG_CFG, 0x8000); 377 | delay(50); 378 | } 379 | 380 | 381 | void ina226_test_capture() { 382 | ESP_LOGI(TAG,"Measuring one-shot sample times"); 383 | Measure.m.cv_meas.scale = SCALE_HI; 384 | for (int inx = 0; inx < NUM_CFG; inx++) { 385 | Measure.m.cv_meas.cfg = Config[inx].reg; 386 | ina226_capture_oneshot(Measure, Buffer, true); 387 | } 388 | } -------------------------------------------------------------------------------- /src/ina226.h: -------------------------------------------------------------------------------- 1 | #ifndef INA226_H_ 2 | #define INA226_H_ 3 | 4 | #define INA226_I2C_ADDR 0x40 // 7-bit address 5 | 6 | #define REG_CFG 0x00 7 | #define REG_SHUNT 0x01 8 | #define REG_VBUS 0x02 9 | #define REG_MASK 0x06 10 | #define REG_ALERT 0x07 11 | #define REG_ID 0xFE 12 | 13 | 14 | #define SCALE_HI 0 // Shunt R = 0.05 ohms, Full scale = 1.64A 15 | #define SCALE_LO 1 // Shunt R = 1.05 ohms, Full scale = 78mA 16 | #define SCALE_AUTO 2 // Automatically switch 17 | 18 | #define MSG_GATE_OPEN 1234 19 | #define MSG_TX_START 1111 20 | #define MSG_TX 2222 21 | #define MSG_TX_COMPLETE 3333 22 | #define MSG_TX_CV_METER 4444 23 | 24 | 25 | typedef struct { 26 | uint16_t reg; 27 | uint32_t periodUs; 28 | } CONFIG_t; 29 | 30 | #define NUM_CFG 4 31 | 32 | extern const CONFIG_t Config[]; 33 | extern int MaxSamples; 34 | extern volatile bool GateOpenFlag; 35 | extern volatile bool DataReadyFlag; 36 | extern volatile int TxSamples; 37 | extern volatile bool EndCaptureFlag; 38 | extern volatile bool MeterReadyFlag; 39 | 40 | void ina226_write_reg(uint8_t regAddr, uint16_t data); 41 | uint16_t ina226_read_reg(uint8_t regAddr); 42 | void ina226_reset(); 43 | bool ina226_capture_oneshot(volatile MEASURE_t &measure, volatile int16_t* buffer, bool manualScale); 44 | bool ina226_capture_averaged_sample(volatile MEASURE_t &measure, volatile int16_t* buffer, bool manualScale); 45 | void ina226_capture_buffer_triggered(volatile MEASURE_t &measure, volatile int16_t* buffer); 46 | void ina226_capture_buffer_gated(volatile MEASURE_t &measure, volatile int16_t* buffer); 47 | void ina226_test_capture(); 48 | 49 | #endif -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "config.h" 6 | #include "nv_data.h" 7 | #include "wifi_cfg.h" 8 | #include "ina226.h" 9 | #include "freq_counter.h" 10 | 11 | const char* FwRevision = "0.97"; 12 | static const char* TAG = "main"; 13 | 14 | volatile int TxSamples; 15 | volatile bool SocketConnectedFlag = false; 16 | uint32_t ClientID; 17 | 18 | #define WIFI_TASK_PRIORITY 1 19 | #define CURRENT_VOLTAGE_TASK_PRIORITY (configMAX_PRIORITIES-1) 20 | #define FREQUENCY_TASK_PRIORITY (configMAX_PRIORITIES-2) 21 | 22 | volatile MEASURE_t Measure; 23 | volatile int16_t* Buffer = NULL; 24 | int MaxSamples; 25 | 26 | #define ST_IDLE 1 27 | #define ST_TX 2 28 | #define ST_TX_COMPLETE 3 29 | #define ST_METER_COMPLETE 4 30 | #define ST_FREQ_COMPLETE 5 31 | 32 | static void wifi_task(void* pvParameter); 33 | static void current_voltage_task(void* pvParameter); 34 | static void reset_flags(); 35 | 36 | // create the desired tasks, and then delete arduino created loopTask that calls setup() and loop(). 37 | // Core 0 : wifi task with web server and websocket communication, and low level esp-idf wifi code 38 | // Core 1 : capture task 39 | 40 | void setup() { 41 | pinMode(pinAlert, INPUT); // external pullup, active low 42 | pinMode(pinGate, INPUT); // external pullup, active low 43 | pinMode(pinFET1, OUTPUT); // external pulldown 44 | pinMode(pinFET05, OUTPUT); // external pulldown 45 | pinMode(pinLED, OUTPUT); 46 | digitalWrite(pinLED, LOW); 47 | 48 | Serial.begin(115200); 49 | ESP_LOGI(TAG,"ESP32_INA226 v%s compiled on %s at %s\n\n", FwRevision, __DATE__, __TIME__); 50 | ESP_LOGI(TAG, "Max task priority = %d", configMAX_PRIORITIES-1); 51 | ESP_LOGI(TAG, "arduino loopTask : setup() running on core %d with priority %d", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 52 | 53 | nv_options_load(Options); 54 | 55 | Measure.mode = MODE_CURRENT_VOLTAGE; 56 | 57 | // web server and web socket connection handler on core 0 along with low level wifi actions (ESP-IDF code) 58 | xTaskCreatePinnedToCore(&wifi_task, "wifi_task", 4096, NULL, WIFI_TASK_PRIORITY, NULL, CORE_0); 59 | // frequency_task on core 1, lower priority than cv capture task 60 | xTaskCreatePinnedToCore(&frequency_task, "freq_task", 4096, NULL, FREQUENCY_TASK_PRIORITY, NULL, CORE_1); 61 | // current_voltage_task on core 1, don't want i2c capture to be pre-empted as far as possible to maintain sampling rate. 62 | xTaskCreatePinnedToCore(¤t_voltage_task, "cv_task", 4096, NULL, CURRENT_VOLTAGE_TASK_PRIORITY, NULL, CORE_1); 63 | 64 | // destroy loopTask which called setup() from arduino:app_main() 65 | vTaskDelete(NULL); 66 | } 67 | 68 | 69 | // never called as loopTask is deleted, but needs to be defined 70 | void loop(){ 71 | } 72 | 73 | void reset_flags() { 74 | DataReadyFlag = false; 75 | GateOpenFlag = false; 76 | EndCaptureFlag = false; 77 | MeterReadyFlag = false; 78 | FreqReadyFlag = false; 79 | LastPacketAckFlag = false; 80 | } 81 | 82 | static void wifi_task(void* pVParameter) { 83 | ESP_LOGD(TAG, "wifi_task running on core %d with priority %d", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 84 | ESP_LOGI(TAG,"Starting web server"); 85 | // do NOT format, partition is built and flashed using 86 | // 1. PlatformIO Build FileSystem Image 87 | // 2. Upload FileSystem Image 88 | if (!LittleFS.begin(false)) { 89 | ESP_LOGE(TAG, "Cannot mount LittleFS, Rebooting"); 90 | delay(1000); 91 | ESP.restart(); 92 | } 93 | // initialize web server and web socket interface 94 | wifi_init(); 95 | int state = ST_IDLE; 96 | int bufferOffset = 0; 97 | int numBytes; 98 | volatile int16_t* pb; 99 | int16_t msg; 100 | uint32_t t1, t2; 101 | reset_flags(); 102 | 103 | while (1) { 104 | vTaskDelay(1); 105 | ws.cleanupClients(); 106 | if (SocketConnectedFlag == true) { 107 | switch (Measure.mode) { 108 | default : 109 | break; 110 | 111 | case MODE_CURRENT_VOLTAGE : 112 | switch (state) { 113 | default : 114 | break; 115 | 116 | case ST_IDLE : 117 | if (MeterReadyFlag == true) { 118 | MeterReadyFlag = false; 119 | LastPacketAckFlag = false; 120 | numBytes = 5 * sizeof(int16_t); 121 | ws.binary(ClientID, (uint8_t*)Buffer, numBytes); 122 | state = ST_METER_COMPLETE; 123 | } 124 | else 125 | if (GateOpenFlag){ 126 | GateOpenFlag = false; 127 | ESP_LOGD(TAG,"Socket msg : Capture Gate Open"); 128 | msg = MSG_GATE_OPEN; 129 | ws.binary(ClientID, (uint8_t*)&msg, 2); 130 | } 131 | else 132 | if (DataReadyFlag == true) { 133 | DataReadyFlag = false; 134 | ESP_LOGD(TAG,"Socket msg : Tx Start"); 135 | numBytes = (3 + TxSamples*2)*sizeof(int16_t); 136 | t1 = micros(); 137 | ws.binary(ClientID, (uint8_t*)Buffer, numBytes); 138 | bufferOffset += numBytes/2; // in uint16_ts 139 | if (EndCaptureFlag == true) { 140 | EndCaptureFlag = false; 141 | state = ST_TX_COMPLETE; 142 | } 143 | else { 144 | state = ST_TX; 145 | } 146 | } 147 | break; 148 | 149 | case ST_TX : 150 | // wait for next packet ready and last packet receive acknowledgement 151 | // before transmitting next packet 152 | if ((DataReadyFlag == true) && (LastPacketAckFlag == true)) { 153 | LastPacketAckFlag = false; 154 | DataReadyFlag = false; 155 | t2 = micros(); 156 | ESP_LOGD(TAG,"Socket msg : %dus, Tx ...", t2-t1); 157 | t1 = t2; 158 | pb = Buffer + bufferOffset; 159 | numBytes = (1 + TxSamples*2)*sizeof(int16_t); 160 | ws.binary(ClientID, (uint8_t*)pb, numBytes); 161 | bufferOffset += numBytes/2; // in uint16_ts 162 | if (EndCaptureFlag == true) { 163 | EndCaptureFlag = false; 164 | state = ST_TX_COMPLETE; 165 | } 166 | } 167 | break; 168 | 169 | case ST_TX_COMPLETE : 170 | // wait for last packet receive acknowledgement before transmitting 171 | // capture end message 172 | if (LastPacketAckFlag == true) { 173 | t2 = micros(); 174 | ESP_LOGD(TAG,"Socket msg : %dus, Tx ...", t2-t1); 175 | ESP_LOGD(TAG,"Socket msg : Tx Complete"); 176 | msg = MSG_TX_COMPLETE; 177 | ws.binary(ClientID, (uint8_t*)&msg, 2); 178 | reset_flags(); 179 | state = ST_IDLE; 180 | TxSamples = 0; 181 | bufferOffset = 0; 182 | } 183 | break; 184 | 185 | case ST_METER_COMPLETE : 186 | if (LastPacketAckFlag == true) { 187 | reset_flags(); 188 | state = ST_IDLE; 189 | } 190 | break; 191 | } 192 | break; 193 | 194 | case MODE_FREQUENCY : 195 | switch (state) { 196 | default : 197 | break; 198 | 199 | case ST_IDLE: 200 | if (FreqReadyFlag == true) { 201 | FreqReadyFlag = false; 202 | LastPacketAckFlag = false; 203 | int32_t buffer[2]; 204 | buffer[0] = MSG_TX_FREQUENCY; 205 | buffer[1] = FrequencyHz; 206 | numBytes = 2*sizeof(int32_t); 207 | ws.binary(ClientID, (uint8_t*)buffer, numBytes); 208 | state = ST_FREQ_COMPLETE; 209 | } 210 | break; 211 | 212 | case ST_FREQ_COMPLETE : 213 | if (LastPacketAckFlag == true) { 214 | reset_flags(); 215 | state = ST_IDLE; 216 | } 217 | break; 218 | } 219 | break; 220 | } 221 | } 222 | else { 223 | // socket disconnection, reset state and flags 224 | reset_flags(); 225 | Measure.mode = MODE_INVALID; 226 | state = ST_IDLE; 227 | } 228 | } 229 | vTaskDelete(NULL); 230 | } 231 | 232 | 233 | static void current_voltage_task(void* pvParameter) { 234 | ESP_LOGI(TAG, "current_voltage_task running on core %d with priority %d", xPortGetCoreID(), uxTaskPriorityGet(NULL)); 235 | CVCaptureFlag = false; 236 | Wire.begin(pinSDA,pinSCL); 237 | Wire.setClock(400000); 238 | uint16_t id = ina226_read_reg(REG_ID); 239 | if (id != 0x5449) { 240 | ESP_LOGE(TAG,"INA226 Manufacturer ID read = 0x%04X, expected 0x5449\n", id); 241 | ESP_LOGE(TAG,"Halting..."); 242 | while (1){ 243 | vTaskDelay(1); 244 | } 245 | } 246 | 247 | ina226_reset(); 248 | 249 | // get largest malloc-able block of byte-addressable free memory 250 | int32_t maxBufferBytes = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); 251 | ESP_LOGI(TAG, "Free memory malloc-able for sample Buffer = %d bytes", maxBufferBytes); 252 | 253 | Buffer = (int16_t*)malloc(maxBufferBytes); 254 | if (Buffer == nullptr) { 255 | ESP_LOGE(TAG, "Could not allocate sample Buffer with %d bytes", maxBufferBytes); 256 | ESP_LOGE(TAG,"Halting..."); 257 | while (1){ 258 | vTaskDelay(1); 259 | } 260 | } 261 | 262 | MaxSamples = (maxBufferBytes - 8)/4; 263 | ESP_LOGI(TAG, "Max Samples = %d", MaxSamples); 264 | #if 0 265 | ina226_test_capture(); 266 | #endif 267 | 268 | while (1){ 269 | if (CVCaptureFlag == true) { 270 | CVCaptureFlag = false; 271 | if (Measure.m.cv_meas.nSamples == 0) { 272 | ESP_LOGD(TAG,"Capturing gated samples using cfg = 0x%04X, scale %d", Measure.m.cv_meas.cfg, Measure.m.cv_meas.scale ); 273 | ina226_capture_buffer_gated(Measure, Buffer); 274 | } 275 | else 276 | if (Measure.m.cv_meas.nSamples == 1) { 277 | int scalemode = Measure.m.cv_meas.scale; 278 | if (scalemode == SCALE_LO) { 279 | ESP_LOGD(TAG,"Capturing meter sample using low scale"); 280 | Measure.m.cv_meas.scale = SCALE_LO; 281 | bool res = ina226_capture_averaged_sample(Measure, Buffer, true); 282 | if (!res) ESP_LOGD(TAG,"Warning : offscale reading"); 283 | } 284 | else 285 | if (scalemode == SCALE_HI) { 286 | ESP_LOGD(TAG,"Capturing meter sample using hi scale"); 287 | Measure.m.cv_meas.scale = SCALE_HI; 288 | bool res = ina226_capture_averaged_sample(Measure, Buffer, true); 289 | if (!res) ESP_LOGD(TAG,"Warning : offscale reading"); 290 | } 291 | else { 292 | ESP_LOGD(TAG,"Capturing meter sample autorange LO"); 293 | Measure.m.cv_meas.scale = SCALE_LO; 294 | bool res = ina226_capture_averaged_sample(Measure, Buffer, false); 295 | if (!res){ 296 | Measure.m.cv_meas.scale = SCALE_HI; 297 | ESP_LOGD(TAG,"Capturing meter sample autorange HI"); 298 | res = ina226_capture_averaged_sample(Measure, Buffer, true); 299 | if (!res) ESP_LOGD(TAG,"Warning : offscale reading"); 300 | } 301 | } 302 | } 303 | else { 304 | ESP_LOGD(TAG,"Capturing %d samples using cfg = 0x%04X, scale %d", Measure.m.cv_meas.nSamples, Measure.m.cv_meas.cfg, Measure.m.cv_meas.scale ); 305 | ina226_capture_buffer_triggered(Measure, Buffer); 306 | } 307 | } 308 | vTaskDelay(1); 309 | } 310 | vTaskDelete(NULL); 311 | } 312 | 313 | 314 | -------------------------------------------------------------------------------- /src/nv_data.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "nv_data.h" 4 | 5 | Preferences Prefs; 6 | 7 | OPTIONS_t Options; 8 | 9 | #define MODE_READ_WRITE false 10 | #define MODE_READ_ONLY true 11 | 12 | 13 | void nv_options_load(OPTIONS_t &options){ 14 | if (Prefs.begin("options", MODE_READ_ONLY) == false) { 15 | Serial.println("Preferences 'options' namespace not found, creating with defaults."); 16 | Prefs.end(); 17 | nv_options_reset(options); 18 | } 19 | else { 20 | options.ssid = Prefs.getString("ssid", ""); 21 | options.password = Prefs.getString("pwd", ""); 22 | Prefs.end(); 23 | nv_options_print(options); 24 | } 25 | } 26 | 27 | 28 | void nv_options_print(OPTIONS_t &options) { 29 | Serial.println("SSID = " + options.ssid); 30 | } 31 | 32 | 33 | void nv_options_reset(OPTIONS_t &options) { 34 | options.ssid = ""; 35 | options.password = ""; 36 | nv_options_store(options); 37 | Serial.println("Set Default Options"); 38 | nv_options_print(options); 39 | } 40 | 41 | 42 | void nv_options_store(OPTIONS_t &options){ 43 | Prefs.begin("options", MODE_READ_WRITE); 44 | Prefs.clear(); 45 | Prefs.putString("ssid", options.ssid); 46 | Prefs.putString("pwd", options.password); 47 | Prefs.end(); 48 | nv_options_print(options); 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/nv_data.h: -------------------------------------------------------------------------------- 1 | #ifndef NVDATA_H_ 2 | #define NVDATA_H_ 3 | 4 | 5 | 6 | 7 | typedef struct { 8 | String ssid; 9 | String password; 10 | } OPTIONS_t; 11 | 12 | 13 | 14 | extern OPTIONS_t Options; 15 | 16 | void nv_options_store(OPTIONS_t &options); 17 | void nv_options_load(OPTIONS_t &options); 18 | void nv_options_reset(OPTIONS_t &options); 19 | void nv_options_print(OPTIONS_t &options); 20 | 21 | #endif 22 | -------------------------------------------------------------------------------- /src/wifi_cfg.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "config.h" 10 | #include "nv_data.h" 11 | #include "ina226.h" 12 | #include "freq_counter.h" 13 | #include "wifi_cfg.h" 14 | 15 | static const char* TAG = "wifi_cfg"; 16 | 17 | extern const char* FwRevision; 18 | 19 | // configure static IP address for connecting as station 20 | static IPAddress Local_IP(192, 168, 43, 236); 21 | static IPAddress Gateway(192, 168, 43, 1); 22 | static IPAddress Subnet(255, 255, 255, 0); 23 | static IPAddress PrimaryDNS(8, 8, 8, 8); 24 | static IPAddress SecondaryDNS(8, 8, 4, 4); 25 | 26 | // stand-alone WiFi Access Point SSID (no password) 27 | const char* szAPSSID = "ESP32_METER"; 28 | 29 | AsyncWebServer* pServer = NULL; 30 | AsyncWebSocket ws("/ws"); 31 | 32 | 33 | void socket_handle_message(void *arg, uint8_t *data, size_t len); 34 | void socket_event_handler(AsyncWebSocket *server, 35 | AsyncWebSocketClient *client, 36 | AwsEventType type, 37 | void *arg, 38 | uint8_t *data, 39 | size_t len); 40 | 41 | 42 | static void wifi_start_as_ap(); 43 | static void wifi_start_as_station(); 44 | static void wifi_start_as_station_static_IP(); 45 | static String string_processor(const String& var); 46 | static void not_found_handler(AsyncWebServerRequest *request); 47 | static void index_page_handler(AsyncWebServerRequest *request); 48 | static void set_defaults_handler(AsyncWebServerRequest *request); 49 | static void get_handler(AsyncWebServerRequest *request); 50 | static void restart_handler(AsyncWebServerRequest *request); 51 | static void capture_handler(AsyncWebServerRequest *request); 52 | 53 | // Replace %txt% placeholder 54 | static String string_processor(const String& var){ 55 | if(var == "FW_REV"){ 56 | return FwRevision; 57 | } 58 | else 59 | if(var == "SSID"){ 60 | return Options.ssid; 61 | } 62 | else 63 | if(var == "PASSWORD"){ 64 | return Options.password; 65 | } 66 | else return "?"; 67 | } 68 | 69 | 70 | static void not_found_handler(AsyncWebServerRequest *request) { 71 | request->send(404, "text/plain", "Not found"); 72 | } 73 | 74 | static void index_page_handler(AsyncWebServerRequest *request) { 75 | request->send(LittleFS, "/index.html", String(), false, string_processor); 76 | } 77 | 78 | static void cv_chart_handler(AsyncWebServerRequest *request) { 79 | request->send(LittleFS, "/cv_chart.html", String(), false, string_processor); 80 | } 81 | 82 | static void cv_meter_handler(AsyncWebServerRequest *request) { 83 | request->send(LittleFS, "/cv_meter.html", String(), false, string_processor); 84 | } 85 | 86 | static void freq_counter_handler(AsyncWebServerRequest *request) { 87 | request->send(LittleFS, "/freq_counter.html", String(), false, string_processor); 88 | } 89 | 90 | static void set_defaults_handler(AsyncWebServerRequest *request) { 91 | nv_options_reset(Options); 92 | request->send(200, "text/html", "Default options set
Return to Home Page"); 93 | } 94 | 95 | 96 | static void restart_handler(AsyncWebServerRequest *request) { 97 | request->send(200, "text/html", "Restarting ..."); 98 | ESP_LOGI(TAG,"Restarting ESP32"); 99 | Serial.flush(); 100 | delay(100); 101 | esp_restart(); 102 | } 103 | 104 | static void get_handler(AsyncWebServerRequest *request) { 105 | String inputMessage; 106 | bool bChange = false; 107 | if (request->hasParam("ssid")) { 108 | inputMessage = request->getParam("ssid")->value(); 109 | bChange = true; 110 | Options.ssid = inputMessage; 111 | } 112 | if (request->hasParam("password")) { 113 | inputMessage = request->getParam("password")->value(); 114 | bChange = true; 115 | Options.password = inputMessage; 116 | } 117 | 118 | if (bChange == true) { 119 | ESP_LOGI(TAG,"Options changed"); 120 | nv_options_store(Options); 121 | bChange = false; 122 | } 123 | request->send(200, "text/html", "Input Processed
Return to Home Page"); 124 | } 125 | 126 | 127 | static void wifi_start_as_ap() { 128 | ESP_LOGI(TAG,"Starting Access Point with SSID=%s, no password\n", szAPSSID); 129 | WiFi.softAP(szAPSSID); 130 | IPAddress ipaddr = WiFi.softAPIP(); 131 | ESP_LOGI(TAG, "Web Server IP address : %s", ipaddr.toString().c_str()); 132 | digitalWrite(pinLED, HIGH); 133 | } 134 | 135 | static void wifi_start_as_station_static_IP() { 136 | ESP_LOGI(TAG,"Connecting as station static IP to Access Point with SSID=%s\n", Options.ssid); 137 | uint32_t startTick = millis(); 138 | // Configures static IP address 139 | if (!WiFi.config(Local_IP, Gateway, Subnet, PrimaryDNS, SecondaryDNS)) { 140 | ESP_LOGI(TAG,"Station static IP config failure"); 141 | } 142 | WiFi.begin(Options.ssid.c_str(), Options.password.c_str()); 143 | if (WiFi.waitForConnectResult(4000UL) != WL_CONNECTED) { 144 | ESP_LOGI(TAG,"Connection failed!"); 145 | wifi_start_as_ap(); 146 | } 147 | else { 148 | IPAddress ipaddr = WiFi.localIP(); 149 | uint32_t endTick = millis(); 150 | ESP_LOGI(TAG, "Connected in %.2f seconds with IP addr %s", (float)(endTick - startTick)/1000.0f, ipaddr.toString().c_str()); 151 | digitalWrite(pinLED, LOW); 152 | } 153 | } 154 | 155 | static void wifi_start_as_station() { 156 | ESP_LOGI(TAG,"Connecting as station to Access Point with SSID=%s", Options.ssid); 157 | uint32_t startTick = millis(); 158 | WiFi.mode(WIFI_STA); 159 | WiFi.begin(Options.ssid.c_str(), Options.password.c_str()); 160 | if (WiFi.waitForConnectResult(10000UL) != WL_CONNECTED) { 161 | ESP_LOGI(TAG,"Connection failed!"); 162 | wifi_start_as_ap(); 163 | } 164 | else { 165 | uint32_t endTick = millis(); 166 | IPAddress ipaddr = WiFi.localIP(); 167 | ESP_LOGI(TAG, "Connected in %.2f seconds with IP addr %s", (float)(endTick - startTick)/1000.0f, ipaddr.toString().c_str()); 168 | digitalWrite(pinLED, LOW); 169 | } 170 | } 171 | 172 | 173 | void wifi_init() { 174 | delay(100); 175 | if (Options.ssid == "") { 176 | wifi_start_as_ap(); 177 | } 178 | else { 179 | wifi_start_as_station_static_IP(); 180 | } 181 | 182 | if (!MDNS.begin("meter")) { // Use http://meter.local for web server page 183 | ESP_LOGI(TAG,"Error starting mDNS service"); 184 | } 185 | pServer = new AsyncWebServer(80); 186 | if (pServer == nullptr) { 187 | ESP_LOGE(TAG, "Error creating AsyncWebServer!"); 188 | ESP.restart(); 189 | } 190 | ws.onEvent(socket_event_handler); 191 | pServer->addHandler(&ws); 192 | pServer->onNotFound(not_found_handler); 193 | pServer->on("/", HTTP_GET, index_page_handler); 194 | pServer->on("/cv_chart", HTTP_GET, cv_chart_handler); 195 | pServer->on("/cv_meter", HTTP_GET, cv_meter_handler); 196 | pServer->on("/freq_counter", HTTP_GET, freq_counter_handler); 197 | pServer->on("/defaults", HTTP_GET, set_defaults_handler); 198 | pServer->on("/get", HTTP_GET, get_handler); 199 | pServer->on("/restart", HTTP_GET, restart_handler); 200 | pServer->serveStatic("/", LittleFS, "/"); 201 | 202 | pServer->begin(); 203 | MDNS.addService("http", "tcp", 80); 204 | } 205 | 206 | 207 | void socket_event_handler(AsyncWebSocket *server, 208 | AsyncWebSocketClient *client, 209 | AwsEventType type, 210 | void *arg, 211 | uint8_t *data, 212 | size_t len) { 213 | 214 | switch (type) { 215 | case WS_EVT_CONNECT: 216 | ESP_LOGI(TAG,"WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str()); 217 | ClientID = client->id(); 218 | SocketConnectedFlag = true; 219 | break; 220 | case WS_EVT_DISCONNECT: 221 | ESP_LOGI(TAG,"WebSocket client #%u disconnected\n", client->id()); 222 | SocketConnectedFlag = false; 223 | ClientID = 0; 224 | break; 225 | case WS_EVT_DATA: 226 | socket_handle_message(arg, data, len); 227 | break; 228 | case WS_EVT_PONG: 229 | case WS_EVT_ERROR: 230 | break; 231 | } 232 | } 233 | 234 | 235 | void socket_handle_message(void *arg, uint8_t *data, size_t len) { 236 | AwsFrameInfo *info = (AwsFrameInfo*)arg; 237 | if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { 238 | if (data[0] == 'x') { 239 | //ESP_LOGI(TAG, "ack = x"); 240 | LastPacketAckFlag = true; 241 | } 242 | else 243 | if (data[0] == 'm') { 244 | Measure.mode = MODE_CURRENT_VOLTAGE; 245 | Measure.m.cv_meas.nSamples = 1; 246 | Measure.m.cv_meas.cfg = Config[1].reg; 247 | Measure.m.cv_meas.periodUs = Config[1].periodUs; 248 | Measure.m.cv_meas.scale = (int)(data[1] - '0'); 249 | CVCaptureFlag = true; 250 | //ESP_LOGI(TAG,"cmd = m"); 251 | } 252 | else 253 | if (data[0] == 'f') { 254 | Measure.mode = MODE_FREQUENCY; 255 | FreqCaptureFlag = true; 256 | //ESP_LOGI(TAG,"cmd = f"); 257 | } 258 | else { 259 | const uint8_t size = JSON_OBJECT_SIZE(4); 260 | StaticJsonDocument json; 261 | DeserializationError err = deserializeJson(json, data); 262 | if (err) { 263 | ESP_LOGI(TAG, "deserializeJson() failed with code %s", err.c_str()); 264 | return; 265 | } 266 | 267 | const char *szAction = json["action"]; 268 | 269 | if (strcmp(szAction, "cv_capture") == 0) { 270 | const char *szCfgIndex = json["cfgIndex"]; 271 | const char *szCaptureSeconds = json["captureSecs"]; 272 | const char *szScale = json["scale"]; 273 | 274 | int cfgIndex = strtol(szCfgIndex, NULL, 10); 275 | int captureSeconds = strtol(szCaptureSeconds, NULL, 10); 276 | int sampleRate = 1000000/Config[cfgIndex].periodUs; 277 | int numSamples = captureSeconds*sampleRate; 278 | int scale = strtol(szScale, NULL, 10); 279 | 280 | Measure.mode = MODE_CURRENT_VOLTAGE; 281 | Measure.m.cv_meas.cfg = Config[cfgIndex].reg; 282 | Measure.m.cv_meas.scale = scale; 283 | Measure.m.cv_meas.nSamples = numSamples; 284 | Measure.m.cv_meas.periodUs = Config[cfgIndex].periodUs; 285 | ESP_LOGI(TAG,"Mode = %d", Measure.mode); 286 | ESP_LOGI(TAG,"cfgIndex = %d", cfgIndex); 287 | ESP_LOGI(TAG,"scale = %d", scale); 288 | ESP_LOGI(TAG,"nSamples = %d", numSamples); 289 | ESP_LOGI(TAG,"periodUs = %d", Config[cfgIndex].periodUs); 290 | CVCaptureFlag = true; 291 | } 292 | else 293 | if (strcmp(szAction, "oscfreq") == 0) { 294 | Measure.mode = MODE_FREQUENCY; 295 | const char *szOscFreqHz = json["freqhz"]; 296 | 297 | ESP_LOGI(TAG,"json[\"action\"]= %s\n", szAction); 298 | ESP_LOGI(TAG,"json[\"freqhz\"]= %s\n", szOscFreqHz); 299 | 300 | OscFreqHz = (uint32_t)strtol(szOscFreqHz, NULL, 10); 301 | OscFreqFlag = true; 302 | } 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/wifi_cfg.h: -------------------------------------------------------------------------------- 1 | #ifndef WIFI_CFG_H_ 2 | #define WIFI_CFG_H_ 3 | 4 | #include 5 | 6 | extern AsyncWebSocket ws; 7 | 8 | extern uint32_t ClientID; 9 | extern volatile bool SocketConnectedFlag; 10 | extern volatile bool CVCaptureFlag; 11 | extern volatile bool FreqCaptureFlag; 12 | extern volatile bool LastPacketAckFlag; 13 | 14 | void wifi_init(); 15 | 16 | #endif 17 | --------------------------------------------------------------------------------