├── .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 |
29 |
30 |
Voltage
31 | vStats
32 |
33 |
34 |
35 |
36 |
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 | Current Full-Scale [Resolution]
19 |
20 |
21 | 1638.35mA [50uA]
22 | 78.017mA [2.4uA]
23 | Auto
24 |
25 |
26 |
27 |
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 |
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 |
55 |
56 |
57 |
58 |
59 |
60 |
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 setReturn 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 ProcessedReturn 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 |
--------------------------------------------------------------------------------