-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathArduino_Battery_Backup_Monitor.ino
502 lines (423 loc) · 19.6 KB
/
Arduino_Battery_Backup_Monitor.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
/**
* @file main.cpp
* @brief Battery Monitoring and Data Logging System
*
* This program is designed to monitor battery usage by measuring the voltage drop across a shunt resistor
* using an ADS1115 Analog-to-Digital Converter. It calculates the current draw from the battery and
* computes the remaining battery capacity. The data is then uploaded to a database for tracking and analysis.
*
* The system is designed to be versatile for deployment in multiple scenarios. Upon initialization,
* it uses the unique MAC address of the device to download specific configurations for the particular
* deployment from a remote server. This approach allows for easy customization and scalability of the system.
*
* The development of this program involved leveraging OpenAI's GPT-4 for a significant portion of the code
* authoring, in collaboration with Ryan Susman, who contributed to the development and refinement of the system.
*
* @author Ryan Susman
* @date 2024-01-21
*/
// Includes
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#include <Adafruit_ADS1X15.h>
#include <Chrono.h>
#include <ArduinoJson.h>
#include "arduino_secrets.h"
#include <time.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <array>
#define ONE_WIRE_BUS D3 // Pin D3 on NodeMCU connected to the data line of DS18B20
// Constants
const int kLedPin = 2;
const int kAnalogPin = A0;
const int kShuntVoltageAdcPin = 1;
const int kBatteryVoltageAdcPin = 0;
const int kDefaultAvgDistance = 5;
const float kShuntAmp = 20.0f; // 75 mV = 20 Amps
const float kShuntDropMv = 0.075f; // 75 millivolts
const float kBatteryCapacityAh = 9.0f;
const bool kSmsEnabled = false;
const float kDischargingThresholdAmps = 0.2f;
const float kChargingThresholdAmps = 0.2f;
const bool kWriteRecordingsToDB = true;
const float kMaxBatteryVoltage = 29.2;
const int SCREEN_WIDTH = 128; // OLED display width, in pixels
const int SCREEN_HEIGHT = 64; // OLED display height, in pixels
const float kShuntOhms = 0.30; // Shunt resistor value in ohms
// Global Variables
WiFiClientSecure client;
HTTPClient http;
Adafruit_ADS1115 ads;
Chrono myChrono;
float batteryCapacityAh = kBatteryCapacityAh;
float remainingCapacityAh = kBatteryCapacityAh;
String batteryName = "Unknown";
String state = "Unknown";
String ipAddress;
String macAddress;
int rollingAvgDistance = kDefaultAvgDistance;
float shuntAmp = kShuntAmp; // 75 mV = 20 Amps
float shuntDropMv = kShuntDropMv; // 75 millivolts
float shuntOhms = kShuntOhms; // Shunt resistance set to 0.25 ohms
bool smsEnabled = kSmsEnabled;
float dischargingThresholdAmps = kDischargingThresholdAmps;
float chargingThresholdAmps = kChargingThresholdAmps;
bool writeRecordingsToDB = kWriteRecordingsToDB;
float maxBatteryVoltage = kMaxBatteryVoltage;
bool smsSent = false;
bool currentExceeded = false;
Chrono smsTimer;
String batteryState = "Charging";
struct MeasurementValues {
float calculatedVoltage;
float measuredVoltage;
};
// Declaration for an SSD1306 display connected to I2C (SCL, SDA pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// Temp Setup
OneWire oneWire(ONE_WIRE_BUS); // Create a OneWire instance
DallasTemperature sensors(&oneWire); // Pass the OneWire reference to Dallas Temperature library
// Function Prototypes
// Connects to a WiFi network using the provided SSID and password
void connectToWiFi(const char* ssid, const char* password, int maxRetries);
// Retrieves battery configuration data from a database using the MAC address
void getBatteryConfig(const String& macAddress);
// Initalize the OLED Screen
void initializeOLED();
// get temp of env
float getTemp();
// Set OLED Screen
void writeOledLine(String text, int line);
void setScreen(String arr[], int size);
void clearScreen();
// Initializes the ADS1115 Analog-to-Digital Converter (ADC)
void initializeADS1115();
// Takes a voltage measurement from a specified ADC pin
MeasurementValues takeMeasurement(int adcPin);
// Writes battery data to a database, including shuntVoltage, amperage, remaining capacity, and other details
void writeToDB(float shuntVoltage, float amperage, float remainingAh, const String& remainingTime, const String& state, const String& macAddress, const String& ipAddress, float remainingPercent, float batteryVoltage, float tempC);
// Formats a given time in seconds into a string in the format "HH:MM:SS"
String formatTime(long seconds);
void setup() {
pinMode(D8, INPUT);
// Temp Vars
String tempArray[8] = {"Starting..."};
// Set up the OLED
initializeOLED();
clearScreen();
// Print State
tempArray[1] = "WiFi:Connecting";
setScreen(tempArray, 1);
// Serial output
Serial.begin(115200);
// Connect to WiFi (SSID and Password in arduino_secrets.h)
connectToWiFi(SECRET_SSID, SECRET_PASS, 5); // Retry up to 5 times
// Print State
tempArray[1] = "WiFi:Connected";
tempArray[2] = "Configs:Downloading";
setScreen(tempArray, 2);
// using the mac_address, pull the configurations for this deployment from MongoDB
getBatteryConfig(macAddress);
// Print State
tempArray[2] = "Configs:Downloaded";
tempArray[3] = "ADS:Initializing";
setScreen(tempArray, 3);
// Set up ADC (ADS1115)
pinMode(kLedPin, OUTPUT);
initializeADS1115();
// Print State
tempArray[3] = "ADS:Initialized";
setScreen(tempArray, 3);
Serial.println("Starting...");
// Setup complete!
}
void loop() {
digitalWrite(kLedPin, HIGH);
// Take a measurment of the shunt
float measurementInterval = myChrono.elapsed();
MeasurementValues measurementShuntValues = takeMeasurement(kShuntVoltageAdcPin);
// Calculate the actual battery voltage using the voltage divider formula
MeasurementValues measurementBatteryValues = takeMeasurement(kBatteryVoltageAdcPin);
float batteryVoltage = (measurementBatteryValues.calculatedVoltage * maxBatteryVoltage)/0.256; // highest = 29.2 (batteryVoltageMeasured * 29.2)/100, because the voltage is greater than 0.256
myChrono.restart();
Serial.println("Starting...");
// Calculate the current in amperes by dividing the average voltage by the shunt resistance
float current = (measurementShuntValues.calculatedVoltage / shuntOhms) * 100;
// Convert the measurement interval from milliseconds to hours for capacity calculation
float timeHours = measurementInterval / 3600000.0;
// Calculate the amount of capacity (in ampere-hours) used in this measurement interval
float usedCapacity = current * timeHours;
// Subtract the used capacity from the remaining battery capacity to update its value
remainingCapacityAh -= usedCapacity;
// Constrain the remaining capacity to be within the range of 0 to the maximum battery capacity
remainingCapacityAh = constrain(remainingCapacityAh, 0.0, batteryCapacityAh);
// Calculate the remaining battery capacity as a percentage of the total capacity
float remainingBatteryPercent = (remainingCapacityAh * 100) / batteryCapacityAh;
// Calculate remaining battery life in hours
float remainingBatteryLifeHours = (current != 0) ? (remainingCapacityAh / current) : 0;
// Convert remaining battery life from hours to seconds
long remainingBatteryLifeSeconds = static_cast<long>(remainingBatteryLifeHours * 3600);
// Format the remaining battery life in HH:MM:SS format
String formattedRemainingBatteryLife = formatTime(remainingBatteryLifeSeconds);
// Check if the SMS alert feature is enabled and if the current measurement conditions warrant an SMS alert.
if (smsEnabled) {
// Check if the current exceeds the threshold of 0.2 amps.
if (current > dischargingThresholdAmps) {
currentExceeded = true; // Set a flag indicating that the current has exceeded the threshold.
// Check if an SMS has not been sent yet or if it has been more than 2 hours since the last SMS.
if (!smsSent || smsTimer.hasPassed(7200000)) { // 7200000 milliseconds = 2 hours
// Send an SMS message to alert that the current has exceeded 0.2 amps. Customize the message and recipient.
smsSent = true; // Set the flag to indicate that an SMS has been sent.
batteryState = "Discharging";
smsTimer.restart(); // Restart the timer to track the interval for the next SMS alert.
}
}
// Check if the current has dropped below 0.2 amps after having previously exceeded it.
else if (currentExceeded && current <= chargingThresholdAmps) {
currentExceeded = false; // Reset the flag as the current is now below the threshold.
// Check if an SMS was sent when the current exceeded the threshold.
if (smsSent) {
// Send an SMS message to alert that the current has dropped back below 0.2 amps. Customize the message and recipient.
smsSent = false; // Reset the flag to allow a new SMS to be sent when the current goes above 0.2 amps again.
batteryState = "Charging";
smsTimer.restart(); // Restart the timer for timing the next SMS alert.
}
}
}
float tempC = getTemp();
Serial.println("-----------|-------");
Serial.print("Shunt Calculated V: "); Serial.println(String(measurementShuntValues.calculatedVoltage, 7));
Serial.print("Shunt Measured V: "); Serial.println(String(measurementShuntValues.measuredVoltage, 7));
Serial.print("Shunt I: "); Serial.println(String(current, 7));
Serial.print("Remain Ah: "); Serial.println(String(remainingCapacityAh, 7));
Serial.print("Remain %: "); Serial.println(String(remainingBatteryPercent, 2));
Serial.print("Remain Time:"); Serial.println(formattedRemainingBatteryLife);
Serial.print("Battery V: "); Serial.println(String(batteryVoltage, 7)); // Testing
Serial.print("Temp C: "); Serial.println(String(tempC, 7)); // Testing
clearScreen();
String screenStringArray[6] = {
"Batty V:" + String(batteryVoltage, 6),
"Shunt V:" + String(measurementShuntValues.calculatedVoltage, 6),
"Shunt I:" + String(current, 6),
"Time :" + formattedRemainingBatteryLife,
"Batty %:" + String(remainingBatteryPercent, 2),
"Temp C :" + String(tempC, 4)
};
int screenArrayLength = sizeof(screenStringArray) / sizeof(screenStringArray[0]);
setScreen(screenStringArray, screenArrayLength);
if (writeRecordingsToDB){
writeToDB(measurementBatteryValues.measuredVoltage, current, remainingCapacityAh, formattedRemainingBatteryLife, "", macAddress, ipAddress, remainingBatteryPercent, batteryState, batteryVoltage, tempC);
}
digitalWrite(kLedPin, LOW);
delay(1000);
}
// Function to connect to WiFi
void connectToWiFi(const char* ssid, const char* password, int maxRetries = 5) {
int retryCount = 0;
bool connected = false;
Serial.print("Attempting to connect to WiFi");
while (retryCount < maxRetries && !connected) {
WiFi.begin(ssid, password); // Start the connection process
// Wait for connection
for (int i = 0; i < 10; i++) { // Wait 5 seconds for connection
if (WiFi.status() == WL_CONNECTED) {
connected = true;
break;
}
delay(500);
Serial.print(".");
}
if (connected) {
break;
}
retryCount++;
Serial.println("\nRetry " + String(retryCount) + "/" + String(maxRetries));
}
if (connected) {
ipAddress = WiFi.localIP().toString();
macAddress = WiFi.macAddress();
Serial.println("\nWiFi connected");
Serial.print("IP address: "); Serial.println(ipAddress);
Serial.print("MAC address: "); Serial.println(macAddress);
} else {
Serial.println("\nFailed to connect to WiFi after retries. Rebooting...");
ESP.restart(); // Reboot the microcontroller
}
}
// Function to get battery configuration
void getBatteryConfig(const String& macAddress) {
WiFiClientSecure client;
HTTPClient http;
StaticJsonDocument<200> configDoc;
client.setInsecure(); // Bypass SSL certificate verification
http.addHeader("Content-Type", "application/json");
const String serverPath = "https://us-east-1.aws.data.mongodb-api.com/app/batterymanagementv1-jkqqf/endpoint/GetBatteryDataV1?secret=" + String(SECRET_MONGODBSECRET);
configDoc["mac_address"] = macAddress;
String jsonString;
serializeJson(configDoc, jsonString);
http.begin(client, serverPath.c_str());
int httpResponseCode = http.POST(jsonString);
if (httpResponseCode == 200) {
String payload = http.getString();
// Serial.println(payload);
DeserializationError error = deserializeJson(configDoc, payload);
if (error) {
Serial.print("deserializeJson() failed: ");
Serial.println(error.c_str());
// Handle the deserialization error (e.g., set default values or take appropriate action)
ESP.restart(); // Restart the ESP if the HTTP request fails
} else {
// Update global variables based on the configuration data
batteryName = configDoc["battery_name"].as<String>();
batteryCapacityAh = configDoc["battery_capasity_ah"].as<float>();
shuntAmp = configDoc["shunt_amp"].as<float>();
shuntDropMv = configDoc["shunt_drop_mv"].as<float>();
rollingAvgDistance = configDoc["rollingAvgDistance"].as<int>();
smsEnabled = configDoc["smsEnabled"].as<bool>();
dischargingThresholdAmps = configDoc["dischargingThresholdAmps"].as<float>();
chargingThresholdAmps = configDoc["chargingThresholdAmps"].as<float>();
writeRecordingsToDB = configDoc["writeRecordingsToDB"].as<bool>();
maxBatteryVoltage = configDoc["maxBatteryVoltage"].as<float>();
remainingCapacityAh = batteryCapacityAh;
}
} else {
Serial.print("HTTP POST request failed: ");
Serial.println(httpResponseCode);
// Handle the HTTP request error (e.g., set default values or take appropriate action)
ESP.restart(); // Restart the ESP if the HTTP request fails
}
http.end();
}
void initializeOLED() {
// Initialize with the I2C addr 0x3C (for the 128x32)
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
else {
Serial.println(F("SSD1306 allocation success"));
}
}
float getTemp() {
sensors.requestTemperatures(); // Send the command to get temperatures
return sensors.getTempCByIndex(0); // temp in Celsius
}
void clearScreen() {
display.clearDisplay();
display.setTextSize(1); // Normal 1:1 pixel scale
display.setTextColor(WHITE); // Draw white text
display.display();
}
void writeOledLine(String text, int line){
display.setCursor(0, line * 8); // Start at top-left corner
display.println(text);
}
void setScreen(String arr[], int size) {
clearScreen();
Serial.println(String(size));
for (int i = 0; i < size; ++i) {
writeOledLine(arr[i], i);
}
display.display();
}
void initializeADS1115() {
// ADS1015 ADS1115
// ------- -------
// ads.setGain(GAIN_TWOTHIRDS); // 2/3x gain +/- 6.144V 1 bit = 3mV 0.1875mV (default)
// ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V 1 bit = 2mV 0.125mV
// ads.setGain(GAIN_TWO); // 2x gain +/- 2.048V 1 bit = 1mV 0.0625mV
// ads.setGain(GAIN_FOUR); // 4x gain +/- 1.024V 1 bit = 0.5mV 0.03125mV
// ads.setGain(GAIN_EIGHT); // 8x gain +/- 0.512V 1 bit = 0.25mV 0.015625mV
ads.setGain(GAIN_SIXTEEN); // 16x gain +/- 0.256V 1 bit = 0.125mV 0.0078125mV
ads.setGain(GAIN_SIXTEEN);
if (!ads.begin()) {
Serial.println("Failed to initialize ADS.");
while (1);
}
}
// Constants
const int kRollingAverageSize = 5; // Set the size of the rolling average
// Global Variables for each ADC pin
std::array<float, kRollingAverageSize> rollingMeasurements0{};
std::array<float, kRollingAverageSize> rollingMeasurements1{};
int rollingIndex0 = 0;
int rollingIndex1 = 0;
MeasurementValues takeMeasurement(int adcPin) {
MeasurementValues mv;
float currentMeasurement = ads.readADC_SingleEnded(adcPin);
// Decide which rolling average array to use based on the ADC pin
if (adcPin == 0) {
// Update rolling average for ADC Pin 0
rollingMeasurements0[rollingIndex0] = currentMeasurement;
rollingIndex0 = (rollingIndex0 + 1) % kRollingAverageSize;
// Calculate the rolling average for ADC Pin 0
float sum0 = 0;
for (float measurement : rollingMeasurements0) {
sum0 += measurement;
}
mv.measuredVoltage = sum0 / kRollingAverageSize;
} else if (adcPin == 1) {
// Update rolling average for ADC Pin 1
rollingMeasurements1[rollingIndex1] = currentMeasurement;
rollingIndex1 = (rollingIndex1 + 1) % kRollingAverageSize;
// Calculate the rolling average for ADC Pin 1
float sum1 = 0;
for (float measurement : rollingMeasurements1) {
sum1 += measurement;
}
mv.measuredVoltage = sum1 / kRollingAverageSize;
// Apply Calibration
mv.measuredVoltage = mv.measuredVoltage + 29;
}
// Convert the average measurement to volts for the specific pin
mv.calculatedVoltage = ads.computeVolts(mv.measuredVoltage);
return mv;
}
void writeToDB(float shuntVoltage, float amperage, float remainingAh, const String& remainingTime, const String& state, const String& macAddress, const String& ipAddress, float remainingPercent, String batteryState, float batteryVoltage, float tempC) {
WiFiClientSecure client;
HTTPClient http;
client.setInsecure(); // Bypass SSL certificate verification
http.addHeader("Content-Type", "application/json");
String serverPath = "https://us-east-1.aws.data.mongodb-api.com/app/batteryupload-ayjsz/endpoint/BatteryUpdaterV3?secret=" + String(SECRET_MONGODBSECRET); // Replace with your actual server URL
StaticJsonDocument<200> doc;
doc["battery_name"] = batteryName;
doc["shuntVoltage"] = shuntVoltage;
doc["amperage"] = amperage;
doc["remaining_ah"] = remainingAh;
doc["remaining_time"] = remainingTime;
doc["state"] = state;
doc["mac_address"] = macAddress;
doc["ip_address"] = ipAddress;
doc["remaining_percent"] = remainingPercent;
doc["batteryState"] = batteryState;
doc["batteryVoltage"] = batteryVoltage;
doc["batteryTempC"] = tempC;
String jsonString;
serializeJson(doc, jsonString);
http.begin(client, serverPath.c_str());
int httpResponseCode = http.POST(jsonString);
if (httpResponseCode > 0) {
// Optionally handle the response content
// String payload = http.getString();
} else {
Serial.print("HTTP POST request failed: ");
Serial.println(httpResponseCode);
// Handle the error appropriately
}
http.end();
}
String formatTime(long seconds) {
long hours = seconds / 3600; // Convert seconds to hours
long remainingSeconds = seconds % 3600;
long minutes = remainingSeconds / 60; // Convert remaining seconds to minutes
long secs = remainingSeconds % 60; // Remaining seconds
char formattedTime[9]; // Buffer to hold the formatted time
sprintf(formattedTime, "%02ld:%02ld:%02ld", hours, minutes, secs);
return String(formattedTime);
}